Mục lục
- Mục Tiêu Bài Học
- storageState Là Option Fixture — Không Phải Config Thụ Động
- Configure Global Qua
use: - Hai Dạng Giá Trị: String Path Vs Object Inline
- Per-File Override
- Per-Describe Override — Multi-Role Trong Cùng File
- Reset Về Guest: Empty Object Override
- Multi-Role Project Matrix
- Setup Project Dependency — CI Pattern (Tổng Quan)
- Reset State Runtime Với
context.clearCookies() - IndexedDB Và
setStorageState()— Mention Qua - 4 Pitfalls Thực Tế
- Tổng Kết
- Quiz Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Hiểu
storageStatehoạt động ở tầng fixture như thế nào — tại sao nó là Option Fixture, không phải config thụ động trongplaywright.config.ts. - Configure
storageStateở globaluse:, per-project, per-file, và per-describe đúng cách. - Biết hai dạng giá trị: string path tới file JSON và object inline
{ cookies, origins }. - Thiết lập multi-role project matrix (admin / user / guest) sử dụng
storageStateper-project. - Reset về trạng thái guest đúng cách bằng empty object — không phải
undefined. - Nắm được pattern setup project dependency trên CI (tổng quan, không deep dive).
- Tránh 4 pitfall phổ biến liên quan file không tồn tại, token hết hạn, worker context conflict, và reset không đúng cách.
Series 1 (bài 307–316) đã cover storageState cơ bản: khái niệm, cú pháp save/load, cấu trúc JSON, folder convention playwright/.auth/. Bài này không lặp lại những nội dung đó — focus vào cách storageState hoạt động với tư cách Option Fixture và các pattern override theo layer.
storageState Là Option Fixture — Không Phải Config Thụ Động
Trong Playwright Test, storageState được định nghĩa như một Option Fixture với annotation { option: true }. Điều này khác với một key thông thường trong playwright.config.ts:
// Simplified từ source packages/playwright/src/common/config.ts
storageState: [undefined, { option: true }],
Vì là Option Fixture, storageState tham gia vào fixture pipeline đầy đủ — có thể override ở bất kỳ layer nào qua use:. Và quan trọng hơn, giá trị của nó được context fixture đọc khi gọi browser.newContext():
storageState option fixture
│
▼
context fixture → browser.newContext({ storageState: value })
│
▼
page fixture → context.newPage()
│
▼
Test body bắt đầu — cookies + localStorage đã được load vào context
Vì storageState được đặt vào context lúc khởi tạo, test body không cần làm thêm gì — page.goto('/dashboard') đầu tiên sẽ thấy context đã authenticated. Đây là điểm khác biệt so với login thủ công trong beforeAll: với Option Fixture, không có code setup, không có phụ thuộc thứ tự giữa các test.
Layer ưu tiên từ thấp đến cao:
- Global
use.storageStatetrongdefineConfig() - Project-level
projects[i].use.storageState - File-level
test.use({ storageState: ... })đặt ngoài mọitest() - Describe-level
test.use({ storageState: ... })đặt bên trongtest.describe()
Layer càng gần test body càng có độ ưu tiên cao hơn. Project-level bị ghi đè bởi file-level, file-level bị ghi đè bởi describe-level.
Configure Global Qua use:
Khi toàn bộ test suite chạy dưới một identity duy nhất (ví dụ: app không có phân quyền phức tạp, mọi test đều login như user), đặt storageState ở global use::
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
storageState: 'playwright/.auth/user.json',
},
});
Với config này, mọi test trong suite — bất kể project nào, bất kể file nào — đều khởi tạo context với state của user.json. Không cần viết login code trong bất kỳ test nào.
Khi chỉ một phần test cần auth, dùng project-level thay vì global — xem mục 8 (Multi-Role Project Matrix).
Hai Dạng Giá Trị: String Path Vs Object Inline
storageState nhận một trong hai dạng giá trị:
String — path tới file JSON
use: {
storageState: 'playwright/.auth/user.json',
}
Path được resolve từ thư mục playwright.config.ts (cùng quy tắc với testDir). File JSON phải tồn tại tại thời điểm test chạy — nếu không Playwright ném lỗi ENOENT: no such file or directory.
Nội dung file JSON có cấu trúc chuẩn:
{
"cookies": [
{
"name": "session",
"value": "abc123",
"domain": "localhost",
"path": "/",
"expires": 1800000000,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{ "name": "authToken", "value": "eyJhbGci..." },
{ "name": "userId", "value": "42" }
]
}
]
}
Cấu trúc JSON chi tiết đã được phân tích trong Series 1 bài 312 — bài này không lặp lại.
Object inline
// Dùng khi muốn set state không có file trên disk
test.use({
storageState: {
cookies: [],
origins: [],
},
});
Object inline thường dùng để reset về trạng thái guest (không có cookies, không có localStorage) — chi tiết ở mục 7. Cũng có thể inject state nhỏ trực tiếp vào test mà không cần tạo file:
test.use({
storageState: {
cookies: [],
origins: [
{
origin: 'http://localhost:3000',
localStorage: [
{ name: 'featureFlag', value: 'new-ui' },
],
},
],
},
});
Dùng object inline hợp lý khi state nhỏ, không cần chia sẻ giữa nhiều file. Khi state lớn hoặc cần reuse, dùng file JSON.
Per-File Override
Khi global config đặt storageState là user.json, một file test cần chạy với quyền admin có thể override toàn bộ file bằng test.use() đặt ở file-scope (ngoài mọi test() và describe()):
// admin.spec.ts
import { test, expect } from '@playwright/test';
// Override cho toàn bộ file này — mọi test trong file dùng admin.json
test.use({ storageState: 'playwright/.auth/admin.json' });
test('admin can delete users', async ({ page }) => {
await page.goto('/admin/users');
// Context đã loaded admin.json — đang login như admin
await page.getByRole('button', { name: 'Delete user' }).first().click();
await expect(page.getByText('User deleted')).toBeVisible();
});
test('admin can view audit log', async ({ page }) => {
await page.goto('/admin/audit-log');
// Vẫn là admin — cùng file-level storageState
await expect(page.getByRole('table')).toBeVisible();
});
Lưu ý: test.use() bên trong test body không có tác dụng — Option Fixture được đọc trước khi test body chạy, ở giai đoạn fixture resolution:
// SAI — không override được
test('wrong', async ({ page }) => {
test.use({ storageState: 'playwright/.auth/admin.json' }); // bị bỏ qua
await page.goto('/admin');
// context vẫn dùng storageState từ config cũ
});
Per-Describe Override — Multi-Role Trong Cùng File
Khi một file test cần kiểm tra hành vi khác nhau theo role, đặt test.use() bên trong từng describe block:
// dashboard-access.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Admin tests', () => {
test.use({ storageState: 'playwright/.auth/admin.json' });
test('admin sees management panel', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('link', { name: 'Management' })).toBeVisible();
});
test('admin can access settings', async ({ page }) => {
await page.goto('/dashboard/settings');
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
});
});
test.describe('Regular user tests', () => {
test.use({ storageState: 'playwright/.auth/user.json' });
test('user does not see management panel', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('link', { name: 'Management' })).not.toBeVisible();
});
});
test.describe('Guest tests', () => {
test.use({ storageState: { cookies: [], origins: [] } }); // empty = no auth
test('guest is redirected to login', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
});
});
Pattern trên cho phép test các role khác nhau trong cùng một file mà không cần tách ra nhiều file. Tuy nhiên, cần biết về worker behavior khi dùng per-describe storageState — xem Pitfall 3 trong mục 12.
Reset Về Guest: Empty Object Override
Khi global hoặc project config đặt storageState để auth, một số test cần chạy như guest (không có auth). Cách reset về guest:
// Đặt ở file-scope — toàn bộ file chạy như guest
test.use({ storageState: { cookies: [], origins: [] } });
test('login form renders correctly', async ({ page }) => {
// Bắt đầu không có cookies, không có localStorage auth
await page.goto('/login');
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
});
test('forgot password link works', async ({ page }) => {
await page.goto('/login');
await page.getByRole('link', { name: 'Forgot password?' }).click();
await expect(page).toHaveURL('/forgot-password');
});
Lý do dùng { cookies: [], origins: [] } thay vì undefined:
storageState: undefinednghĩa là "dùng giá trị mặc định từ layer trên" — không phải reset. Nếu project config đang setuser.json, test dùngundefinedvẫn sẽ loaduser.json.storageState: { cookies: [], origins: [] }là giá trị hợp lệ, ghi đè rõ ràng lên layer trên — context được tạo với state rỗng.
// SAI — không reset về guest, chỉ fallback về default
test.use({ storageState: undefined }); // vẫn dùng user.json từ project config
// ĐÚNG — reset hoàn toàn về guest
test.use({ storageState: { cookies: [], origins: [] } });
Multi-Role Project Matrix
Pattern phổ biến trong test suite có phân quyền là định nghĩa một project cho mỗi role, mỗi project dùng storageState riêng. Toàn bộ test của role đó được assign cho project tương ứng qua testMatch:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'admin',
use: { storageState: 'playwright/.auth/admin.json' },
testMatch: /admin\..*\.spec\.ts/,
},
{
name: 'user',
use: { storageState: 'playwright/.auth/user.json' },
testMatch: /user\..*\.spec\.ts/,
},
{
name: 'guest',
// Không set storageState → context khởi tạo không có auth
testMatch: /guest\..*\.spec\.ts/,
},
],
});
File test được đặt tên theo convention role:
tests/
├── admin.users.spec.ts → chạy với project 'admin'
├── admin.settings.spec.ts → chạy với project 'admin'
├── user.dashboard.spec.ts → chạy với project 'user'
├── user.profile.spec.ts → chạy với project 'user'
└── guest.landing.spec.ts → chạy với project 'guest'
Ưu điểm của pattern này so với per-describe override:
- Mỗi project chạy song song độc lập — không có worker conflict.
- HTML report phân tách rõ theo project name — dễ xác định role nào fail.
- File test gọn hơn — không cần khai báo
test.use()trong mỗi file.
Nhược điểm: khi cần test cross-role trong cùng file (ví dụ: verify admin thấy thứ gì mà user không thấy), vẫn phải dùng per-describe override.
Setup Project Dependency — CI Pattern (Tổng Quan)
Trên CI, file playwright/.auth/*.json không tồn tại sẵn — cần được sinh ra trước khi test chính chạy. Pattern phổ biến là dùng setup project với dependencies:
// playwright.config.ts
export default defineConfig({
projects: [
// 1. Setup project: login UI → lưu storageState
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// 2. Main project: depend vào setup → load storageState
{
name: 'e2e',
use: { storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
});
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate as user', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
// Lưu state sau khi login thành công
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
Setup project chạy một lần trước toàn bộ project phụ thuộc — không phải trước mỗi test. Pattern này phổ biến trong CI workflow vì:
- Login UI chỉ chạy 1 lần dù có hàng trăm test.
- File state được commit vào disk, load lại giữa các test mà không cần browser extra.
- Playwright đảm bảo thứ tự chạy: setup xong mới chạy e2e.
Chương A.6 sẽ deep dive setup project pattern — dependencies matrix, parallel setup cho multi-role, cleanup sau test run. Bài này chỉ cho thấy big picture.
Reset State Runtime Với context.clearCookies()
Đôi khi test cần xóa state trong runtime — ví dụ: kiểm tra behavior sau khi logout, hoặc verify redirect khi session cookie bị xóa. Dùng context.clearCookies() và/hoặc context.clearPermissions():
test('session expired → redirect to login', async ({ page, context }) => {
// context đã load storageState (user đang login)
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Simulate session expire: xóa cookies
await context.clearCookies();
// Navigate → expect redirect về login
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
});
Để xóa cả localStorage (token lưu trong origin), cần dùng page.evaluate():
test('logout clears auth state', async ({ page, context }) => {
await page.goto('/');
// Xóa toàn bộ auth state
await context.clearCookies();
await page.evaluate(() => window.localStorage.clear());
await page.reload();
await expect(page).toHaveURL('/login');
});
Khác với Option Fixture (set khi tạo context), clearCookies() là thao tác runtime — ảnh hưởng từ thời điểm gọi trở đi trong cùng context. Các test khác chạy với context mới (do page-scope fixture) không bị ảnh hưởng.
Lưu ý: context.storageState({ path: 'temp.json' }) trả về state hiện tại của context — hữu ích để debug: gọi trước và sau clearCookies để xác nhận state đã thay đổi.
IndexedDB Và setStorageState() — Mention Qua
Hai tính năng mới liên quan storageState cần biết để tránh nhầm lẫn, nhưng sẽ được deep dive ở chương B (Authentication nâng cao):
IndexedDB support (v1.51+)
Từ v1.51, Playwright thêm khả năng capture IndexedDB vào storageState. Mặc định, context.storageState() chỉ lưu cookies và localStorage. IndexedDB không tự động được include — phải config riêng khi save.
Bài này không deep dive vì IndexedDB storageState có nhiều edge case (size limit, serialization của binary data, browser compatibility). Chương B sẽ cover đầy đủ.
context.setStorageState() (v1.59+)
Từ v1.59, có API context.setStorageState(state) cho phép đổi state của context đang chạy mà không cần tạo context mới. Hữu ích khi test cần switch identity trong runtime — ví dụ: test flow "admin approve → user receive notification" trong cùng một test.
Chương B sẽ deep dive pattern này. Ở đây chỉ cần biết API tồn tại và không nhầm với context.storageState() (đọc state) vs context.setStorageState() (ghi state).
4 Pitfalls Thực Tế
1. File auth.json không tồn tại — test fail với ENOENT
// playwright.config.ts
use: { storageState: 'playwright/.auth/user.json' }
// Lỗi khi chạy nếu file chưa được tạo:
// Error: ENOENT: no such file or directory, open 'playwright/.auth/user.json'
Nguyên nhân: CI checkout mới, không có file auth. Fix: dùng setup project dependency để sinh file trước khi test chính chạy. Không commit file auth vào git (có chứa credentials).
Nếu cần fallback graceful (chạy mà không có auth khi file không tồn tại), kiểm tra file tồn tại trước:
import { defineConfig } from '@playwright/test';
import { existsSync } from 'fs';
const authFile = 'playwright/.auth/user.json';
export default defineConfig({
use: {
storageState: existsSync(authFile) ? authFile : undefined,
},
});
2. JWT Token trong storageState đã expire
storageState lưu giá trị tĩnh tại thời điểm save. Nếu app dùng JWT với thời hạn ngắn (ví dụ 1 giờ), file state sinh lúc sáng sẽ hết hạn vào chiều:
// Dấu hiệu: test fail với lỗi 401 Unauthorized hoặc redirect về /login
// dù storageState được set đúng
// Không phải bug của Playwright — là token hết hạn
// Fix: refresh file state thường xuyên hơn, hoặc dùng refresh token trong setup script
Với app có JWT expiry ngắn, cần run setup project ở đầu mỗi CI job (không cache state giữa job). Hoặc dùng API login thay vì UI login trong setup script để nhanh hơn.
3. Per-describe storageState override và worker context sharing
storageState là test-scope option (khác với headless là worker-scope). Mỗi test tạo context mới với storageState của describe block đó — không share context giữa describe blocks khác nhau. Tuy nhiên, nếu dùng test.describe.configure({ mode: 'serial' }) với shared context, storageState chỉ được apply lần đầu:
// Cẩn thận với serial mode + shared context
test.describe.configure({ mode: 'serial' });
test.describe('Admin serial tests', () => {
test.use({ storageState: 'playwright/.auth/admin.json' });
// OK — storageState apply cho mỗi test riêng (mỗi test có context mới)
});
// Nguy hiểm: khi dùng context fixture trực tiếp với beforeAll
// trong serial mode — context được tạo 1 lần trong beforeAll,
// storageState từ test.use() có thể không apply đúng
Tránh kết hợp test.describe.configure({ mode: 'serial' }) + shared context fixture + per-describe storageState override nếu không hiểu rõ lifecycle.
4. storageState: undefined không reset về guest
// Tình huống: project config set storageState: 'playwright/.auth/user.json'
// Dev muốn một describe block chạy như guest
// SAI — undefined không ghi đè, fallback về project config (user.json)
test.describe('Guest flow', () => {
test.use({ storageState: undefined }); // không reset!
test('should redirect to login', async ({ page }) => {
await page.goto('/dashboard');
// page.goto('/login') không xảy ra — context vẫn load user.json
await expect(page).toHaveURL('/dashboard'); // pass sai
});
});
// ĐÚNG — empty object ghi đè rõ ràng
test.describe('Guest flow', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('should redirect to login', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL('/login'); // đúng
});
});
Tổng Kết
storageStatelà Option Fixture — đượccontextfixture đọc khi gọibrowser.newContext(). Test body nhận context đã có auth, không cần login code.- Nhận hai dạng giá trị: string path tới file JSON hoặc object inline
{ cookies, origins }. - Override per-file: đặt
test.use()ở file-scope (ngoài mọitest()). Override per-describe: đặt bên trongtest.describe(). Không đặttest.use()bên trong test body. - Reset về guest: dùng
{ cookies: [], origins: [] }— không phảiundefined.undefinedfallback về layer trên. - Multi-role project matrix: một project per role,
testMatchtheo naming convention. Sạch hơn per-describe khi test không cần cross-role trong cùng file. - Setup project dependency: sinh file auth trước khi test chính chạy — pattern chuẩn trên CI. Deep dive ở chương A.6.
- IndexedDB support từ v1.51 — không auto-include.
context.setStorageState()từ v1.59 — đổi state runtime. Cả hai deep dive ở chương B. - 4 pitfall chính: file không tồn tại (ENOENT), token expire, serial mode + shared context conflict, và
undefinedkhông reset về guest.
Quiz Củng Cố
Câu 1
Đoạn config sau có vấn đề gì khi chạy trên CI lần đầu tiên (fresh checkout)?
export default defineConfig({
use: {
storageState: 'playwright/.auth/user.json',
},
});
Đáp án
Trên CI fresh checkout, file playwright/.auth/user.json không tồn tại. Playwright sẽ throw ENOENT: no such file or directory khi cố load storageState. Fix: thêm setup project sinh file trước khi test chính chạy, hoặc dùng existsSync(authFile) ? authFile : undefined để fallback graceful.
Câu 2
Tại sao đoạn code sau không reset context về trạng thái guest, dù dev có ý định như vậy?
// playwright.config.ts
use: { storageState: 'playwright/.auth/user.json' }
// guest.spec.ts
test.use({ storageState: undefined });
test('should see login page', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
});
Đáp án
storageState: undefined trong test.use() không ghi đè giá trị từ layer trên — nó chỉ "không cung cấp giá trị", khiến Playwright fallback về project/global config (user.json). Test vẫn chạy với context đã authenticated. Để reset thực sự về guest, phải dùng empty object: test.use({ storageState: { cookies: [], origins: [] } }).
Câu 3
Khi đặt test.use({ storageState: 'playwright/.auth/admin.json' }) bên trong test.describe(), Playwright apply storageState này như thế nào — cho toàn file hay chỉ cho describe block?
Đáp án
Chỉ áp dụng cho các test bên trong describe block đó. Các test bên ngoài describe block (hoặc trong describe block khác không có test.use()) vẫn dùng storageState từ layer cao hơn (file-level hoặc project/global config). Layer rule: describe-level override file-level, file-level override project-level, project-level override global.
Câu 4
Trong multi-role project matrix, tại sao pattern dùng testMatch per-project ưu việt hơn per-describe override trong cùng file, xét về mặt isolation và reporting?
Đáp án
Với testMatch per-project: mỗi project chạy hoàn toàn độc lập với worker riêng — không có nguy cơ context leaking giữa các role. HTML report tách rõ theo project name (admin / user / guest), dễ xác định role nào fail. Mỗi test file chỉ thuộc một role — không cần khai báo test.use() lặp lại. Ngược lại, per-describe trong cùng file tuy linh hoạt cho cross-role test nhưng phức tạp hơn khi debug, report không tách role rõ ràng, và dễ nhầm lẫn nếu describe nesting phức tạp.
Câu 5
Một test dùng storageState với JWT token có expiry 2 giờ. CI job chạy từ 8:00 AM — setup project login lúc 8:05 AM sinh ra user.json. Test suite gồm 300 test bắt đầu chạy song song từ 8:06 AM. Test cuối chạy lúc 10:10 AM. Điều gì sẽ xảy ra với các test chạy sau 10:05 AM?
Đáp án
JWT token trong user.json được sinh lúc 8:05 AM với expiry 2 giờ — token expire lúc 10:05 AM. Các test chạy sau 10:05 AM sẽ dùng context với token đã hết hạn. App server sẽ từ chối request (401 Unauthorized) hoặc redirect về login. Các test này sẽ fail — thường báo lỗi "Expected URL '/dashboard' but got '/login'" hoặc API response 401. Fix: rút ngắn thời gian chạy test suite (tăng parallelism), hoặc config setup project sinh state dùng refresh token, hoặc dùng API login với long-lived session thay vì JWT ngắn hạn.
Bài Tiếp Theo
Bài 13 tiếp tục nhóm Options Fixtures với video, screenshot, và trace — ba option kiểm soát artifact được ghi lại trong quá trình test, quan trọng cho debugging và CI reporting.
