Danh sách bài viết

Bài 14: Option Fixture actionTimeout, navigationTimeout

actionTimeout và navigationTimeout là hai Option Fixtures phân tách timeout theo loại operation: actionTimeout giới hạn mỗi action đơn lẻ (click, fill, hover, check...), navigationTimeout giới hạn mỗi navigation (goto, reload, waitForURL, waitForNavigation). Cả hai default là 0 — không có per-operation limit độc lập, chỉ bị giới hạn bởi test timeout (default 30s). Bài này phân tích cơ chế fixture của hai option này, phân biệt chúng với timeout test và expect.timeout, cách override per-action call, CI vs local timeout pattern, use case cụ thể (slow CI, long upload, animation-heavy flow), timeout hierarchy liên quan, và 4 pitfall hay gặp khi cấu hình sai hai option này.

27/05/2026
14 phút đọc
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 actionTimeoutnavigationTimeout là Option Fixtures phân tách timeout theo nhóm operation riêng biệt.
  • Biết default 0 không có nghĩa là vô hạn thật sự — operation vẫn bị giới hạn bởi test timeout.
  • Phân biệt rõ ba loại timeout độc lập: timeout (test), actionTimeout (per action), navigationTimeout (per navigation), và expect.timeout (assertion).
  • Override đúng layer: config global, test.use(), và per-call { timeout }.
  • Áp dụng CI vs local pattern để tránh flaky test.
  • Tránh được 4 pitfall phổ biến khi cấu hình hai option này.

Bài 239 (Series 1) đã cover expect.timeout default 5s và phân biệt với navigation/action timeout. Bài này không lặp lại phần đó — focus vào cơ chế fixture, override pattern, và edge cases của actionTimeoutnavigationTimeout.

2

Hai Option Fixtures Phân Tách Theo Loại Operation

Playwright chia các operation thành hai nhóm timeout riêng:

actionTimeout — áp dụng cho operations tương tác với element:

  • page.click(), page.dblclick(), page.tap()
  • page.fill(), page.type(), page.pressSequentially()
  • page.hover(), page.focus(), page.blur()
  • page.check(), page.uncheck()
  • page.selectOption(), page.setInputFiles()
  • locator.click(), locator.fill() và các locator action tương tự
  • page.waitForSelector(), locator.waitFor()

navigationTimeout — áp dụng cho operations điều hướng trang:

  • page.goto()
  • page.reload()
  • page.goBack(), page.goForward()
  • page.waitForURL()
  • page.waitForNavigation()

Tại sao tách thành hai nhóm? Vì đặc thù thời gian chờ khác nhau về bản chất: action trên element thường hoàn thành trong vài giây (element phải actionable — visible, enabled, stable), còn navigation liên quan network round-trip — có thể chậm hơn nhiều nếu server load cao hoặc bundle lớn. Tách riêng cho phép tăng navigationTimeout mà không ảnh hưởng đến actionTimeout, và ngược lại.

3

Default 0 Nghĩa Là Gì — Không Phải "Vô Hạn Thật Sự"

Default của cả hai option là 0. Theo Playwright docs, 0 có nghĩa "không có per-operation timeout" — nhưng điều đó không có nghĩa operation có thể chạy mãi mãi:

// playwright.config.ts — default
export default defineConfig({
  timeout: 30_000,       // test timeout: 30s (default)
  use: {
    actionTimeout: 0,    // per-action: không limit riêng
    navigationTimeout: 0, // per-nav: không limit riêng
  },
});

Khi actionTimeout: 0, mỗi action (click, fill...) không có đồng hồ đếm ngược riêng. Thay vào đó, chúng chia sẻ chung budget của test timeout (30s). Nếu test đã dùng 28s, action cuối chỉ còn 2s trước khi test timeout toàn bộ throw TimeoutError.

Ngược lại, khi set actionTimeout: 10_000, mỗi action có đồng hồ riêng 10s — bất kể test timeout còn bao nhiêu. Nếu một page.click() không thể thực hiện trong 10s (element không actionable), action đó throw TimeoutError ngay, test dừng sớm, không cần đợi hết 30s test timeout.

