Danh sách bài viết

Bài 87: `test.setTimeout()` — Thay Đổi Timeout Runtime

test.setTimeout(ms) cho phép set exact timeout cho test hiện tại ngay trong quá trình chạy — override hoàn toàn giá trị từ config. Bài này đi vào cơ chế last-wins khi gọi nhiều lần, timing quan trọng khi gọi sớm hay muộn, pattern conditional timeout và beforeEach, phân biệt với test.slow() và describe.configure(), cùng các pitfall thường gặp.

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.setTimeout(ms) set exact timeout cho test đang chạy và override config hoàn toàn.
  • Biết khi nào gọi sớm hay muộn ảnh hưởng thế nào đến remaining time.
  • Dùng đúng cơ chế last-wins khi gọi nhiều lần.
  • Phân biệt rõ test.setTimeout(N) với test.slow()describe.configure({ timeout }).
  • Tránh 4 pitfall phổ biến khi dùng test.setTimeout().
2

Cú Pháp Và Behavior Cốt Lõi

test.setTimeout(ms) nhận một số nguyên (milliseconds) và đặt đó làm timeout mới cho test đang chạy:

test('long flow', async ({ page }) => {
  test.setTimeout(120_000);  // 2 phút cho test này
  await page.goto('/heavy-dashboard');
  await page.getByRole('button', { name: 'Generate Report' }).click();
  await expect(page.getByTestId('report-ready')).toBeVisible({ timeout: 100_000 });
});

Ba điểm behavior quan trọng:

  • Exact, không phải multiply. test.setTimeout(120_000) đặt timeout = 120 000 ms. Không liên quan đến giá trị config hiện tại.
  • Override config. Ghi đè hoàn toàn timeout từ playwright.config.ts, project config, hoặc describe.configure().
  • Chỉ apply cho test hiện tại. Test khác trong cùng file hoặc describe không bị ảnh hưởng.

Có thể gọi từ test body hoặc từ hook (beforeEach, afterEach). Không gọi được từ bên ngoài test/hook context — Playwright sẽ throw error.

3

Timing: Gọi Sớm vs Gọi Muộn

Thời điểm gọi test.setTimeout() ảnh hưởng trực tiếp đến remaining time của test:

Gọi ở dòng đầu test body (khuyến nghị):

test('upload test', async ({ page }) => {
  test.setTimeout(90_000);  // gọi ngay — test có gần đủ 90s từ đây

  await page.goto('/upload');
  await page.setInputFiles('input[type="file"]', 'large.zip');
  await expect(page.getByTestId('status')).toHaveText('Done', { timeout: 80_000 });
});

Gọi muộn — sau khi thời gian đã trôi:

test('late setTimeout', async ({ page }) => {
  // test bắt đầu, đồng hồ chạy từ đây
  await page.goto('/slow-page');        // mất ~20s
  await page.waitForLoadState('networkidle'); // mất ~5s

  // Đã 25s trôi qua. Config timeout là 30s, còn 5s.
  test.setTimeout(60_000);
  // Bây giờ timeout reset thành 60s tính từ thời điểm test bắt đầu.
  // Remaining = 60_000 - 25_000 = 35s

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

Playwright tính remaining time = setTimeout giá trị mớithời gian đã trôi kể từ start test. Gọi sớm thì remaining gần bằng toàn bộ giá trị mới. Gọi muộn thì remaining nhỏ hơn đáng kể.

Trường hợp nguy hiểm: gọi test.setTimeout(60_000) sau khi đã chạy 65s → remaining âm → test fail ngay lập tức vì đã vượt timeout mới.

4

Gọi Nhiều Lần — Last Wins

Gọi test.setTimeout() nhiều lần trong cùng một test là hợp lệ. Lần gọi cuối cùng thắng:

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

  await page.goto('/dashboard');

  // detect runtime condition
  const isBatchMode = await page.locator('[data-batch]').isVisible();
  if (isBatchMode) {
    test.setTimeout(90_000);  // override thành 90s khi biết cần xử lý nhiều hơn
  }

  await page.getByRole('button', { name: 'Process' }).click();
  await expect(page.getByTestId('done')).toBeVisible();
});

