Skip to main content

验证

介绍

¥Introduction

Playwright 在称为 浏览器上下文 的隔离环境中执行测试。这种隔离模型提高了可重复性并防止级联测试失败。测试可以加载现有的经过身份验证的状态。这消除了在每个测试中进行身份验证的需要,并加快了测试执行速度。

¥Playwright executes tests in isolated environments called browser contexts. This isolation model improves reproducibility and prevents cascading test failures. Tests can load existing authenticated state. This eliminates the need to authenticate in every test and speeds up test execution.

核心理念

¥Core concepts

无论你选择哪种身份验证策略,你都可能将经过身份验证的浏览器状态存储在文件系统上。

¥Regardless of the authentication strategy you choose, you are likely to store authenticated browser state on the file system.

我们建议创建 playwright/.auth 目录并将其添加到你的 .gitignore 中。你的身份验证例程将生成经过身份验证的浏览器状态并将其保存到此 playwright/.auth 目录中的文件中。稍后,测试将重用此状态并开始已通过身份验证的操作。

¥We recommend to create playwright/.auth directory and add it to your .gitignore. Your authentication routine will produce authenticated browser state and save it to a file in this playwright/.auth directory. Later on, tests will reuse this state and start already authenticated.

mkdir -p playwright/.auth
echo $'\nplaywright/.auth' >> .gitignore

基础:所有测试中共享账户

¥Basic: shared account in all tests

这是没有服务器端状态的测试的推荐方法。在设置项目中进行一次身份验证,保存身份验证状态,然后重用它来引导每个已通过身份验证的测试。

¥This is the recommended approach for tests without server-side state. Authenticate once in the setup project, save the authentication state, and then reuse it to bootstrap each test already authenticated.

何时使用

¥When to use

  • 当你可以想象所有测试都使用同一个账户同时运行,而不互相影响时。

    ¥When you can imagine all your tests running at the same time with the same account, without affecting each other.

何时不使用

¥When not to use

  • 你的测试会修改服务器端状态。例如,一个测试检查设置页面的渲染,而另一个测试正在更改设置,并且你可以并行运行测试。在这种情况下,测试必须使用不同的账户。

    ¥Your tests modify server-side state. For example, one test checks the rendering of the settings page, while the other test is changing the setting, and you run tests in parallel. In this case, tests must use different accounts.

  • 你的身份验证是特定于浏览器的。

    ¥Your authentication is browser-specific.

细节

¥Details

创建 tests/auth.setup.ts,它将为所有其他测试准备经过身份验证的浏览器状态。

¥Create tests/auth.setup.ts that will prepare authenticated browser state for all other tests.

tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('authenticate', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// End of authentication steps.

await page.context().storageState({ path: authFile });
});

在配置中创建一个新的 setup 项目,并将其声明为所有测试项目的 dependency。该项目将始终在所有测试之前运行并进行身份验证。所有测试项目都应使用已验证状态为 storageState

¥Create a new setup project in the config and declare it as a dependency for all your testing projects. This project will always run and authenticate before all the tests. All testing projects should use the authenticated state as storageState.

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
projects: [
// Setup project
{ name: 'setup', testMatch: /.*\.setup\.ts/ },

{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},

{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});

测试开始时已经经过身份验证,因为我们在配置中指定了 storageState

¥Tests start already authenticated because we specified storageState in the config.

tests/example.spec.ts
import { test } from '@playwright/test';

test('test', async ({ page }) => {
// page is authenticated
});

请注意,当存储状态过期时,你需要删除它。如果你不需要在测试运行之间保持状态,请在 testProject.outputDir 下写入浏览器状态,该状态会在每次测试运行之前自动清理。

¥Note that you need to delete the stored state when it expires. If you don't need to keep the state between test runs, write the browser state under testProject.outputDir, which is automatically cleaned up before every test run.

UI 模式下的认证

¥Authenticating in UI mode

UI 模式默认不会运行 setup 项目,以提高测试速度。我们建议在现有身份验证到期时不时手动运行 auth.setup.ts 来进行身份验证。

¥UI mode will not run the setup project by default to improve testing speed. We recommend to authenticate by manually running the auth.setup.ts from time to time, whenever existing authentication expires.

首先 在过滤器中启用 setup 项目,然后单击 auth.setup.ts 文件旁边的三角形按钮,然后再次在过滤器中禁用 setup 项目。

¥First enable the setup project in the filters, then click the triangle button next to auth.setup.ts file, and then disable the setup project in the filters again.

Moderate:每个并行工作线程一个账户

¥Moderate: one account per parallel worker

