跳转到主要内容

组件(实验性)

简介

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 会导航到该包的 facade 页面 /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

您可以使用 beforeMountafterMount 钩子来配置您的应用程序。这允许您设置应用程序路由、模拟服务器等,为您提供所需的灵活性。您还可以从测试中的 mount 调用传递自定义配置,该配置可以从 hooksConfig fixture 中访问。这包括在挂载组件之前或之后需要运行的任何配置。下面提供了一个配置路由的示例

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 fixture 来拦截和处理网络请求。有两种方法可以使用 router fixture:

  • 调用 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,提供了一个附加的内置组件测试特定 fixture,称为 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} 中使用的配置选项。

最后,在幕后,每个测试都重复使用了 contextpage fixture 来优化组件测试的速度。它在每个测试之间重置它们,因此在功能上等同于 @playwright/test 保证每个测试都能获得一个新的、独立的 contextpage fixture。

我有一个已经使用 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 imports?

如果您的组件导入 CSS,Vite 将自动处理。您还可以使用 Sass、Less 或 Stylus 等 CSS 预处理器,Vite 也会在无需任何额外配置的情况下处理它们。但是,需要安装相应的 CSS 预处理器。

Vite 对所有 CSS Modules 的命名都有硬性要求,必须命名为 *.module.[css 扩展名]。如果您的项目通常有一个自定义构建配置,并且导入格式为 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.net.cn/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');
});

如何访问组件的方法或实例?

在测试代码中访问组件的内部方法或实例既不推荐也不受支持。相反,应专注于从用户的角度观察和与组件交互,通常通过点击或验证页面上是否有可见内容。当测试避免与内部实现细节(如组件实例或其方法)交互时,测试会变得更健壮且更有价值。请记住,如果用户视角的测试失败,则很可能表明自动化测试已发现您代码中的真正错误。