Sau lần gọi thứ hai, timeout hiệu lực = 90 000 ms. Giá trị 60 000 ms từ lần gọi trước bị bỏ qua. Remaining time vẫn tính từ lúc test bắt đầu:

test('multiple setTimeout', async ({ page }) => {
  test.setTimeout(60_000);
  // ... mất 10s ...
  test.setTimeout(90_000);
  // remaining = 90_000 - 10_000 = 80s tính từ đây
});

Cơ chế last-wins hữu ích để điều chỉnh timeout dựa trên dữ liệu thu được trong quá trình test chạy. Không cần biết trước điều kiện khi viết test.

5

setTimeout(0) — Disable Timeout

Truyền 0 vào test.setTimeout(0) có nghĩa là disable timeout hoàn toàn — test chạy đến khi xong hoặc mãi mãi:

test('no timeout', async ({ page }) => {
  test.setTimeout(0);  // disable — không có giới hạn thời gian

  await page.goto('/debug-flow');
  // Playwright không bao giờ timeout test này
  debugger; // có thể pause lâu không bị kill
});

Use case duy nhất hợp lệ: debug local khi cần step-through Inspector hoặc DevTools mà không muốn bị timeout cắt ngang. Không commit vào codebase và tuyệt đối không để chạy trên CI.

Rủi ro: Test bị hang do bug logic (locator không bao giờ resolve, network stall) sẽ block worker và toàn pipeline CI đến khi job timeout ngoài (thường 6h trên GitHub Actions).

6

Phân Biệt Với test.slow()

Cả hai đều set timeout cho test hiện tại từ bên trong test body, nhưng cơ chế khác nhau:

Đặc điểm test.setTimeout(N) test.slow()
Cơ chế Đặt exact N ms Nhân timeout hiện tại × 3
Phụ thuộc config Không — hardcode N ms Có — tự scale khi config đổi
Config global 30s Timeout = N ms (bất kể 30s) Timeout = 90s (30 × 3)
Config global đổi → 60s Timeout vẫn = N ms Timeout tự thành 180s (60 × 3)
Gọi nhiều lần Last wins Chỉ gọi 1 lần có hiệu lực
Dùng khi Cần kiểm soát chính xác ms Chỉ cần "nhiều hơn bình thường"
// test.slow() — config-driven
test('slow test', async ({ page }) => {
  test.slow();  // 30s × 3 = 90s — hoặc config_timeout × 3
  await page.goto('/heavy-page');
});

// test.setTimeout() — hardcode
test('upload test', async ({ page }) => {
  test.setTimeout(120_000);  // luôn là 120s, bất kể config nói gì
  await page.setInputFiles('input', 'large.zip');
});

Khi dùng test.slow(), thay đổi timeout global ở config tự động scale toàn bộ các test.slow() trong project. Dùng test.setTimeout() khi cần đảm bảo một con số cụ thể không phụ thuộc config.

7

Phân Biệt Với describe.configure()

test.describe.configure({ timeout })test.setTimeout() đều override timeout, nhưng phạm vi và thời điểm áp dụng khác nhau:

Đặc điểm describe.configure({ timeout }) test.setTimeout(N)
Phạm vi Cả describe block Chỉ test đang chạy
Thời điểm Declarative — khai báo trước khi test chạy Runtime — trong lúc test đang chạy
Vị trí gọi Trực tiếp trong callback describe Test body hoặc hook
Điều kiện runtime Không hỗ trợ Hỗ trợ (có thể dùng if/else)
// describe.configure — declarative, áp dụng cho cả group
test.describe('API Integration', () => {
  test.describe.configure({ timeout: 60_000 });  // mọi test trong group đều có 60s

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

// test.setTimeout — runtime, chỉ 1 test, có thể conditional
test.describe('File Operations', () => {
  test('upload file', async ({ page }) => {
    const size = await getFileSize();
    if (size > 50_000_000) {
      test.setTimeout(300_000);  // chỉ test này, chỉ khi file lớn
    }
  });
  test('delete file', async ({ page }) => { /* timeout bình thường */ });
});

Ưu tiên describe.configure() khi biết trước cả nhóm test cần timeout dài. Dùng test.setTimeout() khi cần linh hoạt theo điều kiện runtime hoặc chỉ một test cụ thể cần ngoại lệ.

8

Phân Biệt Với Per-Action Timeout

test.setTimeout() set ceiling cho toàn bộ test. Per-action timeout { timeout } chỉ giới hạn một action hoặc assertion đơn lẻ:

test('compare scopes', async ({ page }) => {
  test.setTimeout(120_000);  // ceiling: cả test được 120s

  await page.goto('/dashboard');

  // per-action timeout — chỉ lần click này được tối đa 10s
  await page.getByRole('button', { name: 'Load' }).click({ timeout: 10_000 });

  // per-assertion timeout — chỉ lần expect này được tối đa 90s
  await expect(page.getByTestId('data-table')).toBeVisible({ timeout: 90_000 });
});

Hai lớp này hoạt động độc lập nhưng ràng buộc lẫn nhau theo quan hệ ceiling:

  • Per-action timeout không thể vượt qua remaining time của test timeout.
  • Nếu test timeout hết trước khi per-action timeout hết, test bị dừng ngay.
test('ceiling constraint', async ({ page }) => {
  test.setTimeout(30_000);  // test ceiling: 30s

  // expect timeout 60s — nhưng test chỉ còn 30s
  // nếu element không xuất hiện sau 30s, test timeout thắng
  await expect(page.getByTestId('slow-element')).toBeVisible({ timeout: 60_000 });
});

Dùng per-action timeout khi muốn fail nhanh tại một action cụ thể mà không cần chờ hết test timeout. Dùng test.setTimeout() khi cần nâng trần cho cả test.

9

testInfo.setTimeout() — Preview Ngắn

testInfo.setTimeout() làm cùng việc với test.setTimeout() về mặt cơ chế — cả hai set timeout của test hiện tại. Điểm khác biệt:

  • test.setTimeout(N) — dùng từ test body, nơi có access vào global test object.
  • testInfo.setTimeout(N) — dùng từ fixture hoặc hook nhận testInfo param, nơi không có test object trực tiếp.
// testInfo.setTimeout từ fixture
const test = base.extend({
  slowResource: async ({}, use, testInfo) => {
    testInfo.setTimeout(90_000);  // tăng timeout cho mọi test dùng fixture này
    const resource = await connectSlowServer();
    await use(resource);
    await resource.disconnect();
  },
});

Bài 88 đi sâu vào testInfo.setTimeout() và các method khác trên testInfo.

10

Pattern: Conditional Timeout

Dùng test.setTimeout() khi timeout cần phụ thuộc vào dữ liệu hoặc state chỉ biết được trong quá trình test chạy:

Conditional theo file size

test('process file', async ({ page }) => {
  await page.goto('/file-manager');

  // lấy file size từ API trước khi upload
  const fileSize = await page.evaluate(() => {
    return fetch('/api/pending-file').then(r => r.json()).then(d => d.size);
  });

  if (fileSize > 100_000_000) {
    test.setTimeout(300_000);  // 5 phút cho file > 100 MB
  } else if (fileSize > 10_000_000) {
    test.setTimeout(120_000);  // 2 phút cho file 10–100 MB
  }
  // file nhỏ hơn → dùng config timeout mặc định

  await page.getByRole('button', { name: 'Process' }).click();
  await expect(page.getByTestId('process-status')).toHaveText('Complete');
});

Conditional theo environment

test('api integration', async ({ page }) => {
  await page.goto('/integrations');

  // check xem có đang dùng staging hay production
  const env = await page.locator('[data-env]').getAttribute('data-env');
  if (env === 'staging') {
    test.setTimeout(90_000);  // staging thường chậm hơn 3x
  }

  await page.getByRole('button', { name: 'Sync Now' }).click();
  await expect(page.getByTestId('sync-result')).toBeVisible();
});

Pattern này cho phép test tự điều chỉnh timeout dựa trên runtime context, tránh phải hardcode timeout quá lớn cho tất cả trường hợp.

11

Pattern: Gọi Trong beforeEach

Gọi test.setTimeout() trong beforeEach khi muốn áp timeout riêng cho tập test có tag hoặc điều kiện chung:

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

test.beforeEach(async ({}, testInfo) => {
  // tăng timeout cho test có tag @slow
  if (testInfo.tags.includes('@slow')) {
    test.setTimeout(120_000);
  }
});

test('normal test', async ({ page }) => {
  // timeout bình thường theo config
  await page.goto('/fast-page');
  await expect(page.getByTestId('content')).toBeVisible();
});

test('batch processing @slow', async ({ page }) => {
  // test.setTimeout(120_000) đã được beforeEach set
  await page.goto('/batch');
  await page.getByRole('button', { name: 'Run Batch' }).click();
  await expect(page.getByTestId('batch-done')).toBeVisible();
});

Lưu ý quan trọng: khi gọi test.setTimeout() trong beforeEach, timeout bắt đầu tính từ lúc test bắt đầu (trước khi beforeEach chạy). Phần thời gian beforeEach đã dùng tính vào budget. Nếu beforeEach mất nhiều thời gian trước khi gọi test.setTimeout(), remaining time trong test body sẽ ít hơn giá trị set.

test.beforeEach(async ({ page }, testInfo) => {
  await page.goto('/login');       // mất 5s
  await loginFlow(page);           // mất 10s
  // 15s đã trôi qua trước khi set timeout
  if (testInfo.tags.includes('@slow')) {
    test.setTimeout(120_000);  // remaining = 120_000 - 15_000 = 105s cho phần còn lại
  }
});
12

Use Cases Thực Tế

1. Upload / download file lớn

test('upload dataset', async ({ page }) => {
  test.setTimeout(300_000);  // 5 phút — upload 500MB CSV

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

2. Long batch operation

test('nightly batch job', async ({ page }) => {
  test.setTimeout(180_000);  // 3 phút — trigger batch và chờ kết quả

  await page.goto('/admin/batch');
  await page.getByRole('button', { name: 'Run Now' }).click();
  await expect(page.getByTestId('job-status')).toHaveText('Success', {
    timeout: 160_000,
  });
});

3. Debug local — step-through

test('debug complex flow', async ({ page }) => {
  test.setTimeout(0);  // CHỈ DÙNG LOCAL — không commit

  await page.goto('/complex-checkout');
  // pause tại đây để inspect bằng Playwright Inspector
  await page.pause();
  await page.getByRole('button', { name: 'Continue' }).click();
});

4. Test phụ thuộc third-party webhook

test('payment webhook', async ({ page }) => {
  test.setTimeout(90_000);  // third-party webhook có thể delay 60s

  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay' }).click();
  // chờ webhook callback từ payment provider
  await expect(page.getByTestId('payment-confirmed')).toBeVisible({
    timeout: 75_000,
  });
});
13

Timeout Fail — Reporter Hiện Gì

Khi test vượt quá timeout đặt bởi test.setTimeout(), Playwright đánh trạng thái timedOut — khác với failed (assertion error):

  ×  1 [chromium] › upload.spec.ts:8:5 › upload dataset (120.0s)
       Test timeout of 120000ms exceeded.

Giá trị timeout hiển thị trong output là giá trị được đặt bởi test.setTimeout(), không phải giá trị config. Nếu test.setTimeout(120_000) được gọi, output sẽ hiện "120000ms", dù config có là 30s hay 60s.

Trong HTML Report, test timedOut xuất hiện trong section "Failed" với trạng thái rõ ràng. Trace Viewer (nếu bật) dừng tại action cuối cùng trước khi timeout — hữu ích để xác định test đang làm gì khi hết giờ.

Khi gặp timedOut sau khi đã dùng test.setTimeout(): kiểm tra action nào đang block khi timeout, và xem có nên tăng thêm timeout hay tối ưu operation đó.

14

Pitfall Thường Gặp

1. Gọi quá muộn — remaining time đã gần hết

// WRONG: test đã chạy ~25s (config timeout 30s), gọi lúc này quá muộn
test('too late', async ({ page }) => {
  await page.goto('/slow-app');           // 15s
  await page.waitForLoadState('networkidle');  // 10s — tổng 25s

  // còn 5s trước khi hết 30s
  test.setTimeout(60_000);
  // remaining = 60_000 - 25_000 = 35s — may mắn còn kịp
  // nếu đã qua 30s trước khi gọi → test fail ngay vì đồng hồ 30s đã hết
});
// CORRECT: gọi sớm ở đầu test body
test('correct timing', async ({ page }) => {
  test.setTimeout(60_000);  // gọi trước mọi action
  await page.goto('/slow-app');
});

2. setTimeout(0) quên xóa trước khi push

// WRONG: quên xóa sau debug
test('checkout flow', async ({ page }) => {
  test.setTimeout(0);  // ← BUG: bị commit, chạy trên CI, hang 6h nếu bug
  await page.goto('/checkout');
  // ...
});

Dùng grep trước khi commit: grep -rn "setTimeout(0)" tests/ để phát hiện.

3. Nhầm setTimeout (exact) với slow (×3)

// WRONG: nghĩ test.setTimeout(3) nhân 3 lần config
test('slow test', async ({ page }) => {
  test.setTimeout(3);  // ← đặt timeout 3ms — test fail ngay lập tức
});
// ĐÚNG nếu muốn nhân 3:
test.slow();  // nhân config × 3

// ĐÚNG nếu muốn 3 phút:
test.setTimeout(180_000);

4. Không nhận ra last-wins khi gọi nhiều lần

// WRONG: nghĩ giá trị lớn nhất sẽ thắng
test('max wins?', async ({ page }) => {
  test.setTimeout(120_000);  // đặt 120s
  // ...
  test.setTimeout(30_000);   // ← THỰC RA đây là giá trị cuối, timeout chỉ còn 30s
                              // nếu test đã chạy 25s trước đây → chỉ còn 5s!
});

Luôn nhớ lần gọi cuối cùng thắng — không phải giá trị lớn nhất. Nếu cần tăng timeout điều kiện, chỉ set một lần ở vị trí phù hợp.

15

Quiz

Câu 1. Config có timeout: 30_000. Test gọi test.setTimeout(60_000) ở dòng đầu body, rồi sau 40s gọi tiếp test.setTimeout(90_000). Remaining time sau lần gọi thứ hai là bao nhiêu?

Đáp án

50s. Remaining = 90_000 − 40_000 = 50 000 ms. Giá trị từ lần gọi trước (60_000) bị bỏ qua. Timeout mới = 90s, đã dùng 40s → còn 50s.

Câu 2. Sự khác biệt về behavior khi đổi timeout global từ 30s lên 60s, giữa test dùng test.setTimeout(90_000) và test dùng test.slow()?

Đáp án

test.setTimeout(90_000): không thay đổi, vẫn là 90s. test.slow(): tự động tăng từ 90s (30×3) lên 180s (60×3). test.setTimeout() hardcode, test.slow() config-driven.

Câu 3. Test bắt đầu lúc 0ms. beforeEach mất 20s, đến giây 20 gọi test.setTimeout(60_000). Config timeout mặc định là 30s. Test có bị timeout ngay không?

Đáp án

Không. Config 30s đã "hết" ở giây 30, nhưng test.setTimeout(60_000) được gọi ở giây 20 — trước khi 30s trôi qua. Timeout mới = 60s, đã dùng 20s → remaining 40s. Test tiếp tục bình thường với 40s còn lại.

Câu 4. Muốn test tự tăng timeout lên 120s khi detect một feature flag SLOW_MODE=true từ page. Viết đoạn code phù hợp.

Đáp án
test('adaptive timeout', async ({ page }) => {
  await page.goto('/app');

  const slowMode = await page.evaluate(() => {
    return document.body.dataset.slowMode === 'true';
  });

  if (slowMode) {
    test.setTimeout(120_000);
  }

  await page.getByRole('button', { name: 'Submit' }).click();
  await expect(page.getByTestId('result')).toBeVisible();
});

Câu 5. Có 1 describe block gồm 5 test, 4 test bình thường và 1 test upload file lớn cần 3 phút. Cách nào phù hợp nhất để set timeout cho test upload mà không ảnh hưởng 4 test kia?

Đáp án

Gọi test.setTimeout(180_000) ngay ở dòng đầu body của test upload. Không dùng describe.configure() vì sẽ áp dụng cho cả 5 test, làm 4 test kia có timeout dài không cần thiết (nếu bị hang sẽ mất 3 phút mới fail).