Danh sách bài viết

Bài 83: `timeout` (Test) — Default 30s Và Cách Override

timeout ở cấp test là thời gian tối đa (ceiling) cho một test duy nhất chạy từ đầu đến cuối — gồm beforeEach hook, test body, afterEach hook và fixture setup/teardown ở test scope. Default 30 000 ms. Bài đi sâu 5 cấp override, phân biệt với globalTimeout và actionTimeout, cơ chế testInfo.setTimeout() từ fixture, pitfall thường gặp và best practice.

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

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

Sau khi hoàn thành bài này, bạn sẽ:

  • Hiểu test timeout là ceiling cho toàn bộ một test, bao gồm hooks và fixture teardown.
  • Biết 5 cấp override timeout và thứ tự ưu tiên.
  • Dùng đúng test.setTimeout()testInfo.setTimeout().
  • Phân biệt rõ test timeout với globalTimeout (toàn run) và actionTimeout (1 action).
  • Tránh 4 pitfall phổ biến khi làm việc với test timeout.
2

Test Timeout Là Gì?

Test timeout là thời gian tối đa cho phép một test đơn lẻ chạy từ khi bắt đầu đến khi kết thúc. Nếu test chưa hoàn thành khi đồng hồ hết, Playwright dừng test ngay và đánh trạng thái timedOut.

Giá trị mặc định: 30 000 ms (30s). Đây là giá trị đã tồn tại từ những phiên bản sớm của Playwright Test Runner và được giữ nhất quán đến nay.

Khai báo trong config:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  timeout: 30_000,  // 30s per test — đây là default, không cần khai báo nếu không thay đổi
});

Nếu không khai báo timeout trong config, Playwright dùng 30 000 ms. Bài này tập trung vào test timeout; globalTimeout (giới hạn cho toàn bộ run) được đề cập trong bài 82.

3

Timeout Bao Gồm Những Gì?

Test timeout đếm toàn bộ thời gian chạy của một test, không chỉ phần test body:

  • beforeEach hooks — tất cả hook trước test body.
  • Test body — phần code bên trong hàm async ({ page }) => { ... }.
  • afterEach hooks — tất cả hook sau test body.
  • Fixture setup và teardown ở test scope — code trước và sau await use(...) trong fixture.
// Ví dụ minh họa những gì được đếm vào timeout 30s
test.beforeEach(async ({ page }) => {
  await page.goto('/login');       // ← đếm vào timeout
  await page.fill('#user', 'qa'); // ← đếm vào timeout
  await page.click('button');     // ← đếm vào timeout
});

test('checkout flow', async ({ page }) => {
  // ← Test body bắt đầu từ đây
  await page.goto('/cart');
  await page.click('text=Checkout');
  await expect(page.getByTestId('confirm')).toBeVisible();
  // ← Test body kết thúc ở đây
});

test.afterEach(async ({ page }) => {
  await page.screenshot({ path: 'fail.png' }); // ← đếm vào timeout
});

Điểm hay bị bỏ qua: nếu beforeEach mất 20s, phần còn lại của test (body + afterEach) chỉ còn 10s trong budget 30s. Hook chậm ăn vào timeout của test body.

Fixture teardown: Chỉ fixture ở test scope được đếm vào test timeout. Fixture ở worker scope không bị giới hạn bởi test timeout mà chạy sau khi test đã kết thúc.

4

5 Cấp Override

Test timeout có thể được ghi đè ở 5 cấp theo thứ tự ưu tiên tăng dần (cấp hẹp hơn win):

Cấp 1: Config global

Áp dụng cho mọi test trong toàn dự án trừ khi bị override:

export default defineConfig({
  timeout: 30_000,  // default 30s
});

Cấp 2: Project

Override cho từng browser project — ví dụ mobile project cần timeout dài hơn vì network throttle:

export default defineConfig({
  timeout: 30_000,       // default cho mọi project
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      // không khai báo timeout → kế thừa 30s
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 13'] },
      timeout: 60_000,   // mobile project cần 60s
    },
  ],
});

Cấp 3: Describe block

test.describe.configure() đặt timeout cho cả nhóm test:

test.describe('External API integration', () => {
  test.describe.configure({ timeout: 60_000 });  // 60s cho cả group này

  test('create order', async ({ page }) => { /* ... */ });
  test('fetch invoice', async ({ page }) => { /* ... */ });
});

describe.configure() phải được gọi trực tiếp bên trong callback của describe, không được lồng vào trong test body hay hook.

Cấp 4: Test runtime — test.setTimeout()

Gọi trong test body để set timeout cho chính test đó. Chi tiết ở mục 5.