Tình huống actionTimeout: 0 actionTimeout: 10_000
Element frozen sau 5s Action tiếp tục chờ, dừng lúc test timeout (30s) Action throw TimeoutError sau 10s
Element xuất hiện sau 8s Action thành công nếu test timeout chưa hết Action throw TimeoutError sau 10s (nếu element xuất hiện ở giây 11+)
Test có 5 action, mỗi action 7s Test fail ở action thứ 5 (35s > 30s test timeout) Test fail ở action đầu tiên sau 10s

Per-action timeout nhỏ hơn test timeout có tác dụng fail-fast: test dừng ngay khi một action cụ thể quá chậm thay vì chờ cạn toàn bộ test timeout.

4

Phân Biệt Với timeout Test Và expect.timeout

Playwright có bốn loại timeout cần phân biệt:

Timeout Config field Default Áp dụng cho
Test timeout timeout (top-level config) 30 000ms Toàn bộ test function — kể từ lúc bắt đầu đến khi kết thúc
Action timeout use.actionTimeout 0 (dùng test timeout) Mỗi action đơn lẻ: click, fill, hover, check, setInputFiles...
Navigation timeout use.navigationTimeout 0 (dùng test timeout) Mỗi navigation: goto, reload, goBack, waitForURL...
Assertion timeout expect.timeout 5 000ms Mỗi web-first assertion: toBeVisible, toHaveText, toHaveURL...

Điểm quan trọng: actionTimeout không áp dụng cho assertion. Khi viết await expect(locator).toBeVisible(), framework dùng expect.timeout (default 5s) — không phải actionTimeout. Đây là nguồn gây nhầm lẫn phổ biến:

// playwright.config.ts
export default defineConfig({
  use: {
    actionTimeout: 15_000,  // click, fill... chờ 15s
    // expect.timeout vẫn là 5s — actionTimeout không ảnh hưởng
  },
});
// test.spec.ts
test('form submission', async ({ page }) => {
  await page.goto('/form');                       // dùng navigationTimeout (0 → test budget)
  await page.fill('input[name="email"]', '[email protected]'); // dùng actionTimeout (15s)
  await page.click('button[type="submit"]');       // dùng actionTimeout (15s)

  // Dùng expect.timeout (5s mặc định) — KHÔNG phải actionTimeout
  await expect(page.getByText('Success')).toBeVisible();

  // Muốn override assertion timeout:
  await expect(page.getByText('Success')).toBeVisible({ timeout: 10_000 });
});

Ba loại timeout hoạt động đồng thời và độc lập. Test timeout là "hard ceiling" — nếu hết, toàn test bị kill dù action/assertion chưa xong.

5

Configure Trong playwright.config.ts

Hai option này nằm trong block use — cùng layer với các Option Fixtures khác:

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

export default defineConfig({
  timeout: 60_000,   // test timeout toàn bộ: 60s
  use: {
    actionTimeout: 10_000,     // mỗi click/fill/... tối đa 10s
    navigationTimeout: 30_000, // mỗi goto/reload/... tối đa 30s
  },
});

Nếu dùng multi-project, có thể override riêng cho từng project:

export default defineConfig({
  timeout: 30_000, // global test timeout
  use: {
    // Default cho mọi project
    actionTimeout: 10_000,
    navigationTimeout: 30_000,
  },
  projects: [
    {
      name: 'api-heavy',
      use: {
        // Project này gọi nhiều API nặng — tăng navigation timeout
        navigationTimeout: 60_000,
        // actionTimeout kế thừa từ global: 10_000
      },
    },
    {
      name: 'smoke',
      use: {
        // Smoke test cần fail-fast
        actionTimeout: 5_000,
        navigationTimeout: 15_000,
      },
    },
  ],
});

Override qua test.use() ở file hoặc describe level cũng hoạt động như mọi Option Fixture khác:

// slow-flow.spec.ts
// File-level override — áp dụng cho mọi test trong file này
test.use({
  actionTimeout: 20_000,
  navigationTimeout: 60_000,
});

test('upload large file', async ({ page }) => {
  // Các action trong file này dùng 20s, navigation dùng 60s
  await page.goto('/upload');
  await page.setInputFiles('input[type="file"]', 'large-file.pdf');
  await page.click('button[type="submit"]');
});
6

Override Per-Action Call

Mọi action và navigation API đều nhận option { timeout: number } để override tại call site. Per-call override có độ ưu tiên cao nhất — ghi đè cả config lẫn test.use():