这是修改服务器端状态的测试的推荐方法。在 Playwright 中,工作进程并行运行。在这种方法中,每个并行工作线程都经过一次身份验证。工作线程运行的所有测试都重复使用相同的身份验证状态。我们将需要多个测试账户,每个并行工作线程一个。

¥This is the recommended approach for tests that modify server-side state. In Playwright, worker processes run in parallel. In this approach, each parallel worker is authenticated once. All tests ran by worker are reusing the same authentication state. We will need multiple testing accounts, one per each parallel worker.

何时使用

¥When to use

  • 你的测试会修改共享服务器端状态。例如,一个测试检查设置页面的渲染,而另一测试则更改设置。

    ¥Your tests modify shared server-side state. For example, one test checks the rendering of the settings page, while the other test is changing the setting.

何时不使用

¥When not to use

  • 你的测试不会修改任何共享的服务器端状态。在这种情况下,所有测试都可以使用单个共享账户。

    ¥Your tests do not modify any shared server-side state. In this case, all tests can use a single shared account.

细节

¥Details

我们将每个 工作进程 进行一次身份验证,每个身份都有一个唯一的账户。

¥We will authenticate once per worker process, each with a unique account.

创建 playwright/fixtures.ts 文件,该文件将 覆盖 storageState 夹具 对每个工作线程进行一次身份验证。使用 testInfo.parallelIndex 来区分工作线程。

¥Create playwright/fixtures.ts file that will override storageState fixture to authenticate once per worker. Use testInfo.parallelIndex to differentiate between workers.

playwright/fixtures.ts
import { test as baseTest, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';

export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState),

// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({ browser }, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);

if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await use(fileName);
return;
}

// Important: make sure we authenticate in a clean environment by unsetting storage state.
const page = await browser.newPage({ storageState: undefined });

// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await acquireAccount(id);

// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill(account.username);
await page.getByLabel('Password').fill(account.password);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// End of authentication steps.

await page.context().storageState({ path: fileName });
await page.close();
await use(fileName);
}, { scope: 'worker' }],
});

现在,每个测试文件应该从我们的装置文件导入 test,而不是 @playwright/test。配置中不需要进行任何更改。

¥Now, each test file should import test from our fixtures file instead of @playwright/test. No changes are needed in the config.

tests/example.spec.ts
// Important: import our fixtures.
import { test, expect } from '../playwright/fixtures';

test('test', async ({ page }) => {
// page is authenticated
});

高级场景

¥Advanced scenarios

使用 API 请求进行身份验证

¥Authenticate with API request

何时使用

¥When to use

  • 你的 Web 应用支持通过 API 进行身份验证,这比与应用 UI 交互更容易/更快。

    ¥Your web application supports authenticating via API that is easier/faster than interacting with the app UI.

细节

¥Details

我们将使用 APIRequestContext 发送 API 请求,然后照常保存经过身份验证的状态。

¥We will send the API request with APIRequestContext and then save authenticated state as usual.

设置项目 中:

¥In the setup project:

tests/auth.setup.ts
import { test as setup } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ request }) => {
// Send authentication request. Replace with your own.
await request.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
await request.storageState({ path: authFile });
});

或者,在 工作线程夹具 中:

¥Alternatively, in a worker fixture:

playwright/fixtures.ts
import { test as baseTest, request } from '@playwright/test';
import fs from 'fs';
import path from 'path';

export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState),

// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({}, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);

if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await use(fileName);
return;
}

// Important: make sure we authenticate in a clean environment by unsetting storage state.
const context = await request.newContext({ storageState: undefined });

// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await acquireAccount(id);

// Send authentication request. Replace with your own.
await context.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});

await context.storageState({ path: fileName });
await context.dispose();
await use(fileName);
}, { scope: 'worker' }],
});

多个登录角色

¥Multiple signed in roles

何时使用

¥When to use

  • 你在端到端测试中拥有多个角色,但你可以在所有测试中重复使用账户。

    ¥You have more than one role in your end to end tests, but you can reuse accounts across all tests.

细节

¥Details

我们将在安装项目中进行多次验证。

¥We will authenticate multiple times in the setup project.

tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const adminFile = 'playwright/.auth/admin.json';

setup('authenticate as admin', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('admin');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// End of authentication steps.

await page.context().storageState({ path: adminFile });
});

const userFile = 'playwright/.auth/user.json';

setup('authenticate as user', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// End of authentication steps.

await page.context().storageState({ path: userFile });
});

之后,为每个测试文件或测试组指定 storageState,而不是在配置中设置它。

¥After that, specify storageState for each test file or test group, instead of setting it in the config.

tests/example.spec.ts
import { test } from '@playwright/test';

