Skip to main content

元件 (實驗性)

簡介

Playwright Test 現在可以測試你的元件。

範例

以下是一個典型元件測試的樣子:

test('event should work', async ({ mount }) => {
let clicked = false;

// Mount a component. Returns locator pointing to the component.
const component = await mount(
<Button title="Submit" onClick={() => { clicked = true }}></Button>
);

// As with any Playwright test, assert locator text.
await expect(component).toContainText('Submit');

// Perform locator click. This will trigger the event.
await component.click();

// Assert that respective events have been fired.
expect(clicked).toBeTruthy();
});

如何開始

將 Playwright Test 添加到現有專案很容易。以下是為 React、Vue、Svelte 或 Solid 專案啟用 Playwright Test 的步驟。

第一步: 為您的相應框架安裝 Playwright Test 來測試元件

npm init playwright@latest -- --ct

這個步驟會在你的工作區域中建立幾個文件:

playwright/index.html
<html lang="en">
<body>
<div id="root"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>

此文件定義了一個 html 文件,將用於測試期間渲染元件。它必須包含 id="root" 的元素,這是元件掛載的地方。它還必須鏈接名為 playwright/index.{js,ts,jsx,tsx} 的腳本。

你可以包含樣式表、應用主題並將程式碼注入到元件安裝的頁面中使用此腳本。它可以是 .js.ts.jsx.tsx 檔案。

playwright/index.ts
// Apply theme here, add anything your component needs at runtime here.

步驟 2. 建立一個測試文件 src/App.spec.{ts,tsx}

import { test, expect } from '@playwright/experimental-ct-react';
import App from './App';

test.use({ viewport: { width: 500, height: 500 } });

test('should work', async ({ mount }) => {
const component = await mount(<App />);
await expect(component).toContainText('Learn React');
});

Step 3. 執行測試

您可以使用 VS Code 擴充套件 或命令列來執行測試。

npm run test-ct

進一步閱讀: 設定報告、瀏覽器、追蹤

請參考 Playwright config 設定你的專案。

測試故事

當 Playwright Test 用於測試 web 元件時,測試在 Node.js 中執行,而元件在真實的瀏覽器中執行。這結合了兩者的優點:元件在真實的瀏覽器環境中執行,觸發真實的點擊,執行真實的佈局,視覺回歸是可能的。同時,測試可以使用 Node.js 的所有功能以及所有 Playwright Test 的功能。因此,在元件測試期間,相同的平行、參數化測試與相同的事後追蹤故事是可用的。

然而,這引入了許多限制:

  • 你不能將複雜的即時物件傳遞給你的元件。只能傳遞純 JavaScript 物件和內建類型,如字串、數字、日期等。
test('this will work', async ({ mount }) => {
const component = await mount(<ProcessViewer process={{ name: 'playwright' }}/>);
});

test('this will not work', async ({ mount }) => {
// `process` is a Node object, we can't pass it to the browser and expect it to work.
const component = await mount(<ProcessViewer process={process}/>);
});
  • 你不能在回呼中同步傳遞資料給你的元件:
test('this will not work', async ({ mount }) => {
// () => 'red' callback lives in Node. If `ColorPicker` component in the browser calls the parameter function
// `colorGetter` it won't get result synchronously. It'll be able to get it via await, but that is not how
// components are typically built.
const component = await mount(<ColorPicker colorGetter={() => 'red'}/>);
});

克服這些和其他限制的方法既快速又優雅: 對於被測試元件的每個使用案例,建立一個專門為測試設計的該元件的包裝器。這不僅能減輕限制,還能為測試提供強大的抽象,讓你能夠定義環境、主題和元件渲染的其他方面。

假設你想測試以下元件:

input-media.tsx
import React from 'react';

type InputMediaProps = {
// Media is a complex browser object we can't send to Node while testing.
onChange(media: Media): void;
};

export function InputMedia(props: InputMediaProps) {
return <></> as any;
}

為你的元件建立一個故事文件:

input-media.story.tsx
import React from 'react';
import InputMedia from './import-media';

type InputMediaForTestProps = {
onMediaChange(mediaName: string): void;
};