test('mixed timeouts', async ({ page }) => {
  // Navigation với timeout custom 60s (override navigationTimeout từ config)
  await page.goto('https://slow-server.example.com', { timeout: 60_000 });

  // Action bình thường — dùng actionTimeout từ config
  await page.fill('input[name="query"]', 'playwright');

  // Action cụ thể cần timeout ngắn hơn (fail-fast nếu button không xuất hiện)
  await page.click('#search-button', { timeout: 3_000 });

  // Locator action cũng hỗ trợ per-call timeout
  await page.getByRole('button', { name: 'Submit' }).click({ timeout: 5_000 });

  // waitForSelector với custom timeout
  await page.waitForSelector('.result-item', { timeout: 15_000 });
});

Khi nào nên dùng per-call override thay vì thay đổi config?

  • Một action cụ thể cần timeout khác biệt rõ ràng so với phần còn lại trong test — ví dụ upload file lớn trong test không phải về upload.
  • Thử nghiệm timeout trước khi quyết định thay đổi config toàn cục.
  • Một-hai test đặc biệt không đủ để tạo riêng project hay file.

Không nên dùng per-call override tràn lan — nếu nhiều action cần cùng timeout cao hơn, đó là dấu hiệu cần điều chỉnh config ở level cao hơn (project hoặc global).

7

CI Vs Local Pattern

CI thường chậm hơn local vì CPU bị chia sẻ, network latency cao hơn, và browser cold-start lâu hơn. Pattern phổ biến là tăng timeout trên CI:

// playwright.config.ts
const isCI = !!process.env.CI;

export default defineConfig({
  timeout: isCI ? 60_000 : 30_000,
  use: {
    actionTimeout: isCI ? 15_000 : 5_000,
    navigationTimeout: isCI ? 60_000 : 30_000,
  },
});

Lý do tách biệt actionTimeout cho CI và local:

  • Local development: timeout ngắn → test fail nhanh → dev nhận feedback sớm.
  • CI: timeout dài hơn → giảm flaky test do container chậm, không mask bug thật sự.

Một biến thể dùng số liệu tương đối — nhân với hệ số CI:

const timeoutMultiplier = process.env.CI ? 3 : 1;

export default defineConfig({
  timeout: 30_000 * timeoutMultiplier,
  use: {
    actionTimeout: 5_000 * timeoutMultiplier,
    navigationTimeout: 30_000 * timeoutMultiplier,
  },
});

Pattern này đơn giản nhưng cần thận trọng: nhân hết là 90s test timeout trên CI — nếu có bug thật gây infinite loop, test mất 90s mới fail. Đánh đổi giữa stability và feedback speed.

8

Use Case Cụ Thể

Slow CI — container CPU thấp

CI runners chia sẻ CPU với nhiều job song song. Playwright action phải chờ element actionable (element không frozen, không disabled, stable position). Khi CPU chậm, animation CSS chạy chậm hơn → element "stable" lâu hơn → action timeout trigger dù không có bug:

// Tăng actionTimeout để bù CI chậm
use: {
  actionTimeout: isCI ? 15_000 : 5_000,
}

Navigation chậm do API phụ thuộc

Trang khi load cần gọi nhiều API bên ngoài (analytics, payment gateway, CDN). Trên staging environment, các dependency này thường chậm hơn production. Tăng riêng navigationTimeout mà không ảnh hưởng actionTimeout:

use: {
  actionTimeout: 10_000,      // action vẫn fast-fail sau 10s
  navigationTimeout: 60_000,  // navigation chờ đủ lâu cho API bên ngoài
}

Long-running action — upload file lớn

Upload file qua page.setInputFiles() có thể mất vài chục giây nếu file lớn và server xử lý chậm. Cần tăng cả test timeout lẫn per-action override:

test('upload large file', async ({ page }) => {
  // Override test timeout cho test này
  test.setTimeout(120_000);  // 2 phút

  await page.goto('/upload');

  // Per-action override cho riêng upload step
  await page.setInputFiles('input[type="file"]', 'big.zip', { timeout: 60_000 });

  // Các action sau không cần timeout đặc biệt — dùng config mặc định
  await page.click('button[type="submit"]');
  await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 90_000 });
});

Animation-heavy UI

