Danh sách bài viết

Bài 84: expect.timeout — Default 5s Cho Assertion

expect.timeout (default 5000ms) quy định thời gian tối đa Playwright retry điều kiện của một web-first assertion trước khi throw TimeoutError. Bài này phân tích cơ chế auto-retry (~100ms/poll), xác định rõ các assertion áp dụng (toBeVisible, toHaveText, toBeEnabled, toHaveValue...) và loại không áp dụng (toBe, toEqual — sync), cách config global qua expect.timeout trong playwright.config.ts, per-assertion override qua option { timeout }, expect.poll và toPass với timeout riêng, soft assertion với timeout, negative assertion và tại sao cần timeout ngắn hơn, hierarchy config (per-call ghi đè global), mối quan hệ với test timeout (bounded), 4 pitfall thực tế, và quiz 4 câu.

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ơ chế auto-retry của web-first assertion: poll mỗi ~100ms, dừng khi pass hoặc hết expect.timeout.
  • Phân biệt assertion áp dụng (web-first: toBeVisible, toHaveText...) với assertion không áp dụng (sync: toBe, toEqual).
  • Config global expect.timeout trong playwright.config.ts và override per-assertion qua { timeout: N }.
  • Hiểu expect.polltoPass nhận timeout riêng trong options object.
  • Xử lý đúng negative assertion — tại sao default 5s có thể làm chậm test khi element không tồn tại.
  • Hiểu mối quan hệ bounded: expect.timeout không thể vượt test timeout.
  • Tránh 4 pitfall: tăng quá rộng, nhầm với actionTimeout, negative assertion mặc định chậm, sync assertion không có retry.

Lưu ý phạm vi: Bài này đi sâu vào hierarchy, config interaction và các edge case của expect.timeout trong series Nâng Cao. Khái niệm cơ bản (default 5s, phân biệt ba loại timeout) đã được đề cập ở bài 239-240 của Series Cơ Bản — bài này không nhắc lại mà giả định bạn đã nắm nền đó.

2

Cơ Chế Auto-Retry Của Web-First Assertion

Khi gọi một web-first assertion, Playwright không check điều kiện một lần rồi kết luận. Thay vào đó, framework poll điều kiện lặp đi lặp lại cho đến khi thoả hoặc hết thời gian:

  1. Re-resolve locator (tìm lại element trong DOM hiện tại).
  2. Evaluate điều kiện của matcher (element visible? text khớp? attribute đúng?).
  3. Nếu điều kiện thoả → assertion pass, dừng ngay.
  4. Nếu chưa thoả và chưa hết expect.timeout → đợi ~100ms rồi quay lại bước 1.
  5. Nếu hết expect.timeout mà vẫn không thoả → throw TimeoutError.

Khoảng cách ~100ms giữa các lần poll không phải hằng số tuyệt đối — Playwright dùng exponential backoff nhẹ ở những lần đầu và có thể co lại khi detect MutationObserver event trên DOM. Điều quan trọng là assertion không busy-wait liên tục mà yield sau mỗi lần check, cho phép các operation khác chạy trong event loop.

// toBeVisible() retry cho đến khi element xuất hiện hoặc 5s trôi qua
await expect(page.getByTestId('success-toast')).toBeVisible();

// toHaveText() retry cho đến khi text khớp hoặc 5s trôi qua
await expect(page.locator('.status')).toHaveText('Completed');

// toBeEnabled() retry cho đến khi button không còn disabled hoặc 5s trôi qua
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();

// toHaveValue() retry cho đến khi input có đúng value hoặc 5s trôi qua
await expect(page.getByLabel('Email')).toHaveValue('[email protected]');

Mỗi assertion trên có ngân sách 5s độc lập. Assertion pass sớm (ví dụ 80ms) giải phóng ngân sách đó — không tích vào assertion tiếp theo.