test.use({ storageState: 'playwright/.auth/admin.json' });

test('admin test', async ({ page }) => {
// page is authenticated as admin
});

test.describe(() => {
test.use({ storageState: 'playwright/.auth/user.json' });

test('user test', async ({ page }) => {
// page is authenticated as a user
});
});

另请参见 UI 模式认证

¥See also about authenticating in the UI mode.

一起测试多个角色

¥Testing multiple roles together

何时使用

¥When to use

  • 你需要在单个测试中测试多个经过身份验证的角色如何交互。

    ¥You need to test how multiple authenticated roles interact together, in a single test.

细节

¥Details

在同一测试中使用多个具有不同存储状态的 BrowserContextPage

¥Use multiple BrowserContexts and Pages with different storage states in the same test.

tests/example.spec.ts
import { test } from '@playwright/test';

test('admin and user', async ({ browser }) => {
// adminContext and all pages inside, including adminPage, are signed in as "admin".
const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = await adminContext.newPage();

// userContext and all pages inside, including userPage, are signed in as "user".
const userContext = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = await userContext.newPage();

// ... interact with both adminPage and userPage ...

await adminContext.close();
await userContext.close();
});

使用 POM 夹具测试多个角色

¥Testing multiple roles with POM fixtures

何时使用

¥When to use

  • 你需要在单个测试中测试多个经过身份验证的角色如何交互。

    ¥You need to test how multiple authenticated roles interact together, in a single test.

细节

¥Details

你可以引入夹具,以提供经过每个角色身份验证的页面。

¥You can introduce fixtures that will provide a page authenticated as each role.

下面是 创建夹具 代表两个 页面对象模型 的例子 - 管理员 POM 和用户 POM。它假设 adminStorageState.jsonuserStorageState.json 文件是在全局设置中创建的。

¥Below is an example that creates fixtures for two Page Object Models - admin POM and user POM. It assumes adminStorageState.json and userStorageState.json files were created in the global setup.

playwright/fixtures.ts
import { test as base, type Page, type Locator } from '@playwright/test';

// Page Object Model for the "admin" page.
// Here you can add locators and helper methods specific to the admin page.
class AdminPage {
// Page signed in as "admin".
page: Page;

// Example locator pointing to "Welcome, Admin" greeting.
greeting: Locator;

constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}

// Page Object Model for the "user" page.
// Here you can add locators and helper methods specific to the user page.
class UserPage {
// Page signed in as "user".
page: Page;

// Example locator pointing to "Welcome, User" greeting.
greeting: Locator;

constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}

// Declare the types of your fixtures.
type MyFixtures = {
adminPage: AdminPage;
userPage: UserPage;
};

export * from '@playwright/test';
export const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = new AdminPage(await context.newPage());
await use(adminPage);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = new UserPage(await context.newPage());
await use(userPage);
await context.close();
},
});

tests/example.spec.ts
// Import test with our new fixtures.
import { test, expect } from '../playwright/fixtures';

// Use adminPage and userPage fixtures in the test.
test('admin and user', async ({ adminPage, userPage }) => {
// ... interact with both adminPage and userPage ...
await expect(adminPage.greeting).toHaveText('Welcome, Admin');
await expect(userPage.greeting).toHaveText('Welcome, User');
});

会话存储

¥Session storage

重用经过身份验证的状态涵盖基于 cookies本地存储 的身份验证。会话存储 很少用于存储与登录状态相关的信息。会话存储特定于特定域,并且不会在页面加载时保留。Playwright 不提供 API 来保存会话存储,但以下代码片段可用于保存/加载会话存储。

¥Reusing authenticated state covers cookies and local storage based authentication. Rarely, session storage is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.

// Get session storage and store as env variable
const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
fs.writeFileSync('playwright/.auth/session.json', sessionStorage, 'utf-8');

// Set session storage in a new context
const sessionStorage = JSON.parse(fs.readFileSync('playwright/.auth/session.json', 'utf-8'));
await context.addInitScript(storage => {
if (window.location.hostname === 'example.com') {
for (const [key, value] of Object.entries(storage))
window.sessionStorage.setItem(key, value);
}
}, sessionStorage);

在某些测试中避免身份验证

¥Avoid authentication in some tests

你可以重置测试文件中的存储状态,以避免为整个项目设置的身份验证。

¥You can reset storage state in a test file to avoid authentication that was set up for the whole project.

not-signed-in.spec.ts
import { test } from '@playwright/test';

// Reset storage state for this file to avoid being authenticated
test.use({ storageState: { cookies: [], origins: [] } });

test('not signed in test', async ({ page }) => {
// ...
});