Mục lục
- Mục Tiêu Bài Học
- Lịch Sử API: v1.41 → v1.47
- Cú Pháp và Ba Behavior Option
behavior: 'wait'— Chờ Pending Handler- Vấn Đề Race Condition Khi Cleanup
- Pattern Test Teardown Với
afterEach - Pattern Reset Mock Giữa Test
- Khác
unroute(url)(Series 1 — Bài 299) - Context vs Page — Scope Cleanup
- Khi Nào Không Cần
unrouteAll - Pitfalls
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
- Nắm lịch sử API:
unrouteAll()ra mắt v1.41, optionbehaviorbổ sung v1.47. - Hiểu ba giá trị
behavior:'wait','ignoreErrors', và mặc định — hành vi cụ thể của mỗi option với pending handler. - Biết
behavior: 'wait'giải quyết vấn đề gì và trong hoàn cảnh nào thực sự cần thiết. - Áp dụng pattern
afterEachcleanup và pattern reset mock giữa test. - Phân biệt rõ
unrouteAll()vớiunroute(url): scope và use case khác nhau. - Tránh 4 pitfall phổ biến khi dùng
behavior: 'wait'.
Lịch Sử API: v1.41 → v1.47
Trước v1.41, xoá toàn bộ route handler phải lặp qua từng pattern đã đăng ký và gọi unroute(url) riêng — không có API batch. Playwright v1.41 ra mắt page.unrouteAll() và context.unrouteAll() để xoá tất cả handler một lần, nhưng chưa có cách kiểm soát hành vi với handler đang xử lý request (pending handler) tại thời điểm gọi.
Vấn đề thực tế: nếu handler async đang await route.fetch() hoặc đang tính toán response, gọi unrouteAll() không cancel handler đó. Handler vẫn chạy nốt, sau đó gọi route.fulfill() — nhưng route có thể đã bị cleanup. Tùy timing, điều này gây ra error bí ẩn hoặc response về sai test.
Playwright v1.47 (tháng 9/2024) bổ sung option behavior cho cả page.unrouteAll() và context.unrouteAll():
// v1.41: unrouteAll không có behavior
await page.unrouteAll();
// v1.47+: unrouteAll với behavior control
await page.unrouteAll({ behavior: 'wait' });
await page.unrouteAll({ behavior: 'ignoreErrors' });
Dòng thời gian tóm tắt:
| Version | Thay đổi |
|---|---|
| Trước v1.41 | Chỉ có unroute(url, handler?) — cleanup thủ công từng pattern |
| v1.41 | page.unrouteAll() và context.unrouteAll() ra mắt — batch cleanup |
| v1.47 | Option behavior bổ sung — kiểm soát pending handler khi cleanup |
Cú Pháp và Ba Behavior Option
Signature đầy đủ của page.unrouteAll() từ v1.47:
page.unrouteAll(options?: {
behavior?: 'wait' | 'ignoreErrors' | 'default';
}): Promise<void>
Tương tự cho context:
context.unrouteAll(options?: {
behavior?: 'wait' | 'ignoreErrors' | 'default';
}): Promise<void>
Ba giá trị behavior và hành vi tương ứng:
| Behavior | Chờ pending handler? | Bắt error từ handler? | Khi nào dùng |
|---|---|---|---|
'wait' |
Có — chờ handler hoàn thành | Có — re-throw nếu handler throw | Teardown cần clean, handler async chậm |
'ignoreErrors' |
Không — cleanup ngay | Không — bỏ qua mọi error | Test đã fail, cleanup nhanh, không cần biết error |
'default' (không truyền) |
Không — cleanup ngay | Không catch — error có thể leak | Handler đơn giản, không async phức tạp |
Điểm khác biệt giữa 'ignoreErrors' và 'default': cả hai đều không chờ pending handler, nhưng 'ignoreErrors' bao bọc error từ handler đang chạy để nó không làm fail test. 'default' không có bảo vệ này — nếu handler throw sau khi route đã unroute, error có thể lan ra ngoài.
behavior: 'wait' — Chờ Pending Handler
Khi gọi unrouteAll({ behavior: 'wait' }), Playwright thực hiện hai bước:
- Đánh dấu tất cả handler là "đang xoá" — handler không nhận thêm request mới.
- Chờ tất cả handler đang xử lý request hiện tại hoàn thành (
route.fulfill,route.continue, hoặcroute.abortđược gọi xong) trước khi Promise resolve.
// Handler async có thể chậm (fetch + transform)
await page.route('**/api/data', async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ json });
});
// Trigger request
await page.goto('/dashboard'); // handler bắt đầu chạy
// behavior: 'wait' — chờ handler hoàn thành rồi mới resolve
await page.unrouteAll({ behavior: 'wait' });
// Tại đây: chắc chắn handler đã fulfill xong, không có request treo
Khác với không có behavior:
// Không behavior — unrouteAll resolve ngay
await page.unrouteAll();
// Handler có thể vẫn đang await route.fetch() ở background
// Khi fetch xong, handler gọi route.fulfill() trên route đã cleanup
// → behavior không xác định, có thể error hoặc response về sai test
Thời điểm behavior: 'wait' thực sự quan trọng: khi handler dùng route.fetch() (gọi backend thật) và response backend chậm. Nếu cleanup ngay, route.fetch() đang pending có thể throw hoặc response về không đúng nơi.
Vấn Đề Race Condition Khi Cleanup
Race condition điển hình với cleanup route:
// Scenario: test A và test B dùng cùng page (shared fixture)
test('test A', async ({ page }) => {
await page.route('**/api/items', async (route) => {
// Handler chậm: fetch backend thật rồi modify
const res = await route.fetch(); // → có thể mất 500ms
const data = await res.json();
data.extra = 'from-test-A';
await route.fulfill({ json: data });
});
await page.goto('/items'); // trigger /api/items → handler bắt đầu chạy
// Test A kết thúc sớm, afterEach cleanup không có 'wait'
// Lúc này handler đang await route.fetch() — chưa xong
});
// afterEach KHÔNG có behavior: 'wait'
test.afterEach(async ({ page }) => {
await page.unrouteAll(); // resolve ngay, handler A vẫn đang pending
});
test('test B', async ({ page }) => {
// Handler của test A vẫn đang chạy background
// fetch() trả về → handler A gọi route.fulfill()
// Test B đang navigate, nhận response với 'extra: from-test-A'
// → test B fail vì data không khớp assertion
await page.goto('/items');
await expect(page.locator('.item')).toHaveText('expected-text'); // FAIL
});
Với behavior: 'wait':
test.afterEach(async ({ page }) => {
await page.unrouteAll({ behavior: 'wait' });
// Chờ handler test A fulfill xong mới resolve
// Test B bắt đầu sau khi handler A đã hoàn thành — không race
});
Tình huống này xuất hiện nhiều nhất khi: test dùng shared page/context fixture, handler dùng route.fetch() để relay đến backend thật, hoặc handler có delay (setTimeout). Test tạo fresh page mỗi lần ít bị ảnh hưởng hơn nhưng vẫn có thể gặp nếu teardown không chờ.
Pattern Test Teardown Với afterEach
Pattern chuẩn cho test suite dùng route mocking:
import { test, expect } from '@playwright/test';
test.afterEach(async ({ page }) => {
await page.unrouteAll({ behavior: 'wait' });
});
test('mock list endpoint', async ({ page }) => {
await page.route('**/api/products', (route) =>
route.fulfill({ json: [{ id: 1, name: 'Widget' }] })
);
await page.goto('/products');
await expect(page.locator('.product-name')).toHaveText('Widget');
// afterEach tự cleanup — không cần unroute trong test
});
test('mock detail endpoint', async ({ page }) => {
// Handler từ test trước đã được cleanup → không leak
await page.route('**/api/products/1', (route) =>
route.fulfill({ json: { id: 1, name: 'Widget', price: 99 } })
);
await page.goto('/products/1');
await expect(page.locator('.price')).toHaveText('99');
});
Pattern fixture nếu muốn áp dụng cho toàn bộ suite:
// fixtures/base.ts
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ page }, use) => {
await use(page);
// Teardown sau mỗi test
await page.unrouteAll({ behavior: 'wait' });
},
});
// Trong mỗi test file
import { test, expect } from '../fixtures/base';
// Không cần afterEach riêng — fixture lo cleanup
Khi test fail giữa chừng và muốn cleanup nhanh hơn:
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status === 'failed') {
// Test fail — cleanup nhanh, không cần chờ handler dở dang
await page.unrouteAll({ behavior: 'ignoreErrors' });
} else {
// Test pass — cleanup sạch, chờ handler pending
await page.unrouteAll({ behavior: 'wait' });
}
});
testInfo.status trả về 'passed', 'failed', 'timedOut', hoặc 'skipped' — có thể dùng để phân nhánh strategy cleanup phù hợp.
Pattern Reset Mock Giữa Test
Ngoài teardown, unrouteAll({ behavior: 'wait' }) còn dùng để reset mock strategy giữa các phase của cùng một test — khi cần đổi toàn bộ mock set chứ không phải từng handler riêng lẻ.
test('switch mock strategy mid-test', async ({ page }) => {
// Phase 1: mock data cũ (v1 API)
await page.route('**/api/data', (route) =>
route.fulfill({ json: { version: 1, items: ['a', 'b'] } })
);
await page.route('**/api/meta', (route) =>
route.fulfill({ json: { schema: 'v1' } })
);
await page.goto('/');
await expect(page.locator('.schema-badge')).toHaveText('v1');
// Reset toàn bộ mock — chờ các handler đang chạy xong
await page.unrouteAll({ behavior: 'wait' });
// Phase 2: mock data mới (v2 API)
await page.route('**/api/data', (route) =>
route.fulfill({ json: { version: 2, items: ['a', 'b', 'c'] } })
);
await page.route('**/api/meta', (route) =>
route.fulfill({ json: { schema: 'v2' } })
);
await page.reload();
await expect(page.locator('.schema-badge')).toHaveText('v2');
await expect(page.locator('.item')).toHaveCount(3);
});
Khi nào cần unrouteAll thay vì thêm handler mới? Nếu chỉ cần override một endpoint, thêm handler mới (LIFO — handler sau ưu tiên hơn) sẽ đơn giản hơn. Dùng unrouteAll khi:
- Cần đổi nhiều endpoint cùng lúc và muốn trạng thái rõ ràng (không handler cũ nào còn sót).
- Handler cũ có side-effect (ghi log, đếm request count) mà phase 2 không muốn.
- Test muốn có một thời điểm rõ ràng "không có mock nào active" giữa hai phase để kiểm soát behavior network thật.
test('verify real API then switch to mock', async ({ page }) => {
// Phase 1: không mock, gọi API thật để verify contract
await page.goto('/data');
const realData = await page.evaluate(() =>
fetch('/api/data').then((r) => r.json())
);
// Phase 2: mock với data giả để test edge case
await page.route('**/api/data', (route) =>
route.fulfill({ json: { ...realData, edge: true } })
);
await page.reload();
await expect(page.locator('.edge-indicator')).toBeVisible();
// Phase 3: reset về API thật, verify không còn mock nào
await page.unrouteAll({ behavior: 'wait' });
await page.reload();
// /api/data đi network thật
});
Khác unroute(url) (Series 1 — Bài 299)
Series 1 Bài 299 đã cover page.unroute(url, handler?) — xoá handler theo pattern URL, với hoặc không có handler reference. unrouteAll() là bước tiếp theo: xoá không filter.
| API | Phạm vi xoá | Cần filter | Option behavior |
|---|---|---|---|
unroute(url) |
Tất cả handler match đúng pattern url |
Có (pattern phải exact match) | Không |
unroute(url, handler) |
Đúng một handler match cả pattern và reference | Có (pattern + function reference) | Không |
unrouteAll() |
Tất cả handler của page/context | Không | Có (wait/ignoreErrors/default) |
Quy tắc chọn:
- Dùng
unroute(url, handler)khi cần gỡ đúng một handler cụ thể, giữ nguyên các handler khác (vd: test-specific handler, fixture handler không bị ảnh hưởng). - Dùng
unroute(url)khi muốn gỡ tất cả handler cho một endpoint nhưng giữ handler của endpoint khác. - Dùng
unrouteAll()khi muốn reset toàn bộ routing state — teardown hoặc mid-test reset mock set.
Điểm quan trọng: unroute(url) không có behavior option — nếu cần kiểm soát pending handler khi xoá handler theo pattern, phải dùng unrouteAll() kết hợp với pattern re-register handler muốn giữ sau đó.
Context vs Page — Scope Cleanup
page.unrouteAll() chỉ xoá handler đăng ký bằng page.route() trên page đó. Handler đăng ký bằng context.route() không bị ảnh hưởng — phải gọi context.unrouteAll() riêng.
// Đăng ký ở hai scope khác nhau
await context.route('**/api/auth', authHandler); // context-level
await page.route('**/api/data', dataHandler); // page-level
// Chỉ cleanup page-level
await page.unrouteAll({ behavior: 'wait' });
// dataHandler đã xoá
// authHandler VẪN CÒN — vẫn intercept /api/auth
// Cleanup context-level riêng
await context.unrouteAll({ behavior: 'wait' });
// authHandler đã xoá
Pattern cleanup đầy đủ cả hai scope:
test.afterEach(async ({ page, context }) => {
await Promise.all([
page.unrouteAll({ behavior: 'wait' }),
context.unrouteAll({ behavior: 'wait' }),
]);
});
Dùng Promise.all ở đây an toàn vì cả hai cleanup độc lập nhau — page-scope và context-scope không cần thứ tự.
Thực tế, hầu hết test chỉ dùng page.route() nên chỉ cần page.unrouteAll(). context.route() thường dùng ở fixture setup-level — chỉ cần cleanup khi context được reuse qua nhiều test (ít gặp với cấu hình mặc định Playwright tạo context mới mỗi test).
Khi Nào Không Cần unrouteAll
Hai trường hợp route handler tự cleanup, không cần gọi unrouteAll:
1. Context đóng — tất cả route tự xoá
Khi BrowserContext đóng (hoặc Browser đóng), toàn bộ route handler của mọi page trong context bị hủy tự động. Playwright mặc định tạo context mới cho mỗi test — route handler không leak qua test boundary khi mỗi test có context riêng.
// playwright.config.ts — cấu hình mặc định
export default defineConfig({
use: {
// Playwright tạo browserContext mới cho mỗi test
// Route handler tự cleanup khi context đóng sau test
},
});
2. Handler đăng ký với times option
page.route(url, handler, { times: N }) tự động xoá handler sau khi handle đúng N request. Không cần unroute thủ công.
// Handler tự xoá sau 1 request
await page.route('**/api/init', (route) =>
route.fulfill({ json: { token: 'abc' } }),
{ times: 1 }
);
// Sau khi /api/init được handle 1 lần, handler tự xoá
// Không cần gọi unroute
Khi nào unrouteAll thực sự cần:
- Reuse context qua nhiều test (custom fixture giữ context qua test).
- Reuse page qua nhiều test.
- Mid-test reset — đổi mock strategy trong cùng test.
- Handler dùng
route.fetch()và cần cleanup có kiểm soát để tránh race.
Pitfalls
Pitfall 1 — 'wait' Với Handler Dùng Infinite Delay
Handler có setTimeout dài hoặc await Promise không resolve trong thời gian hợp lý sẽ khiến unrouteAll({ behavior: 'wait' }) treo (hang) cho đến khi test timeout.
// SAI — handler với delay dài
await page.route('**/api/data', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 60000)); // 60s
await route.fulfill({ json: {} });
});
// Trigger request
await page.goto('/');
// unrouteAll 'wait' sẽ chờ 60s → test timeout trước
await page.unrouteAll({ behavior: 'wait' }); // HANG
Fix: dùng 'ignoreErrors' nếu handler có thể chậm và không quan trọng kết quả handler đó. Hoặc thiết kế handler có timeout nội bộ trước khi gọi unrouteAll({ behavior: 'wait' }):
// Handler có internal timeout
await page.route('**/api/data', async (route) => {
const timeoutMs = 5000;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await route.fetch({ timeout: timeoutMs });
clearTimeout(timer);
await route.fulfill({ response: res });
} catch {
await route.fulfill({ status: 504, body: 'Gateway Timeout' });
}
});
Pitfall 2 — Gọi unrouteAll Trong Test Code, Xoá Cả Fixture Handler
unrouteAll xoá tất cả handler của page, kể cả handler đăng ký bởi beforeEach fixture. Gọi giữa test sẽ vô tình xoá handler fixture đang cần cho các assertion sau.
test.beforeEach(async ({ page }) => {
await page.route('**/api/auth', (route) =>
route.fulfill({ json: { user: 'admin' } })
);
});
test('broken test', async ({ page }) => {
await page.route('**/api/data', dataHandler);
await page.goto('/');
// SAI — xoá cả authHandler từ beforeEach
await page.unrouteAll({ behavior: 'wait' });
await page.goto('/protected');
// /api/auth không còn mock → request thật → fail nếu không có server
});
Fix: dùng unroute(url, handler) để xoá chỉ handler cụ thể cần gỡ giữa test. Dành unrouteAll cho afterEach khi cleanup toàn bộ.
Pitfall 3 — Quên unrouteAll Khi Reset Mid-Test, Mock Cũ Vẫn Active
Khi muốn đổi mock strategy giữa test, chỉ thêm handler mới mà không xoá handler cũ. Kết quả: handler cũ và handler mới cùng active (LIFO — handler mới ưu tiên hơn nhưng handler cũ vẫn có thể nhận request nếu handler mới gọi route.fallback()).
test('mock reset mistake', async ({ page }) => {
// Phase 1
await page.route('**/api/data', (route) =>
route.fulfill({ json: { source: 'mock-A' } })
);
await page.goto('/');
await expect(page.locator('.source')).toHaveText('mock-A');
// SAI — không unroute handler cũ, chỉ thêm handler mới
await page.route('**/api/data', (route) =>
route.fulfill({ json: { source: 'mock-B' } })
);
// Cả hai handler cùng tồn tại — mock-B active (LIFO)
// Nhưng nếu mock-B handler bị xoá, mock-A lại nổi lên
await page.reload();
await expect(page.locator('.source')).toHaveText('mock-B'); // pass
// ... tiếp tục test ... handler-A vẫn "ẩn" trong stack
});
Fix: gọi unrouteAll({ behavior: 'wait' }) trước khi đăng ký mock set mới để trạng thái routing rõ ràng.
Pitfall 4 — Dùng behavior: 'wait' Trên v1 Cũ Hơn v1.47
Option behavior chỉ có từ v1.47. Trên v1.41–v1.46, unrouteAll() tồn tại nhưng truyền { behavior: 'wait' } sẽ bị bỏ qua silently (không error, không có hiệu lực).
// Trên Playwright v1.43: không có lỗi nhưng behavior: 'wait' bị ignore
await page.unrouteAll({ behavior: 'wait' }); // như gọi unrouteAll()
Kiểm tra version Playwright đang dùng:
npx playwright --version
# hoặc
cat package.json | grep '"@playwright/test"'
Nếu project cần support Playwright < v1.47, không nên phụ thuộc vào behavior: 'wait'. Thay thế: gọi page.waitForResponse() để chờ response trước khi cleanup, hoặc đảm bảo handler hoàn thành bằng cách await từ trong test logic.
Quiz
Câu 1
Playwright thêm option behavior vào unrouteAll() từ version nào?
- v1.33
- v1.41
- v1.47
- v1.50
Đáp án
C — v1.47. unrouteAll() ra mắt v1.41 nhưng không có option behavior. v1.47 bổ sung behavior: 'wait' | 'ignoreErrors' | 'default' để kiểm soát cách xử lý pending handler tại thời điểm cleanup. Trên v1.41–v1.46, truyền { behavior: 'wait' } bị bỏ qua silently.
Câu 2
Test có handler dùng route.fetch() đang pending. Gọi page.unrouteAll({ behavior: 'wait' }). Điều gì xảy ra?
- Handler bị cancel ngay,
route.fetch()throw AbortError. - Promise của
unrouteAllchờ cho đến khi handler gọiroute.fulfill()/route.continue()/route.abort()xong, rồi mới resolve. - Handler tiếp tục chạy nhưng
route.fulfill()throw error vì route đã unroute. unrouteAllresolve ngay, không quan tâm handler đang chạy.
Đáp án
B. behavior: 'wait' khiến unrouteAll đợi tất cả pending handler hoàn thành trước khi resolve. Handler không bị cancel — nó chạy bình thường đến khi gọi action cuối (fulfill/continue/abort), sau đó Playwright mới cleanup route. Đây là lý do 'wait' là option an toàn nhất cho teardown.
Câu 3
Test dùng test.beforeEach đăng ký page.route('**/api/auth', authHandler). Giữa test, dev gọi await page.unrouteAll({ behavior: 'wait' }) để reset mock. Sau đó test navigate đến /protected cần auth. Vấn đề gì xảy ra?
- Không vấn đề gì, authHandler vẫn active.
authHandlerđã bị xoá cùng với tất cả handler khác. Request/api/authđi network thật → có thể fail nếu không có backend, hoặc behavior khác với mock.- Playwright tự restore fixture handler sau khi
unrouteAll. - Chỉ handler đăng ký sau
beforeEachbị xoá.
Đáp án
B. unrouteAll không phân biệt handler nào từ fixture, handler nào từ test code — xoá tất cả. Đây là Pitfall 2 của bài. Fix: dùng unroute(url, handler) để xoá chỉ handler cụ thể cần gỡ, giữ lại fixture handler. Hoặc re-register authHandler ngay sau unrouteAll.
Câu 4
Khác biệt chính giữa behavior: 'ignoreErrors' và không truyền behavior (default) là gì?
- Cả hai giống hệt nhau.
'ignoreErrors'chờ pending handler, còn default thì không.- Cả hai đều không chờ pending handler, nhưng
'ignoreErrors'bọc error từ handler đang chạy để nó không làm fail test. Default không có bảo vệ này — error từ handler dở dang có thể leak ra ngoài. 'ignoreErrors'cancel tất cả handler ngay lập tức.
Đáp án
C. Cả 'ignoreErrors' và 'default' đều resolve ngay mà không chờ handler. Điểm khác: 'ignoreErrors' bao bọc error từ handler đang chạy nền — nếu handler sau đó throw (vd gọi route.fulfill() trên route đã cleanup), error bị nuốt. Default không có cơ chế này. Dùng 'ignoreErrors' khi test fail và muốn cleanup nhanh mà không để error từ cleanup làm nhiễu kết quả.
Câu 5
Test cần đổi mock cho /api/data giữa phase 1 và phase 2. Dev làm: (a) đăng ký handler-A cho /api/data; (b) chạy phase 1; (c) đăng ký handler-B cho /api/data (không unroute); (d) chạy phase 2. Phase 2 nhận response từ handler nào?
- Handler-A — vì đăng ký trước.
- Handler-B — vì LIFO, handler đăng ký sau ưu tiên hơn.
- Cả hai — Playwright gọi cả hai handler theo thứ tự.
- Lỗi vì hai handler cùng pattern.
Đáp án
B. Route handler stack hoạt động LIFO — handler đăng ký sau chạy trước. Handler-B chạy đầu tiên và nếu gọi route.fulfill(), request được giải quyết, handler-A không chạy. Tuy nhiên, handler-A vẫn tồn tại trong stack. Nếu handler-B sau đó bị unroute, handler-A lại nổi lên. Để trạng thái rõ ràng, dùng unrouteAll({ behavior: 'wait' }) trước bước (c) thay vì chồng handler.
Bài Tiếp Theo
Bài 124: unrouteAll({ behavior: 'ignoreErrors' }) — Option thứ hai của unrouteAll: cleanup ngay, bỏ qua error từ handler đang chạy. Khi nào dùng thay cho 'wait', trade-off giữa tốc độ cleanup và đảm bảo handler hoàn thành.