Một số matcher quan trọng áp dụng auto-retry:

  • LocatorAssertions: toBeVisible, toBeHidden, toBeEnabled, toBeDisabled, toBeChecked, toHaveText, toContainText, toHaveValue, toHaveAttribute, toHaveClass, toHaveCSS, toHaveCount, toBeInViewport, toBeEditable, toBeEmpty, toHaveId, toHaveScreenshot.
  • PageAssertions: toHaveURL, toHaveTitle, toHaveScreenshot.
  • APIResponseAssertions: toBeOK.
3

Config Global expect.timeout

Trường expect.timeout trong playwright.config.ts đặt default cho mọi web-first assertion trong toàn project:

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

export default defineConfig({
  expect: {
    timeout: 5_000, // default Playwright — 5 giây
  },
});

Một điểm cần nhắc lại: expect là object nằm ngoài use, không giống actionTimeoutnavigationTimeout:

export default defineConfig({
  timeout: 30_000,          // test timeout — top-level

  expect: {
    timeout: 5_000,         // assertion timeout — trong expect {}
  },

  use: {
    actionTimeout: 15_000,      // action timeout — trong use {}
    navigationTimeout: 30_000,  // navigation timeout — trong use {}
  },
});

Nhầm đặt timeout vào sai vị trí là một lỗi thường gặp. Nếu bạn viết use: { expect: { timeout: 10_000 } }, Playwright sẽ bỏ qua (không có error) và vẫn dùng default 5s — rất khó phát hiện.

Pattern phổ biến: phân biệt CI và local bằng env variable:

export default defineConfig({
  expect: {
    timeout: process.env.CI ? 15_000 : 5_000,
  },
});

Playwright không hỗ trợ override expect.timeout ở cấp project (trong mảng projects). Nếu cần giá trị khác nhau giữa các project, chỉ có env variable hoặc per-assertion override.

4

Per-Assertion Override

Mọi web-first matcher nhận option timeout để override cho riêng assertion đó — ghi đè hoàn toàn global config:

// Element xuất hiện sau API call ~10s → cần buffer rộng hơn
await expect(page.getByTestId('result')).toBeVisible({ timeout: 15_000 });

// Text cập nhật sau debounce 2s → 5s default đủ, nhưng nếu cần rõ ràng:
await expect(page.locator('.counter')).toHaveText('Done', { timeout: 8_000 });

// Smoke check — fail nhanh nếu element không có ngay
await expect(page.locator('header')).toBeVisible({ timeout: 2_000 });

Option timeout có mặt trong tất cả matcher signature. Với toHaveText, signature đầy đủ:

toHaveText(
  expected: string | RegExp | Array<string | RegExp>,
  options?: {
    ignoreCase?: boolean;
    useInnerText?: boolean;
    timeout?: number;
  }
): Promise<void>;

Với toHaveAttribute:

toHaveAttribute(
  name: string,
  value?: string | RegExp,
  options?: { timeout?: number }
): Promise<void>;

Per-assertion override hữu ích khi chỉ một số assertion trong test biết trước cần thời gian khác — tránh tăng global ảnh hưởng toàn project.

5

Assertion Không Áp Dụng expect.timeout (Sync)

expect.timeout không có hiệu lực với assertion đồng bộ (non-retrying). Những assertion này check giá trị tức thì, không retry, không async:

// Sync assertion — không retry, không timeout
expect(42).toBe(42);
expect('hello').toContain('ell');
expect([1, 2, 3]).toHaveLength(3);
expect({ a: 1 }).toEqual({ a: 1 });
expect(true).toBeTruthy();

Những assertion này không nhận await và không nhận { timeout } option. Chúng pass hoặc fail ngay lập tức dựa trên giá trị truyền vào — không poll DOM, không retry.

Nhầm lẫn thường gặp:

// SAI — count() trả về Promise, resolve ngay tại thời điểm gọi
// Nếu count chưa đúng lúc này → assertion fail ngay, không retry
const count = await page.locator('.item').count();
expect(count).toBe(5); // sync assertion trên số — không retry