Cấp 5: test.slow()

Nhân timeout hiện tại ×3. Tham chiếu bài 405 của Series Cơ Bản để xem đầy đủ. Mục 7 bài này có reference ngắn.

Thứ tự ưu tiên

Cấp Phương pháp Phạm vi ảnh hưởng
1 (thấp nhất) timeout trong config Toàn dự án
2 timeout trong project config Một browser project
3 test.describe.configure({ timeout }) Một describe block
4 test.setTimeout(N) Một test đơn lẻ
5 (cao nhất) test.slow() Một test đơn lẻ (×3 cơ số)
5

test.setTimeout() Trong Test Body

test.setTimeout(N) set EXACT timeout = N ms cho test đang chạy. Không phụ thuộc giá trị config — ghi đè hoàn toàn:

test('long upload', async ({ page }) => {
  test.setTimeout(120_000);  // 2 phút cho test này, bất kể config nói gì

  await page.goto('/upload');
  await page.setInputFiles('input[type="file"]', 'big-file.zip');
  await expect(page.getByTestId('upload-status')).toHaveText('Upload complete', {
    timeout: 110_000,  // expect timeout riêng cho assertion chờ upload xong
  });
});

Thời điểm gọi: test.setTimeout() tính lại timeout kể từ thời điểm gọi. Gọi ở dòng đầu body để budget được tính từ đầu. Nếu gọi muộn (ví dụ sau 20s đã trôi qua), phần còn lại = N − thời gian đã trôi qua.

Gọi nhiều lần: Mỗi lần gọi reset lại giá trị timeout. Cuộc gọi cuối cùng thắng:

test('dynamic timeout', async ({ page }) => {
  test.setTimeout(60_000);  // đặt 60s ban đầu

  await page.goto('/dashboard');

  if (await page.locator('[data-slow-mode]').isVisible()) {
    test.setTimeout(120_000);  // reset thành 120s khi detect slow mode
  }

  await page.getByRole('button', { name: 'Export' }).click();
  await expect(page.getByText('Done')).toBeVisible();
});

Khác với test.slow() ở điểm quan trọng: test.setTimeout(N) set con số cứng N ms, không phụ thuộc config. Khi đổi config global timeout, test.setTimeout(N) không tự scale theo. Dùng khi cần kiểm soát chính xác thời gian; dùng test.slow() khi chỉ cần "nhiều hơn bình thường".

6

testInfo.setTimeout() Từ Fixture

Khi cần tăng timeout cho test dùng một fixture nhất định, gọi testInfo.setTimeout() từ bên trong fixture thay vì yêu cầu mỗi test tự gọi test.setTimeout():

// fixtures.ts
import { test as base } from '@playwright/test';

export const test = base.extend({
  slowServer: async ({}, use, testInfo) => {
    testInfo.setTimeout(90_000);  // tăng timeout cho bất kỳ test nào dùng fixture này

    const server = await startSlowExternalServer();
    await use(server);
    await server.stop();
  },
});
// my.spec.ts — test này tự động có timeout 90s nhờ fixture
import { test } from './fixtures';

test('test dùng slow server', async ({ slowServer, page }) => {
  // timeout tự động là 90s — không cần gọi test.setTimeout() ở đây
  await page.goto(slowServer.url);
  await expect(page.getByTestId('status')).toHaveText('Ready');
});

testInfo.setTimeout()test.setTimeout() làm cùng một việc về mặt cơ chế — cả hai đều set timeout của test hiện tại. Điểm khác: testInfo.setTimeout() gọi được từ fixture (nơi không có access vào test object trực tiếp), còn test.setTimeout() gọi từ test body.

Pattern này tiện khi fixture wrap một resource chậm (database seeding, external service, file system heavy op) — logic timeout tập trung ở fixture, không rải rắc trong từng test.

7

test.slow() — Reference Ngắn

test.slow() nhân timeout hiện tại lên ×3. Nếu config là 30s, gọi test.slow() → timeout = 90s:

test('slow test', async ({ page }) => {
  test.slow();  // 30s × 3 = 90s (hoặc config_timeout × 3)
  await page.goto('/heavy-page');
});

Khác với test.setTimeout(): test.slow() config-driven (tự scale khi đổi config global), còn test.setTimeout(N) hardcode N ms.

Chi tiết đầy đủ về test.slow() — conditional, describe.slow(), pitfall, best practice — xem Bài 405 (Series Cơ Bản): test.slow() — Đánh Dấu Test Chậm (×3 Timeout).

8

Timeout Là CEILING — Tác Động Thực Tế