Playwright action chờ element "stable" — không di chuyển trong khoảng thời gian nhất định. Nếu UI có animation dài (slide-in panel 800ms, modal với enter transition), action phải chờ animation kết thúc trước khi click. Khi actionTimeout quá ngắn, action timeout trong lúc chờ animation:

// Nếu animation element kéo dài 1-2 giây, tăng actionTimeout
use: {
  actionTimeout: 8_000,  // 8s đủ cho animation 2s + buffer
}

// Hoặc per-call cho element cụ thể có animation dài
await page.click('.modal-close-button', { timeout: 5_000 });

Giải pháp tốt hơn trong nhiều trường hợp: disable animation trong test bằng CSS injection — xem pattern page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; animation: none !important; }' }). Nếu làm được, actionTimeout ngắn vẫn ổn.

9

Timeout Hierarchy — Mức Ưu Tiên

Bốn mức timeout theo thứ tự ưu tiên từ cao đến thấp (mức cao override mức thấp):

globalTimeout (config top-level)
  └─ timeout (test timeout, default 30s)
       └─ actionTimeout / navigationTimeout (per-operation default)
            └─ per-call { timeout } (override tại call site)

Quy tắc quan trọng: test timeout là hard ceiling. Kể cả khi actionTimeout hoặc per-call { timeout } lớn hơn test timeout, test sẽ bị kill khi hết test timeout — action throw lỗi ngay lập tức thay vì chờ đến timeout của chính nó.

// playwright.config.ts
export default defineConfig({
  timeout: 30_000,         // test timeout: 30s — hard ceiling
  use: {
    actionTimeout: 60_000, // per-action: 60s — NHƯNG sẽ không bao giờ đạt được
    // vì test timeout (30s) nhỏ hơn actionTimeout (60s)
    // action sẽ bị kill sau 30s do test timeout, không phải sau 60s do actionTimeout
  },
});

Ví dụ cụ thể về conflict nguy hiểm này:

// Config:
// timeout (test): 30_000
// actionTimeout: 60_000

test('click button', async ({ page }) => {
  await page.goto('/');
  // Giả sử button không bao giờ actionable
  await page.click('#frozen-button');
  // → action chờ... chờ... đến giây 30 → TEST TIMEOUT kill cả test
  // → actionTimeout 60s không bao giờ được tính đến
  // Error: "Test timeout of 30000ms exceeded." — không phải "actionTimeout exceeded"
});

Khi set actionTimeout, luôn đảm bảo nó nhỏ hơn test timeout để per-action timeout có ý nghĩa. Nếu test timeout = 30s và actionTimeout = 10s, một action frozen sẽ fail sau 10s với lỗi rõ ràng hơn.

Deep dive đầy đủ về timeout hierarchy (bao gồm globalTimeout, workers timeout, expect.toPass) sẽ được đề cập ở chương A.9 của series này.

10

testInfo.setTimeout() — Không Ảnh Hưởng Đến actionTimeout

testInfo.setTimeout()test.setTimeout() chỉ override test timeout — không thay đổi actionTimeout hay navigationTimeout:

test('long process', async ({ page }, testInfo) => {
  // Tăng test timeout runtime lên 120s
  testInfo.setTimeout(120_000);
  // hoặc: test.setTimeout(120_000);

  // actionTimeout vẫn là giá trị từ config (ví dụ 10_000)
  // navigationTimeout vẫn là giá trị từ config (ví dụ 30_000)

  await page.goto('/heavy-page');               // navigationTimeout: 30s (config)
  await page.click('#start-process');           // actionTimeout: 10s (config)
  await page.waitForSelector('.done', { timeout: 100_000 }); // per-call override
  // test sẽ không bị kill trước 120s nhờ testInfo.setTimeout(120_000)
});

testInfo.setTimeout() thường dùng trong beforeEach hook để tăng test timeout theo điều kiện — ví dụ khi môi trường staging được phát hiện chậm:

test.beforeEach(async ({}, testInfo) => {
  if (process.env.STAGING === 'true') {
    // Staging chậm hơn — tăng timeout, nhưng actionTimeout vẫn giữ nguyên
    testInfo.setTimeout(testInfo.timeout + 30_000);
  }
});

Phân biệt rõ: test.setTimeout() = test budget; actionTimeout = per-action limit; hai thứ này độc lập và không tự động liên kết với nhau.

11

4 Pitfalls Thực Tế