// ĐÚNG — toHaveCount() là web-first, tự retry
await expect(page.locator('.item')).toHaveCount(5);

Nếu cần retry giá trị tính toán từ nhiều bước hoặc giá trị không phải locator, dùng expect.poll (mục 6).

6

expect.polltoPass Với Timeout Riêng

expect.pollexpect.toPass cũng áp dụng expect.timeout, nhưng thường cần giá trị lớn hơn vì dùng cho các operation chậm hơn. Cú pháp truyền timeout nằm trong options object của chính poll/toPass, không phải của matcher cuối.

expect.poll — polling một hàm async bất kỳ:

// Poll API endpoint cho đến khi status = "ready" hoặc 30s
await expect.poll(
  async () => {
    const res = await fetch('/api/job/status');
    return (await res.json()).status;
  },
  {
    timeout: 30_000,   // timeout của cả polling loop
    intervals: [1_000, 2_000, 5_000], // khoảng cách poll tuỳ chỉnh
  }
).toBe('ready');

timeout trong expect.poll options ghi đè expect.timeout global — nó không kế thừa global mà phải set tường minh nếu cần khác 5s. Trường intervals cho phép tăng dần khoảng poll thay vì mặc định ~100ms — phù hợp với job chạy nhiều giây.

expect.toPass — retry một block assertion cho đến khi tất cả bên trong đều pass:

// Retry block cho đến khi item count > 10 hoặc 15s
await expect(async () => {
  const count = await page.locator('.item').count();
  expect(count).toBeGreaterThan(10);
}).toPass({ timeout: 15_000 });

toPass hữu ích khi cần assert nhiều điều kiện cùng lúc mà không biết cái nào sẽ thoả sau. Khác với expect.poll: toPass retry toàn bộ block async function — block có thể chứa nhiều assertion, action, hoặc cả page interaction.

Lưu ý: expect.polltoPass không phải "nâng cao" riêng của bài này — Series Cơ Bản nhóm 32 đã cover. Ở đây chỉ nhắc cú pháp timeout option để hoàn thiện bức tranh expect.timeout.

7

Soft Assertion Và Timeout

Soft assertion (expect.soft) cũng là web-first assertion — áp dụng đầy đủ auto-retry và expect.timeout. Điểm khác biệt của soft assertion không nằm ở timeout mà ở hành vi khi fail: test không dừng lại mà tiếp tục chạy, tích lỗi và report ở cuối.

// Soft assertion — retry tới 5s như bình thường
// Fail không dừng test ngay
await expect.soft(page.locator('.title')).toHaveText('Dashboard');
await expect.soft(page.locator('.user-name')).toBeVisible();
await expect.soft(page.locator('.notifications')).toHaveCount(3);

// Nếu cần timeout khác — truyền như mọi web-first assertion
await expect.soft(page.getByTestId('slow-widget')).toBeVisible({ timeout: 10_000 });

Một điều cần chú ý với soft assertion: vì test không dừng khi fail, nhiều soft assertion cùng fail sẽ tích lũy thời gian retry. Nếu có 6 soft assertion fail, mỗi cái dùng hết 5s → 30s chỉ cho assertion, cộng với phần còn lại của test có thể vượt test timeout.

// Nguy cơ: 5 soft assertion × 5s = 25s tích lũy khi tất cả fail
await expect.soft(page.locator('.a')).toBeVisible();
await expect.soft(page.locator('.b')).toBeVisible();
await expect.soft(page.locator('.c')).toBeVisible();
await expect.soft(page.locator('.d')).toBeVisible();
await expect.soft(page.locator('.e')).toBeVisible();

// Nếu cả 5 fail → 25s chờ, test timeout 30s có thể bị vượt
// → "Test timeout exceeded" thay vì report đúng các soft failure