Test timeout là ceiling — nó là giới hạn trên, không phải giá trị target. Test hoàn thành sau 5s với timeout 30s là bình thường và không gây vấn đề gì.

Điều quan trọng cần hiểu: mọi sub-timeout (actionTimeout, expect timeout) đều bị ràng buộc bởi test timeout. Nếu test timeout hết trước khi action timeout hết, Playwright dừng test ngay:

// playwright.config.ts
export default defineConfig({
  timeout: 30_000,              // test timeout: 30s
  use: {
    actionTimeout: 60_000,      // action timeout: 60s cho mỗi action
  },
});
// test dưới đây fail ở 30s, không phải 60s
test('misleading config', async ({ page }) => {
  await page.goto('/slow-page');
  // waitForSelector chờ tối đa 60s theo actionTimeout...
  // nhưng test timeout 30s hết trước → test bị dừng ở giây 30
  await page.locator('#result').waitFor();
});

Nguyên tắc: actionTimeout 60s + test timeout 30s → action fail ở 30s. Sub-timeout không thể vượt quá ceiling của test timeout.

Hệ quả thực tế: khi tăng actionTimeout hoặc expect timeout cho một assertion chậm, cần đảm bảo test timeout lớn hơn. Tránh config mâu thuẫn như ví dụ trên.

// Config nhất quán
export default defineConfig({
  timeout: 90_000,              // test timeout >= actionTimeout
  use: {
    actionTimeout: 60_000,      // action timeout < test timeout → hợp lệ
  },
});
9

Phân Biệt: Test Timeout vs globalTimeout vs actionTimeout

Timeout Phạm vi Default Vị trí config
globalTimeout Toàn bộ test run Không giới hạn defineConfig({ globalTimeout })
timeout (test) Một test đơn lẻ (hooks + body) 30 000 ms defineConfig({ timeout })
actionTimeout Một action duy nhất (click, fill...) 0 (không giới hạn) use: { actionTimeout }
navigationTimeout Một navigation (goto, waitForURL...) 0 (không giới hạn) use: { navigationTimeout }
expect.timeout Một assertion (toBeVisible, toHaveText...) 5 000 ms expect: { timeout }

Quan hệ lồng nhau:

globalTimeout (toàn run)
  └── test timeout × N tests
        └── actionTimeout (per action)
        └── expect.timeout (per assertion)
        └── navigationTimeout (per navigation)

Mỗi lớp bị ràng buộc bởi lớp ngoài. Test timeout ≤ globalTimeout còn lại. actionTimeout ≤ test timeout còn lại.

globalTimeout được đề cập chi tiết trong bài 82. expect.timeout sẽ được bài 84 đi sâu.

10

Timeout 0 = No Limit

Truyền 0 vào bất kỳ timeout nào trong Playwright có nghĩa là không giới hạn:

// Disable timeout cho test này — test chạy đến khi xong hoặc mãi mãi
test('debug test', async ({ page }) => {
  test.setTimeout(0);

  await page.goto('/complex-flow');
  // Playwright không bao giờ timeout test này
});

Hoặc trong config:

export default defineConfig({
  timeout: 0,  // disable test timeout toàn dự án — NGUY HIỂM
});

Rủi ro khi dùng timeout 0:

  • Test bị hang do bug logic (vòng lặp vô hạn, waitFor không bao giờ resolve) sẽ block CI runner mãi.
  • Worker bị kẹt → các test khác trong queue chờ vô hạn.
  • Toàn bộ CI pipeline bị block nếu không có timeout ngoài (job timeout).

Dùng timeout: 0 chỉ trong môi trường development cục bộ khi debug một test cụ thể. Không commit vào codebase và tuyệt đối không để trên CI.

11

Pattern CI vs Local

CI runner thường có tài nguyên ít hơn máy local của dev (CPU shared, không có SSD nhanh). Dùng biến môi trường CI để tự động tăng timeout trên CI:

// playwright.config.ts
export default defineConfig({
  timeout: process.env.CI ? 60_000 : 30_000,
});

Biến CI được tự động set bởi hầu hết CI platforms: GitHub Actions, GitLab CI, CircleCI đều set CI=true. Biến này không được set khi chạy local.

Áp dụng cùng pattern cho project-level timeout:

export default defineConfig({
  timeout: process.env.CI ? 60_000 : 30_000,
  projects: [
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 13'] },
      timeout: process.env.CI ? 90_000 : 45_000,  // mobile cần gấp đôi
    },
  ],
});

Khi dùng pattern này, không cần gọi test.setTimeout() hay test.slow() trong từng test chỉ vì CI chậm hơn — config đã xử lý tập trung.

