Skip to main content

夹具

介绍

¥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 Test 设置 page 夹具并将其提供给你的测试函数。

¥The { page } argument tells Playwright Test to setup 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[Page]此测试运行的独立页面。
context[BrowserContext]此测试运行的隔离上下文。page 装置也属于这种情况。了解如何 配置上下文
browser[Browser]跨测试共享浏览器以优化资源。了解如何 配置浏览器
browserName[字符串]当前运行测试的浏览器的名称。chromiumfirefoxwebkit
request[APIRequestContext]用于此测试运行的隔离 APIRequestContext 实例。

不使用夹具

¥Without fixtures

以下是传统测试风格和基于夹具的测试风格之间典型测试环境设置的差异。

¥Here is how typical test environment setup differs between traditional test style and the fixture-based one.

TodoPage 是一个帮助与 Web 应用的 "待办事项清单" 页面交互的类,遵循 页面对象模型 模式。它内部使用 Playwright 的 page

¥TodoPage is a class that helps interacting with a "todo list" page of the web app, following the Page Object Model pattern. It uses Playwright's page internally.

Click to expand the code for the TodoPage
todo-page.ts
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();
}
}
}
todo.spec.ts
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 将设置和拆卸封装在同一个地方,因此更容易编写。

    ¥Fixtures encapsulate setup and teardown in the same place so it is easier to write.

  • 夹具可以在测试文件之间重复使用 - 你可以定义它们一次并在所有测试中使用。这就是 Playwright 的内置 page 装置的工作原理。

    ¥Fixtures are reusable between test files - you can define them once and use in all your tests. That's how Playwright's built-in page fixture works.

  • 夹具按需提供 - 你可以根据需要定义任意数量的夹具,Playwright Test 将仅设置测试所需的夹具,而不会设置其他任何夹具。

    ¥Fixtures are on-demand - you can define as many fixtures as you'd like, and Playwright Test will setup only the ones needed by your test and nothing else.

  • 夹具是可组合的 - 它们可以相互依赖来提供复杂的行为。

    ¥Fixtures are composable - they can depend on each other to provide complex behaviors.

  • 夹具灵活。测试可以使用夹具的任意组合来定制所需的精确环境,而不会影响其他测试。

    ¥Fixtures are flexible. Tests can use any combinations of the fixtures to tailor precise environment they need, without affecting other tests.

  • 夹具简化了分组。你不再需要将测试封装在设置环境的 describe 中,而是可以根据测试的含义自由地对其进行分组。

    ¥Fixtures simplify grouping. You no longer need to wrap tests in describes that set up environment, and are free to group your tests by their meaning instead.

Click to expand the code for the TodoPage
todo-page.ts
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();
}
}
}
example.spec.ts
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.

下面我们创建两个遵循 页面对象模型 模式的灯具 todoPagesettingsPage

¥Below we create two fixtures todoPage and settingsPage that follow the Page Object Model pattern.

Click to expand the code for the TodoPage and SettingsPage
todo-page.ts
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:

settings-page.ts
import type { Page } from '@playwright/test';

export class SettingsPage {
constructor(public readonly page: Page) {
}

async switchToDarkMode() {
// ...
}
}
my-test.ts
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';
注意

自定义灯具名称应以字母或下划线开头,并且只能包含字母、数字、下划线。

¥Custom fixture names should start with a letter or underscore, and can contain only letters, numbers, underscores.

使用夹具

¥Using a fixture

只需在测试函数参数中提及夹具,测试运行程序就会处理它。夹具还有钩子和其他夹具。如果你使用 TypeScript,灯具将具有正确的类型。

¥Just mention fixture in your test function argument, and test runner will take care of it. Fixtures are also available in hooks and other fixtures. If you use TypeScript, fixtures will have the right type.

下面我们使用上面定义的 todoPagesettingsPage 灯具。

¥Below we use the todoPage and settingsPage fixtures 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 some 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().

example.spec.ts

test.use({ baseURL: 'https://playwright.nodejs.cn' });

当基础夹具被完全替换为不同的东西时,夹具也可以被覆盖。例如,我们可以覆盖 testOptions.storageState 夹具来提供我们自己的数据。

¥Fixtures can also be overridden where the base fixture is 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. Similarly 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 login into 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 up this fixture once per worker.

my-test.ts
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 TestInfo object that is available in each test/fixture to retrieve metadata about the test being run.

