Danh sách bài viết

Bài 88: testInfo.setTimeout() — Set Timeout Từ Fixture

testInfo.setTimeout(ms) là cách set test timeout từ bên trong fixture hoặc hook thông qua testInfo object — thay vì dùng global test như với test.setTimeout(). Bài này phân tích cú pháp, sự khác biệt so với test.setTimeout(), pattern relative increase (testInfo.timeout + N), fixture chain cộng dồn timeout, conditional timeout dựa fixture data, và 4 pitfall thực tế khi lạm dụng absolute value hoặc quên tính timeout hiện tại.

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 testInfo.setTimeout(ms) set test timeout từ bên trong fixture hoặc hook.
  • Phân biệt được với test.setTimeout() — cùng kết quả nhưng access point khác nhau.
  • Đọc giá trị timeout hiện tại qua testInfo.timeout để tính relative increase.
  • Áp dụng pattern fixture chain cộng dồn khi nhiều fixture cùng tăng timeout.
  • Nhận biết và tránh 4 pitfall phổ biến liên quan đến absolute value và fixture chain.

Bài 24 đã phân tích fixture timeout option — timeout riêng cho setup/teardown của fixture, tách bạch với test body. Bài này focus vào testInfo.setTimeout() — cách tăng test timeout (toàn bộ test bao gồm body + fixtures) từ bên trong fixture, khi fixture biết rằng test cần thêm thời gian.

2

testInfo.setTimeout() Là Gì

testInfo là object chứa metadata và runtime state của test hiện tại — title, annotations, retry count, timeout, v.v. Playwright inject testInfo như tham số thứ ba của fixture function:

myFixture: async ({}, use, testInfo) => {
  // testInfo có sẵn ở đây
  console.log(testInfo.title);    // tên test đang chạy
  console.log(testInfo.timeout);  // timeout hiện tại (ms)
  await use(/* ... */);
},

testInfo.setTimeout(ms) thay đổi test timeout của test hiện tại ngay tại thời điểm gọi. Timeout mới có hiệu lực ngay lập tức — Playwright reset đồng hồ đang chạy về giá trị mới:

myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(60_000);  // test timeout giờ là 60s
  await use(/* ... */);
},

Đây là cách duy nhất để set test timeout từ bên trong fixture mà không cần truy cập global test object.

3

Cú Pháp Trong Fixture

Fixture nhận testInfo qua tham số thứ ba trong destructure:

import { test as base } from '@playwright/test';

export const test = base.extend<{ slowResource: Resource }>({
  slowResource: async ({}, use, testInfo) => {
    // Tăng test timeout trước khi setup nặng
    testInfo.setTimeout(testInfo.timeout + 30_000);  // +30s tương đối

    const resource = await heavyInit();  // mất ~25s
    await use(resource);
    await resource.cleanup();
  },
});

testInfo cũng có mặt trong hook beforeEachafterEach thông qua tham số thứ hai:

test.beforeEach(async ({ page }, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 10_000);
  await page.goto('/setup-page');
});

Signature đầy đủ: testInfo.setTimeout(timeout: number): void. Nhận số ms dương; truyền 0 tắt timeout (không giới hạn — tương tự test.setTimeout(0)).

4

Khác test.setTimeout() Ở Điểm Nào

Cả hai đều set test timeout của test hiện tại và cho kết quả giống nhau về mặt timeout value. Sự khác biệt nằm ở ngữ cảnh truy cập:

Phương thức Truy cập qua Dùng được ở đâu Ghi chú
test.setTimeout(ms) Global test object Test body, describe block, hook (khi có test trong scope) Phổ biến hơn trong test body
testInfo.setTimeout(ms) testInfo object (tham số fixture) Fixture function, hook Cần thiết khi fixture không có access đến global test

Trong fixture function, test object không được inject — chỉ có các fixture dependencies và testInfo. Đó là lý do testInfo.setTimeout() tồn tại như một API riêng:

// KHÔNG dùng được bên trong fixture:
export const test = base.extend<{ myFix: string }>({
  myFix: async ({}, use) => {
    test.setTimeout(60_000);  // ← test ở đây là extended test, không phải global
    // Thực ra đây là fixture definition, không phải test body
    // Gọi test.setTimeout() trong fixture definition gây unexpected behavior
    await use('value');
  },
});

// ĐÚNG — dùng testInfo:
export const test = base.extend<{ myFix: string }>({
  myFix: async ({}, use, testInfo) => {
    testInfo.setTimeout(testInfo.timeout + 60_000);  // ← rõ ràng, an toàn
    await use('value');
  },
});