12

Use Cases Cần Override

Default 30s đủ cho phần lớn test UI thông thường. Những trường hợp cần override:

1. Long upload / download

test('upload large dataset', async ({ page }) => {
  test.setTimeout(120_000);  // upload 500MB CSV: cần 2 phút

  await page.goto('/data-import');
  await page.setInputFiles('input[type="file"]', 'dataset-500mb.csv');
  await expect(page.getByTestId('import-status')).toHaveText('Complete', {
    timeout: 100_000,
  });
});

2. Slow external API

test('trigger webhook và chờ callback', async ({ page }) => {
  test.setTimeout(90_000);  // third-party webhook delay up to 60s

  await page.goto('/integrations/webhook-test');
  await page.getByRole('button', { name: 'Trigger' }).click();
  await expect(page.getByTestId('webhook-received')).toBeVisible({
    timeout: 75_000,
  });
});

3. Complex E2E flow nhiều step

test('full order lifecycle', async ({ page }) => {
  test.setTimeout(90_000);  // tạo → thanh toán → confirm → ship: ~60s

  await page.goto('/shop');
  await page.getByTestId('product-1').click();
  await page.getByRole('button', { name: 'Add to cart' }).click();
  await page.getByRole('link', { name: 'Checkout' }).click();
  await page.fill('[name=card]', '4242424242424242');
  await page.getByRole('button', { name: 'Pay' }).click();
  await expect(page.getByTestId('order-confirmed')).toBeVisible();
  await expect(page.getByTestId('shipping-label')).toBeVisible();
});

4. Visual regression với rendering nặng

test('dashboard screenshot', async ({ page }) => {
  test.setTimeout(60_000);  // render chart + animation settle

  await page.goto('/dashboard');
  await page.waitForLoadState('networkidle');
  await page.waitForTimeout(2000);  // wait for chart animation
  await expect(page).toHaveScreenshot('dashboard.png');
});
13

Timeout Trong Reporter

Khi test bị timeout, Playwright đánh trạng thái timedOut — khác với failed (assertion error) và passed. Phân biệt này quan trọng để debug:

  ✓  1 [chromium] › login.spec.ts:5:5 › login flow (3.2s)
  ×  2 [chromium] › upload.spec.ts:8:5 › upload large file (30.0s)
       Test timeout of 30000ms exceeded.

Terminal: Test timeout hiển thị thời gian bằng đúng timeout ceiling (30.0s nếu timeout là 30s) kèm message "Test timeout of Xms exceeded".

HTML Report: Test timedOut hiển thị riêng trong section "Failed" với badge màu vàng/cam (tùy theme), phân biệt với assertion fail (đỏ).

Trace Viewer: Khi trace được bật, trace dừng ngay tại action cuối cùng trước khi timeout — hữu ích để xem test đang làm gì khi hết giờ.

Khi gặp timedOut, bước debug đầu tiên: xem action nào đang chạy khi timeout, sau đó quyết định tăng test timeout hay tối ưu action đó.

14

Pitfall Thường Gặp

1. Tăng global timeout rộng thay vì override cụ thể

Thấy một vài test timeout → tăng config global từ 30s lên 120s cho cả project. Hệ quả: test bị hang do bug sẽ mất 120s mới fail thay vì 30s, suite chậm hơn đáng kể, và mask performance regression — test chạy chậm 3x nhưng không ai biết vì timeout đủ rộng. Override cụ thể cho test hoặc describe cần, giữ global sát thực tế.

2. Quên rằng beforeEach hook đếm vào timeout

// beforeEach setup mất ~25s (database seed + auth flow)
test.beforeEach(async ({ page }) => {
  await seedDatabase();           // 15s
  await loginViaUI(page);        // 10s
});

// test body nhận được <5s trong budget 30s → dễ timeout
test('place order', async ({ page }) => {
  // chỉ còn ~5s từ 30s budget
  await page.goto('/shop');
  await page.getByTestId('product-1').click();
  await page.getByRole('button', { name: 'Buy Now' }).click();
  await expect(page.getByText('Order confirmed')).toBeVisible(); // timeout ở đây
});

Fix: tăng timeout cho describe block chứa test này, hoặc di chuyển seed vào worker-scope fixture để không đếm vào test timeout.

3. test.setTimeout(0) trên CI

Dev thêm test.setTimeout(0) khi debug local, quên xóa trước khi push. Test bị hang trên CI do mạng chậm hoặc environment issue, block worker và toàn bộ pipeline đến khi job timeout ngoài (thường 6h trên GitHub Actions).

