コンポーネント(実験的)
はじめに
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 を使用して 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'}/>);
});
これらの制限やその他の制限を回避するのは迅速かつエレガントです。テスト対象のコンポーネントのユースケースごとに、テスト専用に設計されたこのコンポーネントのラッパーを作成します。これにより、制限が軽減されるだけでなく、コンポーネントのレンダリングの環境、テーマ、その他の側面を定義できる強力なテストの抽象化も提供されます。
次のコンポーネントをテストしたいとしましょう。
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 はテストに必要なコンポーネントのリストを作成します。
- 次に、これらのコンポーネントを含むバンドルをコンパイルし、ローカル静的 Web サーバーを使用して提供します。
- テスト内の
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
マウント時にコンポーネントに 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
- Vue3
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、slots/children、および/または events/callbacks を更新します。これらのコンポーネント入力はいつでも変更でき、通常は親コンポーネントによって提供されますが、コンポーネントが新しい入力に対して適切に動作することを保証する必要がある場合があります。
- 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
フィクスチャを使用するには、2 つの方法があります。
- page.route() と同様に動作する
router.route(url, handler)
を呼び出します。詳細については、ネットワークモックガイドを参照してください。 - MSW ライブラリのリクエストハンドラーを渡して
router.use(handlers)
を呼び出します。
以下は、既存の 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
// ...
});
特定のテスト用に 1 回限りのハンドラーを導入することもできます。
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
フィクスチャを再利用します。各テスト間でリセットされるため、テストごとに新しい分離された context
および page
フィクスチャを取得するという @playwright/test
の保証と同等の機能を持つはずです。
すでに 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 が自動的に処理します。Sass、Less、Stylus などの CSS プリプロセッサも使用でき、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.dokyumento.jp/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');
});
コンポーネントのメソッドまたはそのインスタンスにアクセスするにはどうすればよいですか?
テストコード内からコンポーネントの内部メソッドまたはそのインスタンスにアクセスすることは、推奨もサポートもされていません。代わりに、通常はクリックしたり、ページに何かが表示されているかどうかを確認したりすることにより、ユーザーの視点からコンポーネントを観察および操作することに焦点を当てます。テストがコンポーネントインスタンスやそのメソッドなどの内部実装の詳細とのやり取りを避ける場合、テストは脆弱性が低くなり、より価値のあるものになります。ユーザーの視点から実行したときにテストが失敗した場合、それは自動テストがコード内の真のバグを発見した可能性が高いことを意味することに注意してください。