Danh sách bài viết

Bài 89: Override Per-Action { timeout }

Per-action { timeout } là cơ chế override timeout ở cấp granular nhất — áp dụng cho duy nhất 1 operation (action, navigation, hoặc assertion) mà không ảnh hưởng các operation khác trong cùng test. Precedence cao nhất trong hierarchy: ghi đè actionTimeout, navigationTimeout, expect.timeout từ config — nhưng vẫn bị bound bởi test timeout. Bài này tập trung vào cú pháp đầy đủ, use case thực tế (1 action chậm đặc biệt, 1 navigation nặng, 1 assertion poll lâu), pattern combine với test.setTimeout(), phân biệt với config-level timeout, và 4 pitfall phổ biến. Đây là bài kết nhóm A.9 Timeouts.

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ú pháp { timeout } cho action, navigation, và assertion.
  • Hiểu per-call timeout đứng ở đâu trong hierarchy và tại sao nó vẫn bị bound bởi test timeout.
  • Nhận ra 3 nhóm use case thực tế cần per-call override (slow upload, heavy page, async poll).
  • Biết combine per-call timeout với test.setTimeout() để test complex flow.
  • Phân biệt per-call override với config-level actionTimeout/navigationTimeout.
  • Tránh 4 pitfall phổ biến khi dùng per-call timeout.
2

Per-Action Timeout Là Gì?

Mỗi Playwright API trả về Promise đều nhận option timeout — giá trị này ghi đè hoàn toàn actionTimeout, navigationTimeout, hoặc expect.timeout từ config, chỉ cho đúng operation đó.

Đây là cấp override granular nhất trong hierarchy:

test timeout (ceiling)
  └── per-call { timeout }         ← bài này
        ghi đè actionTimeout / navigationTimeout / expect.timeout
        không ảnh hưởng operation khác cùng test

Khi nào cần per-call override: khi 1 operation cụ thể có đặc điểm thời gian khác hẳn mức trung bình của toàn test — quá chậm (file upload lớn, server nặng) hoặc cần fail nhanh hơn (kiểm tra element tức thì).

3

Cú Pháp Đầy Đủ

Action:

await page.click('button', { timeout: 5_000 });
await page.fill('input', 'text', { timeout: 3_000 });
await page.check('input[type=checkbox]', { timeout: 2_000 });
await page.selectOption('select', 'value', { timeout: 4_000 });
await page.hover('.menu-item', { timeout: 2_000 });

Navigation:

await page.goto('/slow-page', { timeout: 60_000 });
await page.reload({ timeout: 30_000 });
await page.goBack({ timeout: 15_000 });
await page.waitForURL('/dashboard', { timeout: 30_000 });

Wait APIs:

await page.waitForSelector('.spinner', { timeout: 10_000 });
await page.waitForLoadState('networkidle', { timeout: 20_000 });
await page.waitForResponse(
  resp => resp.url().includes('/api/data'),
  { timeout: 15_000 }
);

Assertion (web-first):

await expect(locator).toBeVisible({ timeout: 10_000 });
await expect(locator).toHaveText('Done', { timeout: 8_000 });
await expect(locator).toHaveValue('success', { timeout: 5_000 });
await expect(page).toHaveURL('/success', { timeout: 15_000 });

Locator actions:

// Locator API — cú pháp giống page, option cuối
await page.getByRole('button', { name: 'Submit' }).click({ timeout: 5_000 });
await page.locator('input[name=file]').setInputFiles('big.zip', { timeout: 90_000 });
4

Methods Hỗ Trợ { timeout }

Nhóm Methods Timeout ghi đè
Actions click, dblclick, fill, type, check, uncheck, selectOption, hover, focus, tap, press, setInputFiles, dispatchEvent actionTimeout
Navigation goto, reload, goBack, goForward, waitForURL navigationTimeout
Wait APIs waitForSelector, waitForLoadState, waitForResponse, waitForRequest, waitForFunction, waitForEvent actionTimeout
Assertions Tất cả web-first assertions (toBeVisible, toHaveText, toHaveValue, toBeEnabled, toHaveURL...) expect.timeout

Assertion timeout option ghi đè expect.timeout config, không phải actionTimeout. Hai hệ thống này độc lập nhau — bài 84 (expect-timeout) đã nói chi tiết. Bài này không lặp lại.

5

Precedence Trong Hierarchy

Per-call { timeout } đứng cao nhất trong mỗi loại operation:

Action timeout:
  per-call { timeout }  >  test.use({ actionTimeout })  >  config use.actionTimeout

Navigation timeout:
  per-call { timeout }  >  test.use({ navigationTimeout })  >  config use.navigationTimeout

