Danh sách bài viết

Bài 124: unrouteAll({ behavior: 'ignoreErrors' })

unrouteAll({ behavior: 'ignoreErrors' }) [v1.47+] xoá toàn bộ route handlers ngay lập tức và bỏ qua mọi error phát sinh từ handler đang chạy dở. Khác 'wait' — vốn chờ handler hoàn thành trước khi trả về — 'ignoreErrors' phù hợp cho test teardown pragmatic: cleanup nhanh mà không để error phụ mask test result thật.

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

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

Sau bài này, bạn sẽ:

  • Nắm được behavior chính xác của unrouteAll({ behavior: 'ignoreErrors' }): xoá ngay, swallow error từ in-flight handler.
  • Phân biệt rõ ba behavior của unrouteAll(): default, 'wait', và 'ignoreErrors'.
  • Hiểu tại sao 'ignoreErrors' là lựa chọn phù hợp cho afterEach và fixture teardown.
  • Biết cách xây dựng pattern teardown fixture an toàn với 'ignoreErrors'.
  • Tránh 4 pitfall thường gặp khi chọn nhầm behavior trong teardown.
2

Cú Pháp và Behavior

unrouteAll({ behavior: 'ignoreErrors' }) được thêm vào cùng với 'wait' trong v1.47:

await page.unrouteAll({ behavior: 'ignoreErrors' });

Tương tự, gọi trên BrowserContext:

await context.unrouteAll({ behavior: 'ignoreErrors' });

Điều xảy ra khi gọi:

  1. Playwright xoá toàn bộ route handlers đã đăng ký trên page (hoặc context) ngay lập tức.
  2. Nếu có handler nào đang thực thi dở (in-flight) tại thời điểm đó, error từ handler đó bị swallow — không throw ra ngoài, không làm fail test, không ghi vào test output.
  3. Hàm resolve ngay sau khi handlers bị xoá, không chờ handler in-flight hoàn thành.

Điểm khác biệt then chốt so với 'wait' (bài 123): 'wait' block cho đến khi handler đang chạy hoàn thành rồi mới resolve, và nếu handler throw error, error đó surface ra ngoài. 'ignoreErrors' không chờ và không surface.

// Minh họa behavior
await page.route('**/api/heavy', async (route) => {
  // Handler này đang chạy — fetch chậm, chưa xong
  const response = await route.fetch({ timeout: 10_000 });
  await route.fulfill({ response });
});

// Sau khi test chạy xong, gọi cleanup:
await page.unrouteAll({ behavior: 'ignoreErrors' });
// → Xoá handler ngay
// → Handler đang fetch dở → error bị swallow
// → unrouteAll() resolve ngay, không chờ 10s
3

So Sánh 3 Behavior

unrouteAll() có ba behavior riêng biệt:

behavior Handler in-flight Error từ handler Tốc độ cleanup
default (không truyền option) Abort ngay Có thể throw Fast
'wait' Chờ handler hoàn thành Surface (throw) Chậm (tùy handler)
'ignoreErrors' Abort ngay Swallow (không throw) Fast

Default vs 'ignoreErrors': Cả hai đều abort handler ngay và đều fast. Khác biệt nằm ở error handling: default có thể để error từ in-flight handler propagate (tùy Playwright version và context), còn 'ignoreErrors' cam kết swallow tất cả error — đây là guarantee tường minh, phù hợp khi viết teardown cần đảm bảo không throw.

'wait' vs 'ignoreErrors': Đây là hai triết lý khác nhau cho cleanup. 'wait' hướng đến correct shutdown — đảm bảo handler kết thúc đúng trước khi tiếp tục. 'ignoreErrors' hướng đến fast exit — test đã xong, không cần handler sạch sẽ.

4

Tại Sao 'ignoreErrors' Safe Cho Teardown

Sau khi test kết thúc (pass hoặc fail), mọi error phát sinh trong teardown đều là error phụ — không liên quan đến logic test thật sự. Nếu teardown throw, Playwright báo cáo error đó cùng với (hoặc thay cho) test result thật, gây khó phân biệt nguyên nhân fail.

Ví dụ tình huống: test fail ở dòng assertion cuối, đồng thời afterEach dùng 'wait' gặp handler đang fetch dở và throw thêm một error nữa. Kết quả: CI log có hai error, không rõ cái nào là root cause. Với 'ignoreErrors':

  • Test đã pass/fail với lý do thật.
  • Handler dở dang error bị swallow.
  • Test report chỉ có một error — chính xác hơn.

