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

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

はじめに

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

マウント時にコンポーネントに 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

マウント時にコンポーネントに 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、および/または events/callbacks を更新します。これらのコンポーネント入力はいつでも変更でき、通常は親コンポーネントによって提供されますが、コンポーネントが新しい入力に対して適切に動作することを保証する必要がある場合があります。

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

特定のテスト用に 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 という追加の組み込みコンポーネントテスト固有のフィクスチャを提供します。

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

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

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