Assertion timeout:
  per-call { timeout }  >  config expect.timeout

Nhưng: mọi per-call timeout đều bị hard-bound bởi test timeout. Nếu test timeout hết trước khi per-call timeout kịp trigger, test bị kill ngay.

// Config: timeout: 30_000 (test timeout 30s)

test('example', async ({ page }) => {
  // Per-call ghi đè config actionTimeout
  await page.click('.btn', { timeout: 10_000 });
  // → action này có 10s riêng, bất kể config actionTimeout là bao nhiêu

  await page.click('.btn2');
  // → action này vẫn dùng config actionTimeout (không bị per-call ảnh hưởng)
});

Giá trị per-call timeout không thể vượt qua test timeout về mặt thực tế. Nếu đặt { timeout: 60_000 } nhưng test timeout là 30s, operation bị kill ở giây thứ 30 — không phải giây thứ 60.

6

Use Case: 1 Action Chậm Đặc Biệt

Use case phổ biến nhất: test có nhiều action bình thường (mỗi action vài giây) nhưng 1 action đặc biệt cần nhiều thời gian hơn — upload file lớn, trigger server xử lý nặng, hoặc action chờ animation dài.

test('upload large file', async ({ page }) => {
  await page.goto('/upload');

  // Mọi action khác dùng config actionTimeout (vd 10s)
  await page.getByLabel('Title').fill('My Report');
  await page.getByLabel('Category').selectOption('finance');

  // Chỉ upload cần timeout cao — file 200 MB, mất 45-60s tùy kết nối
  await page.locator('input[type=file]').setInputFiles('report-200mb.zip', {
    timeout: 90_000,
  });

  // Sau khi upload xong, submit button bình thường
  await page.getByRole('button', { name: 'Submit' }).click();

  // assertion cũng dùng config expect.timeout
  await expect(page.getByText('Upload successful')).toBeVisible();
});

Nếu dùng config-level actionTimeout: 90_000 thay vì per-call, toàn bộ action trong test sẽ chờ đến 90s trước khi fail — che khuất các action bị stuck khác. Per-call giúp chỉ operation upload có timeout cao, phần còn lại fail-fast như thường.

7

Use Case: 1 Navigation Nặng

Một số page load chậm bất thường — dashboard tổng hợp dữ liệu, trang report có nhiều chart, hoặc route đầu tiên cần cold start server-side. Thay vì tăng navigationTimeout cho toàn bộ test, override chỉ navigation đó:

test('dashboard analytics', async ({ page }) => {
  // Trang login bình thường — không cần override
  await page.goto('/login');
  await page.fill('#email', '[email protected]');
  await page.fill('#password', 'pass');
  await page.click('button[type=submit]');

  // Dashboard load nhiều aggregate query — mất 20-30s lần đầu
  await page.goto('/analytics/dashboard', { timeout: 45_000 });

  // Các navigation tiếp theo bình thường
  await page.goto('/analytics/reports');
});

waitForURL cũng nhận { timeout } — hữu ích khi redirect sau POST cần thời gian server xử lý:

await page.click('#checkout');
// Server validate payment, charge card, tạo order — có thể mất 15s
await page.waitForURL('/order/confirmation', { timeout: 20_000 });
8

Use Case: 1 Assertion Poll Lâu

Web-first assertions trong Playwright poll DOM theo chu kỳ cho đến khi điều kiện thỏa mãn hoặc expect.timeout hết. Khi 1 assertion cần poll lâu hơn default (thường 5s), dùng per-call thay vì tăng toàn bộ expect.timeout:

test('async data fetch', async ({ page }) => {
  await page.goto('/reports');
  await page.click('#generate-report');

  // Server chạy background job, kết quả xuất hiện sau 8-12s
  // expect.timeout default 5s sẽ fail trước khi data ready
  await expect(page.getByTestId('report-table')).toBeVisible({ timeout: 15_000 });

  // Sau khi table visible, kiểm tra row count — element đã có, nhanh
  await expect(page.getByTestId('report-table').locator('tbody tr')).toHaveCount(25);
});

Assertion thứ hai không cần override — table đã visible nên row count kiểm tra ngay, expect.timeout default 5s là đủ.

waitForResponse cũng là pattern phổ biến khi muốn đợi 1 API call cụ thể:

// Trigger action, sau đó đợi response từ slow endpoint
const [response] = await Promise.all([
  page.waitForResponse(
    resp => resp.url().includes('/api/export') && resp.status() === 200,
    { timeout: 30_000 }
  ),
  page.click('#export-button'),
]);
9

Pattern Combine Với test.setTimeout()