Nguyên tắc: teardown không nên tạo thêm signal có thể che khuất test result. 'ignoreErrors' enforce nguyên tắc này.

Đây cũng là lý do tại sao một số test framework (JUnit, RSpec) phân biệt "test failure" và "teardown failure" — failure trong teardown không nên override failure trong test body. Playwright chưa có phân tách tường minh này, nên 'ignoreErrors' là cách thực hành phòng vệ.

5

Pattern afterEach Robust

Pattern chuẩn khi dùng afterEach để cleanup route handlers:

import { test, expect } from '@playwright/test';

test.describe('Cart API mocks', () => {
  test.afterEach(async ({ page }) => {
    await page.unrouteAll({ behavior: 'ignoreErrors' });
    // Cleanup không throw dù handler đang chạy dở
  });

  test('hiển thị giỏ hàng khi API trả về items', async ({ page }) => {
    await page.route('**/api/cart', async (route) => {
      await route.fulfill({
        json: { items: [{ id: 1, name: 'Product A', qty: 2 }] },
      });
    });

    await page.goto('/cart');
    await expect(page.getByText('Product A')).toBeVisible();
  });

  test('hiển thị giỏ hàng rỗng khi API trả về []', async ({ page }) => {
    await page.route('**/api/cart', async (route) => {
      await route.fulfill({ json: { items: [] } });
    });

    await page.goto('/cart');
    await expect(page.getByText('Giỏ hàng trống')).toBeVisible();
  });
});

Mỗi test đăng ký handler riêng cho **/api/cart. afterEach xoá toàn bộ sau mỗi test — test sau không bị ảnh hưởng bởi mock của test trước. Vì dùng 'ignoreErrors', nếu cleanup trùng với request in-flight, teardown không fail và không làm nhiễu test report.

So sánh với cách không có behavior tường minh:

// Có thể có vấn đề — behavior default không cam kết swallow
test.afterEach(async ({ page }) => {
  await page.unrouteAll();  // default: có thể throw nếu có in-flight handler error
});

// An toàn hơn
test.afterEach(async ({ page }) => {
  await page.unrouteAll({ behavior: 'ignoreErrors' });  // tường minh, không throw
});
6

Pattern Fixture Teardown

Fixture là nơi phổ biến nhất cần teardown an toàn. Phần teardown (sau await use()) chạy sau test xong, nên error teardown không nên ảnh hưởng result:

import { test as base, expect } from '@playwright/test';

type Fixtures = {
  mockSetup: void;
};

const test = base.extend<Fixtures>({
  mockSetup: async ({ page }, use) => {
    // Setup: đăng ký route handler
    await page.route('**/api/**', async (route) => {
      const url = route.request().url();

      if (url.includes('/api/user')) {
        await route.fulfill({ json: { id: 1, name: 'Test User' } });
      } else if (url.includes('/api/products')) {
        await route.fulfill({ json: { items: [] } });
      } else {
        await route.continue();
      }
    });

    await use();  // Chạy test body

    // Teardown: cleanup safe
    await page.unrouteAll({ behavior: 'ignoreErrors' });
  },
});

test('user profile load', async ({ page, mockSetup }) => {
  await page.goto('/profile');
  await expect(page.getByText('Test User')).toBeVisible();
});

test('product list empty state', async ({ page, mockSetup }) => {
  await page.goto('/products');
  await expect(page.getByText('Không có sản phẩm')).toBeVisible();
});

Cả hai test dùng cùng fixture mockSetup. Mỗi test có isolated route handler riêng nhờ fixture scope. Teardown với 'ignoreErrors' đảm bảo fixture cleanup không throw dù test fail hoặc có request chưa kịp hoàn thành khi test kết thúc.

Fixture scope và cleanup: Fixture page-scoped chạy teardown sau mỗi test. Fixture worker-scoped chạy teardown sau tất cả test trong worker. Cả hai đều nên dùng 'ignoreErrors' trong unrouteAll() để tránh worker-level failure contaminate nhiều test results.

7

Khi Nào Dùng 'ignoreErrors' vs 'wait'

