Components (experimental)
簡介
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 專案啟用 Playwright Test 的步驟。
步驟 1:為您各自的框架安裝 Playwright Test 元件
- npm
- yarn
- pnpm
npm init playwright@latest -- --ct
yarn create playwright --ct
pnpm create playwright --ct
此步驟會在您的工作區中建立數個檔案:
<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
檔案。
// Apply theme here, add anything your component needs at runtime here.
步驟 2. 建立一個測試檔案 src/App.spec.{ts,tsx}
- React
- Svelte
- Vue
import { test, expect } from '@playwright/experimental-ct-react';
import App from './App';
test('should work', async ({ mount }) => {
const component = await mount(<App />);
await expect(component).toContainText('Learn React');
});
import { test, expect } from '@playwright/experimental-ct-vue';
import App from './App.vue';
test('should work', async ({ mount }) => {
const component = await mount(App);
await expect(component).toContainText('Learn Vue');
});
import { test, expect } from '@playwright/experimental-ct-vue';
import App from './App.vue';
test('should work', async ({ mount }) => {
const component = await mount(<App />);
await expect(component).toContainText('Learn Vue');
});
如果使用 TypeScript 和 Vue,請確保將 vue.d.ts
檔案加入您的專案:
declare module '*.vue';
import { test, expect } from '@playwright/experimental-ct-svelte';
import App from './App.svelte';
test('should work', async ({ mount }) => {
const component = await mount(App);
await expect(component).toContainText('Learn Svelte');
});
步驟 3. 執行測試
您可以使用 VS Code 擴充套件 或命令列來執行測試。
npm run test-ct
進一步閱讀:設定報告、瀏覽器、追蹤
請參考 Playwright config 設定您的專案。
測試故事
當使用 Playwright Test 來測試網頁元件時,測試在 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'}/>);
});
繞過這些和其他限制的方法既快速又優雅:針對待測元件的每個使用案例,建立專門為測試設計的元件包裝器。這不僅能緩解限制,還能為測試提供強大的抽象,讓您能夠定義環境、主題以及元件渲染的其他方面。
假設您想要測試以下元件:
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;
}
為您的元件建立一個故事檔案:
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.
然後透過測試故事來測試元件:
import { test, expect } from '@playwright/experimental-ct-react';
import { InputMediaForTest } from './input-media.story.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
在掛載時為元件提供 props。
- React
- Svelte
- Vue
import { test } from '@playwright/experimental-ct-react';
test('props', async ({ mount }) => {
const component = await mount(<Component msg="greetings" />);
});
import { test } from '@playwright/experimental-ct-svelte';
test('props', async ({ mount }) => {
const component = await mount(Component, { props: { msg: 'greetings' } });
});
import { test } from '@playwright/experimental-ct-vue';
test('props', async ({ mount }) => {
const component = await mount(Component, { props: { msg: 'greetings' } });
});
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('props', async ({ mount }) => {
const component = await mount(<Component msg="greetings" />);
});
callbacks / events
在掛載時為元件提供回呼/事件。
- React
- Svelte
- Vue
import { test } from '@playwright/experimental-ct-react';
test('callback', async ({ mount }) => {
const component = await mount(<Component onClick={() => {}} />);
});
import { test } from '@playwright/experimental-ct-svelte';
test('event', async ({ mount }) => {
const component = await mount(Component, { on: { click() {} } });
});
import { test } from '@playwright/experimental-ct-vue';
test('event', async ({ mount }) => {
const component = await mount(Component, { on: { click() {} } });
});
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('event', async ({ mount }) => {
const component = await mount(<Component v-on:click={() => {}} />);
});
children / slots
在掛載時為元件提供子元素/插槽。
- React
- Svelte
- Vue
import { test } from '@playwright/experimental-ct-react';
test('children', async ({ mount }) => {
const component = await mount(<Component>Child</Component>);
});
import { test } from '@playwright/experimental-ct-svelte';
test('slot', async ({ mount }) => {
const component = await mount(Component, { slots: { default: 'Slot' } });
});
import { test } from '@playwright/experimental-ct-vue';
test('slot', async ({ mount }) => {
const component = await mount(Component, { slots: { default: 'Slot' } });
});
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('children', async ({ mount }) => {
const component = await mount(<Component>Child</Component>);
});
hooks
您可以使用 beforeMount
和 afterMount
掛勾來設定您的應用程式。這讓您可以設定應用程式路由器、模擬伺服器等,為您提供所需的彈性。您也可以從測試中的 mount
呼叫傳遞自訂設定,這可以從 hooksConfig
佈置存取。這包含任何需要在掛載元件之前或之後執行的設定。以下提供設定路由器的範例:
- React
- Vue
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>;
});
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');
});
import { beforeMount, afterMount } from '@playwright/experimental-ct-vue/hooks';
import { router } from '../src/router';
export type HooksConfig = {
enableRouting?: boolean;
}
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
if (hooksConfig?.enableRouting)
app.use(router);
});
import { test, expect } from '@playwright/experimental-ct-vue';
import type { HooksConfig } from '../playwright';
import ProductsPage from './pages/ProductsPage.vue';
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');
});
unmount
從 DOM 卸載已掛載的元件。這對於測試元件卸載時的行為很有用。使用案例包括測試「您確定要離開嗎?」模態框,或確保事件處理程序正確清理以防止記憶體洩漏。
- React
- Svelte
- Vue
import { test } from '@playwright/experimental-ct-react';
test('unmount', async ({ mount }) => {
const component = await mount(<Component/>);
await component.unmount();
});
import { test } from '@playwright/experimental-ct-svelte';
test('unmount', async ({ mount }) => {
const component = await mount(Component);
await component.unmount();
});
import { test } from '@playwright/experimental-ct-vue';
test('unmount', async ({ mount }) => {
const component = await mount(Component);
await component.unmount();
});
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('unmount', async ({ mount }) => {
const component = await mount(<Component/>);
await component.unmount();
});
update
更新已掛載元件的 props、插槽/子元素 和/或 事件/回呼。這些元件輸入可以隨時變更,通常由父元件提供,但有時需要確保您的元件對新輸入的適當行為。
- React
- Svelte
- Vue
import { test } from '@playwright/experimental-ct-react';
test('update', async ({ mount }) => {
const component = await mount(<Component/>);
await component.update(
<Component msg="greetings" onClick={() => {}}>Child</Component>
);
});
import { test } from '@playwright/experimental-ct-svelte';
test('update', async ({ mount }) => {
const component = await mount(Component);
await component.update({
props: { msg: 'greetings' },
on: { click() {} },
slots: { default: 'Child' }
});
});
import { test } from '@playwright/experimental-ct-vue';
test('update', async ({ mount }) => {
const component = await mount(Component);
await component.update({
props: { msg: 'greetings' },
on: { click() {} },
slots: { default: 'Child' }
});
});
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('update', async ({ mount }) => {
const component = await mount(<Component/>);
await component.update(
<Component msg="greetings" v-on:click={() => {}}>Child</Component>
);
});
處理網路請求
Playwright 提供實驗性 router
佈置來攔截和處理網路請求。有兩種方式使用 router
佈置:
- 呼叫
router.route(url, handler)
,其行為類似於 page.route()。詳細資訊請參閱網路模擬指南。 - 呼叫
router.use(handlers)
並將 MSW 函式庫 請求處理程序傳遞給它。
以下是在測試中重複使用現有 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}
有什麼不同?
test('…', async ({ mount, page, context }) => {
// …
});
@playwright/experimental-ct-{react,svelte,vue}
包裝 @playwright/test
,提供額外的內建元件測試專用佈置 mount
:
- React
- Svelte
- Vue
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');
});
import { test, expect } from '@playwright/experimental-ct-vue';
import HelloWorld from './HelloWorld.vue';
test.use({ viewport: { width: 500, height: 500 } });
test('should work', async ({ mount }) => {
const component = await mount(HelloWorld, {
props: {
msg: 'Greetings',
},
});
await expect(component).toContainText('Greetings');
});
import { test, expect } from '@playwright/experimental-ct-svelte';
import HelloWorld from './HelloWorld.svelte';
test.use({ viewport: { width: 500, height: 500 } });
test('should work', async ({ mount }) => {
const component = await mount(HelloWorld, {
props: {
msg: 'Greetings',
},
});
await expect(component).toContainText('Greetings');
});
此外,它還增加了一些您可以在 playwright-ct.config.{ts,js}
中使用的設定選項。
最後,在底層,每個測試都會重複使用 context
和 page
佈置,作為元件測試的速度最佳化。它會在每個測試之間重置它們,因此在功能上應該等同於 @playwright/test
保證您在每個測試中獲得新的、隔離的 context
和 page
佈置。
我的專案已經使用 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'),
},
},
},
},
});
如何使用 CSS 匯入?
如果您有匯入 CSS 的元件,Vite 會自動處理。您也可以使用 CSS 前處理器,如 Sass、Less 或 Stylus,Vite 會在不需要任何額外設定的情況下處理它們。但是,需要安裝對應的 CSS 前處理器。
Vite 有一個嚴格要求,即所有 CSS 模組都必須命名為 *.module.[css extension]
。如果您的專案通常有自訂建置設定,並且有 import styles from 'styles.css'
形式的匯入,您必須重新命名檔案以正確表示它們將被視為模組。您也可以寫一個 Vite 外掛來為您處理這件事。
詳細資訊請查看 Vite 文件。
如何測試使用 Pinia 的元件?
Pinia 需要在 playwright/index.{js,ts,jsx,tsx}
中初始化。如果您在 beforeMount
掛勾中這樣做,initialState
可以在每個測試的基礎上被覆寫:
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')
},
});
});
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');
});
如何存取元件的方法或其實例?
在測試程式碼中存取元件的內部方法或其實例既不建議也不支援。相反,專注於從使用者的角度觀察元件並與之互動,通常是透過點擊或驗證頁面上是否可見某些內容。當測試避免與內部實作細節(如元件實例或其方法)互動時,測試會變得不那麼脆弱且更有價值。請記住,如果測試在從使用者角度執行時失敗,很可能意味著自動化測試發現了程式碼中的真正錯誤。