Per-call { timeout } cao hơn test timeout sẽ không bao giờ trigger — operation bị kill bởi test timeout trước. Khi có nhiều per-call timeout cao trong cùng test, cần mở rộng test timeout đủ để cover tổng thời gian:

test('full upload flow', async ({ page }) => {
  // Test này có nhiều operation chậm → tăng test timeout trước
  test.setTimeout(180_000);  // 3 phút cho toàn test

  // Navigation đầu tiên: cold start server
  await page.goto('/', { timeout: 30_000 });

  // Upload file lớn: 45-60s
  await page.locator('input[type=file]').setInputFiles('data.zip', {
    timeout: 90_000,
  });

  // Assertion: server xử lý file, kết quả sau 30-60s
  await expect(page.getByText('Processing complete')).toBeVisible({
    timeout: 60_000,
  });
  // Tổng ước tính: 30s + 90s + 60s = 180s — vừa đúng test.setTimeout(180_000)
});

Quy tắc tính test timeout cho flow có per-call cao: cộng tất cả per-call timeout (worst case mỗi operation chờ đủ hạn) và thêm buffer ~20% cho overhead giữa các step.

Per-call timeout không ảnh hưởng giá trị test.setTimeout() — chúng là hai trục độc lập. test.setTimeout() đặt ceiling; per-call đặt limit cho từng operation trong ceiling đó.

10

Per-Call Fast Fail

Per-call timeout cũng dùng được theo chiều ngược lại — đặt timeout ngắn hơn config để fail nhanh hơn cho những operation phải xảy ra tức thì:

test('modal closes on click', async ({ page }) => {
  await page.goto('/');
  await page.click('#open-modal');

  // Modal phải visible ngay sau click — nếu không có sau 1s thì chắc chắn là bug
  await expect(page.getByRole('dialog')).toBeVisible({ timeout: 1_000 });

  await page.click('[data-testid="close-modal"]');

  // Modal phải biến mất ngay — animation chỉ 300ms, tối đa 1s
  await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 1_000 });
});

Config expect.timeout: 5_000 vẫn sẽ chờ đủ 5s trước khi fail. Per-call { timeout: 1_000 } giúp test fail ngay sau 1s — CI chạy nhanh hơn và lỗi rõ hơn ("dialog không xuất hiện sau 1s" vs "sau 5s").

11

timeout: 0 Per-Call

{ timeout: 0 } per-call disable timeout riêng cho operation đó — operation không có đồng hồ riêng, chỉ bị giới hạn bởi test timeout còn lại:

// Config: actionTimeout: 10_000

test('wait indefinitely for element', async ({ page }) => {
  await page.goto('/long-loading-app');

  // timeout: 0 → không có per-action limit
  // operation này sẽ chờ cho đến khi test timeout hết
  await page.click('#dynamic-button', { timeout: 0 });
});

Semantic của timeout: 0 per-call khác với timeout: 0 ở config-level actionTimeout:

  • actionTimeout: 0 trong config: toàn bộ action không có đồng hồ riêng, bound bởi test budget.
  • { timeout: 0 } per-call: chỉ operation này không có đồng hồ riêng, ghi đè config actionTimeout nếu đang set.

Use case hợp lý cho per-call { timeout: 0 }: config đang có actionTimeout: 10_000 (fail-fast cho hầu hết action) nhưng 1 action cụ thể cần chờ không xác định — không muốn disable config-level timeout toàn bộ, chỉ disable cho action đó.

12

Phân Biệt Với Config actionTimeout

Tiêu chí Config actionTimeout Per-call { timeout }
Phạm vi Mọi action trong toàn bộ test suite (hoặc project) Chỉ 1 operation cụ thể
Nơi đặt playwright.config.ts Inline tại lệnh gọi
Khi override Tất cả action thay đổi Không ảnh hưởng action khác
Use case Baseline chung cho cả suite Exception cho operation đặc biệt
Verbosity 1 dòng config Thêm option vào mỗi lệnh

Nguyên tắc chọn:

  • Nếu nhiều action có đặc điểm thời gian giống nhau → dùng config actionTimeout.
  • Nếu chỉ 1-2 operation khác hẳn phần còn lại → dùng per-call.
  • Không dùng per-call trên mọi action chỉ vì muốn kiểm soát — làm code verbose và khó đọc.

Tương tự với test.setTimeout(): bài 87 đã nói test.setTimeout() điều chỉnh timeout cho cả test function. Per-call { timeout } chỉ điều chỉnh 1 operation. Hai cơ chế này không thay thế nhau — kết hợp khi cần: test.setTimeout() mở rộng ceiling, per-call định nghĩa limit cho từng operation trong ceiling đó.

