Skip to main content

身份验证

Playwright 可用于自动化需要身份验证的场景。

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

注意:本指南涵盖基于 cookie/token 的身份验证(通过应用程序 UI 登录)。对于 HTTP 身份验证,请使用 browser.newContext([options])

自动化登录

Playwright API 可以 自动化交互 以登录表单。

以下示例自动化登录 GitHub。一旦执行这些步骤,浏览器上下文将被验证。

import { test } from '@playwright/test';

test.beforeEach(async ({ page }) => {
// 在每个测试之前运行并登录每个页面。
await page.goto('https://github.com/login');
await page.getByText('Login').click();
await page.getByLabel('User Name').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByText('Submit').click();
});

test('first', async ({ page }) => {
// 页面已登录。
});

test('second', async ({ page }) => {
// 页面已登录。
});

为每个测试重做登录会减慢测试执行速度。为了缓解这种情况,请重用现有的身份验证状态。

重用登录状态

Playwright 提供了一种在测试中重用登录状态的方法。这样,您只需登录一次,然后就可以跳过所有测试的登录步骤。

Web 应用程序使用基于 cookie 或基于令牌的身份验证,其中经过身份验证的状态存储为 cookies本地存储。Playwright 提供 browserContext.storageState([options]) 方法,可用于从经过身份验证的上下文中检索存储状态,然后创建具有预填充状态的新上下文。

Cookie 和本地存储状态可以在不同的浏览器中使用。它们取决于您的应用程序的身份验证模型:某些应用程序可能同时需要 cookie 和本地存储。

创建一个新的全局设置脚本:

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByText('Sign in').click();
// 将登录状态保存到 'storageState.json'。
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
}

export default globalSetup;

在 Playwright 配置文件中注册全局设置脚本:

// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
globalSetup: require.resolve('./global-setup'),
use: {
// 告诉所有测试从 'storageState.json' 加载登录状态。
storageState: 'storageState.json'
}
};
export default config;

测试开始时已经过身份验证,因为我们指定了由全局设置填充的 storageState

import { test } from '@playwright/test';

test('test', async ({ page }) => {
// 页面已登录。
});
note

如果您可以登录一次并将 storageState.json 提交到存储库中,则根本不需要全局设置,只需如上所述在 Playwright Config 中指定 storageState.json 即可。但是,如果您的应用程序要求您在一段时间后重新进行身份验证,您可能需要定期更新 storageState.json 文件。例如,如果您的应用程序每周提示您登录一次,即使您在同一台计算机/浏览器上也是如此,您也需要至少以此频率更新 storageState.json

通过 API 请求登录

如果您的 Web 应用程序支持通过 API 登录,您可以使用 APIRequestContext 来简化登录流程。上面的示例中的全局设置脚本将如下所示:

// global-setup.ts
import { request } from '@playwright/test';

async function globalSetup() {
const requestContext = await request.newContext();
await requestContext.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
// 将登录状态保存到 'storageState.json'。
await requestContext.storageState({ path: 'storageState.json' });
await requestContext.dispose();
}

export default globalSetup;

避免每个帐户同时有多个会话

默认情况下,Playwright Test 并行运行测试。如果您对所有测试重用单个登录状态,这通常会导致同一帐户同时从多个测试登录。如果这种行为对您的应用程序不可取,您可以在 Playwright Test 创建的每个 工作进程 中使用不同的帐户登录。

在这个例子中,我们 覆盖 storageState fixture 并确保我们每个 worker 只登录一次,使用 testInfo.workerIndex 来区分 worker。

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

const users = [
{ username: 'user-1', password: 'password-1' },
{ username: 'user-2', password: 'password-2' },
// ... 在这里放置您的测试用户 ...
];

export const test = baseTest.extend({
storageState: async ({ browser }, use, testInfo) => {
// 覆盖存储状态,使用 worker 索引查找登录信息并延迟生成。
const fileName = path.join(testInfo.project.outputDir, 'storage-' + testInfo.workerIndex);
if (!fs.existsSync(fileName)) {
// 确保我们没有使用任何其他存储状态。
const page = await browser.newPage({ storageState: undefined });
await page.goto('https://github.com/login');
// 为每个 worker 创建唯一的用户名。
await page.getByLabel('User Name').fill(users[testInfo.workerIndex].username);
await page.getByLabel('Password').fill(users[testInfo.workerIndex].password);
await page.getByText('Sign in').click();
await page.context().storageState({ path: fileName });
await page.close();
}
await use(fileName);
},
});

// example.spec.ts
import { test, expect } from './fixtures';

test('test', async ({ page }) => {
// 页面已登录。
});

多个已登录角色

有时您的端到端测试中有多个已登录用户。您可以通过在 globalSetup 中多次登录这些用户并将该状态保存到不同的文件中来实现这一点。

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const adminPage = await browser.newPage();
// ... 登录
await adminPage.context().storageState({ path: 'adminStorageState.json' });

const userPage = await browser.newPage();
// ... 登录
await userPage.context().storageState({ path: 'userStorageState.json' });
await browser.close();
}

export default globalSetup;

之后,您可以为每个测试文件或每个测试组指定要使用的用户:

import { test } from '@playwright/test';

test.use({ storageState: 'adminStorageState.json' });

test('admin test', async ({ page }) => {
// 页面以管理员身份登录。
});

test.describe(() => {
test.use({ storageState: 'userStorageState.json' });

test('user test', async ({ page }) => {
// 页面以用户身份登录。
});
});

一起测试多个角色

如果您需要测试多个经过身份验证的角色如何相互交互,请在同一测试中使用具有不同存储状态的多个 BrowserContextPage。上面创建多个存储状态文件的任何方法都适用。