Với test dùng nhiều soft assertion, cân nhắc giảm expect.timeout global hoặc dùng per-call timeout ngắn hơn cho những assertion kỳ vọng không tốn nhiều thời gian.

8

Negative Assertion — Tại Sao Cần Timeout Khác

Negative assertion (not.toBeVisible, not.toHaveText...) áp dụng cùng cơ chế retry và cùng expect.timeout. Nhưng có một gotcha quan trọng:

Với positive assertion, test pass ngay khi điều kiện thoả (thường vài trăm ms). Với negative assertion, nếu element vẫn còn visible, test phải chờ đủ timeout mới throw — không có cách thoát sớm trừ khi element biến mất.

// Positive — pass ngay sau ~200ms khi element appear
await expect(page.getByTestId('success-toast')).toBeVisible(); // ~200ms

// Negative — nếu element VẪN visible → phải đợi hết 5s mới fail
await expect(page.getByTestId('error-modal')).not.toBeVisible(); // ~5000ms nếu modal vẫn còn

Trường hợp negative assertion check element không bao giờ xuất hiện (element absent từ đầu):

// Element không có trong DOM → Playwright detect ngay → pass ~50ms
await expect(page.locator('.nonexistent')).not.toBeVisible();

Nhưng nếu element hiện diện và đang fade out hay chờ điều kiện nào đó để biến mất — test chờ đến khi nó biến mất hoặc hết timeout.

Hai pattern thực tế:

1. Giảm timeout cho negative assertion biết trước element không có:

// Sau action delete, modal đóng trong 300ms → không cần chờ 5s
await page.getByRole('button', { name: 'Confirm Delete' }).click();
await expect(page.locator('.confirm-dialog')).not.toBeVisible({ timeout: 1_500 });

2. Giữ timeout đủ dài khi cần chờ element biến mất sau async operation:

// Loading spinner biến mất sau khi API call ~3s trả về
await expect(page.locator('.loading-spinner')).not.toBeVisible({ timeout: 10_000 });

Quy tắc: với negative assertion, luôn nghĩ xem element đang ở trạng thái nào và mất bao lâu để điều kiện "absent" thoả. Set timeout tương ứng thay vì dùng default.

9

Hierarchy: Per-Call, Global, Test Timeout

Priority của các nguồn config timeout cho assertion (từ cao xuống thấp):

  1. Per-assertion option{ timeout: N } trên chính call expect → ghi đè tất cả.
  2. Global configexpect: { timeout: N } trong playwright.config.ts → áp dụng cho mọi assertion không có per-call override.
  3. Playwright default — 5000ms nếu không có nguồn nào set.

Quan trọng: test timeout là ceiling. Dù expect.timeout set bao nhiêu, một assertion không thể retry lâu hơn thời gian còn lại của test. Playwright tự tính toán thời gian tối đa thực tế cho assertion = min(expect.timeout, remaining test time).

// playwright.config.ts
export default defineConfig({
  timeout: 10_000,          // test timeout: 10s
  expect: { timeout: 8_000 },  // assertion timeout: 8s
});

// Trong test:
test('bounded timeout', async ({ page }) => {
  // Bước này dùng 7s
  await page.goto('/slow-page'); // actionTimeout 7s

  // Assertion bắt đầu khi test còn lại 3s
  // expect.timeout = 8s, nhưng test chỉ còn 3s
  // → assertion thực tế chỉ có ~3s, không phải 8s
  await expect(page.locator('.content')).toBeVisible();
  // Nếu .content chưa visible sau 3s → test timeout, không phải assertion timeout
});

Biết được sự bounded này giúp lý giải tại sao đôi lúc assertion không chờ đủ thời gian đã set — không phải bug, là test timeout bị vượt.

Bảng tóm tắt mối quan hệ:

Config Áp dụng cho Ghi đè bởi Bounded bởi
Playwright default 5000ms Mọi web-first assertion Global config, per-call Test timeout
expect: { timeout: N } Mọi assertion trong project Per-call option Test timeout
{ timeout: N } per-call Assertion cụ thể đó Không gì cả Test timeout
10