13

Best Practices

1. Dùng per-call cho exception, config cho baseline

Per-call phù hợp khi 1 operation là ngoại lệ rõ ràng. Nếu nhiều operation cùng cần override giống nhau, đặt vào config hoặc tạo fixture wrapper.

2. Luôn đảm bảo test timeout đủ lớn

Trước khi đặt per-call timeout cao, tính tổng thời gian tối đa của test và dùng test.setTimeout() nếu cần. Per-call timeout lớn hơn test timeout không có tác dụng.

3. Chú thích tại sao cần override

Per-call timeout cao không self-documenting. Thêm comment ngắn nói rõ lý do:

// File ~200MB, average upload time 30-45s trên staging network
await page.locator('input[type=file]').setInputFiles('data.zip', {
  timeout: 90_000,
});

4. Không over-use per-call

Nếu mọi action trong test đều có { timeout }, code rối, khó maintain, và che khuất ý định. Config-level timeout thường là lựa chọn tốt hơn trong trường hợp đó.

5. Per-call assertion timeout độc lập với actionTimeout

Khi assertion poll chậm, override expect option, không override action. actionTimeout cao không giúp assertion chờ lâu hơn.

14

Limitations

1. Verbose khi nhiều operation cần override

Mỗi per-call thêm option vào lệnh gọi. Nếu 10 action cùng cần timeout cao, code dài và lặp lại — config-level hoặc custom fixture wrapper phù hợp hơn.

2. Vẫn bị bound bởi test timeout

Không thể bypass test timeout bằng per-call. Flow có nhiều per-call timeout cao phải kết hợp test.setTimeout() — không phải config actionTimeout.

3. Không apply cho code bên trong evaluate

page.evaluate() chạy JavaScript trong browser context — không nhận { timeout } theo nghĩa Playwright timeout. Timeout cho evaluate là option riêng và hành vi khác.

4. Per-call không kế thừa giữa test file

Không có cơ chế "default per-call timeout cho group" — mỗi file phải viết lại. Nếu muốn timeout mặc định cho nhóm test, dùng config hoặc fixture.

15

Pitfalls

Pitfall 1: Per-call timeout lớn hơn test timeout — không bao giờ trigger

// Config: timeout: 30_000 (test timeout 30s)

test('upload', async ({ page }) => {
  await page.goto('/upload');
  // Per-call 90s nhưng test timeout 30s → operation bị kill ở giây 30
  // Lỗi: "Test timeout of 30000ms exceeded" — không phải per-call timeout
  await page.locator('input[type=file]').setInputFiles('big.zip', {
    timeout: 90_000,  // không có tác dụng vì test timeout nhỏ hơn
  });
});

// Fix: mở rộng test timeout trước
test('upload', async ({ page }) => {
  test.setTimeout(120_000);  // test có 2 phút
  await page.goto('/upload');
  await page.locator('input[type=file]').setInputFiles('big.zip', {
    timeout: 90_000,  // bây giờ per-call timeout mới có ý nghĩa
  });
});

Pitfall 2: Nhầm per-call actionTimeout với assertion timeout

// SAI: tưởng rằng actionTimeout cao sẽ giúp assertion chờ lâu hơn
await page.click('#load-data', { timeout: 30_000 });
await expect(page.getByText('Loaded')).toBeVisible();  // vẫn dùng expect.timeout (5s)

// ĐÚNG: override assertion riêng
await page.click('#load-data');
await expect(page.getByText('Loaded')).toBeVisible({ timeout: 15_000 });
// actionTimeout và expect.timeout là hai hệ thống độc lập

Pitfall 3: Over-use per-call làm code khó đọc

// Rất verbose, không cần thiết
test('form', async ({ page }) => {
  await page.fill('#name', 'John', { timeout: 5_000 });
  await page.fill('#email', '[email protected]', { timeout: 5_000 });
  await page.fill('#phone', '0912345678', { timeout: 5_000 });
  await page.selectOption('#country', 'VN', { timeout: 5_000 });
  await page.click('#submit', { timeout: 5_000 });
  // Tất cả đều 5s → đặt actionTimeout: 5_000 trong config là đủ
});

// Tốt hơn:
// playwright.config.ts: use: { actionTimeout: 5_000 }
test('form', async ({ page }) => {
  await page.fill('#name', 'John');
  await page.fill('#email', '[email protected]');
  // ...
});

Pitfall 4: Quên comment khi per-call timeout cao — khó review sau

