Mục lục
- Mục Tiêu Bài Học
- Storage Leak Là Gì Và Tại Sao Nó Xảy Ra
- Default Isolation — Test-Scope Context
- Ba Isolation Levels
- Khi Nào Isolation Bị Phá Vỡ
- Pattern Verify Isolation
- Pattern Clear State Explicit
- storageState Reset Per Test
- Clean Context Fixture
- Common Leak Sources
- Giới Hạn Của Context Isolation
- Common Pitfalls
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài này, bạn sẽ:
- Giải thích được vì sao test-scope context đảm bảo storage isolation theo mặc định trong Playwright.
- Nhận diện đúng 4 tình huống khiến isolation bị phá vỡ: worker-scope context, manual context reuse, serial mode, module-level state.
- Phân biệt ba isolation levels (test-scope, worker-scope, persistent context) và biết khi nào dùng cái nào.
- Viết cặp test verify isolation đang hoạt động cho cookies, localStorage và IndexedDB.
- Implement pattern clear state explicit trong
beforeEachkhi cần thiết. - Reset
storageStatevề rỗng per test khi cần context không có auth state. - Biết giới hạn của context isolation: server-side state không bị reset bởi browser context mới.
Storage Leak Là Gì Và Tại Sao Nó Xảy Ra
Storage leak xảy ra khi state của một test (cookie, localStorage, sessionStorage, IndexedDB) được nhìn thấy bởi test khác, dẫn đến kết quả test phụ thuộc vào thứ tự chạy hoặc vào trạng thái do test trước để lại.
Ví dụ điển hình:
// Test A — login, lưu token vào localStorage
test('login thành công', async ({ page }) => {
await page.goto('/login');
await page.fill('[name=email]', '[email protected]');
await page.fill('[name=password]', 'secret');
await page.click('button[type=submit]');
// App lưu token vào localStorage sau khi login thành công
await expect(page).toHaveURL('/dashboard');
});
// Test B — expect guest state, NHƯNG nếu share context với test A:
test('homepage hiển thị nút login cho guest', async ({ page }) => {
await page.goto('/');
// KỲ VỌNG: thấy nút Login (guest chưa đăng nhập)
// NẾU LEAK: localStorage vẫn có token từ test A
// → app đọc token → render Dashboard, không có nút Login
await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
// ❌ FAIL — không phải do bug app, mà do state leak từ test A
});
Test B fail không phải vì ứng dụng có lỗi. Chạy test B đơn lẻ thì pass, chạy sau test A thì fail — dấu hiệu kinh điển của state leak.
Bốn loại storage phổ biến bị leak:
- Cookies — auth token, session ID, CSRF token.
- localStorage / sessionStorage — auth token, user preference, feature flag.
- IndexedDB — offline data, cached response trong PWA.
- Service Worker registration — SW cũ intercept request của test sau.
Default Isolation — Test-Scope Context
Playwright Test Runner tạo một BrowserContext mới cho mỗi test theo mặc định (test-scope). Tất cả storage (cookies, localStorage, sessionStorage, IndexedDB, Cache Storage, Service Worker, permissions) gắn vào context. Khi test kết thúc, runner đóng context — toàn bộ storage biến mất.
Worker process
└── Browser (worker-scope, reuse)
├── Test 1
│ └── Context (test-scope, NEW) ← cookies/localStorage/IndexedDB trắng
│ └── Page
├── Test 2
│ └── Context (test-scope, NEW) ← không liên quan Test 1
│ └── Page
└── Test 3
└── Context (test-scope, NEW)
└── Page
Hệ quả trực tiếp:
- Mỗi test nhận context trắng — mọi storage type đều rỗng khi test bắt đầu.
- Không cần
afterEachxoá cookies haybeforeEachreset localStorage — runner tự làm. - Nếu test load
storageState(ví dụ từ file auth JSON), context được khởi tạo với state đó — nhưng vẫn là context riêng, không liên quan context của test khác.
Ví dụ cho thấy isolation đang hoạt động mà không cần viết cleanup:
import { test, expect } from '@playwright/test';
test('test 1 sets state', async ({ page }) => {
await page.goto('/');
await page.evaluate(() => localStorage.setItem('token', 'abc123'));
// Xác nhận đã set
const val = await page.evaluate(() => localStorage.getItem('token'));
expect(val).toBe('abc123');
});
test('test 2 has fresh state', async ({ page }) => {
await page.goto('/');
// Context hoàn toàn mới — localStorage trắng
const val = await page.evaluate(() => localStorage.getItem('token'));
expect(val).toBeNull(); // pass mà không cần cleanup thủ công
});
Không có beforeEach, không có localStorage.clear() — test 2 vẫn thấy null vì đây là context khác.
Ba Isolation Levels
Playwright có ba mức cô lập tương ứng với ba cách quản lý context:
Test-scope context (mặc định). Mỗi test nhận context mới, state tự cleanup. Đây là lựa chọn phù hợp với tuyệt đại đa số test. Isolation đầy đủ — không có bất kỳ state nào rò giữa test.
// Mặc định — không cần config thêm
test('isolated test', async ({ page }) => {
// page thuộc context mới, trắng hoàn toàn
});
Worker-scope context (share trong worker). Fixture context được override sang scope worker, tất cả test trong cùng worker chia sẻ context. Phù hợp khi bạn cố ý chia sẻ auth state trong worker để tránh login nhiều lần (bài 104 — per-worker auth). Rủi ro: test mutate storage ảnh hưởng lẫn nhau trong worker.
// fixtures.ts — worker-scope context (opt-in tường minh)
import { test as base } from '@playwright/test';
export const test = base.extend<{}, { workerContext: import('@playwright/test').BrowserContext }>({
workerContext: [async ({ browser }, use) => {
const context = await browser.newContext();
await use(context);
await context.close();
}, { scope: 'worker' }],
});
Persistent context (share cross run). Dùng chromium.launchPersistentContext() với profile directory trên disk — state tồn tại qua nhiều lần chạy. Dùng cho automation sử dụng profile người dùng thật, không dùng trong test suite thông thường vì không có isolation giữa các lần chạy.
Isolation level | Scope | Storage sau test
------------------|---------------|-----------------
Test-scope | Mỗi test | Xóa tự động
Worker-scope | Mỗi worker | Xóa khi worker done
Persistent | Cross-run | Tồn tại vĩnh viễn
Khi Nào Isolation Bị Phá Vỡ
Có bốn tình huống khiến isolation bị phá vỡ:
1. Worker-scope context với mutating test. Khi context được share ở scope worker, test A login và lưu token vào localStorage; test B chạy sau trong cùng worker vẫn thấy token đó. Mục đích của pattern này (bài 104) là share auth state một cách có chủ đích — nhưng nếu test B mong thấy state trắng, nó sẽ fail do context đã bị "làm bẩn" bởi test A.
2. Manual context reuse. Khi bạn tạo context trong test.beforeAll rồi dùng chung cho nhiều test trong describe block, các test cùng share state trên context đó. Đây là intentional nhưng phải cleanup thủ công.
test.describe('suite with shared context', () => {
let sharedContext: import('@playwright/test').BrowserContext;
test.beforeAll(async ({ browser }) => {
sharedContext = await browser.newContext();
// Login một lần, tất cả test trong describe dùng chung
});
test.afterAll(async () => {
await sharedContext.close();
});
test('test A', async () => {
const page = await sharedContext.newPage();
// ... state changes ở đây visible với test B
await page.close();
});
test('test B', async () => {
const page = await sharedContext.newPage();
// THẤY state từ test A nếu không clear thủ công
});
});
3. Serial mode với shared state. test.describe.configure({ mode: 'serial' }) khiến các test trong describe chạy tuần tự và chia sẻ context. State từ test trước visible trong test sau — đây là mục đích của serial mode, nhưng cũng là nguồn leak nếu không kiểm soát.
4. Module-level state. Biến khai báo ngoài test() trong file test — biến này thuộc Node.js module của worker process, không phải fixture. Tất cả test trong worker dùng chung biến đó, không bị cleanup giữa test.
// ANTI-PATTERN — module-level mutable state
let authToken = ''; // Leak qua mọi test trong worker!
test('test A sets token', async ({ page }) => {
authToken = 'secret-jwt';
// ...
});
test('test B expects no token', async ({ page }) => {
// authToken vẫn là 'secret-jwt' từ test A
expect(authToken).toBe(''); // ❌ FAIL
});
Pattern Verify Isolation
Cách trực tiếp nhất để xác nhận isolation đang hoạt động là viết cặp test: test 1 set state, test 2 assert state rỗng. Nếu test 2 pass, isolation OK. Nếu test 2 fail (thấy state của test 1), bạn biết ngay có gì đó share context.
import { test, expect } from '@playwright/test';
// --- Verify cookies isolated ---
test('isolation-check: test 1 đặt cookie', async ({ page, context }) => {
await page.goto('https://example.com');
await context.addCookies([{
name: 'auth_token',
value: 'test-token-123',
domain: 'example.com',
path: '/',
}]);
const cookies = await context.cookies();
expect(cookies.find(c => c.name === 'auth_token')?.value).toBe('test-token-123');
});
test('isolation-check: test 2 không thấy cookie', async ({ page, context }) => {
await page.goto('https://example.com');
const cookies = await context.cookies();
expect(cookies.find(c => c.name === 'auth_token')).toBeUndefined();
});
// --- Verify localStorage isolated ---
test('isolation-check: test 3 đặt localStorage', async ({ page }) => {
await page.goto('https://example.com');
await page.evaluate(() => localStorage.setItem('user_role', 'admin'));
const role = await page.evaluate(() => localStorage.getItem('user_role'));
expect(role).toBe('admin');
});
test('isolation-check: test 4 không thấy localStorage', async ({ page }) => {
await page.goto('https://example.com');
const role = await page.evaluate(() => localStorage.getItem('user_role'));
expect(role).toBeNull();
});
// --- Verify IndexedDB isolated ---
test('isolation-check: test 5 ghi IndexedDB', async ({ page }) => {
await page.goto('https://example.com');
await page.evaluate(async () => {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open('testDB', 1);
req.onupgradeneeded = () => req.result.createObjectStore('store');
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
await new Promise<void>((resolve) => {
const tx = db.transaction('store', 'readwrite');
tx.objectStore('store').put('value', 'key');
tx.oncomplete = () => resolve();
});
});
});
test('isolation-check: test 6 không thấy dữ liệu IndexedDB', async ({ page }) => {
await page.goto('https://example.com');
const result = await page.evaluate(async () => {
return new Promise<unknown>((resolve) => {
const req = indexedDB.open('testDB', 1);
req.onupgradeneeded = () => req.result.createObjectStore('store');
req.onsuccess = () => {
const tx = req.result.transaction('store', 'readonly');
const getReq = tx.objectStore('store').get('key');
getReq.onsuccess = () => resolve(getReq.result);
};
req.onerror = () => resolve(null);
});
});
expect(result).toBeUndefined(); // DB mới, chưa có key
});
Chạy cả file — tất cả test phải pass. Nếu một trong các test "không thấy" bị fail, tìm fixture config hoặc mode: 'serial' đang share context ngoài ý muốn.
Pattern Clear State Explicit
Khi không thể tránh việc share context (ví dụ: worker-scope auth, describe block dùng shared context), cần clear state thủ công. Hai approach:
Approach 1 — Clear trong beforeEach.
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ context, page }) => {
// Xóa tất cả cookies của context
await context.clearCookies();
// Xóa localStorage và sessionStorage qua page evaluate
// (phải goto một trang trong origin cần xóa trước)
await page.goto('https://app.example.com');
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});
test('test với fresh browser storage', async ({ page }) => {
await page.goto('https://app.example.com');
// Context đã được reset — không có state thừa
});
Approach 2 — Clear ngay đầu mỗi test. Hữu ích khi chỉ một số test cần state sạch:
test('test yêu cầu guest state', async ({ page, context }) => {
// Reset storage trước khi bắt đầu logic test
await context.clearCookies();
await page.goto('/');
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
// Bây giờ test với state sạch
await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
});
Lưu ý về IndexedDB. context.clearCookies() chỉ xóa cookies. Để xóa IndexedDB phải dùng JS API trực tiếp:
await page.evaluate(async () => {
const databases = await indexedDB.databases();
await Promise.all(
databases.map(db => new Promise<void>((resolve, reject) => {
const req = indexedDB.deleteDatabase(db.name!);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
}))
);
});
storageState Reset Per Test
Khi playwright.config.ts đặt use: { storageState: 'playwright/.auth/user.json' }, tất cả test tự động load auth state từ file đó. Một số test cần chạy với guest state (không load auth). Để override cho test hoặc describe cụ thể:
// Trong file test cụ thể — override storageState về rỗng
test.use({ storageState: { cookies: [], origins: [] } });
test('guest: homepage phải có nút Login', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
});
test('guest: không thể vào dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/);
});
test.use() ở đầu file áp dụng cho tất cả test trong file đó. Để áp dụng cho một describe block cụ thể:
import { test, expect } from '@playwright/test';
// Test thông thường — dùng storageState từ config (logged in)
test('logged-in: xem được dashboard', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
// Describe block override về guest state
test.describe('guest scenarios', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('guest: redirect về login', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/);
});
test('guest: homepage hiện hero section', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('link', { name: 'Get Started' })).toBeVisible();
});
});
Pattern này đảm bảo từng test nhận đúng state cần thiết — không cần clear thủ công vì mỗi test vẫn có context riêng, và storageState được seed khác nhau theo config.
Clean Context Fixture
Nếu cần một fixture đảm bảo context luôn sạch — dù config có đặt storageState hay không — wrap fixture context và clear cả trước lẫn sau:
// fixtures/clean-context.ts
import { test as base, BrowserContext } from '@playwright/test';
type CleanContextFixtures = {
cleanContext: BrowserContext;
};
export const test = base.extend<CleanContextFixtures>({
cleanContext: async ({ context }, use) => {
// Xóa state trước khi test dùng
await context.clearCookies();
// Không thể clear localStorage ở đây vì chưa có page,
// nên clear trong body test hoặc beforeEach
await use(context);
// Cleanup sau test (optional — vì context sẽ bị đóng anyway)
await context.clearCookies();
},
});
export { expect } from '@playwright/test';
// test.spec.ts
import { test, expect } from '../fixtures/clean-context';
test('dùng cleanContext', async ({ cleanContext, page }) => {
// cleanContext đã được clear cookies
// page vẫn là fixture mặc định, dùng context từ base
});
// Hoặc tạo page từ cleanContext
test('page từ cleanContext', async ({ cleanContext }) => {
const page = await cleanContext.newPage();
await page.goto('/');
// ...
await page.close();
});
Fixture này hữu ích khi bạn cần đảm bảo test không bị ảnh hưởng bởi storageState được seed từ config, mà không muốn đặt test.use({ storageState: ... }) lặp đi lặp lại.
Common Leak Sources
Bốn nguồn rò rỉ state phổ biến nhất trong Playwright test suite:
1. Module-level variable. Biến hoặc object mutable khai báo ngoài test(). Biến này thuộc scope của Node.js module, tồn tại suốt vòng đời worker process.
// LEAK ❌
let capturedToken: string;
test('capture token', async ({ page }) => {
capturedToken = 'jwt.header.payload';
});
test('check no token', async () => {
// capturedToken vẫn là 'jwt.header.payload'
expect(capturedToken).toBeUndefined(); // FAIL
});
2. Worker-scope fixture mutable. Fixture scope worker tốt khi chỉ đọc (như auth state đã được login). Khi test mutate fixture này (logout, thay đổi setting), test sau trong cùng worker nhận state đã bị thay đổi.
3. Persistent context. chromium.launchPersistentContext(userDataDir) lưu state xuống disk. Lần chạy sau đọc lại state đó — cookie, localStorage, IndexedDB, Extension data đều tồn tại cross-run.
4. Shared storageState file bị mutate. Nếu test A ghi thêm data vào file playwright/.auth/user.json trong lúc chạy (ví dụ: call context.storageState({ path: '...' }) để update), test B đọc file đó sau đó sẽ nhận state khác với state ban đầu.
// NGUY HIỂM — ghi đè auth file trong test
test('mutate auth file', async ({ context }) => {
// Làm gì đó thay đổi state trong context...
// Rồi ghi đè file auth shared
await context.storageState({ path: 'playwright/.auth/user.json' });
// → Tất cả test chạy SAU sẽ nhận state bị thay đổi!
});
Quy tắc an toàn: file auth JSON chỉ được ghi bởi global setup hoặc setup project, không bao giờ bởi test thông thường.
Giới Hạn Của Context Isolation
Context isolation chỉ cô lập browser-side storage. Nó không ảnh hưởng đến state trên server.
Ví dụ: Test A tạo một user mới thông qua form. Server lưu user vào database. Test B mở form tạo user với cùng email — bị lỗi "email đã tồn tại" dù context của test B hoàn toàn mới, không có cookie hay localStorage từ test A.
Test A (context A) Server Test B (context B)
───────────────── ────────── ─────────────────
POST /register ──────────→ INSERT INTO users context B TRẮNG
{email: '[email protected]'} → DB có user (cookie/LS rỗng)
POST /register ──────────────────────────────────→ 409 Conflict
{email: '[email protected]'} ← email đã tồn tại ←←←←←
Context isolation không giải quyết được server-side state conflict. Cần các biện pháp riêng:
- Dùng email unique per test:
`user-${Date.now()}@example.com`hoặcfaker.internet.email(). - Cleanup DB sau test qua API endpoint test-only (Series 3).
- Dùng DB transaction rollback hoặc snapshot/restore.
- Tách database per worker (phức tạp, hữu ích cho enterprise scale).
Boundary rõ ràng: context isolation = browser state. Server state = phải tự quản lý.
Common Pitfalls
-
Worker-scope context với mutating test. Share worker context để tránh login lại nhiều lần (per-worker auth) là hợp lý. Nhưng nếu test bên trong đó logout, xóa cookie, hoặc thay đổi localStorage, test tiếp theo trong cùng worker sẽ thấy state đã bị thay đổi. Chỉ dùng worker-scope context cho các test chỉ đọc (read-only), không mutate auth state.
-
Module-level state share.
const data = { items: [] }khai báo ngoài test tích lũy state qua mỗi test chạy trong worker. Không có cleanup nào từ Playwright — biến tồn tại suốt vòng đời worker process. Dùng fixture thay vì biến module khi cần share data trong worker. -
Server-side state không isolated bởi context. Xóa cookie không xóa session trên server. Test sau vào trang "yêu cầu login" có thể bị redirect tới trang "bạn đã đăng nhập" nếu session server vẫn còn hiệu lực dù context browser trắng. Cần gọi logout API hoặc dùng server-side cleanup riêng.
-
Persistent context giữ state cross run. Nếu dùng
launchPersistentContexttrong test (ví dụ để test extension Chrome), mỗi lần chạy tiếp tục từ state của lần trước. Phải xóa thủ cônguserDataDirtrước mỗi run, hoặc dùngglobalSetupđể cleanup. -
Shared storageState file bị overwrite trong test. Gọi
await context.storageState({ path: 'auth/user.json' })trong test thông thường ghi đè file auth dùng chung, khiến test chạy sau nhận state khác. File auth chỉ được ghi bởi global setup hoặc setup project, không bao giờ bởi test.
Tổng Kết
- Playwright test-scope context là cơ chế mặc định đảm bảo mỗi test nhận context hoàn toàn mới — cookies, localStorage, sessionStorage, IndexedDB, Service Worker đều rỗng khi test bắt đầu, tự cleanup khi test kết thúc.
- Isolation bị phá vỡ khi: worker-scope context (share trong worker), manual context reuse trong describe/beforeAll, serial mode, module-level mutable variable.
- Ba isolation levels: test-scope (default, full isolation), worker-scope (share trong worker), persistent context (share cross run). Default phù hợp với tuyệt đại đa số test.
- Khi phải share context, clear state thủ công bằng
context.clearCookies()cộngpage.evaluate(() => localStorage.clear())trongbeforeEachhoặc đầu test. - Override
storageStatevề{ cookies: [], origins: [] }per test hoặc per describe để loại bỏ auth state được seed từ config khi cần chạy guest scenario. - Không ghi đè file auth JSON trong test — chỉ global setup hoặc setup project mới được phép ghi.
- Context isolation không ảnh hưởng server-side state (database, session server). Cần chiến lược riêng: unique data per test, cleanup API, transaction rollback.
Chương B — Authentication nâng cao đã cover: multi-role auth, per-worker auth, API login, session refresh, SSO/OAuth mock, JWT injection, 2FA/OTP, IndexedDB auth, set storageState runtime và storage isolation. Bài 116 mở Chương C — Network nâng cao với routing advanced, HAR, WebSocket và API testing.
Bài Tập Củng Cố
Câu 1
Test A đặt cookie session_id=abc, test B kiểm tra xem context có cookie đó không. Khi chạy cả hai test trong một file (không có config đặc biệt), test B có thấy cookie đó không? Giải thích.
Đáp án
Không. Mặc định mỗi test nhận một BrowserContext mới (test-scope). Cookie gắn vào context của test A. Khi test A kết thúc, context của nó bị đóng — cookie biến mất. Test B nhận context hoàn toàn mới, không liên quan context của test A, do đó không có cookie session_id.
Câu 2
Trong một fixture worker-scope, bạn login một lần và chia sẻ context cho nhiều test. Test thứ 3 trong cùng worker gọi await page.evaluate(() => localStorage.clear()). Điều gì xảy ra với test thứ 4?
Đáp án
Test thứ 4 dùng cùng context với worker-scope fixture. localStorage.clear() trong test 3 xóa localStorage trên origin đã clear. Khi test 4 goto cùng origin, localStorage sẽ rỗng (hoặc chỉ có những gì test 4 tự set). Nếu app của test 4 phụ thuộc vào localStorage token để tự động xác thực, test 4 có thể fail với "không đăng nhập". Đây là ví dụ worker-scope context bị "làm bẩn" bởi mutating test.
Câu 3
playwright.config.ts đặt use: { storageState: 'playwright/.auth/admin.json' }. Bạn có một file test cần chạy với guest state (không có auth). Viết cú pháp override cho toàn bộ file đó.
Đáp án
// Đầu file test
import { test, expect } from '@playwright/test';
test.use({ storageState: { cookies: [], origins: [] } });
// Tất cả test trong file này chạy với context trắng (guest)
test('guest test 1', async ({ page }) => { /* ... */ });
test('guest test 2', async ({ page }) => { /* ... */ });
{ cookies: [], origins: [] } là giá trị rỗng hợp lệ của storageState — không load auth state, context bắt đầu hoàn toàn trắng.
Câu 4
Một file test khai báo let capturedId: number ở module level. Test A gán capturedId = 42. Test B expect capturedId là undefined. Kết quả khi chạy với fullyParallel: true — hai test trên cùng worker?
Đáp án
Test B sẽ fail. Biến capturedId là module-level, thuộc Node.js process của worker. Playwright không reset biến module giữa các test — chỉ context trình duyệt mới được tạo mới. Khi test A và test B chạy trên cùng worker, chúng dùng cùng instance module, cùng biến capturedId. Sau test A gán 42, capturedId còn 42 khi test B chạy → expect(capturedId).toBeUndefined() fail.
Nếu hai test trên hai worker khác nhau, mỗi worker là một process Node.js riêng với module riêng — biến không leak giữa worker. Nhưng phụ thuộc vào cách runner phân test vào worker, không thể đảm bảo chắc chắn.
Câu 5
Test A tạo tài khoản qua form trên browser (state thay đổi trên DB). Test B chạy sau, dùng context mới hoàn toàn, cũng tạo tài khoản với cùng email — gặp lỗi 409 từ server. Context isolation có giúp gì trong trường hợp này không?
Đáp án
Không. Context isolation chỉ cô lập browser-side storage (cookies, localStorage, IndexedDB). Server đã lưu tài khoản vào database từ test A — đó là server-side state, hoàn toàn nằm ngoài phạm vi của Playwright context. Context mới của test B không thể "xóa" bản ghi trong database server.
Giải pháp phù hợp: dùng email unique per test (`test-${Date.now()}@example.com`), gọi cleanup API để xóa account sau mỗi test, hoặc dùng database transaction rollback trong môi trường test.
Bài Tiếp Theo
Chương C — Network nâng cao bắt đầu với routing nâng cao. Bài 116 đi vào route.fetch() với maxRedirects và các option kiểm soát redirect behavior khi intercept request.