1. actionTimeout lớn hơn test timeout — per-action timeout không bao giờ trigger

// SAI — actionTimeout (60s) > test timeout (30s)
export default defineConfig({
  timeout: 30_000,
  use: {
    actionTimeout: 60_000,  // Không bao giờ trigger vì test timeout kill trước
  },
});
// Kết quả: action frozen → test fail sau 30s với lỗi "Test timeout exceeded"
// → không biết action nào bị stuck, debug khó hơn

// ĐÚNG — actionTimeout nhỏ hơn test timeout để có ý nghĩa
export default defineConfig({
  timeout: 60_000,
  use: {
    actionTimeout: 15_000,  // fail-fast per-action, còn budget cho các action khác
  },
});

2. Đặt actionTimeout quá cao — mask test thật sự chậm

// TRÁNH — set 60s global chỉ vì một vài test cần lâu
export default defineConfig({
  use: {
    actionTimeout: 60_000,  // Quá cao: một element không actionable sau 60s
    // là dấu hiệu bug — không nên chờ lâu đến vậy
  },
});

// TỐT HƠN — dùng per-call hoặc file-level cho test đặc biệt
// Global giữ thấp để fail-fast, test đặc biệt override cục bộ
export default defineConfig({
  use: {
    actionTimeout: 10_000,  // global fast
  },
});

// Trong file cần lâu:
test.use({ actionTimeout: 30_000 });

3. Nhầm actionTimeout với expect.timeout — assertion vẫn timeout sau 5s

// Situation: set actionTimeout cao nhưng assertion vẫn fail sau 5s
export default defineConfig({
  use: {
    actionTimeout: 30_000,  // action thoải mái
    // expect.timeout vẫn là default 5s — không liên quan actionTimeout
  },
});

test('slow render', async ({ page }) => {
  await page.goto('/');
  await page.click('#load-data');
  // Dữ liệu render sau 8s — assertion fail sau 5s vì expect.timeout = 5s
  await expect(page.getByText('Data loaded')).toBeVisible(); // ❌ TimeoutError sau 5s
});

// FIX — tăng expect.timeout riêng
export default defineConfig({
  expect: {
    timeout: 10_000,  // assertion timeout 10s
  },
  use: {
    actionTimeout: 30_000,
  },
});
// hoặc per-assertion:
await expect(page.getByText('Data loaded')).toBeVisible({ timeout: 10_000 });

4. Per-call override tình cờ thấp hơn config — gây flaky

// Tình huống: config global actionTimeout: 10_000
// Dev copy-paste per-call timeout từ nơi khác, vô tình set thấp hơn

// playwright.config.ts
use: { actionTimeout: 10_000 }

// test.spec.ts
await page.click('#submit', { timeout: 2_000 });  // Vô tình quá ngắn
// Action bình thường mất 3-4s trên CI → flaky chỉ trên CI
// Local nhanh nên không phát hiện

// FIX — xóa per-call override nếu không có lý do cụ thể
await page.click('#submit');  // dùng actionTimeout từ config: 10s
12

Tổng Kết

  • actionTimeout giới hạn mỗi action đơn lẻ (click, fill, hover...); navigationTimeout giới hạn mỗi navigation (goto, reload, waitForURL...).
  • Cả hai default là 0 — không có per-operation limit riêng, operation dùng chung budget test timeout (30s).
  • actionTimeout không áp dụng cho assertion (expect.toBeVisible...). Assertion dùng expect.timeout (default 5s) riêng.
  • Test timeout là hard ceiling: nếu actionTimeout lớn hơn test timeout, per-action timeout không bao giờ trigger — operation bị kill bởi test timeout trước.
  • Per-call { timeout } có độ ưu tiên cao nhất — override config và test.use().
  • testInfo.setTimeout() chỉ thay đổi test timeout — không ảnh hưởng đến actionTimeout hay navigationTimeout.
  • CI vs local pattern: tăng timeout trên CI để giảm flaky, giữ thấp local để fail-fast.
  • Không set actionTimeout quá cao toàn cục — dùng per-call hoặc file-level cho test đặc biệt để tránh mask bug thật sự chậm.
13

Quiz Củng Cố

Câu 1

Config sau có vấn đề gì không? Nếu có, giải thích hệ quả:

