メインコンテンツへスキップ

コンポーネント (実験的)

はじめに

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 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}` を作成する

app.spec.tsx
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');
});

ステップ3. テストを実行する

VS Code拡張機能またはコマンドラインを使用してテストを実行できます。

npm run test-ct

さらに読む: レポート、ブラウザ、トレースの構成

プロジェクトの構成については、Playwright設定を参照してください。

テストストーリー

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.spec.tsx
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を提供します。

component.spec.tsx
import { test } from '@playwright/experimental-ct-react';

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

コールバック / イベント

コンポーネントがマウントされるときにコールバック/イベントを提供します。

component.spec.tsx
import { test } from '@playwright/experimental-ct-react';

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

children / slots

コンポーネントがマウントされるときにchildren/slotsを提供します。

component.spec.tsx
import { test } from '@playwright/experimental-ct-react';

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

hooks

アプリを設定するには、`beforeMount`および`afterMount`フックを使用できます。これにより、アプリルーターやフェイクサーバーなどをセットアップでき、必要な柔軟性が得られます。また、テストから`mount`呼び出しでカスタム設定を渡すこともでき、これは`hooksConfig`フィクスチャからアクセスできます。これには、コンポーネントをマウントする前後に実行する必要があるすべての設定が含まれます。ルーターを設定する例を以下に示します。

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');
});

unmount

マウントされたコンポーネントをDOMからアンマウントします。これは、アンマウント時のコンポーネントの動作をテストするのに役立ちます。「本当に終了しますか?」のようなモーダルをテストしたり、メモリリークを防ぐためにイベントハンドラーの適切なクリーンアップを保証したりするユースケースがあります。

component.spec.tsx
import { test } from '@playwright/experimental-ct-react';

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

update

マウントされたコンポーネントのprops、slots/children、および/またはイベント/コールバックを更新します。これらのコンポーネントの入力はいつでも変更される可能性があり、通常は親コンポーネントによって提供されますが、新しい入力に対してコンポーネントが適切に動作することを確認する必要がある場合があります。

component.spec.tsx
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>
);
});

ネットワークリクエストの処理

Playwrightは、ネットワークリクエストをインターセプトして処理するための**実験的な**`router`フィクスチャを提供します。`router`フィクスチャを使用する方法は2つあります。

テストで既存の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`と呼ばれる追加の組み込みコンポーネントテスト固有のフィクスチャを提供します。

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}`で使用できるいくつかの設定オプションが追加されます。

最後に、内部では、各テストはコンポーネントテストの速度最適化として`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`をテストごとに上書きできます。

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.dokyumento.jp/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');
});

コンポーネントのメソッドやインスタンスにアクセスするにはどうすればよいですか?

テストコード内でコンポーネントの内部メソッドやインスタンスにアクセスすることは、推奨もサポートもされていません。代わりに、ユーザーの視点からコンポーネントを観察し、操作することに焦点を当ててください。通常、クリックしたり、ページに何かが表示されているかどうかを確認したりします。コンポーネントインスタンスやそのメソッドなどの内部実装の詳細との相互作用を避けることで、テストは壊れにくくなり、より価値のあるものになります。ユーザーの視点から実行されたときにテストが失敗した場合、それは自動テストがコード内の真のバグを発見した可能性が高いことを覚えておいてください。