export function InputMediaForTest(props: InputMediaForTestProps) {
// Instead of sending a complex `media` object to the test, send the media name.
return <InputMedia onChange={media => props.onMediaChange(media.name)} />;
}
// Export more stories here.

然後透過測試故事來測試元件:

input-media.test.spec.tsx
test('changes the image', async ({ mount }) => {
let mediaSelected: string | null = null;

const component = await mount(
<InputMediaForTest
onMediaChange={mediaName => {
mediaSelected = mediaName;
}}
/>
);
await component
.getByTestId('imageInput')
.setInputFiles('src/assets/logo.png');

await expect(component.getByAltText(/selected image/i)).toBeVisible();
await expect.poll(() => mediaSelected).toBe('logo.png');
});

因此,對於每個元件,你將擁有一個故事檔案,該檔案會匯出所有實際測試的故事。這些故事存在於瀏覽器中,並將複雜的物件“轉換”成可以在測試中訪問的簡單物件。

引擎內部

以下是元件測試的工作原理:

  • 一旦測試被執行,Playwright 會建立測試所需的元件清單。
  • 然後它會編譯一個包含這些元件的捆綁包,並使用本地靜態網頁伺服器提供服務。
  • 在測試中的 mount 呼叫時,Playwright 會導航到此捆綁包的外觀頁面 /playwright/index.html 並告訴它渲染元件。
  • 事件會被傳回到 Node.js 環境以允許驗證。

Playwright 是使用 Vite 來建立元件包並提供服務。

API 參考

props

在掛載時向元件提供道具。

test('props', async ({ mount }) => {
const component = await mount(<Component msg="greetings" />);
});

callbacks / events

在元件掛載時提供回呼/事件。

test('callback', async ({ mount }) => {
const component = await mount(<Component callback={() => {}} />);
});

子項 / 插槽

提供子項/插槽給元件當掛載時。

test('children', async ({ mount }) => {
const component = await mount(<Component>Child</Component>);
});

hooks

你可以使用 beforeMountafterMount hooks 來配置你的應用程式。這讓你可以設定像是應用程式路由、假伺服器等,給予你所需的彈性。你也可以從測試中的 mount 呼叫傳遞自訂配置,這些配置可以從 hooksConfig fixture 訪問。這包括任何需要在掛載元件之前或之後執行的配置。以下提供了一個配置路由的範例:

playwright/index.tsx
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';

export type HooksConfig = {
enableRouting?: boolean;
}

beforeMount<HooksConfig>(async ({ App, hooksConfig }) => {
if (hooksConfig?.enableRouting)
return <BrowserRouter><App /></BrowserRouter>;
});
src/pages/ProductsPage.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import type { HooksConfig } from '../playwright';
import { ProductsPage } from './pages/ProductsPage';

test('configure routing through hooks config', async ({ page, mount }) => {
const component = await mount<HooksConfig>(<ProductsPage />, {
hooksConfig: { enableRouting: true },
});
await expect(component.getByRole('link')).toHaveAttribute('href', '/products/42');
});

卸載

將已掛載的元件從 DOM 中卸載。這對於測試元件在卸載時的行為非常有用。使用案例包括測試 "您確定要離開嗎?" 模態視窗 (model dialog) 或確保正確清理事件處理器以防止記憶體洩漏。

test('unmount', async ({ mount }) => {
const component = await mount(<Component/>);
await component.unmount();
});

更新

更新已掛載元件的 props、slots/children 和/或 events/callbacks。這些元件輸入可以隨時變更,通常由父元件提供,但有時需要確保您的元件對新輸入的行為適當。

test('update', async ({ mount }) => {
const component = await mount(<Component/>);
await component.update(
<Component msg="greetings" callback={() => {}}>Child</Component>
);
});

處理網路請求

Playwright 提供一個實驗性router 固定裝置來攔截和處理網路請求。有兩種方式使用 router 固定裝置:

這裡是一個在測試中重複使用現有 MSW 處理器的範例。

import { handlers } from '@src/mocks/handlers';

test.beforeEach(async ({ router }) => {
// install common handlers before each test
await router.use(...handlers);
});

test('example test', async ({ mount }) => {
// test as usual, your handlers are active
// ...
});

你也可以為特定測試引入一次性處理程式。