Về mặt kỹ thuật, test.setTimeout()testInfo.setTimeout() đều ghi vào cùng một field timeout của TestInfo object đằng sau. Kết quả timeout cuối cùng giống nhau — chỉ khác nơi gọi.

5

testInfo.timeout — Đọc Giá Trị Hiện Tại

testInfo.timeout (property, không phải method) trả về giá trị timeout hiện tại của test tính bằng ms. Giá trị này phản ánh timeout sau tất cả các lần override trước đó — config base, test.setTimeout() ở describe level, hoặc testInfo.setTimeout() từ fixture trước:

// playwright.config.ts: timeout: 30_000

myFixture: async ({}, use, testInfo) => {
  console.log(testInfo.timeout);  // 30000 — từ config

  testInfo.setTimeout(testInfo.timeout + 20_000);
  console.log(testInfo.timeout);  // 50000 — đã cập nhật

  await use(/* ... */);
},

testInfo.timeout luôn phản ánh trạng thái hiện tại, không phải giá trị config gốc. Điều này quan trọng khi nhiều fixture cùng gọi testInfo.setTimeout() — mỗi fixture đọc đúng giá trị sau tất cả các tăng trước đó.

Trường hợp đặc biệt: nếu timeout đã bị set về 0 (no timeout) bởi fixture trước, testInfo.timeout trả về 0. Khi đó testInfo.timeout + N bằng N — fixture tiếp theo vô tình đặt lại timeout có giới hạn.

6

Pattern Relative Increase

Thay vì hardcode giá trị absolute, dùng testInfo.timeout + N để tăng tương đối so với timeout hiện tại:

// TRÁNH — absolute value
myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(60_000);  // hardcode 60s — override mọi config
  await use(/* ... */);
},

// ƯU TIÊN — relative increase
myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 30_000);  // thêm 30s vào base
  await use(/* ... */);
},

Lý do ưu tiên relative: nếu project config đặt timeout: 60_000, absolute value 60_000 không thêm gì. Absolute value 30_000 thậm chí giảm timeout so với config. Relative testInfo.timeout + N luôn giữ base config và thêm đúng phần fixture cần:

// Ví dụ đầy đủ với relative increase
import { test as base } from '@playwright/test';

export const test = base.extend<{ slowSetup: Resource }>({
  slowSetup: async ({}, use, testInfo) => {
    // Fixture này cần ~50s để setup
    // Tăng 60s để có buffer, không quan tâm config base là bao nhiêu
    testInfo.setTimeout(testInfo.timeout + 60_000);

    const resource = await heavyInit();  // ~50s
    await use(resource);
    await resource.cleanup();
  },
});

Nếu config base là 30s → test timeout thành 90s.
Nếu config base là 60s → test timeout thành 120s.
Fixture luôn nhận đúng 60s buffer, không phụ thuộc vào project config.

7

Use Case: Fixture Cần Thêm Thời Gian

Trường hợp phổ biến nhất: fixture setup chậm cần thêm budget cho toàn bộ test (không chỉ fixture). Khác với fixture timeout option (budget riêng cho setup/teardown), testInfo.setTimeout() tăng test timeout toàn bộ — fixture và test body cùng được hưởng:

// fixtures/data.ts
import { test as base } from '@playwright/test';

type CatalogData = {
  categories: Category[];
  products: Product[];
};

export const test = base.extend<{ catalogData: CatalogData }>({
  catalogData: async ({}, use, testInfo) => {
    // Fixture setup cần ~45s để gọi API tạo dữ liệu
    // Test body sau đó cần ~20s để verify
    // Tổng: 65s — vượt default 30s
    // Tăng 60s để cover setup + buffer cho test body
    testInfo.setTimeout(testInfo.timeout + 60_000);

    const categories = await api.createCategories(50);
    const products = await api.createProducts(categories, 500);

    await use({ categories, products });

    await api.cleanup([...categories, ...products]);
  },
});

Fixture này thông báo cho Playwright rằng "test nào dùng catalogData cần thêm 60s". Test author không phải tự nhớ gọi test.setTimeout() — fixture tự quản lý.

Pattern này đặc biệt hữu ích khi fixture dùng trong nhiều test file — mỗi test tự động nhận đúng timeout cần thiết mà không cần config thêm.

8

Use Case: Conditional Từ Fixture Data

Fixture có thể kiểm tra điều kiện runtime — kích thước dataset, trạng thái môi trường, feature flag — trước khi quyết định có cần tăng timeout không:

// fixtures/dynamic-data.ts
export const test = base.extend<{ dynamicData: Data[] }>({
  dynamicData: async ({}, use, testInfo) => {
    const size = await api.getDatasetSize();

    // Chỉ tăng timeout khi dataset lớn
    if (size > 1000) {
      // Ước tính: mỗi 1000 record mất ~30s để load
      const extraMs = Math.ceil(size / 1000) * 30_000;
      testInfo.setTimeout(testInfo.timeout + extraMs);
    }

    const data = await api.loadData();
    await use(data);
  },
});

Ví dụ khác — conditional theo CI environment:

// CI thường chậm hơn local vì resource chia sẻ
envAwareFixture: async ({}, use, testInfo) => {
  if (process.env.CI) {
    testInfo.setTimeout(testInfo.timeout + 30_000);  // +30s cho CI
  }
  const resource = await initResource();
  await use(resource);
},

Conditional approach giữ timeout gọn cho trường hợp thông thường, chỉ tăng khi thực sự cần — tránh tất cả test luôn dùng timeout lớn dù dataset nhỏ.

9

Pattern Fixture Chain Cộng Dồn

Khi test dùng nhiều fixture, mỗi fixture gọi testInfo.setTimeout(testInfo.timeout + N), các tăng này cộng dồn vì mỗi fixture đọc testInfo.timeout sau khi fixture trước đã tăng:

// fixtures/compound.ts
export const test = base.extend<{
  fixtureA: ResourceA;
  fixtureB: ResourceB;
}>({
  fixtureA: async ({}, use, testInfo) => {
    // testInfo.timeout ban đầu: 30000 (từ config)
    testInfo.setTimeout(testInfo.timeout + 30_000);  // → 60000
    const a = await initA();  // ~25s
    await use(a);
  },

  fixtureB: async ({}, use, testInfo) => {
    // testInfo.timeout lúc này: 60000 (sau fixtureA đã tăng)
    testInfo.setTimeout(testInfo.timeout + 20_000);  // → 80000
    const b = await initB();  // ~15s
    await use(b);
  },
});

// Test dùng cả hai: timeout tổng = 30 + 30 + 20 = 80s
test('uses both fixtures', async ({ fixtureA, fixtureB }) => {
  // Test body có budget từ 80s tính từ lúc test bắt đầu
});

Thứ tự fixture setup ảnh hưởng đến thứ tự cộng dồn, nhưng kết quả cuối cùng giống nhau khi dùng relative increase. Nếu fixtureB setup trước fixtureA, testInfo.timeout khi fixtureA chạy đã là 50s (30 + 20), và kết quả cuối vẫn là 80s.

Khi dùng absolute value thay vì relative, fixture chain không cộng dồn đúng — fixture sau ghi đè fixture trước (last wins):

// VẤN ĐỀ — absolute, last wins
fixtureA: async ({}, use, testInfo) => {
  testInfo.setTimeout(60_000);  // set 60s
  await use(/* ... */);
},

fixtureB: async ({}, use, testInfo) => {
  testInfo.setTimeout(50_000);  // override về 50s — fixtureA bị mất
  await use(/* ... */);
},
// Timeout cuối: 50s — không phải 60+50=110s
// Và fixtureA chỉ còn 50s thay vì 60s đã yêu cầu
10

Dùng Trong Hook beforeEach

testInfo cũng có mặt trong hook beforeEach qua tham số thứ hai. testInfo.setTimeout() trong hook có tác dụng tương tự — set test timeout cho test hiện tại. Điều này tương đương gọi test.setTimeout() ở đầu test body:

test.describe('slow tests group', () => {
  test.beforeEach(async ({ page }, testInfo) => {
    // Tăng timeout cho mọi test trong describe này
    testInfo.setTimeout(testInfo.timeout + 20_000);
    // Setup chung cho mọi test
    await page.goto('/app');
    await page.waitForSelector('.app-ready');
  });

  test('test one', async ({ page }) => {
    // Có thêm 20s từ beforeEach
  });

  test('test two', async ({ page }) => {
    // Cũng có thêm 20s từ beforeEach
  });
});

Phân biệt rõ: testInfo.setTimeout() trong fixture ảnh hưởng test nào dùng fixture đó. testInfo.setTimeout() trong beforeEach ảnh hưởng mọi test trong scope của hook đó. Cả hai cộng dồn nếu cùng được áp dụng cho một test.

11

Limitation

Không tách bạch fixture time và test body time

Đây là hạn chế căn bản so với fixture timeout option. Khi dùng testInfo.setTimeout(), fixture setup và test body chia sẻ chung một budget:

// Với testInfo.setTimeout: chia sẻ budget
myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 60_000);  // budget tổng = 90s
  const resource = await init();  // nếu mất 70s, test body chỉ còn ~20s
  await use(resource);
},

// Với fixture timeout option: tách bạch
myFixture: [
  async ({}, use) => {
    const resource = await init();  // fixture có 60s riêng
    await use(resource);
  },
  { timeout: 60_000 },
  // Test body vẫn có đầy đủ 30s riêng sau khi fixture xong
],

Khi fixture setup mất nhiều thời gian hơn dự kiến, testInfo.setTimeout() không bảo vệ được test body budget. Fixture timeout option phù hợp hơn khi cần guarantee budget cho test body.

testInfo.setTimeout() chỉ có tác dụng khi gọi đúng context

Gọi sau khi test đã bắt đầu chạy là hợp lệ. Gọi sau khi timeout đã trigger thì không có tác dụng — test đã bị mark là fail.

globalTimeout không bị ảnh hưởng

testInfo.setTimeout() chỉ thay đổi test timeout. globalTimeout (cap toàn bộ test run) không bị ảnh hưởng — test vẫn có thể bị kill bởi globalTimeout dù test timeout đã được tăng.

12

4 Pitfalls Thực Tế

1. Hardcode absolute → conflict với fixture khác (last wins)

// SAI — absolute, last wins
fixtureA: async ({}, use, testInfo) => {
  testInfo.setTimeout(90_000);   // set 90s
  await use(/* ... */);
},

fixtureB: async ({}, use, testInfo) => {
  testInfo.setTimeout(60_000);   // override về 60s — fixtureA mất
  await use(/* ... */);
},
// Test timeout cuối: 60s
// fixtureA yêu cầu 90s nhưng chỉ nhận 60s → có thể timeout

// ĐÚNG — relative, cộng dồn
fixtureA: async ({}, use, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 60_000);
  await use(/* ... */);
},

fixtureB: async ({}, use, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 30_000);
  await use(/* ... */);
},
// Kết quả: base + 60 + 30 = đúng budget cần

2. Quên `testInfo.timeout +` → set quá thấp

// SAI — quên đọc current, vô tình giảm timeout
myFixture: async ({}, use, testInfo) => {
  // Config base là 60s, nhưng fixture chỉ set 30s
  testInfo.setTimeout(30_000);   // ← đây là absolute 30s, KHÔNG phải +30s
  // test timeout giờ là 30s — thấp hơn config gốc!
  await use(/* ... */);
},

// ĐÚNG
myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 30_000);  // +30s tương đối
  await use(/* ... */);
},

3. Fixture chain absolute → các fixture trước không còn đủ budget

// Kịch bản nguy hiểm với nhiều fixture absolute
// Fixture setup theo thứ tự: A → B → C

fixtureA: async ({}, use, testInfo) => {
  testInfo.setTimeout(120_000);  // A cần 2 phút
  await veryHeavyA();            // mất 90s ✓ còn 30s
  await use(/* ... */);
},

// fixtureB chạy sau fixtureA với testInfo.timeout = 120_000
fixtureB: async ({}, use, testInfo) => {
  testInfo.setTimeout(60_000);   // B set 60s — override A!
  await mediumB();               // mất 55s ✓ còn 5s
  await use(/* ... */);
},
// Test body chỉ còn 5s — không phải 120s hay 60s
// Và A setup dùng hết 90s trong budget 120s từ lúc đầu
// Nhưng sau khi B set 60s, timeline bị reset về 60s tổng

4. Over-extend → mask slow fixture cần optimize

// TRÁNH — tăng quá mức vô lý
myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(testInfo.timeout + 300_000);  // +5 phút
  // Nếu heavyInit() bình thường mất 10s nhưng đôi khi mất 4 phút
  // → fixture có vấn đề, cần optimize, không phải tăng timeout
  const resource = await heavyInit();
  await use(resource);
},

// TỐT HƠN: tìm hiểu nguyên nhân slowness trước
// Nếu thật sự cần thời gian dài, document rõ lý do
myFixture: async ({}, use, testInfo) => {
  // heavyInit() xử lý dataset lớn (~2 triệu record), bình thường mất 3 phút
  // Timeout +4 phút để cover worst case + buffer
  testInfo.setTimeout(testInfo.timeout + 240_000);
  const resource = await heavyInit();
  await use(resource);
},

Over-extend ẩn đi các vấn đề performance của fixture. CI sẽ chờ lâu hơn trước khi fail, và vấn đề thực sự không được phát hiện sớm. Đặt timeout vừa đủ với buffer hợp lý (~20-30% trên thời gian expected).