Dùng 'ignoreErrors' khi:

  • Test đã pass hoặc fail — cleanup không cần chính xác, chỉ cần không throw thêm error.
  • Handler async lâu (fetch real backend, sleep delay) — không muốn teardown block thêm thời gian.
  • Error từ cleanup không relevant với test result — không cần biết handler có hoàn thành không.
  • Đây là phần teardown trong afterEach hoặc sau await use() trong fixture.

Dùng 'wait' khi:

  • Handler đang thực hiện operation quan trọng giữa chừng — ví dụ: lưu state, ghi log, cập nhật external mock server.
  • Reset mid-test (không phải teardown cuối): cần đảm bảo handler cũ đã thoát hoàn toàn trước khi đăng ký handler mới.
  • Cần phát hiện lỗi trong handler để debug — 'wait' surface error, 'ignoreErrors' che đi.

Tóm tắt decision tree:

  • Đây là teardown sau test → 'ignoreErrors'.
  • Đây là mid-test reset, cần clean shutdown → 'wait'.
  • Đây là mid-test reset, cần nhanh, không quan tâm error → 'ignoreErrors'.
8

Limitation

1. v1.47+ only

Option behavior: 'ignoreErrors' (cùng với 'wait') chỉ có từ v1.47. Phiên bản cũ hơn không nhận option này — truyền vào bị ignore hoặc gây type error. Kiểm tra version:

npx playwright --version

Nếu project đang dùng v1.46 trở xuống, cần upgrade hoặc dùng workaround thủ công (wrap unrouteAll() trong try/catch).

2. 'ignoreErrors' có thể che bug handler thật

Khi dùng 'ignoreErrors' trong teardown, mọi error từ handler đều bị swallow — kể cả lỗi logic trong handler (ví dụ: route.fulfill() được gọi với body không hợp lệ). Nếu handler có bug, 'ignoreErrors' sẽ che bug đó. Trong quá trình viết test mới, có thể tạm dùng 'wait' hoặc không có option để surface lỗi, sau đó chuyển sang 'ignoreErrors' khi handler đã verified.

3. Không hoàn tác network request đã gửi

unrouteAll() chỉ xoá handler — request đã được browser gửi đi vẫn tiếp tục đến server (dù handler không còn intercept). Nếu cần block hoàn toàn request cả sau khi cleanup, cần dùng cơ chế khác (ví dụ: abort trước khi unroute, hoặc dùng context-level firewall).

9

Pitfalls

Pitfall 1: Dùng 'ignoreErrors' cho mid-test cleanup khi handler cần hoàn thành

// SAI — handler đang save auth token, dùng 'ignoreErrors' → token không được save
test('login flow', async ({ page }) => {
  let savedToken = '';

  await page.route('**/api/auth', async (route) => {
    const response = await route.fetch();
    const body = await response.json();
    savedToken = body.token;  // operation quan trọng
    await route.fulfill({ response });
  });

  await page.click('#login-button');

  // Reset giữa test để chuẩn bị phase 2
  await page.unrouteAll({ behavior: 'ignoreErrors' });
  // → Nếu handler đang fetch, token chưa được save → savedToken vẫn rỗng
  // → Phase 2 fail vì thiếu token

  // ĐÚNG — dùng 'wait' để chắc chắn handler hoàn thành
  // await page.unrouteAll({ behavior: 'wait' });
  // → savedToken đã có giá trị, phase 2 hoạt động đúng
});

Pitfall 2: 'ignoreErrors' che bug handler — khó phát hiện trong quá trình dev

// Handler có bug logic nhưng bị che bởi 'ignoreErrors' trong afterEach
test.afterEach(async ({ page }) => {
  await page.unrouteAll({ behavior: 'ignoreErrors' });
});

test('order checkout', async ({ page }) => {
  await page.route('**/api/order', async (route) => {
    // Bug: route.fulfill() gọi với status không hợp lệ
    await route.fulfill({ status: 999, json: { id: 123 } });
  });

  // Test pass vì app không check status code chặt
  // Handler throw sau test → bị swallow bởi 'ignoreErrors'
  // Bug không được phát hiện cho đến khi deploy
});
// Cách detect: tạm đổi thành 'wait' trong lúc debug

Pitfall 3: Nhầm giữa context-level và page-level unrouteAll

// SAI — đăng ký trên context nhưng cleanup trên page
test.beforeEach(async ({ context }) => {
  await context.route('**/api/**', handler);  // đăng ký trên context
});