import { test } from '@playwright/test';

test('admin and user', async ({ browser }) => {
// adminContext 及其内部的所有页面(包括 adminPage)都以 "admin" 身份登录。
const adminContext = await browser.newContext({ storageState: 'adminStorageState.json' });
const adminPage = await adminContext.newPage();

// userContext 及其内部的所有页面(包括 userPage)都以 "user" 身份登录。
const userContext = await browser.newContext({ storageState: 'userStorageState.json' });
const userPage = await userContext.newPage();

// ... 与 adminPage 和 userPage 交互 ...
});

使用 POM fixtures 测试多个角色

如果您的许多测试需要同一个测试中的多个经过身份验证的角色,您可以为每个角色引入 fixtures。上面创建多个存储状态文件的任何方法都适用。

下面是一个示例,它为两个 Page Object Models(admin POM 和 user POM)创建 fixtures。它假设 adminStorageState.jsonuserStorageState.json 文件已创建。

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

// "admin" 页面的页面对象模型。
// 在这里,您可以添加特定于 admin 页面的定位器和辅助方法。
class AdminPage {
// 以 "admin" 身份登录的页面。
page: Page;

constructor(page: Page) {
this.page = page;
}

static async create(browser: Browser) {
const context = await browser.newContext({ storageState: 'adminStorageState.json' });
const page = await context.newPage();
return new AdminPage(page);
}
}

// "user" 页面的页面对象模型。
// 在这里,您可以添加特定于 user 页面的定位器和辅助方法。
class UserPage {
// 以 "user" 身份登录的页面。
page: Page;

// 指向 "Welcome, User" 问候语的示例定位器。
greeting: Locator;

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

static async create(browser: Browser) {
const context = await browser.newContext({ storageState: 'userStorageState.json' });
const page = await context.newPage();
return new UserPage(page);
}
}

// 声明 fixtures 的类型。
type MyFixtures = {
adminPage: AdminPage;
userPage: UserPage;
};

// 通过提供 "adminPage" 和 "userPage" 扩展基础测试。
// 这个新的 "test" 可以在多个测试文件中使用,并且每个文件都将获得 fixtures。
export const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
await use(await AdminPage.create(browser));
},
userPage: async ({ browser }, use) => {
await use(await UserPage.create(browser));
},
});


// example.spec.ts
// 导入带有我们新 fixtures 的 test。
import { test, expect } from './fixtures';

// 在测试中使用 adminPage 和 userPage fixtures。
test('admin and user', async ({ adminPage, userPage }) => {
// ... 与 adminPage 和 userPage 交互 ...
await adminPage.page.screenshot();
await expect(userPage.greeting).toHaveText('Welcome, User');
});

在多个测试中重用已登录的页面

虽然不鼓励,但有时必须牺牲隔离性并在同一页面中运行多个测试。在这种情况下,您可以在 beforeAll 中登录该页面一次,然后在所有测试中使用该同一页面。请注意,您需要使用 test.describe.serial 串行运行这些测试才能实现此目的:

// example.spec.ts

import { test, Page } from '@playwright/test';

test.describe.configure({ mode: 'serial' });

let page: Page;

test.beforeAll(async ({ browser }) => {
// 创建页面一次并登录。
page = await browser.newPage();
await page.goto('https://github.com/login');
await page.getByLabel('User Name').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByText('Sign in').click();
});

test.afterAll(async () => {
await page.close();
});

test('first test', async () => {
// 页面已登录。
});

test('second test', async () => {
// 页面已登录。
});
note

您也可以在创建 browser.newPage([options]) 时使用 storageState 属性,以便向其传递现有的登录状态。

会话存储 (Session Storage)

在极少数情况下,session storage 会被用于存储与登录状态相关的信息。会话存储特定于某个特定域,且不会在页面加载之间持久保存。Playwright 没有提供持久化会话存储的 API,但可以使用以下代码片段来保存/加载会话存储。

// 获取 session storage 并将其存储为环境变量
const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
process.env.SESSION_STORAGE = sessionStorage;

// 在新上下文中设置 session storage
const sessionStorage = process.env.SESSION_STORAGE;
await context.addInitScript(storage => {
if (window.location.hostname === 'example.com') {
const entries = JSON.parse(storage);
for (const [key, value] of Object.entries(entries)) {
window.sessionStorage.setItem(key, value);
}
}
}, sessionStorage);

多重身份验证 (MFA)

使用多重身份验证 (MFA) 的账户无法完全自动化,通常需要人工干预。持久化身份验证可用于部分自动化 MFA 场景。

持久化身份验证

请注意,持久化身份验证不适合 CI 环境,因为它依赖于磁盘位置。用户数据目录特定于浏览器类型,无法在不同浏览器类型之间共享。

用户数据目录可以与 browserType.launchPersistentContext(userDataDir[, options]) API 一起使用。

const { chromium } = require('playwright');

const userDataDir = '/path/to/directory';
const context = await chromium.launchPersistentContext(userDataDir, { headless: false });
// 在浏览器窗口中手动执行登录步骤

生命周期

  1. 在磁盘上创建一个用户数据目录。
  2. 使用该用户数据目录启动持久化上下文,并登录 MFA 账户。
  3. 重用该用户数据目录运行自动化场景。

手动重用登录状态

以下代码片段从已认证的上下文中检索状态,并使用该状态创建一个新上下文。

// 将存储状态保存到文件中。
await context.storageState({ path: 'state.json' });

// 使用保存的存储状态创建一个新上下文。
const context = await browser.newContext({ storageState: 'state.json' });