Skip to main content

Authentication

簡介

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

核心概念

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

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

danger

瀏覽器狀態檔案可能包含敏感的 cookies 和標頭,這些可能被用來冒充您或您的測試帳戶。我們強烈建議不要將它們提交到私人或公共儲存庫。

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 }) => {
// 執行驗證步驟。將這些操作替換為您自己的。
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();
// 等待頁面接收 cookies。
//
// 有時登入流程會在幾次重新導向的過程中設定 cookies。
// 等待最終 URL 以確保 cookies 實際上已設定。
await page.waitForURL('https://github.com/');
// 或者,您可以等待頁面達到所有 cookies 都已設定的狀態。
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// 驗證步驟結束。

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

在設定中建立一個新的 setup 專案,並將其宣告為所有測試專案的依賴項。這個專案將始終執行並在所有測試之前進行驗證。所有測試專案都應該使用已驗證的狀態作為 storageState

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

export default defineConfig({
projects: [
// 設定專案
{ name: 'setup', testMatch: /.*\.setup\.ts/ },

{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// 使用準備好的驗證狀態。
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},

{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
// 使用準備好的驗證狀態。
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});

測試開始時已經經過驗證,因為我們在設定中指定了 storageState

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

test('test', async ({ page }) => {
// page 已經經過驗證
});

請注意,當儲存的狀態過期時,您需要刪除它。如果您不需要在測試執行之間保持狀態,請將瀏覽器狀態寫入testProject.outputDir下,這會在每次測試執行前自動清理。

在 UI 模式中進行驗證

預設情況下,UI 模式不會執行 setup 專案以提高測試速度。我們建議通過手動執行 auth.setup.ts 進行驗證,當現有驗證過期時偶爾執行。

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

中等:每個平行工作者一個帳戶

這是針對修改伺服器端狀態的測試的建議方法。在 Playwright 中,工作者程序以平行方式執行。在這種方法中,每個平行工作者都會進行一次驗證。由工作者執行的所有測試都重用相同的驗證狀態。我們需要多個測試帳戶,每個平行工作者一個。

何時使用

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

何時不使用

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

詳細說明

我們將為每個工作者程序進行一次驗證,每個都使用唯一的帳戶。

建立 playwright/fixtures.ts 檔案,它將覆寫 storageState 佈置以便每個工作者進行一次驗證。使用 testInfo.parallelIndex 來區分工作者。

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 }>({
// 為此工作者中的所有測試使用相同的儲存狀態。
storageState: ({ workerStorageState }, use) => use(workerStorageState),

// 使用工作者範圍的佈置為每個工作者進行一次驗證。
workerStorageState: [async ({ browser }, use) => {
// 使用 parallelIndex 作為每個工作者的唯一識別碼。
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);

if (fs.existsSync(fileName)) {
// 如果有的話,重用現有的驗證狀態。
await use(fileName);
return;
}

// 重要:確保我們在乾淨的環境中進行驗證,取消設定儲存狀態。
const page = await browser.newPage({ storageState: undefined });

// 取得唯一帳戶,例如建立新帳戶。
// 或者,您可以有一個預先建立的測試帳戶清單。
// 確保帳戶是唯一的,這樣多個團隊成員
// 可以同時執行測試而不會互相干擾。
const account = await acquireAccount(id);

// 執行驗證步驟。將這些操作替換為您自己的。
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();
// 等待頁面接收 cookies。
//
// 有時登入流程會在幾次重新導向的過程中設定 cookies。
// 等待最終 URL 以確保 cookies 實際上已設定。
await page.waitForURL('https://github.com/');
// 或者,您可以等待頁面達到所有 cookies 都已設定的狀態。
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// 驗證步驟結束。

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

現在,每個測試檔案都應該從我們的佈置檔案中導入 test,而不是從 @playwright/test。設定中不需要任何變更。

tests/example.spec.ts
// 重要:導入我們的佈置。
import { test, expect } from '../playwright/fixtures';

test('test', async ({ page }) => {
// page 已經經過驗證
});

進階情境

使用 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 }) => {
// 發送驗證請求。替換為您自己的。
await request.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
await request.storageState({ path: authFile });
});

或者,在工作者佈置中:

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 }>({
// 為此工作者中的所有測試使用相同的儲存狀態。
storageState: ({ workerStorageState }, use) => use(workerStorageState),

// 使用工作者範圍的佈置為每個工作者進行一次驗證。
workerStorageState: [async ({}, use) => {
// 使用 parallelIndex 作為每個工作者的唯一識別碼。
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);

if (fs.existsSync(fileName)) {
// 如果有的話,重用現有的驗證狀態。
await use(fileName);
return;
}

// 重要:確保我們在乾淨的環境中進行驗證,取消設定儲存狀態。
const context = await request.newContext({ storageState: undefined });

// 取得唯一帳戶,例如建立新帳戶。
// 或者,您可以有一個預先建立的測試帳戶清單。
// 確保帳戶是唯一的,這樣多個團隊成員
// 可以同時執行測試而不會互相干擾。
const account = await acquireAccount(id);

// 發送驗證請求。替換為您自己的。
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 }) => {
// 執行驗證步驟。將這些操作替換為您自己的。
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();
// 等待頁面接收 cookies。
//
// 有時登入流程會在幾次重新導向的過程中設定 cookies。
// 等待最終 URL 以確保 cookies 實際上已設定。
await page.waitForURL('https://github.com/');
// 或者,您可以等待頁面達到所有 cookies 都已設定的狀態。
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// 驗證步驟結束。

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

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

setup('authenticate as user', async ({ page }) => {
// 執行驗證步驟。將這些操作替換為您自己的。
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();
// 等待頁面接收 cookies。
//
// 有時登入流程會在幾次重新導向的過程中設定 cookies。
// 等待最終 URL 以確保 cookies 實際上已設定。
await page.waitForURL('https://github.com/');
// 或者,您可以等待頁面達到所有 cookies 都已設定的狀態。
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();

// 驗證步驟結束。

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 已驗證為管理員
});

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

test('user test', async ({ page }) => {
// page 已驗證為使用者
});
});

另請參見在 UI 模式中進行驗證

一起測試多個角色

何時使用

  • 您需要在單一測試中測試多個已驗證角色如何互動。

詳細說明

在同一個測試中使用多個 BrowserContext 和具有不同儲存狀態的 Page

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

test('admin and user', async ({ browser }) => {
// adminContext 及其內部的所有頁面(包括 adminPage)都已登入為「admin」。
const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = await adminContext.newPage();

// userContext 及其內部的所有頁面(包括 userPage)都已登入為「user」。
const userContext = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = await userContext.newPage();

// ... 與 adminPage 和 userPage 互動 ...

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

使用 POM 佈置測試多個角色

何時使用

  • 您需要在單一測試中測試多個已驗證角色如何互動。

詳細說明

您可以引入佈置來提供以每個角色驗證的頁面。

下面是一個建立佈置用於兩個頁面物件模型的範例 - 管理員 POM 和使用者 POM。它假設 adminStorageState.jsonuserStorageState.json 檔案是在全域設定中建立的。

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

// 「admin」頁面的頁面物件模型。
// 在這裡您可以新增特定於管理員頁面的定位器和輔助方法。
class AdminPage {
// 以「admin」身份登入的頁面。
page: Page;

// 指向「Welcome, Admin」問候語的範例定位器。
greeting: Locator;

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

// 「user」頁面的頁面物件模型。
// 在這裡您可以新增特定於使用者頁面的定位器和輔助方法。
class UserPage {
// 以「user」身份登入的頁面。
page: Page;

// 指向「Welcome, User」問候語的範例定位器。
greeting: Locator;

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

// 宣告您的佈置類型。
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, expect } from '../playwright/fixtures';

// 在測試中使用 adminPage 和 userPage 佈置。
test('admin and user', async ({ adminPage, userPage }) => {
// ... 與 adminPage 和 userPage 互動 ...
await expect(adminPage.greeting).toHaveText('Welcome, Admin');
await expect(userPage.greeting).toHaveText('Welcome, User');
});

Session storage

重用已驗證狀態涵蓋基於 cookies本地儲存IndexedDB 的驗證。很少情況下,session storage 用於儲存與已登入狀態相關的資訊。Session storage 特定於特定網域,不會跨頁面載入持續存在。Playwright 不提供持續存儲 session storage 的 API,但可以使用以下程式碼片段來儲存/載入 session storage。

// 取得 session storage 並儲存為環境變數
const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
fs.writeFileSync('playwright/.auth/session.json', sessionStorage, 'utf-8');

// 在新情境中設定 session storage
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';

// 為此檔案重設儲存狀態以避免被驗證
test.use({ storageState: { cookies: [], origins: [] } });

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