Use Case Tăng Và Giảm expect.timeout

Khi Nào Tăng

Slow render sau API call lâu: Element xuất hiện sau khi backend xử lý xong một operation nặng (generate report, batch processing). Assertion cần chờ đủ.

// Report generation ~8s → cần buffer cho assertion
await page.getByRole('button', { name: 'Generate Report' }).click();
await expect(page.getByTestId('report-ready')).toBeVisible({ timeout: 15_000 });

Polling state từ external service: Dashboard hiển thị trạng thái từ third-party API, poll mỗi 2s — có thể cần đến 10-15s để state đổi.

// Status badge cập nhật khi job hoàn tất — có thể 10s
await expect(page.locator('.job-status')).toHaveText('Completed', { timeout: 20_000 });

Animation fade-in dài: Marketing site, onboarding screen với animation 3-4s trước khi element fully visible.

// Hero section fade-in 3s
await expect(page.locator('.hero-cta')).toBeVisible({ timeout: 6_000 });

Khi Nào Giảm

Fast feedback cho assertion biết trước pass ngay: Sau action đồng bộ, element thay đổi tức thì — không cần 5s buffer.

// Checkbox check là sync update — 500ms là quá đủ
await page.getByLabel('Accept terms').check();
await expect(page.getByLabel('Accept terms')).toBeChecked({ timeout: 1_000 });

Negative assertion element vừa bị xoá: Sau click delete, element rời khỏi DOM ngay lập tức — không cần chờ 5s.

await page.locator('.item').first().getByRole('button', { name: 'Remove' }).click();
// Item biến mất ngay → 1s đủ, không cần 5s
await expect(page.locator('.item').first()).not.toBeVisible({ timeout: 1_500 });

Smoke test fast-fail: Nếu trang chính không load trong 2s, đó là vấn đề nghiêm trọng — không cần đợi đủ 5s.

// @smoke tag — fail nhanh
await expect(page.locator('main')).toBeVisible({ timeout: 2_000 });

Quy tắc chung: ưu tiên per-assertion override thay vì thay đổi global. Thay đổi global ảnh hưởng toàn bộ test suite — kể cả những assertion không cần thay đổi.

11

4 Pitfall Thực Tế

Pitfall 1: Tăng expect.timeout Quá Rộng Làm Test Chậm Khi Fail

Tăng global expect.timeout lên 30s để "fix" flaky test là anti-pattern. Khi có bug thật, mỗi assertion fail phải chờ đủ 30s. Test suite 50 bài với 3-5 assertion mỗi bài có thể mất 1-2 giờ chỉ để fail.

// Sai — global quá rộng
export default defineConfig({
  expect: { timeout: 30_000 }, // 30s cho mọi assertion?
});

// Đúng — global reasonable, override per-call cho chỗ cần
export default defineConfig({
  expect: { timeout: 5_000 }, // giữ default
});

// Chỉ assertion nào thực sự cần lâu mới override
await expect(page.getByTestId('slow-result')).toBeVisible({ timeout: 20_000 });

Pitfall 2: Nhầm expect.timeout Với actionTimeout

Assertion fail với message "Timed out 5000ms waiting for expect(locator).toBeEnabled()". Dev tăng actionTimeout → vẫn fail. Nguyên nhân: đây là assertion timeout, không phải action timeout.

// Sai — tăng nhầm chỗ
export default defineConfig({
  use: { actionTimeout: 30_000 }, // không ảnh hưởng assertion
});

// Đúng — tăng đúng assertion timeout
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled({ timeout: 10_000 });
// hoặc
export default defineConfig({
  expect: { timeout: 10_000 },
});

Cách phân biệt từ error message:

  • "Timed out Xms waiting for expect(...)..."expect.timeout.
  • "page.click: Timeout..." hoặc "locator.fill: Timeout..."actionTimeout.

