夹具
介绍
🌐 Introduction
Playwright 测试基于测试夹具的概念。测试夹具用于为每个测试建立环境,为测试提供其所需的全部内容而不多余。测试夹具在测试之间是隔离的。有了夹具,你可以根据测试的意义而不是其共同的设置来对测试进行分组。
🌐 Playwright Test is based on the concept of test fixtures. Test fixtures are used to establish the environment for each test, giving the test everything it needs and nothing else. Test fixtures are isolated between tests. With fixtures, you can group tests based on their meaning, instead of their common setup.
内置夹具
🌐 Built-in fixtures
你已经在第一次测试中使用了测试装置。
🌐 You have already used test fixtures in your first test.
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.nodejs.cn/');
await expect(page).toHaveTitle(/Playwright/);
});
{ page } 参数告诉 Playwright 测试设置 page 夹具,并将其提供给你的测试函数。
🌐 The { page } argument tells Playwright Test to set up the page fixture and provide it to your test function.
以下是你大部分时间可能使用的预定义装置的列表:
🌐 Here is a list of the pre-defined fixtures that you are likely to use most of the time:
| 夹具 | 类型 | 描述 |
|---|---|---|
| page | [页面] | 此测试运行的独立页面。 |
| context | [浏览器上下文] | 此测试运行的独立上下文。page 夹具也属于此上下文。了解如何配置上下文。 |
| browser | [浏览器] | 浏览器在测试之间共享以优化资源。了解如何配置浏览器。 |
| browserName | [字符串] | 当前运行测试的浏览器名称。可以是 chromium、firefox 或 webkit。 |
| request | [API请求上下文] | 此测试运行的独立 APIRequestContext 实例。 |
不使用夹具
🌐 Without fixtures
以下是传统测试风格和基于 Fixture 的测试环境设置之间的区别。
🌐 Here is how a typical test environment setup differs between the traditional test style and the fixture-based one.
TodoPage 是一个类,帮助我们与 Web 应用的“待办事项”页面交互,遵循 页面对象模型 模式。它在内部使用了 Playwright 的 page。
点击展开 TodoPage 的代码
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
const { test } = require('@playwright/test');
const { TodoPage } = require('./todo-page');
test.describe('todo tests', () => {
let todoPage;
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
});
test.afterEach(async () => {
await todoPage.removeAll();
});
test('should add an item', async () => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async () => {
await todoPage.remove('item1');
// ...
});
});
使用夹具
🌐 With fixtures
与前/后钩子相比,夹具具有许多优点:
🌐 Fixtures have a number of advantages over before/after hooks:
- 测试夹具(Fixtures)封装了设置和拆卸的过程在同一个地方,因此更容易编写。所以如果你有一个在前置钩子(before hook)中创建内容的后置钩子(after hook)来进行拆卸,考虑将它们转换成一个测试夹具。
- 测试夹具可以在多个测试文件之间重复使用——你可以只定义一次,然后在所有测试中使用。这就是 Playwright 内置的
page夹具的工作原理。因此,如果你有一个在多个测试中使用的辅助函数,可以考虑将其转换为夹具。 - Fixtures 是按需的——你可以定义任意数量的 fixtures,Playwright Test 只会设置测试所需的 fixtures,而不会设置其他的。
- 夹具是可组合的——它们可以相互依赖以提供复杂的行为。
- 测试夹具是灵活的。测试可以使用任意组合的夹具来精确地定制环境,以满足它们的需求,而不会影响其他测试。
- 夹具简化了分组。你不再需要将测试封装在设置其环境的
describe中,而可以根据测试的意义自由地对其进行分组。
点击展开 TodoPage 的代码
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Extend basic test by providing a "todoPage" fixture.
const test = base.extend<{ todoPage: TodoPage }>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
await use(todoPage);
await todoPage.removeAll();
},
});
test('should add an item', async ({ todoPage }) => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async ({ todoPage }) => {
await todoPage.remove('item1');
// ...
});
创建夹具
🌐 Creating a fixture
要创建你自己的夹具,请使用 test.extend() 来创建一个包含它的新 test 对象。
🌐 To create your own fixture, use test.extend() to create a new test object that will include it.
下面我们创建了两个夹具 todoPage 和 settingsPage,它们遵循 页面对象模型 模式。
🌐 Below we create two fixtures todoPage and settingsPage that follow the Page Object Model pattern.
点击展开 TodoPage 和 SettingsPage 的代码
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
设置页面类似:
🌐 SettingsPage is similar:
import type { Page } from '@playwright/test';
export class SettingsPage {
constructor(public readonly page: Page) {
}
async switchToDarkMode() {
// ...
}
}
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
// Declare the types of your fixtures.
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
export { expect } from '@playwright/test';
自定义夹具名称应以字母或下划线开头,并且只能包含字母、数字和下划线。
使用夹具
🌐 Using a fixture
只需在测试函数的参数中提及一个 fixture,测试运行器就会处理它。Fixture 同样可以在 hook 和其他 fixture 中使用。如果你使用 TypeScript,fixture 将是类型安全的。
🌐 Just mention a fixture in your test function argument, and the test runner will take care of it. Fixtures are also available in hooks and other fixtures. If you use TypeScript, fixtures will be type safe.
下面我们使用上面定义的 todoPage 和 settingsPage 夹具。
🌐 Below we use the todoPage and settingsPage fixtures that we defined above.
import { test, expect } from './my-test';
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.getByTestId('todo-title')).toContainText(['something nice']);
});
压倒性的装置
🌐 Overriding fixtures
除了创建你自己的测试夹具外,你还可以覆盖现有的测试夹具以满足你的需求。考虑以下示例,它通过自动导航到 baseURL 来覆盖 page 测试夹具:
🌐 In addition to creating your own fixtures, you can also override existing fixtures to fit your needs. Consider the following example which overrides the page fixture by automatically navigating to the baseURL:
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
await page.goto(baseURL);
await use(page);
},
});
请注意,在此示例中,page 夹具能够依赖其他内置夹具,例如 testOptions.baseURL。我们现在可以在配置文件中配置 baseURL,或者在测试文件中使用 test.use() 本地配置。
🌐 Notice that in this example, the page fixture is able to depend on other built-in fixtures such as testOptions.baseURL. We can now configure baseURL in the configuration file, or locally in the test file with test.use().
test.use({ baseURL: 'https://playwright.nodejs.cn' });
夹具也可以被重写,从而导致基础夹具被完全替换为其他东西。例如,我们可以重写 testOptions.storageState 夹具来提供我们自己的数据。
🌐 Fixtures can also be overridden, causing the base fixture to be completely replaced with something different. For example, we could override the testOptions.storageState fixture to provide our own data.
import { test as base } from '@playwright/test';
export const test = base.extend({
storageState: async ({}, use) => {
const cookie = await getAuthCookie();
await use({ cookies: [cookie] });
},
});
工作进程范围内的夹具
🌐 Worker-scoped fixtures
Playwright Test 使用工作进程来运行测试文件。类似于为单个测试运行设置测试夹具,工作进程夹具是为每个工作进程设置的。在这里,你可以设置服务、运行服务器等。只要它们的工作进程夹具匹配,从而环境相同,Playwright Test 就会尽可能重用工作进程来运行多个测试文件。
🌐 Playwright Test uses worker processes to run test files. Similar to how test fixtures are set up for individual test runs, worker fixtures are set up for each worker process. That's where you can set up services, run servers, etc. Playwright Test will reuse the worker process for as many test files as it can, provided their worker fixtures match and hence environments are identical.
下面我们将创建一个 account 夹具,该夹具将在同一工作进程中的所有测试中共享,并重写 page 夹具,以便每个测试都登录到此账户。为了生成唯一的账户,我们将使用任何测试或夹具都可用的 workerInfo.workerIndex。注意工作进程夹具的类似元组的语法——我们必须传入 {scope: 'worker'},这样测试运行器才能为每个工作进程只设置一次该夹具。
🌐 Below we'll create an account fixture that will be shared by all tests in the same worker, and override the page fixture to log in to this account for each test. To generate unique accounts, we'll use the workerInfo.workerIndex that is available to any test or fixture. Note the tuple-like syntax for the worker fixture - we have to pass {scope: 'worker'} so that test runner sets this fixture up once per worker.
除了每个工作进程只运行一次之外,工作进程范围的 fixture 还会获得一个等于默认测试超时的单独超时。你可以通过传递 timeout 选项来更改它。有关更多详情,请参见 fixture 超时。
🌐 In addition to only being run once per worker, worker-scoped fixtures also get a separate timeout equal to the default test timeout. You can change it by passing the timeout option. See fixture timeout for more details.
import { test as base } from '@playwright/test';
type Account = {
username: string;
password: string;
};
// Note that we pass worker fixture types as a second template parameter.
export const test = base.extend<{}, { account: Account }>({
account: [async ({ browser }, use, workerInfo) => {
// Unique username.
const username = 'user' + workerInfo.workerIndex;
const password = 'verysecure';
// Create the account with Playwright.
const page = await browser.newPage();
await page.goto('/signup');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign up').click();
// Make sure everything is ok.
await expect(page.getByTestId('result')).toHaveText('Success');
// Do not forget to cleanup.
await page.close();
// Use the account value.
await use({ username, password });
}, { scope: 'worker' }],
page: async ({ page, account }, use) => {
// Sign in with our account.
const { username, password } = account;
await page.goto('/signin');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign in').click();
await expect(page.getByTestId('userinfo')).toHaveText(username);
// Use signed-in page in the test.
await use(page);
},
});
export { expect } from '@playwright/test';
自动夹具
🌐 Automatic fixtures
每个测试/工作进程都会设置自动夹具,即使测试没有直接列出。要创建自动灯具,使用元组语法并传递 { auto: true }。
🌐 Automatic fixtures are set up for each test/worker, even when the test does not list them directly. To create an automatic fixture, use the tuple syntax and pass { auto: true }.
这里是一个示例夹具,当测试失败时会自动附加调试日志,这样我们可以在报告中稍后查看这些日志。注意它如何使用在每个测试/夹具中可用的 TestInfo 对象来获取正在运行的测试的元数据。
🌐 Here is an example fixture that automatically attaches debug logs when the test fails, so we can later review the logs in the reporter. Note how it uses the TestInfo object that is available in each test/fixture to retrieve metadata about the test being run.
import debug from 'debug';
import fs from 'fs';
import { test as base } from '@playwright/test';
export const test = base.extend<{ saveLogs: void }>({
saveLogs: [async ({}, use, testInfo) => {
// Collecting logs during the test.
const logs = [];
debug.log = (...args) => logs.push(args.map(String).join(''));
debug.enable('myserver');
await use();
// After the test we can check whether the test passed or failed.
if (testInfo.status !== testInfo.expectedStatus) {
// outputPath() API guarantees a unique file name.
const logFile = testInfo.outputPath('logs.txt');
await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8');
testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile });
}
}, { auto: true }],
});
export { expect } from '@playwright/test';
夹具超时
🌐 Fixture timeout
夹具被视为测试的一部分,因此其设置和拆卸运行时间计入测试超时。因此,较慢的夹具可能会导致测试超时。你可以为这样的夹具设置单独的较长超时,同时保持整体测试超时较短。
🌐 Fixture is considered to be a part of a test, and so its setup and teardown running time counts towards the test timeout. Therefore, a slow fixture may cause test timeouts. You can set a separate larger timeout for such a fixture, and keep the overall test timeout small.
import { test as base, expect } from '@playwright/test';
const test = base.extend<{ slowFixture: string }>({
slowFixture: [async ({}, use) => {
// ... perform a slow operation ...
await use('hello');
}, { timeout: 60000 }]
});
test('example test', async ({ slowFixture }) => {
// ...
});
与常规的测试作用域夹具不同,每个工作进程作用域夹具都有自己的超时时间,与测试超时相同。你可以以相同的方式更改工作进程作用域夹具的超时时间。
🌐 Unlike regular test-scoped fixtures, each worker-scoped fixture has its own timeout, equal to the test timeout. You can change the timeout for a worker-scoped fixture in the same way.
夹具选项
🌐 Fixtures-options
Playwright 测试支持运行多个可以单独配置的测试项目。你可以使用“option”夹具使你的配置选项具有声明性和类型安全性。了解更多关于参数化测试的信息。
🌐 Playwright Test supports running multiple test projects that can be configured separately. You can use "option" fixtures to make your configuration options declarative and type safe. Learn more about parameterizing tests.
下面我们将创建一个 defaultItem 选项,作为其他示例中的 todoPage 夹具之外的补充。此选项将设置在配置文件中。注意元组语法和 { option: true } 参数。
🌐 Below we'll create a defaultItem option in addition to the todoPage fixture from other examples. This option will be set in the configuration file. Note the tuple syntax and { option: true } argument.
点击展开 TodoPage 的代码
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Declare your options to type-check your configuration.
export type MyOptions = {
defaultItem: string;
};
type MyFixtures = {
todoPage: TodoPage;
};
// Specify both option and fixture types.
export const test = base.extend<MyOptions & MyFixtures>({
// Define an option and provide a default value.
// We can later override it in the config.
defaultItem: ['Something nice', { option: true }],
// Our "todoPage" fixture depends on the option.
todoPage: async ({ page, defaultItem }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo(defaultItem);
await use(todoPage);
await todoPage.removeAll();
},
});
export { expect } from '@playwright/test';
我们现在可以像往常一样使用 todoPage 夹具,并在配置文件中设置 defaultItem 选项。
🌐 We can now use the todoPage fixture as usual, and set the defaultItem option in the configuration file.
import { defineConfig } from '@playwright/test';
import type { MyOptions } from './my-test';
export default defineConfig<MyOptions>({
projects: [
{
name: 'shopping',
use: { defaultItem: 'Buy milk' },
},
{
name: 'wellbeing',
use: { defaultItem: 'Exercise!' },
},
]
});
数组作为选项值
如果你的选项值是一个数组,例如 [{ name: 'Alice' }, { name: 'Bob' }],在提供该值时,你需要将其封装在一个额外的数组中。通过一个例子可以更好地说明这一点。
🌐 If the value of your option is an array, for example [{ name: 'Alice' }, { name: 'Bob' }], you'll need to wrap it into an extra array when providing the value. This is best illustrated with an example.
type Person = { name: string };
const test = base.extend<{ persons: Person[] }>({
// Declare the option, default value is an empty array.
persons: [[], { option: true }],
});
// Option value is an array of persons.
const actualPersons = [{ name: 'Alice' }, { name: 'Bob' }];
test.use({
// CORRECT: Wrap the value into an array and pass the scope.
persons: [actualPersons, { scope: 'test' }],
});
test.use({
// WRONG: passing an array value directly will not work.
persons: actualPersons,
});
重置选项
你可以通过将选项设置为 undefined 来将其重置为配置文件中定义的值。请考虑以下设置了 baseURL 的配置示例:
🌐 You can reset an option to the value defined in the config file by setting it to undefined. Consider the following config that sets a baseURL:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://playwright.nodejs.cn',
},
});
你现在可以为一个文件配置 baseURL,也可以选择对单个测试退出。
🌐 You can now configure baseURL for a file, and also opt-out for a single test.
import { test } from '@playwright/test';
// Configure baseURL for this file.
test.use({ baseURL: 'https://playwright.nodejs.cn/docs/intro' });
test('check intro contents', async ({ page }) => {
// This test will use "https://playwright.nodejs.cn/docs/intro" base url as defined above.
});
test.describe(() => {
// Reset the value to a config-defined one.
test.use({ baseURL: undefined });
test('can navigate to intro from the home page', async ({ page }) => {
// This test will use "https://playwright.nodejs.cn" base url as defined in the config.
});
});
如果你想将值完全重置为 undefined,请使用长格式夹具表示法。
🌐 If you would like to completely reset the value to undefined, use a long-form fixture notation.
import { test } from '@playwright/test';
// Completely unset baseURL for this file.
test.use({
baseURL: [async ({}, use) => use(undefined), { scope: 'test' }],
});
test('no base url', async ({ page }) => {
// This test will not have a base url.
});
执行顺序
🌐 Execution order
每个夹具在夹具中的 await use() 调用前后都有设置和拆卸阶段。设置在运行需要它的测试/钩子之前执行,而拆卸在测试/钩子不再使用该夹具时执行。
🌐 Each fixture has a setup and teardown phase before and after the await use() call in the fixture. Setup is executed before the test/hook requiring it is run, and teardown is executed when the fixture is no longer being used by the test/hook.
夹具遵循以下规则来确定执行顺序:
🌐 Fixtures follow these rules to determine the execution order:
- 当夹具 A 依赖于夹具 B 时:B 总是在 A 之前设置,并在 A 之后拆除。
- 非自动夹具仅在测试/钩子需要时才会延迟执行。
- 测试范围的 Fixture 会在每次测试结束后被拆除,而 Worker 范围的 Fixture 仅在执行测试的工作进程被拆除时被拆除。
考虑以下示例:
🌐 Consider the following example:
import { test as base } from '@playwright/test';
const test = base.extend<{
testFixture: string,
autoTestFixture: string,
unusedFixture: string,
}, {
workerFixture: string,
autoWorkerFixture: string,
}>({
workerFixture: [async ({ browser }) => {
// workerFixture setup...
await use('workerFixture');
// workerFixture teardown...
}, { scope: 'worker' }],
autoWorkerFixture: [async ({ browser }) => {
// autoWorkerFixture setup...
await use('autoWorkerFixture');
// autoWorkerFixture teardown...
}, { scope: 'worker', auto: true }],
testFixture: [async ({ page, workerFixture }) => {
// testFixture setup...
await use('testFixture');
// testFixture teardown...
}, { scope: 'test' }],
autoTestFixture: [async () => {
// autoTestFixture setup...
await use('autoTestFixture');
// autoTestFixture teardown...
}, { scope: 'test', auto: true }],
unusedFixture: [async ({ page }) => {
// unusedFixture setup...
await use('unusedFixture');
// unusedFixture teardown...
}, { scope: 'test' }],
});
test.beforeAll(async () => { /* ... */ });
test.beforeEach(async ({ page }) => { /* ... */ });
test('first test', async ({ page }) => { /* ... */ });
test('second test', async ({ testFixture }) => { /* ... */ });
test.afterEach(async () => { /* ... */ });
test.afterAll(async () => { /* ... */ });
通常,如果所有测试都通过并且没有抛出错误,则执行顺序如下。
🌐 Normally, if all tests pass and no errors are thrown, the order of execution is as following.
- 工作设置和
beforeAll部分:browser安装是必需的,因为autoWorkerFixture需要它。autoWorkerFixture设置,因为自动工作夹具总是在其他任何东西之前设置。beforeAll运行。
first test部分:autoTestFixture设置,因为自动测试夹具总是在测试和beforeEach钩子之前设置。page设置,因为它在beforeEach钩子中是必需的。beforeEach运行。first test运行。afterEach运行。page被拆除,因为它是一个测试范围的夹具,应该在测试完成后拆除。autoTestFixture被拆除,因为它是一个测试范围的夹具,应该在测试完成后拆除。
second test部分:autoTestFixture设置,因为自动测试夹具总是在测试和beforeEach钩子之前设置。page设置,因为它在beforeEach钩子中是必需的。beforeEach运行。workerFixture的设置,因为它是testFixture所必需的,而testFixture又是second test所必需的。testFixture的设置,因为second test需要它。second test运行。afterEach运行。testFixture被拆除,因为它是一个测试范围的夹具,应该在测试完成后拆除。page被拆除,因为它是一个测试范围的夹具,应该在测试完成后拆除。autoTestFixture被拆除,因为它是一个测试范围的夹具,应该在测试完成后拆除。
afterAll和工作进程拆解部分:afterAll运行。workerFixture的清理操作因为它是一个以工作者为作用域的夹具,应该在结束时只清理一次。autoWorkerFixture的清理操作因为它是一个以工作者为作用域的夹具,应该在结束时只清理一次。browser的清理操作因为它是一个以工作者为作用域的夹具,应该在结束时只清理一次。
一些观察:
🌐 A few observations:
page和autoTestFixture会为每个测试进行设置和拆卸,作为测试范围的夹具。unusedFixture从未被设置,因为它没有被任何测试/钩子使用。testFixture依赖于workerFixture并触发其设置。workerFixture在第二个测试之前懒加载设置,但作为工作器级别的夹具,在工作器关闭时只拆除一次。autoWorkerFixture已为beforeAll钩子设置,但autoTestFixture尚未设置。
组合来自多个模块的定制夹具
🌐 Combine custom fixtures from multiple modules
你可以合并来自多个文件或模块的测试装置:
🌐 You can merge test fixtures from multiple files or modules:
import { mergeTests } from '@playwright/test';
import { test as dbTest } from 'database-test-utils';
import { test as a11yTest } from 'a11y-test-utils';
export const test = mergeTests(dbTest, a11yTest);
import { test } from './fixtures';
test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});
盒子装置
🌐 Box fixtures
通常,自定义夹具会在 UI 模式、Trace Viewer 和各种测试报告中被作为单独的步骤报告。它们也会出现在测试运行器的错误信息中。对于经常使用的夹具,这可能会产生大量噪音。你可以通过“打包”夹具来阻止它们的步骤在 UI 中显示。
🌐 Usually, custom fixtures are reported as separate steps in the UI mode, Trace Viewer and various test reports. They also appear in error messages from the test runner. For frequently used fixtures, this can mean lots of noise. You can stop the fixtures steps from being shown in the UI by "boxing" it.
import { test as base } from '@playwright/test';
export const test = base.extend({
helperFixture: [async ({}, use, testInfo) => {
// ...
}, { box: true }],
});
这对于不重要的辅助夹具很有用。例如,一个自动化夹具可以用来设置一些常见数据,并且可以安全地在测试报告中隐藏起来。
🌐 This is useful for non-interesting helper fixtures. For example, an automatic fixture that sets up some common data can be safely hidden from a test report.
你也可以将夹具标记为 box: 'self',只隐藏该特定夹具,但在测试报告中包含夹具内的所有步骤。
🌐 You can also mark the fixture as box: 'self' to only hide that particular fixture, but include all the steps inside the fixture in the test report.
自定义装置标题
🌐 Custom fixture title
除了通常的装置名称外,你还可以为装置指定一个自定义标题,该标题将显示在测试报告和错误消息中。
🌐 Instead of the usual fixture name, you can give fixtures a custom title that will be shown in test reports and error messages.
import { test as base } from '@playwright/test';
export const test = base.extend({
innerFixture: [async ({}, use, testInfo) => {
// ...
}, { title: 'my fixture' }],
});
添加全局 beforeEach/afterEach 钩子
🌐 Adding global beforeEach/afterEach hooks
test.beforeEach() 和 test.afterEach() 钩子会在同一文件和同一 test.describe() 块(如果有的话)中声明的每个测试之前/之后运行。如果你想声明在全局范围内每个测试之前/之后运行的钩子,可以将它们声明为自动 fixture,如下所示:
import { test as base } from '@playwright/test';
export const test = base.extend<{ forEachTest: void }>({
forEachTest: [async ({ page }, use) => {
// This code runs before every test.
await page.goto('http://localhost:8000');
await use();
// This code runs after every test.
console.log('Last URL:', page.url());
}, { auto: true }], // automatically starts for every test.
});
然后在所有测试中导入这些装置:
🌐 And then import the fixtures in all your tests:
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ page }) => {
expect(page).toHaveURL('http://localhost:8000');
await page.goto('https://playwright.nodejs.cn');
});
添加全局 beforeAll/afterAll 钩子
🌐 Adding global beforeAll/afterAll hooks
test.beforeAll() 和 test.afterAll() 钩子会在同一个文件中同一个 test.describe() 块(如果有)内声明的所有测试之前/之后运行,每个工作进程运行一次。如果你想声明在每个文件中的所有测试之前/之后运行的钩子,可以将它们作为自动夹具用 scope: 'worker' 声明,如下所示:
import { test as base } from '@playwright/test';
export const test = base.extend<{}, { forEachWorker: void }>({
forEachWorker: [async ({}, use) => {
// This code runs before all the tests in the worker process.
console.log(`Starting test worker ${test.info().workerIndex}`);
await use();
// This code runs after all the tests in the worker process.
console.log(`Stopping test worker ${test.info().workerIndex}`);
}, { scope: 'worker', auto: true }], // automatically starts for every worker.
});
然后在所有测试中导入这些装置:
🌐 And then import the fixtures in all your tests:
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
请注意,夹具仍然会在每个工作进程中运行一次,但你不需要在每个文件中重新声明它们。
🌐 Note that the fixtures will still run once per worker process, but you don't need to redeclare them in every file.