组件(实验)
介绍
¥Introduction
Playwright Test 现在可以测试你的组件。
¥Playwright Test can now test your components.
示例
¥Example
典型的组件测试如下所示:
¥Here is what a typical component test looks like:
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();
});
如何开始
¥How to get started
将 Playwright 测试添加到现有项目中很容易。以下是为 React、Vue 或 Svelte 项目启用 Playwright Test 的步骤。
¥Adding Playwright Test to an existing project is easy. Below are the steps to enable Playwright Test for a React, Vue or Svelte project.
步骤 1:为你各自的框架安装 Playwright Test 组件
¥Step 1: Install Playwright Test for components for your respective framework
- npm
- yarn
- pnpm
npm init playwright@latest -- --ct
yarn create playwright --ct
pnpm create playwright --ct
此步骤在你的工作区中创建多个文件:
¥This step creates several files in your workspace:
<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}
的脚本。
¥This file defines an html file that will be used to render components during testing. It must contain element with id="root"
, that's where components are mounted. It must also link the script called playwright/index.{js,ts,jsx,tsx}
.
你可以使用此脚本包含样式表、应用主题并将代码注入到安装组件的页面中。它可以是 .js
、.ts
、.jsx
或 .tsx
文件。
¥You can include stylesheets, apply theme and inject code into the page where component is mounted using this script. It can be either a .js
, .ts
, .jsx
or .tsx
file.
// Apply theme here, add anything your component needs at runtime here.
步骤 2.创建测试文件 src/App.spec.{ts,tsx}
¥Step 2. Create a test file 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
文件添加到你的项目中:
¥If using TypeScript and Vue make sure to add a vue.d.ts
file to your project:
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.运行测试
¥Step 3. Run the tests
你可以使用 VS Code 扩展 或命令行运行测试。
¥You can run tests using the VS Code extension or the command line.
npm run test-ct
进一步阅读:配置报告、浏览器、跟踪
¥Further reading: configure reporting, browsers, tracing
请参阅 Playwright 配置 配置你的项目。
¥Refer to Playwright config for configuring your project.
测试故事
¥Test stories
当 Playwright Test 用于测试 Web 组件时,测试在 Node.js 中运行,而组件在真实浏览器中运行。这汇集了两全其美的优点:组件在真实的浏览器环境中运行,触发真实的点击,执行真实的布局,视觉回归是可能的。同时,测试可以使用 Node.js 的所有功能以及所有 Playwright Test 功能。因此,在组件测试期间可以使用相同的事后跟踪故事进行相同的并行参数化测试。
¥When Playwright Test is used to test web components, tests run in Node.js, while components run in the real browser. This brings together the best of both worlds: components run in the real browser environment, real clicks are triggered, real layout is executed, visual regression is possible. At the same time, test can use all the powers of Node.js as well as all the Playwright Test features. As a result, the same parallel, parametrized tests with the same post-mortem Tracing story are available during component testing.
然而,这带来了一些限制:
¥This however, is introducing a number of limitations:
-
你无法将复杂的活动对象传递给你的组件。只能传递纯 JavaScript 对象和内置类型,如字符串、数字、日期等。
¥You can't pass complex live objects to your component. Only plain JavaScript objects and built-in types like strings, numbers, dates etc. can be passed.
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}/>);
});
-
你无法在回调中将数据同步传递到组件:
¥You can't pass data to your component synchronously in a callback:
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'}/>);
});
解决这些和其他限制是快速而优雅的:对于测试组件的每个用例,创建专门为测试设计的该组件的封装器。它不仅可以减轻限制,而且还可以为测试提供强大的抽象,你可以在其中定义组件渲染的环境、主题和其他方面。
¥Working around these and other limitations is quick and elegant: for every use case of the tested component, create a wrapper of this component designed specifically for test. Not only it will mitigate the limitations, but it will also offer powerful abstractions for testing where you would be able to define environment, theme and other aspects of your component rendering.
假设你想测试以下组件:
¥Let's say you'd like to test following component:
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;
}
为你的组件创建一个故事文件:
¥Create a story file for your component:
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.
然后通过测试故事来测试组件:
¥Then test the component via testing the story:
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');
});
因此,对于每个组件,你都会有一个故事文件,用于导出实际测试的所有故事。这些故事存在于浏览器中,并将 "convert" 复杂对象转换为可以在测试中访问的简单对象。
¥As a result, for every component you'll have a story file that exports all the stories that are actually tested. These stories live in the browser and "convert" complex object into the simple objects that can be accessed in the test.
在引擎盖下
¥Under the hood
以下是组件测试的工作原理:
¥Here is how component testing works:
-
执行测试后,Playwright 将创建测试所需的组件列表。
¥Once the tests are executed, Playwright creates a list of components that the tests need.
-
然后,它编译一个包含这些组件的包,并使用本地静态 Web 服务器为其提供服务。
¥It then compiles a bundle that includes these components and serves it using a local static web server.
-
在测试中调用
mount
时,Playwright 导航到该包的外观页面/playwright/index.html
并告诉它渲染组件。¥Upon the
mount
call within the test, Playwright navigates to the facade page/playwright/index.html
of this bundle and tells it to render the component. -
事件被编组回 Node.js 环境以进行验证。
¥Events are marshalled back to the Node.js environment to allow verification.
Playwright 使用 Vite 创建组件包并为其提供服务。
¥Playwright is using Vite to create the components bundle and serve it.
API 参考
¥API reference
props
安装时向组件提供属性。
¥Provide props to a component when mounted.
- 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
安装时向组件提供回调/事件。
¥Provide callbacks/events to a component when mounted.
- 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
安装时向组件提供子项/插槽。
¥Provide children/slots to a component when mounted.
- 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
夹具访问该配置。这包括安装组件之前或之后需要运行的任何配置。下面提供了配置路由的示例:
¥You can use beforeMount
and afterMount
hooks to configure your app. This lets you set up things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the mount
call from a test, which is accessible from the hooksConfig
fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below:
- 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 中卸载已安装的组件。这对于测试卸载时组件的行为很有用。用例包括测试 "你确定要离开吗?" 模式或确保正确清理事件处理程序以防止内存泄漏。
¥Unmount the mounted component from the DOM. This is useful for testing the component's behavior upon unmounting. Use cases include testing an "Are you sure you want to leave?" modal or ensuring proper cleanup of event handlers to prevent memory leaks.
- 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
更新已安装组件的属性、插槽/子项和/或事件/回调。这些组件输入可以随时更改,通常由父组件提供,但有时需要确保你的组件对新输入做出适当的行为。
¥Update props, slots/children, and/or events/callbacks of a mounted component. These component inputs can change at any time and are typically provided by the parent component, but sometimes it is necessary to ensure that your components behave appropriately to new inputs.
- 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>
);
});
处理网络请求
¥Handling network requests
Playwright 提供了一个实验性的 router
装置来拦截和处理网络请求。有两种使用 router
装置的方法:
¥Playwright provides an experimental router
fixture to intercept and handle network requests. There are two ways to use the router
fixture:
-
调用行为类似于 page.route() 的
router.route(url, handler)
。有关更多详细信息,请参阅 网络模拟指南。¥Call
router.route(url, handler)
that behaves similarly to page.route(). See the network mocking guide for more details. -
调用
router.use(handlers)
并将 MSW 库 请求处理程序传递给它。¥Call
router.use(handlers)
and pass MSW library request handlers to it.
以下是在测试中重用现有 MSW 处理程序的示例。
¥Here is an example of reusing your existing MSW handlers in the test.
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
// ...
});
你还可以为特定测试引入一次性处理程序。
¥You can also introduce a one-off handler for a specific test.
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
// ...
});
常见问题
¥Frequently asked questions
@playwright/test
和 @playwright/experimental-ct-{react,svelte,vue}
有什么区别?
¥What's the difference between @playwright/test
and @playwright/experimental-ct-{react,svelte,vue}
?
test('…', async ({ mount, page, context }) => {
// …
});
@playwright/experimental-ct-{react,svelte,vue}
封装 @playwright/test
以提供额外的内置组件测试特定夹具,称为 mount
:
¥@playwright/experimental-ct-{react,svelte,vue}
wrap @playwright/test
to provide an additional built-in component-testing specific fixture called 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}
中使用的配置选项。
¥Additionally, it adds some config options you can use in your playwright-ct.config.{ts,js}
.
最后,在幕后,每个测试都重新使用 context
和 page
夹具作为组件测试的速度优化。它会在每次测试之间重置它们,因此它在功能上应该等同于 @playwright/test
的保证,即每次测试你都会获得一个新的、独立的 context
和 page
夹具。
¥Finally, under the hood, each test re-uses the context
and page
fixture as a speed optimization for Component Testing. It resets them in between each test so it should be functionally equivalent to @playwright/test
's guarantee that you get a new, isolated context
and page
fixture per-test.
我有一个项目已经使用了 Vite。我可以重复使用该配置吗?
¥I have a project that already uses Vite. Can I reuse the config?
此时,Playwright 与打包程序无关,因此它不会重用你现有的 Vite 配置。你的配置可能有很多我们无法重用的东西。因此,现在,你可以将路径映射和其他高级设置复制到 Playwright 配置的 ctViteConfig
属性中。
¥At this point, Playwright is bundler-agnostic, so it is not reusing your existing Vite config. Your config might have a lot of things we won't be able to reuse. So for now, you would copy your path mappings and other high level settings into the ctViteConfig
property of Playwright config.
import { defineConfig } from '@playwright/experimental-ct-react';
export default defineConfig({
use: {
ctViteConfig: {
// ...
},
},
});
你可以通过 Vite 配置指定插件来进行测试设置。请注意,一旦开始指定插件,你还负责指定框架插件,在本例中为 vue()
:
¥You can specify plugins via Vite config for testing settings. Note that once you start specifying plugins, you are responsible for specifying the framework plugin as well, vue()
in this case:
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'),
},
},
},
},
});
如何测试使用 Pinia 的组件?
¥How can I test components that uses Pinia?
Pinia 需要在 playwright/index.{js,ts,jsx,tsx}
中初始化。如果你在 beforeMount
钩子内执行此操作,则可以在每次测试的基础上覆盖 initialState
:
¥Pinia needs to be initialized in playwright/index.{js,ts,jsx,tsx}
. If you do this inside a beforeMount
hook, the initialState
can be overwritten on a per-test basis:
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.nodejs.cn/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');
});
如何访问组件的方法或其实例?
¥How do I access the component's methods or its instance?
不建议也不支持在测试代码中访问组件的内部方法或其实例。相反,专注于从用户的角度观察和与组件交互,通常通过单击或验证页面上是否可见某些内容。当测试避免与内部实现细节(例如组件实例或其方法)交互时,它们会变得更不脆弱且更有价值。请记住,如果从用户的角度运行测试失败,则可能意味着自动化测试发现了代码中的真正错误。
¥Accessing a component's internal methods or its instance within test code is neither recommended nor supported. Instead, focus on observing and interacting with the component from a user's perspective, typically by clicking or verifying if something is visible on the page. Tests become less fragile and more valuable when they avoid interacting with internal implementation details, such as the component instance or its methods. Keep in mind that if a test fails when run from a user’s perspective, it likely means the automated test has uncovered a genuine bug in your code.