Pitfall 3: Negative Assertion Default 5s Làm Test Chậm

Test kiểm tra error message không xuất hiện sau submit form hợp lệ. Error message không có trong DOM → assertion pass ngay. Nhưng nếu error message đang hiện từ lần trước (state sót), assertion phải đợi 5s.

// Scenario: test chạy sau test khác, error message vẫn visible
await expect(page.locator('.error-msg')).not.toBeVisible();
// → nếu error-msg visible → đợi 5s rồi fail
// Ẩn ý: state không được reset đúng → tìm root cause thay vì giảm timeout

// Nếu xác nhận state đã clean và element không nên tồn tại:
await expect(page.locator('.error-msg')).not.toBeVisible({ timeout: 500 });
// → fail nhanh nếu element vẫn còn, giúp phát hiện state leak sớm

Pitfall 4: Sync Assertion Không Có Timeout — Dùng Sai Matcher

Dev muốn "retry" check count nhưng dùng sync assertion trên giá trị đã resolve:

// Sai — count() resolve tại thời điểm gọi, không retry
// Nếu DOM chưa render đủ item → fail ngay
const count = await page.locator('.product-item').count();
expect(count).toBe(12); // sync, không retry

// Đúng — toHaveCount() retry cho đến khi count đúng hoặc timeout
await expect(page.locator('.product-item')).toHaveCount(12);

// Hoặc nếu cần tính toán phức tạp hơn:
await expect.poll(async () => {
  return await page.locator('.product-item').count();
}, { timeout: 10_000 }).toBe(12);
12

Tổng Kết + Quiz

Tổng Kết

  • Auto-retry mechanism: Poll ~100ms, dừng khi pass hoặc hết expect.timeout. Assertion pass sớm không "giữ" thời gian còn lại.
  • Áp dụng cho: LocatorAssertions, PageAssertions, APIResponseAssertions (toBeOK), expect.poll, expect.toPass. Không áp dụng cho sync assertion (toBe, toEqual, toHaveLength...).
  • Config global: trường expect: { timeout: N } ở top-level config, nằm ngoài use. Per-assertion override qua { timeout: N } trong options.
  • expect.poll và toPass: nhận timeout trong options object riêng của chúng — thường cần set tường minh vì default 5s thường không đủ.
  • Soft assertion: áp dụng đầy đủ retry và timeout. Nhiều soft assertion fail cùng lúc tích lũy thời gian, có thể vượt test timeout.
  • Negative assertion: nếu element vẫn còn visible → phải đợi đủ timeout. Cân nhắc giảm timeout cho negative assertion biết trước element không có.
  • Bounded bởi test timeout: assertion không thể retry lâu hơn thời gian còn lại của test — min(expect.timeout, remaining test time).
  • 4 pitfall: tăng quá rộng làm chậm fail, nhầm với actionTimeout, negative assertion default chậm, dùng sync assertion thay vì web-first.

Quiz 4 Câu

Câu 1

Code sau có vấn đề gì?

export default defineConfig({
  use: {
    expect: { timeout: 10_000 },
  },
});
  1. Playwright báo lỗi khi khởi động — config không hợp lệ.
  2. Config bị bỏ qua silently — expect.timeout phải nằm ở top-level, không trong use. Mọi assertion vẫn dùng default 5s.
  3. Config áp dụng đúng, không có vấn đề gì.
  4. Config chỉ áp dụng cho Chromium project, không cho Firefox và WebKit.
Đáp án

B. expect: { timeout } phải đặt ở top-level của defineConfig, không nằm trong use. Khi đặt sai, Playwright không throw error mà đơn giản bỏ qua field không nhận dạng — mọi assertion vẫn dùng default 5s. Cách đúng: defineConfig({ expect: { timeout: 10_000 }, use: { ... } }). A sai — không có error. C sai — config không được đọc. D sai — không liên quan browser.

