Mục lục
- Mục Tiêu Bài Học
- Fixture Timeout Là Gì — Giới Hạn Riêng Cho Setup/Teardown
- Cú Pháp Khai Báo
- Default Fixture Timeout Bằng Test Timeout
- Timeout Chỉ Apply Cho Fixture, Không Apply Cho Test Body
- Phân Biệt Với
test.setTimeout()VàtestInfo.setTimeout() - Use Case: DB Seed Nặng
- Use Case: Container Start (testcontainers)
- Use Case: Build Asset Và External Service
- Worker-Scope Fixture Với Timeout Riêng
- Multi-Fixture: Mỗi Fixture Timeout Riêng
- Không Kế Thừa Timeout Qua Dependency
- Limitation Thực Tế
- 4 Pitfalls Thực Tế
- Tổng Kết
- Quiz Củng Cố
- 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 fixture timeout áp dụng riêng cho phase setup và teardown của fixture — không ảnh hưởng test body.
- Biết cú pháp khai báo
timeoutoption cùng vớiscope,auto. - Phân biệt fixture
timeoutvớitest.setTimeout()vàtestInfo.setTimeout(). - Nhận biết khi nào cần custom fixture timeout — DB seed, container, build asset, external service.
- Hiểu quy tắc không kế thừa timeout qua dependency chain.
- Tránh được 4 pitfall phổ biến khi cấu hình fixture timeout.
Bài 14 đã phân tích actionTimeout và navigationTimeout — timeout cho operation bên trong test. Bài này focus vào một loại timeout khác hoàn toàn: timeout dành riêng cho fixture setup và teardown.
Fixture Timeout Là Gì — Giới Hạn Riêng Cho Setup/Teardown
Một custom fixture có cấu trúc setup → use → teardown:
async ({ dep1, dep2 }, use) => {
// SETUP: code trước await use()
const resource = await initSomething();
await use(resource); // ← ranh giới
// TEARDOWN: code sau await use()
await resource.cleanup();
}
Mặc định, toàn bộ vòng đời fixture (setup + teardown) tính chung vào budget test timeout (30s). Nếu fixture setup mất nhiều thời gian — ví dụ seed 10 000 record vào database, hoặc spin up PostgreSQL container — test sẽ timeout không phải vì bản thân test logic chậm, mà vì fixture setup quá lâu.
Fixture timeout option cho phép đặt một giới hạn riêng: fixture setup + teardown được phép dùng bao nhiêu ms. Giá trị này hoàn toàn độc lập với test timeout và với timeout của fixture khác.
| Đối tượng | Timeout áp dụng | Khai báo ở đâu |
|---|---|---|
| Test body | Test timeout (config timeout) |
playwright.config.ts, test.setTimeout() |
| Fixture setup + teardown | Fixture timeout option | Tham số thứ hai của fixture: [fn, { timeout }] |
| Action đơn lẻ (click, fill…) | actionTimeout |
use.actionTimeout trong config |
Cú Pháp Khai Báo
Khi không cần option đặc biệt, fixture khai báo trực tiếp bằng function:
export const test = base.extend<{ simpleData: string }>({
simpleData: async ({}, use) => {
await use('hello');
},
});
Khi cần timeout (hoặc scope, auto, option), fixture khai báo dạng tuple [function, options]:
import { test as base } from '@playwright/test';
type SeedData = { users: User[]; products: Product[] };
export const test = base.extend<{ heavySeed: SeedData }>({
heavySeed: [
async ({}, use) => {
// Setup: có thể mất vài phút với dataset lớn
const data = await seedDatabase();
await use(data);
// Teardown: xoá data sau test
await cleanupDatabase();
},
{ timeout: 120_000 }, // 2 phút cho setup + teardown
],
});
Tham số thứ hai của tuple là object options. Các key hợp lệ:
| Key | Type | Mô tả |
|---|---|---|
timeout |
number |
Timeout (ms) cho setup + teardown của fixture này |
scope |
'test' | 'worker' |
Scope vòng đời — mặc định 'test' |
auto |
boolean |
Tự chạy dù test không destructure fixture — mặc định false |
option |
boolean |
Đánh dấu là configurable option (parametrize qua project config) |
box |
boolean |
Ẩn step bên trong fixture khỏi trace viewer |
title |
string |
Tên hiển thị trong reporter / trace |
Có thể kết hợp nhiều option trong cùng object:
heavySeed: [
async ({}, use) => { /* ... */ },
{ scope: 'worker', timeout: 120_000, auto: false },
],
Default Fixture Timeout Bằng Test Timeout
Khi không khai báo timeout option, fixture sử dụng test timeout làm giới hạn mặc định. Điều này có nghĩa setup + teardown của fixture cộng vào cùng "budget" 30s của test:
// playwright.config.ts — test timeout default 30s
export default defineConfig({
timeout: 30_000,
});
// fixtures/auth.ts — không khai báo fixture timeout
export const test = base.extend<{ authedPage: Page }>({
authedPage: async ({ page }, use) => {
// Setup (login): dùng 3-4s
await page.goto('/login');
await page.fill('#email', '[email protected]');
await page.fill('#password', 'secret');
await page.click('[type="submit"]');
await page.waitForURL('/dashboard');
await use(page);
// Teardown: nhỏ, gần như 0s
},
// Không có timeout option → dùng test timeout (30s)
});
Với fixture nhẹ (login, seed vài record, tạo API client), default 30s là đủ. Custom timeout chỉ cần thiết khi fixture setup thực sự cần nhiều thời gian hơn — thường là các tác vụ infrastructure-level.
Timeout Chỉ Apply Cho Fixture, Không Apply Cho Test Body
Đây là điểm khác biệt quan trọng nhất so với test.setTimeout(). Fixture timeout option chỉ tính thời gian cho code trong fixture function — phần setup (trước await use()) và teardown (sau await use()). Code trong test body vẫn dùng test timeout riêng.
export const test = base.extend<{ dbSeed: SeedData }>({
dbSeed: [
async ({}, use) => {
// ←─ đồng hồ fixture timeout BẮT ĐẦU ở đây
const data = await seedDatabase(); // có thể mất 90s
await use(data);
await cleanupDatabase(); // có thể mất 10s
// ←─ đồng hồ fixture timeout KẾT THÚC ở đây
// Tổng setup + teardown được phép: 120s (2 phút)
},
{ timeout: 120_000 },
],
});
// test.spec.ts
test('verify seeded data', async ({ dbSeed }) => {
// ←─ đồng hồ test timeout BẮT ĐẦU ở đây (riêng biệt)
// Test body có 30s (hoặc giá trị test timeout từ config)
// dbSeed đã được inject xong — fixture timeout đã kết thúc
expect(dbSeed.users).toHaveLength(1000);
await page.goto('/admin/users');
// ←─ test timeout kết thúc sau 30s từ đây
});
Hai đồng hồ này chạy tuần tự, không overlap: fixture setup chạy trước với fixture timeout, sau đó test body chạy với test timeout.
Nếu fixture timeout hết trong phase setup (ví dụ seedDatabase() mất 130s nhưng timeout là 120s), Playwright throw TimeoutError ngay trong fixture, test body không bao giờ chạy.
Phân Biệt Với test.setTimeout() Và testInfo.setTimeout()
Ba cách tác động đến timeout liên quan đến fixture — nhưng hoạt động ở các layer khác nhau:
| Cú pháp | Tác dụng lên | Khai báo ở đâu | Scope |
|---|---|---|---|
[fn, { timeout: N }] |
Setup + teardown của fixture đó | Trong test.extend() |
Fixture cụ thể |
test.setTimeout(N) |
Test timeout toàn bộ — test body + fixtures combined | Đầu test function hoặc describe block |
Test hoặc suite |
testInfo.setTimeout(N) |
Test timeout toàn bộ — runtime, set giữa chừng | Trong fixture hoặc test body | Test hiện tại |
Ví dụ sự khác biệt rõ nhất:
// Cách 1: fixture timeout — chỉ fixture mới có budget dài
export const test = base.extend<{ dbSeed: SeedData }>({
dbSeed: [
async ({}, use) => {
const data = await seedDatabase(); // 90s được phép
await use(data);
},
{ timeout: 120_000 }, // fixture có 120s
// Test body vẫn là 30s (config mặc định)
],
});
// Cách 2: test.setTimeout() — cả test + fixture đều dùng chung 120s
test('example', async ({ dbSeed }) => {
test.setTimeout(120_000); // tăng budget toàn bộ test lên 120s
// Nhưng nếu fixture setup mất 90s, test body chỉ còn 30s
// Không có giới hạn riêng cho fixture
expect(dbSeed.users).toHaveLength(1000);
});
Điểm khác biệt thực tế: khi dùng fixture timeout option, test body luôn có đầy đủ test timeout (30s) dù fixture setup mất bao lâu. Khi dùng test.setTimeout(), fixture setup và test body chia sẻ chung budget — fixture nặng sẽ cắt vào thời gian test body.
testInfo.setTimeout() hoạt động tương tự test.setTimeout() nhưng có thể gọi runtime bên trong fixture:
// Trong fixture setup — tăng test timeout nếu phát hiện môi trường chậm
export const test = base.extend<{ dbSeed: SeedData }>({
dbSeed: async ({}, use, testInfo) => {
if (process.env.CI) {
// CI chậm hơn — mở rộng test timeout, nhưng không tách riêng fixture timeout
testInfo.setTimeout(testInfo.timeout + 60_000);
}
const data = await seedDatabase();
await use(data);
},
});
Nhược điểm của pattern testInfo.setTimeout() bên trong fixture: không tách bạch rõ ràng giữa fixture time và test time. Nếu seed chậm, test body bị giảm budget mà không có cảnh báo rõ ràng.
Use Case: DB Seed Nặng
Seed database với số lượng record lớn qua API thường mất nhiều phút — đặc biệt khi mỗi record cần một HTTP request riêng (rate limit, business logic validation) hoặc khi xử lý quan hệ nhiều bảng:
// fixtures/db-seed.ts
import { test as base } from '@playwright/test';
import { apiClient } from '../lib/api-client';
type SeedData = {
users: { id: string; email: string }[];
products: { id: string; sku: string; price: number }[];
orders: { id: string; userId: string; productId: string }[];
};
export const test = base.extend<{ fullCatalog: SeedData }>({
fullCatalog: [
async ({}, use) => {
// Tạo 1000 users — mỗi call ~50ms → ~50s
const users = await apiClient.bulkCreateUsers(1000);
// Tạo 5000 products — có thể mất thêm 60s
const products = await apiClient.bulkCreateProducts(5000);
// Tạo orders liên kết — thêm 30s
const orders = await apiClient.createOrderMatrix(users, products);
await use({ users, products, orders });
// Teardown: xoá toàn bộ — 20s
await apiClient.cleanupTestData([...users, ...products, ...orders]);
},
{ timeout: 180_000 }, // 3 phút: 50+60+30+20 = 160s + buffer
],
});
Fixture này thường dùng ở scope worker nếu nhiều test trong cùng worker cần cùng dataset (seed once, reuse many):
export const test = base.extend<{}, { fullCatalog: SeedData }>({
// WorkerFixtures — generic thứ hai, không phải thứ nhất
fullCatalog: [
async ({}, use) => {
const data = await seedFullCatalog();
await use(data);
await cleanupFullCatalog();
},
{ scope: 'worker', timeout: 180_000 },
],
});
Use Case: Container Start (testcontainers)
Testcontainers (thư viện testcontainers-node) spin up Docker container thật — PostgreSQL, Redis, MinIO — trong quá trình setup. Container cold-start, pull image (nếu chưa cache), và migration thường mất 30-90s trên CI:
// fixtures/postgres.ts
import { test as base } from '@playwright/test';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
type PostgresFixture = {
connectionString: string;
container: InstanceType<typeof PostgreSqlContainer>;
};
export const test = base.extend<{}, { postgres: PostgresFixture }>({
postgres: [
async ({}, use) => {
// Pull image + start container: 30-60s trên CI (image đã cache)
// Pull lần đầu: có thể 3-5 phút
const container = await new PostgreSqlContainer('postgres:16-alpine').start();
// Chạy migration sau khi container ready: thêm 10-20s
await runMigrations(container.getConnectionUri());
await use({
connectionString: container.getConnectionUri(),
container,
});
// Teardown: stop container
await container.stop();
},
{ scope: 'worker', timeout: 60_000 },
// worker scope: 1 container cho mọi test trong worker
// timeout 60s: đủ cho cached image + migration
// Nếu CI chưa cache image, cần tăng lên 180_000+
],
});
Pattern Redis tương tự:
import { GenericContainer } from 'testcontainers';
redis: [
async ({}, use) => {
const container = await new GenericContainer('redis:7-alpine')
.withExposedPorts(6379)
.start();
const redisUrl = `redis://localhost:${container.getMappedPort(6379)}`;
await use(redisUrl);
await container.stop();
},
{ scope: 'worker', timeout: 45_000 },
],
Một lưu ý quan trọng với testcontainers: nếu CI không cache Docker images, lần chạy đầu tiên pull image từ registry có thể mất vài phút. Cân nhắc tăng timeout lần đầu hoặc pre-pull image trong CI pipeline.
Use Case: Build Asset Và External Service
Build asset (Webpack / Vite component test)
Component testing với Playwright CT cần build component trước. Nếu fixture tự trigger build:
builtApp: [
async ({}, use) => {
// Build production bundle trước khi test
await execAsync('npm run build'); // 60-120s cho project lớn
const server = await startStaticServer('./dist');
await use(server.url);
await server.stop();
},
{ scope: 'worker', timeout: 150_000 },
],
External service initialization
Kết nối AWS SDK, Stripe, hay khởi tạo mock server có thể cần nhiều giây:
stripeTestEnv: [
async ({}, use) => {
// Khởi tạo Stripe test mode, đăng ký webhook endpoint, chờ xác nhận
const stripe = new Stripe(process.env.STRIPE_TEST_KEY!);
const webhookEndpoint = await stripe.webhookEndpoints.create({
url: process.env.TEST_WEBHOOK_URL!,
enabled_events: ['payment_intent.succeeded'],
});
await use({ stripe, webhookEndpoint });
// Cleanup webhook sau test
await stripe.webhookEndpoints.del(webhookEndpoint.id);
},
{ timeout: 30_000 }, // External API call — 30s đủ nhưng cần explicit
],
Với external services, timeout 30s thường đủ nhưng cần khai báo explicit để tách bạch với test body timeout — tránh trường hợp fixture init fail mà error message lại bị gán cho test body.
Worker-Scope Fixture Với Timeout Riêng
Worker-scope fixture setup một lần cho mọi test trong worker và cleanup sau test cuối cùng. timeout option hoạt động giống test-scope — áp dụng cho setup + teardown của fixture, không phụ thuộc vào test timeout:
export const test = base.extend<{}, { postgresContainer: PostgresFixture }>({
postgresContainer: [
async ({}, use) => {
// Setup once per worker — timeout riêng 60s
const container = await new PostgreSqlContainer().start();
await runMigrations(container.getConnectionUri());
await use(container);
// Teardown once per worker — cũng trong budget 60s
await container.stop();
},
{ scope: 'worker', timeout: 60_000 },
],
});
Điểm khác biệt của worker-scope timeout so với test-scope timeout: timeout chỉ áp dụng cho lần setup đầu tiên và lần teardown cuối cùng. Các test trong worker không "trả phí" timeout của fixture — chúng chỉ nhận fixture đã được init sẵn.
Nếu worker-scope fixture setup fail (timeout hết), toàn bộ test trong worker đó đều fail với error từ fixture — không có test nào chạy được.
Multi-Fixture: Mỗi Fixture Timeout Riêng
Khi một test dùng nhiều fixture, mỗi fixture có timeout riêng độc lập. Các timeout này không cộng lại thành một giá trị chung:
type Fixtures = {
quickFixture: string;
slowFixture: HeavyResource;
heaviestFixture: MassiveDataset;
};
export const test = base.extend<Fixtures>({
quickFixture: async ({}, use) => {
// Không cần timeout riêng — default (test timeout) là đủ
await use('fast-value');
},
slowFixture: [
async ({}, use) => {
const r = await heavyInit(); // ~60s
await use(r);
await r.cleanup(); // ~5s
},
{ timeout: 90_000 }, // 90s cho fixture này
],
heaviestFixture: [
async ({}, use) => {
const data = await massiveSeed(); // ~150s
await use(data);
await massiveCleanup(); // ~30s
},
{ timeout: 200_000 }, // 200s cho fixture này
],
});
Khi test dùng cả slowFixture và heaviestFixture, Playwright setup chúng tuần tự theo dependency graph. Mỗi fixture có đồng hồ riêng — slowFixture có 90s, heaviestFixture có 200s.
Tổng thời gian setup có thể lên đến 90 + 200 = 290s, nhưng test body vẫn có đầy đủ test timeout (30s) sau khi mọi fixture setup xong.
Không Kế Thừa Timeout Qua Dependency
Fixture không kế thừa timeout từ fixture mà nó phụ thuộc. Mỗi fixture quản lý timeout của chính nó:
export const test = base.extend<{
dbConnection: DbConn;
seededData: SeedData;
}>({
// Fixture A: setup connection — timeout riêng 30s
dbConnection: [
async ({}, use) => {
const conn = await createDbConnection();
await use(conn);
await conn.close();
},
{ timeout: 30_000 },
],
// Fixture B phụ thuộc Fixture A — có timeout riêng 120s
// timeout của dbConnection (30s) KHÔNG ảnh hưởng timeout của seededData (120s)
seededData: [
async ({ dbConnection }, use) => {
// dbConnection đã ready khi seededData setup chạy
const data = await seedViaConnection(dbConnection); // 90s
await use(data);
await cleanupViaConnection(dbConnection, data); // 20s
},
{ timeout: 120_000 }, // timeout này chỉ tính cho seededData setup+teardown
],
});
Nếu dbConnection setup mất 35s (vượt timeout 30s), nó sẽ fail trước. seededData không bao giờ chạy, test fail với error từ dbConnection. Đây là hành vi đúng — từng fixture có trách nhiệm quản lý thời gian setup của chính nó.
Hệ quả: khi khai báo fixture với dependency có timeout riêng, cần tự đánh giá xem fixture của mình cần bao nhiêu thêm ngoài thời gian dependency đã dùng.
Limitation Thực Tế
Fixture timeout không override test timeout làm ceiling
Nếu fixture timeout (ví dụ 120s) lớn hơn test timeout (30s), fixture vẫn sẽ bị kill bởi test timeout trước. Fixture timeout chỉ có ý nghĩa khi nhỏ hơn test timeout — hoặc khi hiểu rằng fixture timeout được tính ngoài test timeout (xem bên dưới).
Thực tế trong Playwright: fixture timeout và test timeout là hai đồng hồ riêng, chạy tuần tự. Fixture setup chạy trước khi test body — nếu fixture timeout hết, test chưa bắt đầu. Khi test body chạy, test timeout mới bắt đầu. Vì vậy, fixture timeout KHÔNG bị giới hạn bởi test timeout theo cách của actionTimeout.
Tuy nhiên, nếu không khai báo fixture timeout (dùng default), fixture setup tính chung vào test timeout. Khi đó, test timeout là ceiling thật sự.
// Với fixture timeout riêng → hai đồng hồ độc lập
dbSeed: [
async ({}, use) => { /* 90s */ await use(data); },
{ timeout: 120_000 }, // đồng hồ riêng 120s
],
// Test body → đồng hồ test timeout (30s) chạy sau
// Không có fixture timeout → tính chung vào test timeout
dbSeed: async ({}, use) => {
/* 90s — vượt test timeout 30s → fail */
await use(data);
},
Fixture timeout không áp dụng cho action bên trong fixture
Code bên trong fixture setup có thể gọi page.click(), page.goto(). Các call này vẫn dùng actionTimeout và navigationTimeout từ config — fixture timeout option không thay thế chúng:
authedPage: [
async ({ page }, use) => {
// goto dùng navigationTimeout từ config (không phải fixture timeout)
await page.goto('/login');
// click dùng actionTimeout từ config
await page.click('[type="submit"]');
// Fixture timeout là budget tổng cho toàn bộ setup
await use(page);
},
{ timeout: 30_000 },
],
Không thể override fixture timeout per-test
Fixture timeout khai báo một lần trong test.extend(), áp dụng cho mọi test dùng fixture đó. Không có cú pháp override fixture timeout tại call site (khác với test.setTimeout() có thể gọi trong test function). Nếu muốn timeout khác, phải re-implement fixture với test.extend() mới.
4 Pitfalls Thực Tế
1. Không khai báo fixture timeout cho fixture chậm → setup timeout, error message mơ hồ
// SAI — fixture nặng nhưng không có timeout riêng
export const test = base.extend<{ bigSeed: SeedData }>({
bigSeed: async ({}, use) => {
const data = await seedDatabase(); // 90s — vượt test timeout 30s
await use(data);
},
// Không có timeout riêng → dùng test timeout (30s) → fail ở giây 30
// Error: "Test timeout of 30000ms exceeded"
// Không rõ nguyên nhân là do fixture setup, không phải test logic
});
// ĐÚNG — khai báo timeout explicit cho fixture nặng
export const test = base.extend<{ bigSeed: SeedData }>({
bigSeed: [
async ({}, use) => {
const data = await seedDatabase(); // 90s — nằm trong budget 120s
await use(data);
},
{ timeout: 120_000 },
// Error nếu vượt: "Fixture 'bigSeed' timeout of 120000ms exceeded"
// Rõ ràng hơn nhiều khi debug
],
});
2. Fixture timeout lớn hơn test timeout (khi không khai báo fixture timeout riêng)
// Tình huống nguy hiểm: dùng testInfo.setTimeout trong fixture
// thay vì khai báo fixture timeout riêng
export const test = base.extend<{ slowData: Data }>({
slowData: async ({}, use, testInfo) => {
testInfo.setTimeout(120_000); // Tăng test timeout từ trong fixture
const data = await heavyInit();
await use(data);
// Test body chỉ còn bao nhiêu? Không rõ — phụ thuộc heavyInit() mất bao lâu
},
});
// Vấn đề: test body không có budget đảm bảo
// ĐÚNG — dùng fixture timeout riêng để đảm bảo test body có đủ budget
slowData: [
async ({}, use) => {
const data = await heavyInit(); // Budget riêng 120s
await use(data);
},
{ timeout: 120_000 },
],
// Test body vẫn có đầy đủ test timeout (30s) sau khi setup xong
3. Timeout quá rộng → CI build chậm nếu fixture bị stuck
// TRÁNH — timeout 10 phút vô lý
postgres: [
async ({}, use) => {
const container = await new PostgreSqlContainer().start();
await use(container);
await container.stop();
},
{ scope: 'worker', timeout: 600_000 }, // 10 phút — quá rộng
// Nếu container stuck (daemon crash, network issue), CI chờ 10 phút mới fail
];
// TỐT HƠN — đủ buffer nhưng không quá rộng
postgres: [
async ({}, use) => { /* ... */ },
{ scope: 'worker', timeout: 90_000 },
// 90s: cached image 30s + migration 30s + buffer 30s
// Nếu fail sau 90s → có issue thật sự, CI nhận kết quả sớm hơn
],
4. Nhầm fixture timeout với expect.timeout hoặc actionTimeout
// Fixture timeout option: tính cho setup+teardown của fixture function
dbSeed: [
async ({}, use) => { await use(data); },
{ timeout: 120_000 }, // ← đây là fixture timeout
],
// expect.timeout: tính cho từng assertion (await expect(...).toBeVisible())
// Khai báo trong playwright.config.ts:
// expect: { timeout: 10_000 }
// Không liên quan đến fixture timeout
// actionTimeout: tính cho mỗi action đơn lẻ bên trong test body
// Khai báo trong playwright.config.ts:
// use: { actionTimeout: 10_000 }
// Không liên quan đến fixture timeout — dù action nằm trong fixture setup
Ba loại timeout này độc lập hoàn toàn. Thay đổi fixture timeout không ảnh hưởng đến expect.timeout hay actionTimeout.
Tổng Kết
- Fixture
timeoutoption giới hạn riêng phase setup + teardown của fixture, khai báo dạng tuple[fn, { timeout: N }]. - Khi có timeout riêng, fixture chạy với đồng hồ độc lập trước test body — test body vẫn có đầy đủ test timeout sau khi setup xong.
- Khi không khai báo timeout riêng (function form), fixture tính chung vào test timeout (default 30s).
- Fixture
timeoutkháctest.setTimeout():test.setTimeout()set budget toàn bộ test (test body + fixtures), trong khi fixture timeout chỉ budget cho setup+teardown của fixture đó. - Fixture không kế thừa timeout từ dependency — mỗi fixture tự quản lý timeout riêng.
- Worker-scope fixture cũng dùng
timeoutoption theo cùng cú pháp — áp dụng cho lần setup và teardown duy nhất trong worker. - Fixture timeout không thay thế
actionTimeout— action bên trong fixture setup vẫn dùngactionTimeouttừ config. - Custom fixture timeout chỉ cần thiết cho tác vụ infrastructure-level nặng: DB seed lớn, container start, build asset, external service init. Fixture nhẹ không cần timeout riêng.
Quiz Củng Cố
Câu 1
Fixture dưới đây khai báo đúng cú pháp chưa? Nếu chưa, cần sửa gì?
export const test = base.extend<{ bigData: Data }>({
bigData: async ({}, use) => {
const data = await loadHeavyDataset();
await use(data);
},
timeout: 120_000,
});
Đáp án
Sai cú pháp. timeout không phải key trực tiếp trong object truyền vào test.extend() — nó là option của fixture cụ thể, phải khai báo cùng fixture dưới dạng tuple:
export const test = base.extend<{ bigData: Data }>({
bigData: [
async ({}, use) => {
const data = await loadHeavyDataset();
await use(data);
},
{ timeout: 120_000 }, // option object là phần tử thứ hai của tuple
],
});
Câu 2
Test dưới đây có vấn đề gì? Timeout nào thực sự áp dụng cho seedDatabase()?
// playwright.config.ts: timeout: 30_000 (default)
const test = base.extend<{ seed: SeedData }>({
seed: async ({}, use) => {
const data = await seedDatabase(); // mất 60s
await use(data);
},
});
test('check seed', async ({ seed }) => {
test.setTimeout(90_000);
expect(seed.users).toHaveLength(500);
});
Đáp án
Vấn đề: test.setTimeout(90_000) gọi bên trong test body — nhưng fixture seed đã chạy trước khi test body bắt đầu, nên test.setTimeout(90_000) không có tác dụng với fixture setup. Fixture seed không có timeout riêng → dùng test timeout mặc định là 30s. seedDatabase() mất 60s → fixture timeout sau 30s.
Fix: khai báo timeout riêng cho fixture, hoặc nếu muốn dùng test.setTimeout(), phải đặt nó ở describe level hoặc dùng testInfo.setTimeout() bên trong fixture:
// Fix 1: fixture timeout riêng
seed: [
async ({}, use) => {
const data = await seedDatabase();
await use(data);
},
{ timeout: 90_000 },
],
// Fix 2: testInfo.setTimeout trong fixture
seed: async ({}, use, testInfo) => {
testInfo.setTimeout(testInfo.timeout + 60_000);
const data = await seedDatabase();
await use(data);
},
Câu 3
Hai fixture A và B, B phụ thuộc A. Nếu A có timeout: 30_000 và B có timeout: 90_000, điều gì xảy ra nếu A setup mất 35s?
Đáp án
Fixture A setup fail sau 30s với lỗi "Fixture 'A' timeout of 30000ms exceeded". Fixture B không bao giờ chạy vì dependency A không thành công. Test fail với error từ A — không phải từ B, không phải từ test body. Fixture timeout không cộng gộp, không kế thừa — mỗi fixture có đồng hồ riêng.
Câu 4
Fixture postgresContainer dưới đây dùng worker scope với timeout 60s. Nếu worker chạy 5 test, fixture timeout 60s được áp dụng bao nhiêu lần?
postgresContainer: [
async ({}, use) => {
const container = await new PostgreSqlContainer().start();
await use(container);
await container.stop();
},
{ scope: 'worker', timeout: 60_000 },
],
Đáp án
Hai lần: một lần cho setup (trước test đầu tiên của worker) và một lần cho teardown (sau test cuối cùng của worker). 5 test trong worker đều nhận container đã init — không có thêm setup/teardown nào. Fixture timeout 60s không liên quan đến từng test riêng lẻ.
Câu 5
Đoạn code sau đặt actionTimeout: 5_000 trong config. Bên trong fixture setup có await page.click('#login'). Click này có bị giới hạn bởi fixture timeout (30_000) không? Và có bị giới hạn bởi actionTimeout (5_000) không?
// playwright.config.ts
export default defineConfig({
use: { actionTimeout: 5_000 },
});
// fixture
authedPage: [
async ({ page }, use) => {
await page.goto('/login');
await page.click('#login'); // timeout này là bao nhiêu?
await use(page);
},
{ timeout: 30_000 },
],
Đáp án
Click này chịu cả hai giới hạn — nhưng theo cách khác nhau:
actionTimeout: 5_000áp dụng trực tiếp chopage.click()— nếu click không hoàn thành trong 5s, action throwTimeoutError.- Fixture
timeout: 30_000là budget tổng cho toàn bộ setup. Nếupage.goto()+page.click()+ code khác tổng cộng vượt 30s, fixture timeout trigger.
Thực tế: click sẽ fail sau 5s (actionTimeout) nếu element không actionable, không phải sau 30s (fixture timeout). Fixture timeout là ceiling cho cả setup function — actionTimeout là ceiling cho từng action call.
Bài Tiếp Theo
Bài 25 tiếp tục nhóm Custom Fixtures với box: true — option ẩn các step bên trong fixture khỏi trace viewer và reporter, giúp trace gọn hơn khi fixture phức tạp có nhiều bước nội bộ.