4. Nhầm test timeout với expect timeout

// Test đang fail với: "expect(locator).toBeVisible()
//   Timeout 5000ms exceeded"
// Dev thêm test.setTimeout(60_000) nhưng vẫn fail ở 5s

test('wait for modal', async ({ page }) => {
  test.setTimeout(60_000);  // KHÔNG FIX ĐƯỢC vấn đề này

  await page.getByRole('button', { name: 'Open Modal' }).click();
  // fail ở đây do expect timeout mặc định 5s, không phải test timeout
  await expect(page.locator('.modal')).toBeVisible();
});
// FIX ĐÚNG: truyền timeout riêng cho assertion
  await expect(page.locator('.modal')).toBeVisible({ timeout: 15_000 });

Xem thông báo lỗi: "Test timeout Xms exceeded" là test timeout; "Timeout Xms exceeded" trong expect là expect timeout. Hai thứ khác nhau, cách fix khác nhau.

15

Best Practice

  • Giữ global timeout sát thực tế: 30s đủ cho phần lớn test UI. Tăng cụ thể theo describe hoặc test, không tăng global vì một vài test outlier.
  • Ưu tiên test.slow() thay vì hardcode: Khi cần timeout dài hơn nhưng không cần con số chính xác, dùng test.slow() — config-driven, tự scale khi global timeout đổi.
  • Dùng testInfo.setTimeout() trong fixture: Khi fixture wrap resource chậm, đặt timeout logic ở fixture thay vì yêu cầu mỗi test tự gọi. DRY và dễ maintain.
  • Config nhất quán — tránh sub-timeout > test timeout: Kiểm tra actionTimeoutexpect.timeout nhỏ hơn timeout (test). Config mâu thuẫn gây confusion khi debug.
  • Pattern CI vs local ở config level: Dùng process.env.CI ? 60_000 : 30_000 trong config thay vì rải test.setTimeout() theo điều kiện CI trong từng test.
  • Không dùng timeout: 0 ngoài debug session: Luôn có timeout cụ thể trên CI. Nếu cần timeout rất dài, đặt số cụ thể (ví dụ 300s) thay vì 0.
16

Quiz

Câu 1. Test có beforeEach mất 20s và test body cần thêm 15s. Config global timeout là 30s. Test có pass không?

Đáp án

Không. Test timeout (30s) đếm cả beforeEach. Sau 20s hook xong, còn 10s cho test body nhưng body cần 15s → timeout ở giây 30. Cần tăng timeout cho describe block này hoặc tối ưu hook.

Câu 2. Config có actionTimeout: 60_000timeout: 30_000. Một action bắt đầu ở giây 28. Khi nào action bị dừng?

Đáp án

Ở giây 30 — test timeout ceiling thắng. Action timeout 60s chỉ có hiệu lực trong phạm vi còn lại của test timeout. Dù actionTimeout nói 60s, test timeout 30s đã hết trước nên Playwright dừng test ở giây 30.

Câu 3. Sự khác biệt giữa test.setTimeout(90_000)test.slow() khi global timeout là 30s?

Đáp án

Cả hai đều cho kết quả 90s timeout trong trường hợp này. Khác biệt khi đổi config: nếu global timeout tăng lên 60s, test.slow() tự thành 180s, còn test.setTimeout(90_000) vẫn là 90s. test.slow() config-driven; test.setTimeout(N) hardcode N ms.

Câu 4. Muốn mọi test trong một describe block có timeout 60s thay vì 30s global. Cú pháp nào đúng và đặt ở đâu?

Đáp án
test.describe('Slow integration tests', () => {
  test.describe.configure({ timeout: 60_000 });  // đặt trực tiếp bên trong callback

  test('test A', async ({ page }) => { /* ... */ });
  test('test B', async ({ page }) => { /* ... */ });
});

test.describe.configure({ timeout }) phải được gọi trực tiếp bên trong callback của describe, không trong test body hay hook.

Câu 5. Test fail với lỗi "Test timeout of 30000ms exceeded" nhưng bạn biết action đó thực ra chỉ cần 25s — hook setup mất 10s, body 15s. Cách fix tốt nhất là gì?

Đáp án

Tùy nguyên nhân: nếu hook setup cần 10s là hợp lệ và không thể tối ưu, tăng timeout cho describe block (ví dụ timeout: 45_000). Nếu hook setup có thể di chuyển sang worker-scope fixture, làm vậy để setup chỉ chạy 1 lần và không đếm vào test timeout từng test. Không tăng global timeout vì chỉ nhóm test này có hook chậm.