Mục lục
- Mục Tiêu Bài Học
- setStorageState() Là Gì
- Cú Pháp
- So Sánh Với newContext({ storageState })
- Use Case 1 — Switch Role Mid-Test
- Use Case 2 — Re-Auth Runtime
- Use Case 3 — Test State Transition
- Behavior Sau Khi Gọi — Reload Bắt Buộc
- Khác clearCookies + addCookies
- Trade-Off Với newContext
- Pre-v1.59 Workaround
- Limitation
- 4 Pitfall
- Quiz
- 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
context.setStorageState()làm gì và tại sao cần đến v1.59 mới có. - Phân biệt rõ sự khác nhau giữa
setStorageState()vàbrowser.newContext({ storageState }). - Áp dụng ba use case: switch role mid-test, re-auth runtime, và test state transition.
- Biết tại sao cần gọi
page.reload()sausetStorageState()và khi nào không cần. - Hiểu sự khác nhau so với
clearCookies+addCookies. - Nhận biết 4 pitfall thực tế và cách tránh.
setStorageState() Là Gì
context.setStorageState() là method được thêm vào v1.59 cho phép nạp một storage state mới vào BrowserContext đang tồn tại — không cần đóng context và tạo lại.
Trước v1.59, storageState chỉ được đặt khi tạo context: qua browser.newContext({ storageState }) hoặc qua option fixture. Khi test cần thay đổi auth state giữa chừng, cách duy nhất là tạo context mới — kéo theo phải tạo page mới và mất toàn bộ trạng thái browser hiện tại.
Từ v1.59, setStorageState() cho phép thay thế state ngay trên context đang chạy:
- Replace toàn bộ cookies và localStorage của context bằng state mới.
- Context vẫn tồn tại — không bị đóng hay tạo lại.
- Page hiện tại giữ nguyên trong DOM; state mới chỉ apply khi page navigate hoặc reload.
Method thuộc BrowserContext — không phải Page. Một context có thể có nhiều page; setStorageState() ảnh hưởng đến mọi page trong context đó.
Cú Pháp
Signature:
// v1.59+
await context.setStorageState(options);
Tham số options nhận cùng hai dạng như browser.newContext({ storageState }):
Dạng 1 — đọc từ file JSON:
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
Dạng 2 — object inline:
await context.setStorageState({
cookies: [
{
name: 'session',
value: 'abc123',
domain: 'localhost',
path: '/',
expires: -1,
httpOnly: true,
secure: false,
sameSite: 'Lax' as const,
},
],
origins: [
{
origin: 'http://localhost:3000',
localStorage: [
{ name: 'authToken', value: 'eyJhbGci...' },
],
},
],
});
Gọi setStorageState() là bất đồng bộ — phải await. Method trả về Promise<void>.
Khác với context.storageState() (đọc state hiện tại ra file hoặc object), context.setStorageState() đi ngược chiều: ghi state mới vào context.
So Sánh Với newContext({ storageState })
Hai cách set storageState có thời điểm khác nhau và tradeoff khác nhau:
| Tiêu chí | newContext({ storageState }) |
context.setStorageState() |
|---|---|---|
| Thời điểm set | Khi TẠO context mới | Bất kỳ lúc nào trong runtime |
| Context lifecycle | Context mới — clean slate | Context hiện tại — reuse |
| Isolation | Hoàn toàn — không có cache hay memory state cũ | Không hoàn toàn — cache trong memory có thể linger |
| Page hiện tại | Phải tạo page mới | Page cũ vẫn còn — cần reload để apply state |
| Tốc độ | Chậm hơn — tạo context mới tốn thêm overhead | Nhanh hơn — reuse context hiện tại |
| Version yêu cầu | Không giới hạn | v1.59+ |
| Use case chính | Khởi đầu test với role cố định | Thay đổi role/state giữa chừng trong test |
Hai cách không thay thế nhau hoàn toàn. Khi cần isolation nghiêm ngặt (không muốn bất kỳ state nào từ identity cũ còn lại), newContext() vẫn là lựa chọn chắc chắn hơn. setStorageState() phù hợp khi test cần mô phỏng chuyển tiếp giữa các state trong cùng một flow.
Use Case 1 — Switch Role Mid-Test
Test cần chứng minh rằng cùng một URL hiển thị khác nhau tùy theo role — user thấy "User View", admin thấy "Admin Panel". Thay vì viết hai test riêng với hai context, có thể dùng setStorageState() để switch role trong cùng một test:
// tests/dashboard-view.spec.ts
import { test, expect } from '@playwright/test';
test('dashboard hiển thị đúng theo role', async ({ context, page }) => {
// Bắt đầu với user state
await context.setStorageState({ path: 'playwright/.auth/user.json' });
await page.goto('/dashboard');
await expect(page.getByText('User View')).toBeVisible();
// Switch sang admin state
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
await page.reload(); // apply state mới
await expect(page.getByText('Admin Panel')).toBeVisible();
});
Pattern này kiểm tra cùng một endpoint (/dashboard) với hai identity liên tiếp, giảm code lặp và giữ context của browser nhất quán giữa hai lần assert.
Lưu ý về thứ tự: setStorageState() phải gọi trước khi navigate/reload. Gọi sau page.goto() không ảnh hưởng đến request đã đi.
Use Case 2 — Re-Auth Runtime
Test cần kiểm tra behavior sau khi token expire và được refresh. Thay vì tạo context mới, có thể inject fresh state vào context hiện tại:
import { test, expect } from '@playwright/test';
import { generateFreshState } from '../helpers/auth';
test('user có thể tiếp tục sau khi token được refresh', async ({ context, page }) => {
// Bắt đầu với state ban đầu
await context.setStorageState({ path: 'playwright/.auth/user.json' });
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Giả lập token expire và refresh: inject fresh state
const freshState = await generateFreshState('user');
await context.setStorageState(freshState);
// Reload để verify app nhận state mới
await page.reload();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Verify không bị redirect về login dù state đã thay đổi
await expect(page).toHaveURL('/dashboard');
});
generateFreshState() trong ví dụ là helper tự viết — gọi API login và trả về object state tươi. Pattern này mô phỏng vòng đời: bắt đầu có auth → token expire → refresh → tiếp tục không gián đoạn.
Use Case 3 — Test State Transition
Test cần kiểm tra các mốc chuyển đổi trong một flow: guest → authenticated → upgraded role. Mỗi mốc cần assert trạng thái khác nhau:
test('flow onboarding: guest → user → premium', async ({ context, page }) => {
// Bước 1: guest — chưa đăng nhập
// Context mặc định không có state → page redirect về login
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
// Bước 2: sau khi đăng nhập thành công → inject user state
await context.setStorageState({ path: 'playwright/.auth/user.json' });
await page.goto('/dashboard');
await expect(page.getByText('Free Plan')).toBeVisible();
// Verify premium features bị ẩn
await expect(page.getByRole('button', { name: 'Export CSV' })).not.toBeVisible();
// Bước 3: sau khi nâng cấp → inject premium state
await context.setStorageState({ path: 'playwright/.auth/premium.json' });
await page.reload();
await expect(page.getByText('Premium Plan')).toBeVisible();
// Verify premium features hiện ra
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeVisible();
});
Pattern này hữu ích khi app thay đổi UI dựa trên subscription level hoặc role, và bạn muốn test toàn bộ flow chuyển tiếp trong một test — không phải ba test riêng biệt.
Trade-off của pattern này: test dài hơn, khi fail khó xác định fail ở bước nào. Với flow phức tạp, cân nhắc thêm test.step() để phân đoạn rõ ràng hơn trong report.
Behavior Sau Khi Gọi — Reload Bắt Buộc
setStorageState() thay đổi state tại tầng BrowserContext — cookies và localStorage được replace. Tuy nhiên page hiện tại không tự động cập nhật:
- DOM hiện tại vẫn giữ nguyên — JavaScript đang chạy trong page không biết state đã thay đổi.
- Cookies mới chỉ được gửi kèm request tiếp theo (sau navigate hoặc reload).
- localStorage mới chỉ được đọc bởi JavaScript sau khi page load lại.
Do đó, sau mỗi lần gọi setStorageState(), cần trigger navigation để apply:
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
// Cách 1 — reload page hiện tại
await page.reload();
// Cách 2 — navigate đến URL mới
await page.goto('/admin');
// Cách 3 — navigate rồi quay lại (nếu muốn giữ URL)
await page.goto('/');
await page.goto('/dashboard');
Nếu quên reload, assert ngay sau setStorageState() sẽ đọc DOM cũ với state cũ — test pass/fail sai. Đây là pitfall phổ biến nhất khi dùng API này.
Trường hợp ngoại lệ duy nhất không cần reload là khi gọi setStorageState() trước lần page.goto() đầu tiên — lúc này không có page nào đang load, state được apply ngay khi navigate.
Khác clearCookies + addCookies
Có thể tự hỏi tại sao không dùng context.clearCookies() + context.addCookies() thay vì setStorageState(). Sự khác biệt là phạm vi:
| Thao tác | Phạm vi |
|---|---|
clearCookies() + addCookies() |
Chỉ cookies |
setStorageState() |
Cookies + localStorage (+ IndexedDB nếu state file có) |
Khi auth state chỉ dùng cookie (session cookie truyền thống), clearCookies() + addCookies() đủ dùng. Nhưng khi app lưu token trong localStorage — phổ biến với SPA dùng JWT — hoặc dùng IndexedDB, chỉ thao tác cookie không đủ để switch identity.
setStorageState() comprehensive hơn vì replace toàn bộ state cùng một lúc, nhất quán với format file JSON được tạo bởi context.storageState(). Không cần biết app lưu auth ở đâu — cứ truyền file state, API lo phần còn lại.
// Chỉ dùng clearCookies + addCookies khi biết chắc app chỉ dùng cookie
await context.clearCookies();
await context.addCookies([{ name: 'session', value: 'xyz', domain: 'localhost', path: '/' }]);
// Dùng setStorageState khi muốn replace toàn bộ state (cookies + localStorage)
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
Trade-Off Với newContext
Khi test cần chạy với hai identity khác nhau, có hai hướng tiếp cận. Mỗi hướng phù hợp với tình huống khác nhau:
Dùng setStorageState()
test('admin vs user view', async ({ context, page }) => {
await context.setStorageState({ path: 'playwright/.auth/user.json' });
await page.goto('/dashboard');
await expect(page.getByText('User View')).toBeVisible();
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
await page.reload();
await expect(page.getByText('Admin Panel')).toBeVisible();
});
Ưu: nhanh hơn (reuse context), ít code hơn. Nhược: state cũ có thể linger trong cache hoặc memory của JavaScript đang chạy — ví dụ in-memory store của framework như Redux, Vuex. Khi app dùng in-memory state phức tạp, switch role bằng setStorageState() có thể không đủ để clear toàn bộ.
Dùng browser.newContext()
test('admin vs user view', async ({ browser }) => {
const userCtx = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = await userCtx.newPage();
await userPage.goto('/dashboard');
await expect(userPage.getByText('User View')).toBeVisible();
await userCtx.close();
const adminCtx = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = await adminCtx.newPage();
await adminPage.goto('/dashboard');
await expect(adminPage.getByText('Admin Panel')).toBeVisible();
await adminCtx.close();
});
Ưu: clean slate hoàn toàn — không có gì từ context cũ còn lại. Nhược: chậm hơn (tạo hai context), code dài hơn, phải tự quản lý close context.
Nguyên tắc chọn: nếu cần mô phỏng chuyển tiếp (transition) giữa state trong cùng browser session → dùng setStorageState(). Nếu cần hai view hoàn toàn độc lập và isolation là ưu tiên số 1 → dùng newContext().
Pre-v1.59 Workaround
Nếu project đang dùng Playwright phiên bản trước v1.59 và chưa thể upgrade, có hai workaround:
Workaround 1 — Tạo context mới
// Playwright < v1.59
test('admin vs user view', async ({ browser }) => {
// User view
const userCtx = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const userPage = await userCtx.newPage();
await userPage.goto('/dashboard');
await expect(userPage.getByText('User View')).toBeVisible();
await userCtx.close();
// Admin view: tạo context mới với admin state
const adminCtx = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
});
const adminPage = await adminCtx.newPage();
await adminPage.goto('/dashboard');
await expect(adminPage.getByText('Admin Panel')).toBeVisible();
await adminCtx.close();
});
Workaround 2 — Thao tác cookie + localStorage thủ công
// Playwright < v1.59 — chỉ dùng khi biết chắc cấu trúc auth state
import adminState from '../playwright/.auth/admin.json';
test('switch to admin manually', async ({ context, page }) => {
// Xóa cookies cũ và set cookies của admin
await context.clearCookies();
await context.addCookies(adminState.cookies);
// Set localStorage của admin cho từng origin
for (const origin of adminState.origins) {
await page.goto(origin.origin);
await page.evaluate((items) => {
for (const { name, value } of items) {
localStorage.setItem(name, value);
}
}, origin.localStorage);
}
await page.goto('/dashboard');
await expect(page.getByText('Admin Panel')).toBeVisible();
});
Workaround 2 phức tạp hơn và brittle — phụ thuộc vào cấu trúc state file. Nếu dự án dùng v1.59+, không cần dùng đến cách này.
Limitation
- v1.59+ only — method không tồn tại trên các version cũ hơn. Gọi trên v1.58 hoặc cũ hơn sẽ throw
TypeError: context.setStorageState is not a function. - Page hiện tại không tự apply — luôn cần
page.reload()hoặc navigate sau khi gọi. JavaScript đang chạy trong page không nhận được event khi state thay đổi. - In-memory state của framework không reset —
setStorageState()chỉ thay đổi cookies và localStorage ở tầng browser. State lưu trong memory của JavaScript (Redux store, Vue reactive state, React context) vẫn giữ giá trị cũ cho đến khi page reload và app re-initialize. - Không reset service worker cache — nếu app dùng service worker cache authentication (ví dụ: cache API responses có kèm auth header),
setStorageState()không ảnh hưởng đến cache đó. - Multi-page context —
setStorageState()ảnh hưởng context, tức là tất cả page trong context đó. Nếu context có nhiều page đang mở, tất cả đều cần reload để nhận state mới.
4 Pitfall
Pitfall 1 — Quên reload sau setStorageState()
// SAI — assert ngay sau setStorageState, không reload
test('switch to admin', async ({ context, page }) => {
await page.goto('/dashboard');
await expect(page.getByText('User View')).toBeVisible();
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
// Quên reload — DOM vẫn là user view
await expect(page.getByText('Admin Panel')).toBeVisible(); // FAIL
});
// ĐÚNG
test('switch to admin', async ({ context, page }) => {
await page.goto('/dashboard');
await expect(page.getByText('User View')).toBeVisible();
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
await page.reload(); // apply state mới
await expect(page.getByText('Admin Panel')).toBeVisible(); // PASS
});
Pitfall 2 — Dùng trên Playwright < v1.59
// Khi version < v1.59:
// TypeError: context.setStorageState is not a function
// Kiểm tra version trước khi dùng
// package.json:
// "@playwright/test": "^1.59.0" ← cần ít nhất 1.59
Lỗi này không có message rõ ràng về version. Khi gặp TypeError: context.setStorageState is not a function, bước đầu tiên là kiểm tra @playwright/test version trong package.json.
Pitfall 3 — Kỳ vọng isolation như newContext
App dùng React với auth state lưu trong useContext hoặc Redux. Sau khi gọi setStorageState() + page.reload(), phần lớn state sẽ đúng — nhưng nếu app có caching layer (ví dụ: React Query cache, Apollo cache), data cũ có thể được serve từ cache thay vì fetch lại với credentials mới.
Nếu thấy test pass nhưng kết quả không nhất quán sau khi switch state, nghi ngờ client-side cache. Cách kiểm tra: dùng browser.newContext() thay thế — nếu test ổn định hơn, nguyên nhân là cache linger. Giải pháp: clear cache trong app hoặc dùng newContext() khi isolation là yêu cầu bắt buộc.
Pitfall 4 — Nhầm setStorageState() với storageState()
// storageState() — ĐỌC state ra
const state = await context.storageState();
// hoặc
await context.storageState({ path: 'output.json' });
// setStorageState() — GHI state vào
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
// Nhầm chiều:
await context.setStorageState({ path: 'output.json' }); // đọc nhầm file output
// → context nhận state từ file snapshot cũ thay vì file auth đúng
Hai method trông gần giống nhau. Quy tắc nhớ: storageState() không có prefix "set" → đọc (read). setStorageState() có prefix "set" → ghi (write).
Quiz
Câu 1. Sau khi gọi await context.setStorageState({ path: 'admin.json' }), tại sao cần gọi page.reload()?
Đáp án
setStorageState() thay đổi cookies và localStorage ở tầng BrowserContext, nhưng không trigger re-render hay re-fetch trong page đang hiển thị. JavaScript đang chạy trong page không biết storage đã thay đổi — DOM vẫn render data cũ, network requests tiếp theo mới dùng cookies mới. page.reload() buộc page load lại từ đầu, đọc cookies mới khi gửi request và đọc localStorage mới khi khởi tạo app.
Câu 2. Tình huống nào nên dùng setStorageState() thay vì browser.newContext({ storageState })?
Đáp án
setStorageState() phù hợp khi test cần mô phỏng chuyển đổi state trong cùng một browser session — ví dụ: switch role mid-test, inject fresh token sau expire, hoặc test transition guest → authenticated → premium trong một luồng. newContext() phù hợp hơn khi cần clean slate hoàn toàn (không muốn bất kỳ state cũ nào linger) hoặc khi cần hai page của hai identity chạy song song độc lập. setStorageState() nhanh hơn (reuse context) nhưng không đảm bảo isolation như newContext().
Câu 3. Đoạn test sau có vấn đề gì không? Nếu có, sửa thế nào?
test('switch state', async ({ context, page }) => {
await page.goto('/dashboard');
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
await expect(page.getByText('Admin Panel')).toBeVisible();
});
Đáp án
Thiếu page.reload() (hoặc navigate) sau setStorageState(). Sau khi set state, page hiện tại vẫn đang hiển thị DOM cũ với state cũ. Assertion getByText('Admin Panel') sẽ fail vì DOM chưa được cập nhật. Sửa bằng cách thêm reload:
test('switch state', async ({ context, page }) => {
await page.goto('/dashboard');
await context.setStorageState({ path: 'playwright/.auth/admin.json' });
await page.reload(); // thêm dòng này
await expect(page.getByText('Admin Panel')).toBeVisible();
});
Câu 4. App dùng SPA với JWT lưu trong localStorage và Redux store. Sau khi setStorageState() + page.reload(), test vẫn thấy data của user cũ trong một số component. Nguyên nhân có thể là gì?
Đáp án
Khả năng cao nhất là client-side cache. Sau page.reload(), app khởi tạo lại từ localStorage mới (JWT của identity mới), nhưng nếu app dùng caching layer như React Query hoặc Apollo Client, cache có thể được hydrate từ trước và phục vụ data cũ trước khi fetch lại từ server. Một số framework lưu cache vào localStorage hoặc sessionStorage — khi setStorageState() ghi đè localStorage, cache key từ identity cũ có thể bị overwrite nhưng cũng có thể không (tùy thuộc vào tên key). Để debug: dùng browser.newContext() thay thế — context mới không có cache cũ. Nếu test ổn định, nguyên nhân là cache linger. Giải pháp: clear cache trong app khi switch identity, hoặc dùng newContext() khi isolation là yêu cầu.
Bài Tiếp Theo
Bài 115 đi sâu vào storage isolation per test — cách đảm bảo mỗi test nhận context sạch, không bị ảnh hưởng bởi state từ test trước.