import { http, HttpResponse } from 'msw';

test('example test', async ({ mount, router }) => {
await router.use(http.get('/data', async ({ request }) => {
return HttpResponse.json({ value: 'mocked' });
}));

// test as usual, your handler is active
// ...
});

常見問題

@playwright/test@playwright/experimental-ct-{react,svelte,vue,solid} 之間有什麼區別?

test('…', async ({ mount, page, context }) => {
// …
});

@playwright/experimental-ct-{react,svelte,vue,solid} 包裝 @playwright/test 以提供一個額外的內建元件測試專用的固定裝置,稱為 mount:

import { test, expect } from '@playwright/experimental-ct-react';
import HelloWorld from './HelloWorld';

test.use({ viewport: { width: 500, height: 500 } });

test('should work', async ({ mount }) => {
const component = await mount(<HelloWorld msg="greetings" />);
await expect(component).toContainText('Greetings');
});

此外,它增加了一些可以在 playwright-ct.config.{ts,js} 中使用的配置選項。

最後,在底層,每個測試都會重新使用 contextpage 固件作為元件測試的速度最佳化。它會在每個測試之間重置它們,因此應該在功能上等同於 @playwright/test 保證每個測試都會獲得一個新的、隔離的 contextpage 固件。

我有一個已經使用 Vite 的專案。我可以重複使用這個配置嗎?

此時,Playwright 是與打包工具無關的,因此它不會重用你現有的 Vite 配置。你的配置可能包含很多我們無法重用的東西。所以現在,你需要將路徑映射和其他高級設定複製到 Playwright 配置的 ctViteConfig 屬性中。

import { defineConfig } from '@playwright/experimental-ct-react';

export default defineConfig({
use: {
ctViteConfig: {
// ...
},
},
});

你可以透過 Vite 配置來指定外掛以測試設定。請注意,一旦你開始指定外掛,你也需要負責指定框架外掛,在這個例子中是 vue()

import { defineConfig, devices } from '@playwright/experimental-ct-vue';

import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';

export default defineConfig({
testDir: './tests/component',
use: {
trace: 'on-first-retry',
ctViteConfig: {
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'vue-router',
'@vueuse/head',
'pinia',
{
'@/store': ['useStore'],
},
],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true,
},
}),
Components({
dirs: ['src/components'],
extensions: ['vue'],
}),
],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
},
},
});

我如何測試使用 Pinia 的元件?

Pinia 需要在 playwright/index.{js,ts,jsx,tsx} 中初始化。如果你在 beforeMount 鉤子內執行此操作,initialState 可以在每次測試時被覆寫:

playwright/index.ts
import { beforeMount, afterMount } from '@playwright/experimental-ct-vue/hooks';
import { createTestingPinia } from '@pinia/testing';
import type { StoreState } from 'pinia';
import type { useStore } from '../src/store';

export type HooksConfig = {
store?: StoreState<ReturnType<typeof useStore>>;
}

beforeMount<HooksConfig>(async ({ hooksConfig }) => {
createTestingPinia({
initialState: hooksConfig?.store,
/**
* Use http intercepting to mock api calls instead:
* https://playwright.dev/docs/mock#mock-api-requests
*/
stubActions: false,
createSpy(args) {
console.log('spy', args)
return () => console.log('spy-returns')
},
});
});
src/pinia.spec.ts
import { test, expect } from '@playwright/experimental-ct-vue';
import type { HooksConfig } from '../playwright';
import Store from './Store.vue';

test('override initialState ', async ({ mount }) => {
const component = await mount<HooksConfig>(Store, {
hooksConfig: {
store: { name: 'override initialState' }
}
});
await expect(component).toContainText('override initialState');
});

我如何存取元件的方法或其實例?

在測試程式碼中存取元件的內部方法或其實例既不建議也不支援。相反地,應專注於從使用者的角度觀察和互動元件,通常是通過點擊或驗證頁面上是否有東西可見。當測試避免與內部實作細節(如元件實例或其方法)互動時,測試會變得不那麼脆弱且更有價值。請記住,如果從使用者的角度執行測試失敗,這很可能意味著自動化測試已經發現了程式碼中的真正錯誤。