跳到主要内容

身份验证

简介

Playwright 在称为浏览器上下文的独立环境中执行测试。这种隔离模型提高了可重现性并防止测试失败的连锁反应。测试可以加载现有的认证状态。这消除了在每个测试中进行认证的需要,并加快了测试执行速度。

核心概念

无论您选择哪种认证策略,您很可能都会将已认证的浏览器状态存储在文件系统中。

我们建议创建 playwright/.auth 目录并将其添加到您的 .gitignore 中。您的认证程序将生成已认证的浏览器状态,并将其保存到此 playwright/.auth 目录中的文件。稍后,测试将重用此状态并从已认证的状态开始运行。

危险

浏览器状态文件可能包含敏感的 Cookie 和 Header,这些信息可用于冒充您或您的测试账号。我们强烈建议不要将其提交到私有或公共仓库中。

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

基本:在所有测试中使用共享账号

这是针对**不涉及服务器端状态**的测试的**推荐**方法。在 **setup 项目**中认证一次,保存认证状态,然后重用该状态来启动每个已认证的测试。

何时使用

  • 当您可以想象所有测试使用同一个账号同时运行时,互不影响。

何时不使用

  • 您的测试修改服务器端状态。例如,一个测试检查设置页面的渲染,而另一个测试正在修改设置,并且您并行运行测试。在这种情况下,测试必须使用不同的账号。
  • 您的认证特定于浏览器。

详情

创建 tests/auth.setup.ts 文件,它将为所有其他测试准备已认证的浏览器状态。

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 项目,并将其声明为您所有测试项目的依赖项。此项目将在所有测试之前始终运行并进行认证。所有测试项目都应该使用已认证的状态作为 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/example.spec.ts
import { test } from '@playwright/test';

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

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

在 UI 模式下进行认证

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

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

中级:每个并行 Worker 一个账号

这是针对**修改服务器端状态**的测试的**推荐**方法。在 Playwright 中,worker 进程并行运行。在此方法中,每个并行 worker 会认证一次。由该 worker 运行的所有测试都会重用相同的认证状态。我们将需要多个测试账号,每个并行 worker 一个。

何时使用

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

何时不使用

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

详情

我们将为每个worker 进程认证一次,每个 worker 使用一个唯一的账号。

创建 playwright/fixtures.ts 文件,该文件将覆盖 storageState fixture,以实现每个 worker 认证一次。使用 testInfo.parallelIndex 来区分不同的 worker。

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' }],
});

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

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

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

高级场景

使用 API 请求认证

何时使用

  • 您的 web 应用程序支持通过 API 进行认证,这种方式比与应用程序 UI 交互更容易/更快。

详情

我们将使用 APIRequestContext 发送 API 请求,然后像往常一样保存已认证的状态。

基本:在所有测试中使用共享账号章节描述的 setup 项目中

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 });
});

或者,在中级:每个并行 Worker 一个账号章节描述的 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' }],
});

多个已登录角色

何时使用

  • 在您的端到端测试中,您有不止一个角色,但可以在所有测试中重用账号。

详情

我们将在 setup 项目中认证多次。

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,**而不是**在配置中设置它。

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 模式下进行认证

同时测试多个角色

何时使用

  • 您需要在单个测试中测试多个已认证角色如何协同交互。

详情

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

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 fixtures 测试多个角色

何时使用

  • 您需要在单个测试中测试多个已认证角色如何协同交互。

详情

您可以引入 fixtures,这些 fixtures 将提供以每个角色身份认证过的页面。

下面是一个示例,它为两个页面对象模型(POM)(admin POM 和 user POM)创建 fixtures。它假定 adminStorageState.jsonuserStorageState.json 文件已在全局 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

重用已认证状态涵盖了基于 cookieslocal storageIndexedDB 的认证。极少情况下,session storage 用于存储与登录状态相关的信息。Session storage 特定于某个域,并且不会在页面加载之间持久化。Playwright 未提供用于持久化 session storage 的 API,但可以使用以下代码片段来保存/加载 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);

在某些测试中避免认证

您可以在测试文件中重置存储状态,以避免使用为整个项目设置的认证。

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