Câu 2

expect.timeout áp dụng cho assertion nào trong số các assertion sau?

// A
await expect(page.locator('.status')).toHaveText('Done');

// B
expect(await page.locator('.item').count()).toBe(5);

// C
await expect.poll(async () => fetchStatus(), { timeout: 20_000 }).toBe('ready');

// D
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
  1. A và C.
  2. B và D.
  3. A, B, C, D đều áp dụng.
  4. Chỉ A.
Đáp án

A. A (toHaveText) là web-first assertion — áp dụng expect.timeout (5s default). C (expect.poll) cũng áp dụng, nhưng override bằng timeout: 20_000 trong options riêng của nó. B check giá trị số đã resolve — toBe(5) là sync assertion, không retry. D là sync assertion trên plain object, không retry. Đáp án A và C áp dụng timeout — B chọn A là đúng nhất trong context bài (web-first + poll cùng nhóm áp dụng, sync không áp dụng).

Câu 3

Test timeout = 10s. expect.timeout global = 8s. Test bắt đầu một assertion sau khi các step trước đã dùng hết 7s. Assertion này thực tế có bao nhiêu thời gian để retry?

  1. 8 giây — vì đó là expect.timeout đã set.
  2. 3 giây — expect.timeout global bị ghi đè bởi per-call nên dùng default 5s, nhưng test chỉ còn 3s.
  3. Khoảng 3 giây — vì test timeout còn lại ~3s, assertion bị bounded bởi min(8s, 3s).
  4. 0 giây — test đã timeout ở bước trước.
Đáp án

C. expect.timeout bị bounded bởi test timeout còn lại. Test timeout 10s đã dùng 7s → còn 3s. expect.timeout = 8s, nhưng min(8s, 3s) = 3s. Assertion chỉ có ~3s. Nếu điều kiện không thoả trong 3s → test timeout ("Test timeout of 10000ms exceeded"), không phải assertion timeout. A sai — không tính remaining. B sai — không có per-call ghi đè, dùng global 8s nhưng vẫn bounded. D sai — test chưa timeout, còn 3s.

Câu 4

Sau khi click nút "Delete", modal xác nhận đóng lại. Code test kiểm tra modal không còn visible:

await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.locator('.confirm-modal')).not.toBeVisible();

Modal đóng sau 400ms (CSS transition). Test pass nhưng mất ~400ms, đúng như mong đợi. Tuy nhiên trong một CI run khác, tất cả các assertion not.toBeVisible kiểu này đột nhiên mất 5s mỗi cái. Nguyên nhân khả năng nhất?

  1. CI bị chậm, modal transition mất 5s.
  2. Modal không đóng thực sự — vẫn visible trong DOM sau click, phải đợi đủ expect.timeout (5s) rồi mới fail. Cần debug xem click có trigger close action không.
  3. Playwright version mới thay đổi behavior của not.toBeVisible.
  4. Test timeout bị set quá thấp làm assertion chỉ được 5s.
Đáp án

B. Negative assertion pass ngay khi element biến mất. Nếu assertion mất đúng 5s (tức hết timeout), có nghĩa element vẫn visible trong suốt 5s — assertion cuối cùng fail (không phải pass). Nếu assertion "mất 5s rồi pass" không xảy ra — negative assertion hoặc pass nhanh hoặc fail sau 5s, không có trường hợp "đợi đủ 5s rồi pass". Câu hỏi nói "mất 5s" gợi ý assertion fail sau 5s — nghĩa là modal không đóng, và assertion đang fail. Cần debug click có hoạt động không, có network call nào đang pending chặn close action không. A có thể đúng nhưng B là root cause cần kiểm tra trước. C sai — behavior không đổi. D sai — test timeout không ảnh hưởng theo hướng đó.

Bài Tiếp Theo

Bài 85: actionTimeoutnavigationTimeout — Config Và Hierarchy