Mục lục
- Mục Tiêu Bài Học
- Cú Pháp Và Behavior Cốt Lõi
- Timing: Gọi Sớm vs Gọi Muộn
- Gọi Nhiều Lần — Last Wins
- setTimeout(0) — Disable Timeout
- Phân Biệt Với test.slow()
- Phân Biệt Với describe.configure()
- Phân Biệt Với Per-Action Timeout
- testInfo.setTimeout() — Preview Ngắn
- Pattern: Conditional Timeout
- Pattern: Gọi Trong beforeEach
- Use Cases Thực Tế
- Timeout Fail — Reporter Hiện Gì
- Pitfall Thường Gặp
- Quiz
- Bài Tiếp Theo
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ớitest.slow()vàdescribe.configure({ timeout }). - Tránh 4 pitfall phổ biến khi dùng
test.setTimeout().
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
timeouttừplaywright.config.ts, project config, hoặcdescribe.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.
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ới − thờ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.
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.
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).
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.
Phân Biệt Với describe.configure()
test.describe.configure({ timeout }) và 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ệ.
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.
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 globaltestobject.testInfo.setTimeout(N)— dùng từ fixture hoặc hook nhậntestInfoparam, nơi không cótestobject 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.
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.
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
}
});
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,
});
});
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 đó.
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.
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).
