https://playwright.dev/docs/auth
Giới thiệu
- Playwright chạy các test ở các context riêng biệt, không dùng chung cookie, localStorage. Việc này giúp các test độc lập với nhau, không bị phụ thuộc.
- Mô hình này giúp Playwright chạy test trong các điều kiện giống nhau, dễ tái hiện khi có bug.
- Tuy nhiên nếu bạn muốn dùng lại thì cũng có cách luôn.
Core concepts
- Dù là xác thực kiểu gì đi nữa, thì bạn cũng sẽ lưu dữ liệu xác thực ở cookie hoặc localStorage.
- Bạn nên tạo thư mục
playwright/.authđể quản lý authentication state của trình duyệt và thêm thư mục đó và.gitignoređể đẩy lên git. - Khi chạy test đầu tiên, authentication state của trình duyệt sẽ được lưu vào
playwright/.auth, giúp tiết kiệm thời gian xác thực cho mỗi lần kiểm thử. - Đây là cách tạo folder và thêm vào gitignore dùng command line:
mkdir -p playwright/.auth
echo "\nplaywright/.auth" >> .gitignore
Cách đơn giản: share account dùng chung cho tất cả các test
- Nếu các test của bạn là độc lập với nhau, bạn có thể chạy
setup projectlần đầu, sau đó lưu lại authentication state để dùng lại trong các test sau. - Bạn hiểu đơn giản là test của bạn không làm thay đổi dữ liệu làm ảnh hưởng đến test khác thì dùng cách này được.
- Một số trường hợp không nên dùng:
- Ví dụ một test đổi theme, một test kiểm tra UI ~> conflict lẫn nhau.
- Test trên các trình duyệt khác nhau.
- Cách code như sau:
- Tạo file
tests/auth.setup.ts:
- Tạo file
import { test as setup, expect } from '@playwright/test';
const authFile = '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 });
});
- Tại phần projects file
playwright.config.ts, tạo projectsetupvà khai báo nó làdependencycủa tất cả các projects khác. Projectsetupsẽ luôn được chạy và thực hiện việc xác thực trước các test:
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'],
},
],
});
- Làm như trên thì các test đã được login sẵn rồi, bạn không cần login lại nữa.
import { test } from '@playwright/test';
test('test', async ({ page }) => {
// page is authenticated
});
Xác thực với UI mode
- Mặc định thì UI mode sẽ không chạy
setupproject. - Vì vậy nếu có thì bạn cần lưu ý chạy tay trước.
Cách khó hơn chút: mỗi tài khoản trên một worker
- Cách này giải quyết vấn đề của cách đơn giản bằng cách:
- Cho mấy test liên quan đến nhau chạy cùng 1 worker
- Mỗi worker config dùng một account.
- Để implement điều này, bạn làm như sau:
- Tạo file
playwright/fixtures.tssẽ ghi đèstorageStatefixture để thực hiện authenticate cho mỗi worker. - Sử dụng
testInfo.parallelIndexcho các worker khác nhau.
- Tạo file
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' }],
});
- Từ giờ, chúng ta có thể sử dụng fixture
testthay cho@playwright/testđể sử dụng authentication state có sẵn:
// Important: import our fixtures.
import { test, expect } from '../playwright/fixtures';
test('test', async ({ page }) => {
// page is authenticated
});
Một vài cách nâng cao hơn
Xác thực với API
- Nếu dùng API làm nhanh hơn là click ở UI thì nên dùng API.
- Có mấy cách để xử lý: dùng
setup project, dùng fixture. - Trường hợp sử dụng
setup project:
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 });
});
- Trường hợp sử dụng worker fixture
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' }],
});
Authen với nhiều roles khác nhau
- Đại khái là sẽ lưu thành 2 file state khác nhau. Trong test dùng file nào thì define vào.
- Đầu tiên thì trong file setup định nghĩa nhiều setup khác nhau, mỗi setup lưu vào 1 file authentication state khác nhau.
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 });
});
- Bây giờ storageState không định nghĩa trong file
playwright.config.tsnữa, mà dùng file nào thì trong test define ra (dùngtest.use):
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
});
});
- Trường hợp bạn muốn authen nhiều role trong cùng một test, sử dụng browserContext:
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();
});
Test multiple roles với POM fixtures
- Cũng khá đơn giản: sử dụng POM kết hợp với fixture để tổ chức code cho gọn gàng
import { test as base, type Page, type Locator } from '@playwright/test';
class AdminPage {
// Page signed in as "admin".
page: Page;
greeting: Locator;
constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}
class UserPage {
// Page signed in as "user".
page: Page;
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();
},
});
- Sau khi tạo fixture, chúng ta có thể sử dụng fixture để khởi tạo thông tin xác thực cho hai người dùng với vai role khác nhau:
// 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
- Thường thì authentication state sẽ lưu ở cookie và localStorage.
- Tuy nhiên có vài trường hợp cá biệt lưu vào
sessionStorage. - Playwright không cung cấp API để tương tác với
sessionStorage, tuy nhiên có thể workaround như sau:
// 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);
Ignore authentication trong 1 số test
- Giả sử bây giờ bạn cần bỏ authentication trong một số test đi, thì hãy define storageState rỗng:
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 }) => {
// ...
});
Trả lời