test.afterEach(async ({ page }) => {
  await page.unrouteAll({ behavior: 'ignoreErrors' });
  // → Chỉ xoá handler đăng ký trên page, KHÔNG xoá handler đăng ký trên context
  // → Handler context tích lũy qua các test
});

// ĐÚNG — cleanup đúng level
test.afterEach(async ({ context }) => {
  await context.unrouteAll({ behavior: 'ignoreErrors' });
});

Pitfall 4: Không cleanup trong trường hợp test bị skip hoặc timeout

// 'ignoreErrors' trong afterEach chạy sau skip và timeout — đây là behavior đúng
// Nhưng nếu cleanup đặt trong test body thay vì afterEach, sẽ không chạy khi test bị skip/timeout

// SAI — cleanup trong test body
test('my test', async ({ page }) => {
  await page.route('**/api/**', handler);
  await page.goto('/page');
  // ... test actions ...
  await page.unrouteAll({ behavior: 'ignoreErrors' });  // Không chạy nếu test fail / timeout
});

// ĐÚNG — cleanup trong afterEach (luôn chạy)
test.afterEach(async ({ page }) => {
  await page.unrouteAll({ behavior: 'ignoreErrors' });
});
10

Quiz

Câu 1. Handler đang fetch real backend mất 8s. Test kết thúc sau 3s. Gọi unrouteAll({ behavior: 'ignoreErrors' }). Điều gì xảy ra với handler đang fetch dở?

Đáp án

Handler bị abort ngay — không chờ 8s còn lại. Error từ fetch dở dang bị swallow, không throw ra ngoài. unrouteAll() resolve ngay lập tức. Test teardown hoàn thành nhanh mà không cần chờ backend phản hồi.

Câu 2. afterEach dùng 'wait' thay vì 'ignoreErrors'. Test fail ở dòng assertion, đồng thời handler vẫn đang fetch. Điều gì xảy ra trong CI report?

Đáp án

'wait' chờ handler hoàn thành. Nếu handler throw error (ví dụ: backend trả 500, fetch timeout...), error đó surface ra ngoài từ afterEach. CI log có thể có hai error: một từ test assertion, một từ afterEach teardown. Khó xác định root cause. Với 'ignoreErrors', chỉ có error từ test assertion — report rõ hơn.

Câu 3. Đoạn code sau có vấn đề gì?

test('save order', async ({ context }) => {
  await context.route('**/api/order', async (route) => {
    await route.continue();  // forward to real backend
  });

  await page.click('#submit');
  // test assertions...
});

test.afterEach(async ({ page }) => {
  await page.unrouteAll({ behavior: 'ignoreErrors' });
});
Đáp án

Handler đăng ký trên context nhưng afterEach gọi page.unrouteAll(). Chỉ handlers trên page bị xoá; handler trên context vẫn còn và tích lũy qua mọi test. Fix: đổi thành context.unrouteAll({ behavior: 'ignoreErrors' }) hoặc đăng ký handler trên page thay vì context.

Câu 4. Đang phát triển handler mới cho endpoint /api/checkout. Khi nào nên dùng 'ignoreErrors', khi nào nên dùng 'wait' (hoặc không có behavior) trong giai đoạn này?

Đáp án

Trong giai đoạn phát triển handler, nên dùng 'wait' hoặc không truyền behavior để error handler surface ra ngoài — giúp phát hiện bug trong handler ngay. Sau khi handler đã verified và ổn định, chuyển teardown về 'ignoreErrors' để cleanup an toàn trong CI. Dùng 'ignoreErrors' ngay từ đầu có thể che bug handler, khiến mọi test pass nhưng handler thực ra không hoạt động đúng.

Câu 5. Handler đăng ký trong fixture dùng worker scope. Có nên dùng 'ignoreErrors' trong teardown fixture đó không? Vì sao?

Đáp án

Có. Worker-scoped fixture teardown chạy sau tất cả test trong worker — nếu teardown throw, toàn bộ test result trong worker đó có thể bị nhiễu. 'ignoreErrors' ngăn teardown worker-level throw, giúp result các test riêng lẻ không bị contaminate bởi cleanup error của worker. Đặc biệt quan trọng khi chạy nhiều test song song.

11

Bài Tiếp Theo

Bài 125: Block Resources — chặn tài nguyên tĩnh (images, fonts, stylesheets) bằng route handler để tăng tốc test và giảm flakiness do resource load.