13

Tổng Kết

  • testInfo.setTimeout(ms) set test timeout từ fixture hoặc hook thông qua testInfo object — tham số thứ ba của fixture function.
  • Kết quả giống test.setTimeout() — cả hai ghi vào cùng test timeout. Khác biệt: testInfo.setTimeout() dùng được trong fixture khi không có access đến global test.
  • testInfo.timeout (property) đọc giá trị hiện tại sau tất cả các override trước đó.
  • Pattern testInfo.setTimeout(testInfo.timeout + N) là relative increase — bảo toàn base config, cộng dồn đúng khi nhiều fixture cùng tăng.
  • Fixture chain cộng dồn khi dùng relative; last wins khi dùng absolute — absolute gây conflict giữa các fixture.
  • Khác fixture timeout option: testInfo.setTimeout() tăng budget chung (fixture + test body chia sẻ), không tách bạch riêng cho fixture setup.
  • Không over-extend — timeout lớn bất thường che giấu vấn đề performance của fixture.
14

Quiz Củng Cố

Câu 1

Config đặt timeout: 30_000. Fixture A gọi testInfo.setTimeout(testInfo.timeout + 20_000), fixture B (chạy sau A) gọi testInfo.setTimeout(testInfo.timeout + 15_000). Test timeout cuối cùng là bao nhiêu?

Đáp án

65s. Sau fixture A: 30 + 20 = 50s. Sau fixture B: 50 + 15 = 65s. Vì dùng relative increase, testInfo.timeout của B đọc đúng 50s (sau A đã tăng), cộng thêm 15s thành 65s.

Câu 2

Fixture A gọi testInfo.setTimeout(80_000) (absolute), fixture B (chạy sau) gọi testInfo.setTimeout(50_000) (absolute). Test timeout cuối là bao nhiêu? Fixture A setup mất 75s — có bị timeout không?

Đáp án

Test timeout cuối là 50s (B override A). Fixture A chạy đầu với timeout 80s (lúc A chạy B chưa set) — không timeout sau 75s. Nhưng khi B set 50s, đồng hồ reset về 50s tổng từ đầu test — lúc B bắt đầu còn lại khoảng 5s trong budget 50s. Test body không có đủ thời gian. Đây là lý do tránh absolute trong fixture chain.

Câu 3

Đoạn code sau có lỗi không? Nếu có, lỗi ở đâu?

myFixture: async ({}, use, testInfo) => {
  testInfo.setTimeout(30_000);  // fixture cần +30s
  await use(/* ... */);
},
Đáp án

Có lỗi logic. Nếu config base là 60s, dòng này set test timeout xuống 30s — thấp hơn config gốc. Ý định là "thêm 30s" nhưng thực tế là "set cứng 30s". Đúng phải là: testInfo.setTimeout(testInfo.timeout + 30_000).

Câu 4

Fixture dùng testInfo.setTimeout(testInfo.timeout + 60_000) bên trong, fixture setup mất 55s. Sau khi fixture setup xong, test body có bao nhiêu ms còn lại trong budget?

Đáp án

Phụ thuộc vào thời điểm đồng hồ bắt đầu và các giai đoạn khác. Nếu base config là 30s và test bắt đầu lúc t=0: timeout set lên 90s (30+60). Fixture setup từ t=0 đến t≈55s. Test body bắt đầu ~t=55s, còn lại ~35s trong budget 90s. Không có guarantee cố định — budget test body = timeout mới trừ đi thời gian đã dùng từ lúc test bắt đầu (bao gồm fixture setup).

Câu 5

Khi nào nên dùng testInfo.setTimeout() trong fixture thay vì dùng fixture timeout option [fn, { timeout: N }]?

Đáp án

testInfo.setTimeout() phù hợp khi: (1) muốn cả test body cũng được hưởng thêm thời gian — không chỉ setup, (2) cần conditional timeout dựa trên runtime data, (3) fixture chain cần cộng dồn. Fixture timeout option phù hợp hơn khi: cần tách bạch rõ ràng setup budget và test body budget, hoặc khi chỉ setup nặng còn test body gọn. Nếu fixture setup mất 2 phút nhưng test body chỉ cần 30s, fixture option tách bạch rõ hơn và bảo vệ test body budget.

15

Bài Tiếp Theo

Bài 89 tiếp tục nhóm Timeouts với override per-action timeout — cách set timeout cho từng action riêng lẻ (page.click(), page.fill()) thay vì dùng global actionTimeout từ config.

Bài 89: Override Timeout Cho Từng Action