my-test.ts
import * as debug from 'debug';
import * as 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 与测试共享超时。然而,对于慢速比赛,尤其是 worker-scoped 的比赛,单独的暂停是很方便的。通过这种方式,你可以保持整体测试超时较小,并为慢速装置提供更多时间。

¥By default, fixture shares timeout with the test. However, for slow fixtures, especially worker-scoped ones, it is convenient to have a separate timeout. This way you can keep the overall test timeout small, and give the slow fixture more time.

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

夹具选项

¥Fixtures-options

Playwright Test 支持运行多个可以单独配置的测试项目。你可以使用 "option" 夹具来使你的配置选项具有声明性并进行类型检查。了解有关 参数化测试 的更多信息。

¥Playwright Test supports running multiple test projects that can be separately configured. You can use "option" fixtures to make your configuration options declarative and type-checked. Learn more about parametrizing tests.

除了其他示例中的 todoPage 夹具之外,下面我们还将创建一个 defaultItem 选项。该选项将在配置文件中设置。请注意元组语法和 { option: true } 参数。

¥Below we'll create a defaultItem option in addition to the todoPage fixture from other examples. This option will be set in configuration file. Note the tuple syntax and { option: true } argument.

Click to expand the code for the TodoPage
todo-page.ts
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();
}
}
}
my-test.ts
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 todoPage fixture as usual, and set the defaultItem option in the config file.

playwright.config.ts
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!' },
},
]
});

数组作为选项值

¥Array as an option value

如果选项的值是一个数组,例如 [{ 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,
});

执行顺序

¥Execution order

每个装置都有一个设置和拆卸阶段,由装置中的 await use() 调用分隔开。setup 在测试/钩子使用夹具之前执行,而拆卸在测试/钩子不再使用夹具时执行。

¥Each fixture has a setup and teardown phase separated by the await use() call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore.

夹具遵循以下规则来确定执行顺序:

¥Fixtures follow these rules to determine the execution order:

  • 当夹具 A 依赖于夹具 B 时:B 始终在 A 之前设置,在 A 之后拆除。

    ¥When fixture A depends on fixture B: B is always set up before A and torn down after A.

  • 非自动夹具仅在测试/钩子需要时才会延迟执行。

    ¥Non-automatic fixtures are executed lazily, only when the test/hook needs them.

  • 测试范围的装置在每次测试后都会被拆除,而工作范围的装置仅在执行测试的工作进程关闭时才会被拆除。

    ¥Test-scoped fixtures are torn down after each test, while worker-scoped fixtures are only torn down when the worker process executing tests is shutdown.

考虑以下示例:

¥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 部分:

    ¥worker setup and beforeAll section:

    • browser 设置,因为 autoWorkerFixture 需要它。

      ¥browser setup because it is required by autoWorkerFixture.

    • autoWorkerFixture 设置,因为自动工作线程夹具总是先于其他任何事情设置。

      ¥autoWorkerFixture setup because automatic worker fixtures are always set up before anything else.

    • beforeAll 运行。

      ¥beforeAll runs.

  • first test 节:

    ¥first test section:

    • autoTestFixture 设置是因为自动测试夹具总是在测试和 beforeEach 钩子之前设置。

      ¥autoTestFixture setup because automatic test fixtures are always set up before test and beforeEach hooks.

    • page 设置,因为 beforeEach 钩子需要它。

      ¥page setup because it is required in beforeEach hook.

    • beforeEach 运行。

      ¥beforeEach runs.

    • first test 运行。

      ¥first test runs.

    • afterEach 运行。

      ¥afterEach runs.

    • page 拆卸,因为它是一个测试范围的装置,应该在测试完成后拆除。

      ¥page teardown because it is a test-scoped fixture and should be torn down after the test finishes.

    • autoTestFixture 拆卸,因为它是一个测试范围的装置,应该在测试完成后拆除。

      ¥autoTestFixture teardown because it is a test-scoped fixture and should be torn down after the test finishes.

  • second test 节:

    ¥second test section:

    • autoTestFixture 设置是因为自动测试夹具总是在测试和 beforeEach 钩子之前设置。

      ¥autoTestFixture setup because automatic test fixtures are always set up before test and beforeEach hooks.

    • page 设置,因为 beforeEach 钩子需要它。

      ¥page setup because it is required in beforeEach hook.

    • beforeEach 运行。

      ¥beforeEach runs.

    • workerFixture 设置是因为 testFixture 需要它,而 second test 也需要它。

      ¥workerFixture setup because it is required by testFixture that is required by the second test.

    • testFixture 设置,因为 second test 需要它。

      ¥testFixture setup because it is required by the second test.

    • second test 运行。

      ¥second test runs.

    • afterEach 运行。

      ¥afterEach runs.

    • testFixture 拆卸,因为它是一个测试范围的装置,应该在测试完成后拆除。

      ¥testFixture teardown because it is a test-scoped fixture and should be torn down after the test finishes.

    • page 拆卸,因为它是一个测试范围的装置,应该在测试完成后拆除。

      ¥page teardown because it is a test-scoped fixture and should be torn down after the test finishes.

    • autoTestFixture 拆卸,因为它是一个测试范围的装置,应该在测试完成后拆除。

      ¥autoTestFixture teardown because it is a test-scoped fixture and should be torn down after the test finishes.

  • afterAll 和 worker 拆解部分:

    ¥afterAll and worker teardown section:

    • afterAll 运行。

      ¥afterAll runs.

    • workerFixture 拆卸,因为它是一个工作者范围的装置,应该在最后拆除一次。

      ¥workerFixture teardown because it is a workers-scoped fixture and should be torn down once at the end.

    • autoWorkerFixture 拆卸,因为它是一个工作者范围的装置,应该在最后拆除一次。

      ¥autoWorkerFixture teardown because it is a workers-scoped fixture and should be torn down once at the end.

    • browser 拆卸,因为它是一个工作者范围的装置,应该在最后拆除一次。

      ¥browser teardown because it is a workers-scoped fixture and should be torn down once at the end.

一些观察:

¥A few observations:

  • pageautoTestFixture 作为测试范围的设备,为每个测试设置和拆除。

    ¥page and autoTestFixture are set up and torn down for each test, as test-scoped fixtures.

  • unusedFixture 永远不会被设置,因为它没有被任何测试/钩子使用。

    ¥unusedFixture is never set up because it is not used by any tests/hooks.

  • testFixture 依赖于 workerFixture 并触发其设置。

    ¥testFixture depends on workerFixture and triggers its setup.

  • workerFixture 在第二次测试之前被延迟设置,但在工作程序关闭期间被拆除一次,作为工作程序范围的装置。

    ¥workerFixture is lazily set up before the second test, but torn down once during worker shutdown, as a worker-scoped fixture.

  • autoWorkerFixture 是为 beforeAll 钩子设置的,但 autoTestFixture 不是。

    ¥autoWorkerFixture is set up for beforeAll hook, but autoTestFixture is not.

组合来自多个模块的定制夹具

¥Combine custom fixtures from multiple modules

你可以合并来自多个文件或模块的测试装置:

¥You can merge test fixtures from multiple files or modules:

fixtures.ts
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);
test.spec.ts
import { test } from './fixtures';

test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});

盒子装置

¥Box fixtures

通常,自定义装置在 UI 模式、跟踪查看器和各种测试报告中作为单独的步骤报告。它们也出现在测试运行器的错误消息中。对于常用装置,这可能意味着很多噪音。你可以通过 "boxing" 来阻止装置步骤显示在 UI 中。

¥Usually, custom fixtures are reported as separate steps in 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 }],
});

这对于不感兴趣的辅助装置很有用。例如,设置一些常用数据的 automatic 装置可以安全地隐藏在测试报告中。

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

自定义装置标题

¥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() 块(如果有)中声明的每个测试之前/之后运行。如果你想声明全局在每个测试之前/之后运行的钩子,你可以像这样将它们声明为自动装置:

¥test.beforeEach() and test.afterEach() hooks run before/after each test declared in the same file and same test.describe() block (if any). If you want to declare hooks that run before/after each test globally, you can declare them as auto fixtures like this:

fixtures.ts
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:

mytest.spec.ts
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' 将它们声明为自动装置,如下所示:

¥test.beforeAll() and test.afterAll() hooks run before/after all tests declared in the same file and same test.describe() block (if any), once per worker process. If you want to declare hooks that run before/after all tests in every file, you can declare them as auto fixtures with scope: 'worker' as follows:

fixtures.ts
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:

mytest.spec.ts
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.