Skip to main content

身份驗證

簡介

Playwright 在稱為瀏覽器上下文的隔離環境中執行測試。這種隔離模型提高了可重現性並防止連鎖測試失敗。測試可以加載現有的已驗證狀態。這消除了在每個測試中進行身份驗證的需要,並加快了測試執行速度。

核心概念

無論您選擇哪種驗證策略,您可能會將已驗證的瀏覽器狀態存儲在檔案系統上。

我們建議建立 playwright/.auth 目錄並將其添加到 .gitignore 中。您的身份驗證程序將生成已驗證的瀏覽器狀態並將其保存到此 playwright/.auth 目錄中的檔案。稍後,測試將重用此狀態並以已驗證的方式開始。

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

基本: 所有測試中的共用帳戶

這是推薦的方法,用於沒有伺服器端狀態的測試。在設定專案中進行一次身份驗證,保存身份驗證狀態,然後重複使用它來啟動每個已經驗證的測試。

何時使用

  • 當你可以想像所有的測試同時使用相同的帳戶執行,而不會互相影響時。

何時不使用

  • 你的測試會修改伺服器端的狀態。例如,一個測試檢查設定頁面的渲染,而另一個測試正在更改設定,並且你平行執行測試。在這種情況下,測試必須使用不同的帳戶。
  • 你的身份驗證是特定於瀏覽器的。

細節

建立 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 專案在 config 中,並將其宣告為所有測試專案的相依套件。此專案將在所有測試之前執行並進行驗證。所有測試專案應使用驗證過的狀態作為 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 mode 將不會預設執行 setup 專案以提高測試速度。我們建議在現有身份驗證過期時,偶爾手動執行 auth.setup.ts 來進行身份驗證。

首先在篩選器中啟用 setup 專案,然後點擊 auth.setup.ts 文件旁邊的三角形按鈕,然後再次在篩選器中停用 setup 專案。

Moderate: 每個平行工作者一個帳戶

這是推薦的測試方法,用於修改伺服器端狀態。在 Playwright 中,工作程序平行執行。在這種方法中,每個平行工作程序只需認證一次。所有由工作程序執行的測試都會重用相同的認證狀態。我們將需要多個測試帳戶,每個平行工作程序一個。

何時使用

  • 您的測試會修改共享的伺服器端狀態。例如,一個測試檢查設定頁面的渲染,而另一個測試則在更改設定。

何時不使用

  • 您的測試不會修改任何共享的伺服器端狀態。在這種情況下,所有測試都可以使用單一共享帳戶。

細節

我們將在每個worker process中進行一次身份驗證,每個都有一個唯一的帳戶。

建立 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 請求,然後像往常一樣保存已驗證的狀態。

設定專案 中:

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

多個已登入角色

何時使用

  • 在端到端測試中,你有多個角色,但你可以在所有測試中重複使用帳戶。

細節

我們將在設定專案中多次進行身份驗證。

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。

以下是一個建立測試夾具的範例,適用於兩個頁面物件模型 - admin POM 和 user POM。它假設 adminStorageState.jsonuserStorageState.json 檔案已在全域設定中建立。

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 local storage 基於驗證。很少使用 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 }) => {
// ...
});