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

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

はじめに

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 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.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

マウント時にコンポーネントにプロパティを提供します。

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

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

callbacks / events

マウント時にコンポーネントにコールバック/イベントを提供します。

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

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

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

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

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` フィクスチャをコンポーネントテストの速度最適化として再利用します。各テスト間でそれらをリセットするため、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 が自動的に処理します。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');
});

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

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