Danh sách bài viết

Bài 24: Fixture Với timeout Riêng

Mỗi custom fixture có thể có một giá trị timeout độc lập — áp dụng riêng cho phase setup (trước await use()) và teardown (sau await use()) của fixture đó, không liên quan đến timeout của test body hay của fixture khác. Default fixture timeout bằng test timeout (30s). Bài này phân tích cú pháp khai báo, các use case cần custom fixture timeout (DB seed nặng, testcontainer spin up, build asset), phân biệt với test.setTimeout() và testInfo.setTimeout(), worker-scope fixture timeout, quy tắc không kế thừa timeout qua dependency chain, limitation thực tế, và 4 pitfall hay gặp.

27/05/2026
13 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 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 timeout option cùng với scope, auto.
  • Phân biệt fixture timeout với test.setTimeout()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 actionTimeoutnavigationTimeout — 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.

2

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
3

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 },
],
4

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.

5

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.

6

Phân Biệt Với test.setTimeout()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.

7

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 },
  ],
});
8

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.

9

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.

10

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.

11

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ả slowFixtureheaviestFixture, 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.

12

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.

13

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 actionTimeoutnavigationTimeout 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.

14

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.

15

Tổng Kết

  • Fixture timeout option 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 timeout khác test.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 timeout option 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ùng actionTimeout từ 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.
16

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 cho page.click() — nếu click không hoàn thành trong 5s, action throw TimeoutError.
  • Fixture timeout: 30_000 là budget tổng cho toàn bộ setup. Nếu page.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.

17

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ộ.

Bài 25: Fixture box: true — Ẩn Step