// Code không rõ tại sao cần 90s
await page.locator('input[type=file]').setInputFiles('file.zip', {
  timeout: 90_000,  // 90 giây — tại sao? Reviewer không biết
});

// Tốt hơn: comment ngắn
// File lên đến 500MB; staging upload speed khoảng 5-8 MB/s
await page.locator('input[type=file]').setInputFiles('file.zip', {
  timeout: 90_000,
});
16

Quiz

Câu 1. Config có actionTimeout: 10_000 và test timeout 30s. Đoạn code sau có vấn đề gì không?

test('submit', async ({ page }) => {
  await page.goto('/');
  await page.click('#submit', { timeout: 60_000 });
  await expect(page.getByText('Done')).toBeVisible();
});
Đáp án

Per-call { timeout: 60_000 } lớn hơn test timeout (30s). Kết quả: click bị kill ở giây thứ 30 bởi test timeout — không phải ở giây 60 bởi per-call timeout. Lỗi sẽ là "Test timeout of 30000ms exceeded", không phải action timeout. Per-call 60s không có ý nghĩa.

Fix: thêm test.setTimeout(70_000) để test timeout đủ lớn hơn per-call timeout.

Câu 2. Assertion sau fail sau 5s dù server trả về data sau 7s. Config có actionTimeout: 30_000. Giải thích và fix.

await expect(page.getByText('Result')).toBeVisible();
Đáp án

actionTimeout không áp dụng cho assertion. toBeVisible() dùng expect.timeout (default 5s). Hai hệ thống này độc lập — tăng actionTimeout không giúp assertion chờ lâu hơn.

Fix: override per-call hoặc tăng config.

// Per-call:
await expect(page.getByText('Result')).toBeVisible({ timeout: 15_000 });

// Hoặc config (nếu nhiều assertion cùng cần):
// playwright.config.ts: expect: { timeout: 10_000 }

Câu 3. Đâu là cách đúng để disable timeout riêng cho 1 action (trong khi config có actionTimeout: 10_000)?

// Option A
await page.click('.btn', { timeout: undefined });

// Option B
await page.click('.btn', { timeout: 0 });

// Option C
await page.click('.btn', { timeout: null });

// Option D
await page.click('.btn', { timeout: Infinity });
Đáp án

Option B: { timeout: 0 }. Đây là giá trị đặc biệt trong Playwright — disable timeout riêng cho operation đó, operation bị giới hạn bởi test timeout còn lại. Option A (undefined) không ghi đè config, action vẫn dùng config actionTimeout: 10_000. Option C và D không phải giá trị hợp lệ.

Câu 4. Test có 3 per-call timeout: navigation 30s, upload 60s, assertion 30s. Test timeout config là 30s. test.setTimeout() cần đặt bao nhiêu?

Đáp án

Worst case: navigation 30s + upload 60s + assertion 30s = 120s. Cộng thêm buffer 20% overhead giữa các step: 120s × 1.2 = 144s. Đặt test.setTimeout(150_000) là hợp lý (làm tròn lên). Quan trọng: phải gọi test.setTimeout() trước các operation đó, thường là đầu test body.

Câu 5. Code sau có thể viết gọn hơn không? Nếu có, viết lại.

test('checkout flow', async ({ page }) => {
  await page.fill('#name', 'Alice', { timeout: 8_000 });
  await page.fill('#address', '123 Main St', { timeout: 8_000 });
  await page.fill('#city', 'Hanoi', { timeout: 8_000 });
  await page.selectOption('#country', 'VN', { timeout: 8_000 });
  await page.click('#next', { timeout: 8_000 });
  await page.fill('#card', '4111111111111111', { timeout: 8_000 });
  await page.click('#pay', { timeout: 8_000 });
});
Đáp án

Có. Mọi action đều dùng cùng timeout 8s — đây là use case của config-level actionTimeout, không phải per-call. Viết lại:

// playwright.config.ts (hoặc test.use trong file):
// use: { actionTimeout: 8_000 }

test('checkout flow', async ({ page }) => {
  await page.fill('#name', 'Alice');
  await page.fill('#address', '123 Main St');
  await page.fill('#city', 'Hanoi');
  await page.selectOption('#country', 'VN');
  await page.click('#next');
  await page.fill('#card', '4111111111111111');
  await page.click('#pay');
});

Per-call phù hợp khi 1 operation là ngoại lệ, không phải khi tất cả cùng override bằng cùng giá trị.

17

Bài Tiếp Theo

Bài 90: for...of Data-Driven Tests — mở nhóm A.10 Parameterize: chạy cùng test logic với nhiều bộ dữ liệu khác nhau bằng vòng lặp for...of, so sánh với test.each, và cách tổ chức test data.