export default defineConfig({
  timeout: 20_000,
  use: {
    actionTimeout: 30_000,
    navigationTimeout: 60_000,
  },
});
Đáp án

Có vấn đề: cả actionTimeout (30s) lẫn navigationTimeout (60s) đều lớn hơn test timeout (20s). Điều này có nghĩa per-operation timeout không bao giờ trigger vì test timeout kill toàn bộ test trước. Một action frozen sẽ fail sau 20s với lỗi "Test timeout of 20000ms exceeded" — không phải lỗi actionTimeout. Debug khó hơn vì không biết action nào cụ thể bị stuck.

Fix: tăng test timeout lên đủ cao, hoặc giảm actionTimeout/navigationTimeout xuống thấp hơn test timeout:

export default defineConfig({
  timeout: 60_000,        // test timeout phải lớn hơn mọi per-op timeout
  use: {
    actionTimeout: 15_000,
    navigationTimeout: 30_000,
  },
});

Câu 2

Test sau fail với lỗi "Timeout 5000ms exceeded" dù đã set actionTimeout: 30_000 trong config. Giải thích tại sao và cách fix:

test('slow render', async ({ page }) => {
  await page.click('#load-data');
  await expect(page.getByText('Loaded')).toBeVisible();
  // Error: Timeout 5000ms exceeded.
});
Đáp án

actionTimeout không áp dụng cho assertion. await expect(...).toBeVisible() dùng expect.timeout (default 5s), độc lập với actionTimeout. Cần tăng expect.timeout trong config hoặc override per-assertion:

// Fix 1: per-assertion
await expect(page.getByText('Loaded')).toBeVisible({ timeout: 15_000 });

// Fix 2: config global
// playwright.config.ts
expect: { timeout: 15_000 }

Câu 3

Sự khác biệt giữa testInfo.setTimeout(60_000) và set actionTimeout: 60_000 trong config là gì?

Đáp án

testInfo.setTimeout(60_000) thay đổi test timeout runtime — toàn bộ test function có 60s để hoàn thành. Nó không ảnh hưởng đến actionTimeout hay navigationTimeout.

actionTimeout: 60_000 trong config set per-action default — mỗi action (click, fill...) có 60s riêng. Không ảnh hưởng test timeout (vẫn 30s mặc định). Nếu actionTimeout (60s) > test timeout (30s), action bị kill bởi test timeout trước.

Câu 4

Đoạn code sau dùng override theo mức nào? Giá trị timeout nào được áp dụng cho từng operation?

// playwright.config.ts: actionTimeout: 10_000, navigationTimeout: 30_000

test.use({ navigationTimeout: 60_000 });

test('example', async ({ page }) => {
  await page.goto('/slow-page');                        // (A)
  await page.click('#button', { timeout: 3_000 });     // (B)
  await page.fill('input', 'hello');                   // (C)
  await page.reload({ timeout: 45_000 });              // (D)
});
Đáp án
  • (A) goto: dùng navigationTimeout file-level override = 60 000ms.
  • (B) click: per-call override = 3 000ms (ghi đè actionTimeout config 10s).
  • (C) fill: không có override → dùng actionTimeout từ config = 10 000ms.
  • (D) reload: per-call override = 45 000ms (ghi đè navigationTimeout file-level 60s).

Câu 5

Khi nào nên đặt actionTimeout: 0 thay vì một giá trị cụ thể?

Đáp án

actionTimeout: 0 (default) phù hợp khi:

  • Test timeout được set hợp lý và đủ làm "safety net" cho tất cả action.
  • Không cần fail-fast per-action — muốn test tự nhiên chạy đến hết budget.
  • Test suite không có vấn đề flaky liên quan action timeout cụ thể.

Nên set giá trị cụ thể (không phải 0) khi:

  • Muốn fail-fast: một action stuck phải fail ngay, không chờ hết test timeout.
  • Cần phân biệt lỗi "action quá chậm" với "test quá chậm".
  • CI flaky do action timeout không rõ ràng — thêm per-action limit giúp debug.
14

Bài Tiếp Theo

Bài 15 tiếp tục nhóm Options Fixtures với testIdAttribute — option cho phép thay đổi data attribute mà Playwright dùng khi gọi getByTestId(), từ default data-testid sang attribute khác phù hợp với convention của từng team.

Bài 15: Option Fixture testIdAttribute