Danh sách bài viết

Bài 123: unrouteAll({ behavior: 'wait' }) [v1.47]

Từ v1.47, unrouteAll() nhận option behavior để kiểm soát cách xử lý handler đang xử lý request tại thời điểm cleanup. Với behavior: 'wait', Playwright chờ tất cả handler đang chạy hoàn thành trước khi xoá route — loại bỏ race condition khi teardown. Bài này phân tích lịch sử API (v1.41 thêm unrouteAll, v1.47 thêm behavior), so sánh ba option wait/ignoreErrors/default, pattern afterEach cleanup, pattern reset mock giữa test, và 4 pitfall quan trọng.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

  • Nắm lịch sử API: unrouteAll() ra mắt v1.41, option behavior bổ 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 afterEach cleanup và pattern reset mock giữa test.
  • Phân biệt rõ unrouteAll() với unroute(url): scope và use case khác nhau.
  • Tránh 4 pitfall phổ biến khi dùng behavior: 'wait'.
2

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()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()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()context.unrouteAll() ra mắt — batch cleanup
v1.47 Option behavior bổ sung — kiểm soát pending handler khi cleanup
3

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''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.

4

behavior: 'wait' — Chờ Pending Handler

Khi gọi unrouteAll({ behavior: 'wait' }), Playwright thực hiện hai bước:

  1. Đánh dấu tất cả handler là "đang xoá" — handler không nhận thêm request mới.
  2. Chờ tất cả handler đang xử lý request hiện tại hoàn thành (route.fulfill, route.continue, hoặc route.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.

5

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ờ.

6

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.

7

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
});
8

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 đó.

9

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).

10

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.
11

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.

12

Quiz

Câu 1

Playwright thêm option behavior vào unrouteAll() từ version nào?

  1. v1.33
  2. v1.41
  3. v1.47
  4. 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?

  1. Handler bị cancel ngay, route.fetch() throw AbortError.
  2. Promise của unrouteAll chờ cho đến khi handler gọi route.fulfill() / route.continue() / route.abort() xong, rồi mới resolve.
  3. Handler tiếp tục chạy nhưng route.fulfill() throw error vì route đã unroute.
  4. unrouteAll resolve 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?

  1. Không vấn đề gì, authHandler vẫn active.
  2. 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.
  3. Playwright tự restore fixture handler sau khi unrouteAll.
  4. Chỉ handler đăng ký sau beforeEach bị 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ì?

  1. Cả hai giống hệt nhau.
  2. 'ignoreErrors' chờ pending handler, còn default thì không.
  3. 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.
  4. 'ignoreErrors' cancel tất cả handler ngay lập tức.
Đáp án

C. Cả 'ignoreErrors''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?

  1. Handler-A — vì đăng ký trước.
  2. Handler-B — vì LIFO, handler đăng ký sau ưu tiên hơn.
  3. Cả hai — Playwright gọi cả hai handler theo thứ tự.
  4. 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.

13

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.