Danh sách bài viết

Bài 27: Fixture Dependencies — Destructuring Inject

Fixture không hoạt động độc lập — một fixture có thể depend fixture khác (built-in hoặc custom) bằng cách khai báo chúng trong tham số đầu của fixture function thông qua destructuring. Playwright tự detect dependency graph, sắp xếp thứ tự khởi tạo bằng topological sort, và đảm bảo dependency sẵn sàng trước khi fixture phụ thuộc vào chúng được tạo. Bài này cover cú pháp inject, cơ chế resolve order, chain dependency pattern, scope rules, shared dependency, override fixture với dependency, circular dependency, testInfo/workerInfo inject, fixture composition theo layer, limitation và pitfall thực tế.

27/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau khi đọc xong bài này, bạn sẽ:

  • Biết cú pháp khai báo dependency giữa các fixture qua destructuring tham số đầu.
  • Hiểu Playwright resolve dependency order như thế nào (topological sort).
  • Viết được chain dependency: option fixture → client fixture → data fixture → UI fixture.
  • Biết scope rules: test fixture depend worker được, worker fixture không depend test.
  • Hiểu shared dependency — nhiều fixture cùng depend một fixture, instance tạo bao nhiêu lần.
  • Phân biệt cách inject testInfo / workerInfo so với inject fixture thông thường.
  • Tránh được 4 pitfall phổ biến: TypeScript error, scope violation, circular dependency, over-engineering.
2

Cách Fixture Nhận Dependency

Fixture function nhận hai tham số: tham số đầu là object chứa tất cả fixtures có sẵn (destructure để lấy cái cần), tham số thứ hai là use.

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

class AdminAPI {
  constructor(private request: any, private baseURL: string) {}
  async getUsers() { /* ... */ }
}

export const test = base.extend<{
  authedPage: Page;
  adminApi: AdminAPI;
}>({
  // authedPage depend: page (built-in), baseURL (option built-in)
  authedPage: async ({ page, baseURL }, use) => {
    await page.goto(`${baseURL}/login`);
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Password').fill('secret');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');
    await use(page);
  },

  // adminApi depend: request (built-in), baseURL (option built-in)
  adminApi: async ({ request, baseURL }, use) => {
    const api = new AdminAPI(request, baseURL ?? '');
    await use(api);
  },
});

Cả page, request, baseURL đều là built-in fixture. Playwright inject chúng vào tham số đầu khi fixture function được gọi. Fixture authedPage nhận pagebaseURL — Playwright khởi tạo chúng trước, rồi mới gọi authedPage.

Depend Custom Fixture Khác

Dependency không giới hạn ở built-in. Custom fixture cũng depend được custom fixture khác, miễn là cùng được khai báo trong cùng base.extend() hoặc trong chain extend:

export const test = base.extend<{
  testUser: { email: string; id: string };
  authedPage: Page;
}>({
  // testUser không depend fixture nào ngoài request
  testUser: async ({ request }, use) => {
    const res = await request.post('/api/users', {
      data: { email: '[email protected]', role: 'user' },
    });
    const user = await res.json();
    await use(user);
    // teardown: xoá user sau test
    await request.delete(`/api/users/${user.id}`);
  },

  // authedPage depend testUser (custom) + page (built-in)
  authedPage: async ({ page, testUser }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill(testUser.email);
    await page.getByLabel('Password').fill('password');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Playwright phân tích tên property trong destructuring ({ page, testUser }), từ đó biết fixture này phụ thuộc pagetestUser.

3

Resolve Order — Topological Sort

Playwright không khởi tạo fixture theo thứ tự khai báo trong object. Thay vào đó, nó xây dựng dependency graph của tất cả fixture được dùng trong test, rồi thực hiện topological sort để tìm thứ tự hợp lệ.

Quy tắc topological sort

  • Fixture không có dependency (leaf nodes) được khởi tạo trước.
  • Fixture có dependency được khởi tạo sau khi tất cả dependency của nó đã sẵn sàng.
  • Nếu hai fixture không phụ thuộc nhau, thứ tự tương đối giữa chúng không được đảm bảo.

Ví dụ với graph đơn giản:

page  ──→  authedPage
             │
testUser ───┘

Thứ tự khởi tạo: pagetestUser (song song hoặc bất kỳ thứ tự) → authedPage.

Thứ tự cleanup (teardown)

Cleanup chạy ngược lại: fixture được tạo sau sẽ cleanup trước. Với ví dụ trên:

  1. Cleanup authedPage (sau use(page))
  2. Cleanup testUser (xoá user)
  3. Cleanup page (đóng page)

Điều này đảm bảo fixture phụ thuộc luôn được teardown trước khi dependency của nó bị huỷ.

Chỉ khởi tạo fixture được dùng

Playwright chỉ khởi tạo fixture nào thực sự được dùng trong test — kể cả transitive dependency. Nếu test không destructure authedPage, toàn bộ chain không được tạo ra, kể cả testUser.

// test chỉ dùng page — authedPage và testUser không được khởi tạo
test('unauthenticated homepage', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
});

// test dùng authedPage — Playwright tạo cả testUser và page trước
test('dashboard hiển thị đúng', async ({ authedPage }) => {
  await expect(authedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
4

Chain Dependency Pattern

Dependency chain là pattern phổ biến nhất khi tổ chức fixture cho project lớn. Mỗi fixture chỉ biết dependency trực tiếp của nó — không cần biết fixture phía trên chain.

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

class APIClient {
  constructor(private baseURL: string) {}
  async createUser(data: object) { /* ... */ return { id: '1', email: '[email protected]' }; }
  async deleteUser(id: string) { /* ... */ }
}

type Fixtures = {
  apiBaseURL: string;
  apiClient: APIClient;
  testUser: { id: string; email: string };
  authedPage: Page;
};

export const test = base.extend<Fixtures>({
  // Layer 0: URL — option fixture, không depend gì
  apiBaseURL: ['https://api.dev.example.com', { option: true }],

  // Layer 1: Client — depend apiBaseURL
  apiClient: [
    async ({ apiBaseURL }, use) => {
      const client = new APIClient(apiBaseURL);
      await use(client);
    },
    { option: true },
  ],

  // Layer 2: Data — depend apiClient
  testUser: async ({ apiClient }, use) => {
    const user = await apiClient.createUser({ role: 'user' });
    await use(user);
    // cleanup: xoá user sau test
    await apiClient.deleteUser(user.id);
  },

  // Layer 3: UI — depend page (built-in) + testUser
  authedPage: async ({ page, testUser }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill(testUser.email);
    await page.getByLabel('Password').fill('test-password');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Thứ tự khởi tạo khi test dùng authedPage:

  1. apiBaseURL — không depend gì → khởi tạo trước
  2. apiClient — depend apiBaseURL → khởi tạo sau
  3. page (built-in) và testUsertestUser depend apiClient, song song với page
  4. authedPage — depend pagetestUser → khởi tạo cuối

Thứ tự teardown ngược lại: authedPagetestUser (xoá user) → apiClientpage.

Override apiBaseURL per project

apiBaseURL là option fixture, config project có thể override để trỏ tới môi trường khác nhau mà không cần sửa fixture:

// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'staging',
      use: { apiBaseURL: 'https://api.staging.example.com' },
    },
    {
      name: 'production-readonly',
      use: { apiBaseURL: 'https://api.example.com' },
    },
  ],
});
5

Scope Rules Khi Depend

Scope của fixture ảnh hưởng trực tiếp đến ai có thể depend ai:

  • Test fixture có thể depend test fixture khác hoặc worker fixture.
  • Worker fixture CHỈ có thể depend worker fixture khác — không được depend test fixture.

Lý do: worker fixture được khởi tạo một lần cho toàn bộ worker (nhiều test), còn test fixture khởi tạo lại cho mỗi test. Nếu worker fixture depend test fixture, không có test fixture nào tồn tại tại thời điểm worker khởi tạo.

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

export const test = base.extend<
  { pageWithToken: void },    // test-scope fixtures
  { dbPool: DbPool }          // worker-scope fixtures
>({
  // worker fixture — chỉ depend worker fixture khác (hoặc không depend gì)
  dbPool: [
    async ({}, use) => {
      const pool = await DbPool.connect(process.env.DATABASE_URL!);
      await use(pool);
      await pool.close();
    },
    { scope: 'worker' },
  ],

  // test fixture — depend dbPool (worker) → OK
  pageWithToken: async ({ page, dbPool }, use) => {
    const token = await dbPool.query('SELECT token FROM test_tokens LIMIT 1');
    await page.addInitScript((t) => {
      localStorage.setItem('auth_token', t);
    }, token.rows[0].token);
    await use();
  },
});

Test fixture depend worker fixture — reuse instance

Khi test fixture depend worker fixture, test fixture nhận được cùng instance của worker fixture đang được dùng bởi worker đó. Không có instance mới nào được tạo — đây chính là mục đích của worker scope: tái dùng resource nặng (DB pool, browser instance, WS server).

// worker-scope: dbPool tạo 1 lần per worker
// 10 test trong cùng worker đều nhận chung 1 dbPool instance
6

Shared Dependency

Khi nhiều fixture cùng depend một fixture, instance của dependency được tạo bao nhiêu lần?

Câu trả lời phụ thuộc scope của dependency:

  • Nếu dependency là test scope: tạo 1 lần per test, tất cả fixture trong test đó dùng chung instance đó.
  • Nếu dependency là worker scope: tạo 1 lần per worker, tất cả test trong worker dùng chung.
export const test = base.extend<{
  apiClient: APIClient;
  fixtureA: DataA;
  fixtureB: DataB;
}>({
  // test-scope
  apiClient: async ({ request, baseURL }, use) => {
    const client = new APIClient(request, baseURL ?? '');
    await use(client);
  },

  // fixtureA depend apiClient
  fixtureA: async ({ apiClient }, use) => {
    const data = await apiClient.fetchA();
    await use(data);
  },

  // fixtureB cũng depend apiClient
  fixtureB: async ({ apiClient }, use) => {
    const data = await apiClient.fetchB();
    await use(data);
  },
});

Trong một test dùng cả fixtureAfixtureB, apiClient chỉ được khởi tạo 1 lần — fixtureAfixtureB nhận cùng instance. Cleanup apiClient cũng chỉ chạy 1 lần sau khi cả hai fixture đã cleanup xong.

test('A và B dùng chung apiClient', async ({ fixtureA, fixtureB }) => {
  // apiClient: 1 instance duy nhất
  // fixtureA và fixtureB đều đã được khởi tạo xong
  console.log(fixtureA, fixtureB);
});

Đây là hành vi quan trọng khi thiết kế fixture: nếu fixture setup side effect trên shared dependency (ví dụ thêm header vào client), tất cả fixture khác dùng cùng dependency đó sẽ thấy thay đổi đó.

7

Override Fixture Với Dependency

Khi override built-in fixture, fixture mới vẫn có thể nhận built-in fixture gốc trong dependency — tức là nhận instance đã được tạo bởi Playwright trước khi override chạy.

// Override built-in page: inject thêm init script vào mỗi page
export const test = base.extend<{ page: Page }>({
  page: async ({ page, context }, use) => {
    // page ở đây là built-in page đã được tạo sẵn
    // context là BrowserContext của page đó
    await page.addInitScript(() => {
      // Inject mock feature flags vào window
      (window as any).__FLAGS__ = { newCheckout: true, betaDashboard: false };
    });
    // Truyền cùng page instance xuống test
    await use(page);
  },
});

Playwright cho phép override fixture nhận chính fixture đó làm dependency — không bị circular dependency vì Playwright hiểu đây là override chain: built-in page → override page.

Override với thêm behavior từ context

export const test = base.extend<{
  page: Page;
  authedPage: Page;
}>({
  // Override page: thêm init script cho tất cả test
  page: async ({ page }, use) => {
    await page.addInitScript(() => {
      (window as any).__TEST_ENV__ = true;
    });
    await use(page);
  },

  // authedPage depend page (đã được override ở trên)
  // → authedPage tự động nhận page có __TEST_ENV__ = true
  authedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Password').fill('password');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Dependency chain ở đây: built-in page → override page (thêm init script) → authedPage. Test dùng authedPage sẽ nhận page đã có cả init script lẫn login state.

8

Circular Dependency

Circular dependency xảy ra khi fixture A depend fixture B, và fixture B lại depend fixture A. Playwright detect pattern này và throw error tại runtime.

// LỖI: circular dependency
export const test = base.extend<{
  fixtureA: string;
  fixtureB: string;
}>({
  // fixtureA depend fixtureB
  fixtureA: async ({ fixtureB }, use) => {
    await use(`A:${fixtureB}`);
  },

  // fixtureB depend fixtureA → CIRCULAR
  fixtureB: async ({ fixtureA }, use) => {
    await use(`B:${fixtureA}`);
  },
});

Error message runtime:

Error: Fixtures "fixtureA" and "fixtureB" are circular.

Circular dependency thường là dấu hiệu của thiết kế fixture có vấn đề — hai fixture đang cố gắng làm công việc lẽ ra nên được tách vào fixture thứ ba:

// Giải pháp: tách phần chung vào fixture riêng
export const test = base.extend<{
  sharedData: SharedData;  // fixture trung gian
  fixtureA: string;
  fixtureB: string;
}>({
  sharedData: async ({}, use) => {
    await use({ value: 'shared' });
  },

  fixtureA: async ({ sharedData }, use) => {
    await use(`A:${sharedData.value}`);
  },

  fixtureB: async ({ sharedData }, use) => {
    await use(`B:${sharedData.value}`);
  },
});
9

testInfo Và workerInfo Inject

testInfoworkerInfo không phải fixture — chúng được inject vào fixture function qua tham số thứ ba (không phải destructuring từ tham số đầu như fixture thông thường).

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

export const test = base.extend<{
  tempFile: string;
  workerLog: void;
}>({
  // testInfo là tham số thứ 3 (sau fixtures và use)
  tempFile: async ({ }, use, testInfo) => {
    // testInfo.title: tên test đang chạy
    // testInfo.outputDir: thư mục output per-test
    const filePath = `${testInfo.outputDir}/temp-data.json`;
    await use(filePath);
    // Cleanup: không cần xoá — outputDir được Playwright quản lý
  },
});

Với worker-scope fixture, tham số thứ ba là workerInfo:

export const test = base.extend<
  {},
  { workerLog: void }
>({
  workerLog: [
    async ({}, use, workerInfo) => {
      // workerInfo.workerIndex: index của worker (0, 1, 2, ...)
      // workerInfo.parallelIndex: index trong parallel run
      console.log(`Worker ${workerInfo.workerIndex} started`);
      await use();
      console.log(`Worker ${workerInfo.workerIndex} done`);
    },
    { scope: 'worker' },
  ],
});

Điểm quan trọng: testInfo KHÔNG inject được vào worker-scope fixture — test chưa tồn tại khi worker khởi tạo. TypeScript sẽ báo lỗi nếu dùng sai.

Các property testInfo thường dùng trong fixture

  • testInfo.title — tên test, dùng để đặt tên file log, screenshot
  • testInfo.outputDir — thư mục riêng per-test cho artifacts
  • testInfo.retry — số lần retry hiện tại (0 = lần đầu)
  • testInfo.annotations — mảng annotation, có thể push thêm từ fixture
  • testInfo.attach(name, options) — đính file vào test report từ fixture
10

Pattern Fixture Composition

Với project lớn, fixture thường được tổ chức theo layer. Mỗi layer chỉ depend layer dưới — không depend lên hoặc skip layer.

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

class APIClient { /* ... */ }

type TestFixtures = {
  // Layer 1: Data
  testUser: { id: string; email: string };
  testOrder: { id: string; total: number };

  // Layer 2: UI
  authedPage: Page;

  // Layer 3: Page Objects
  dashboardPage: Page;
  ordersPage: Page;
};

export const test = base.extend<TestFixtures>({
  // ─── Layer 1: Data ───────────────────────────────
  testUser: async ({ request }, use) => {
    const res = await request.post('/api/test/users');
    const user = await res.json();
    await use(user);
    await request.delete(`/api/test/users/${user.id}`);
  },

  testOrder: async ({ request, testUser }, use) => {
    const res = await request.post('/api/test/orders', {
      data: { userId: testUser.id, items: [{ sku: 'DEMO', qty: 1 }] },
    });
    const order = await res.json();
    await use(order);
    await request.delete(`/api/test/orders/${order.id}`);
  },

  // ─── Layer 2: UI ─────────────────────────────────
  authedPage: async ({ page, testUser }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill(testUser.email);
    await page.getByLabel('Password').fill('test-password');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');
    await use(page);
  },

  // ─── Layer 3: Page Objects ────────────────────────
  dashboardPage: async ({ authedPage }, use) => {
    await authedPage.goto('/dashboard');
    await use(authedPage);
  },

  ordersPage: async ({ authedPage }, use) => {
    await authedPage.goto('/orders');
    await use(authedPage);
  },
});

Test sử dụng fixture theo nhu cầu — không cần biết dependency chain bên dưới:

test('dashboard hiện đúng tên user', async ({ dashboardPage, testUser }) => {
  await expect(dashboardPage.getByText(testUser.email)).toBeVisible();
});

test('orders page có order vừa tạo', async ({ ordersPage, testOrder }) => {
  await expect(ordersPage.getByText(testOrder.id)).toBeVisible();
});

// Test này dùng cả ordersPage lẫn testOrder
// → testUser, authedPage, ordersPage, testOrder đều được tạo
// → testUser chỉ tạo 1 lần dù cả testOrder và authedPage đều depend nó

Tách fixture ra nhiều file

Trong project thực tế, fixture theo layer thường được tách file rồi merge:

// fixtures/data.ts — layer 1
export const dataTest = base.extend<DataFixtures>({ testUser: ..., testOrder: ... });

// fixtures/ui.ts — layer 2, extend từ dataTest
import { dataTest } from './data';
export const uiTest = dataTest.extend<UIFixtures>({ authedPage: ... });

// fixtures/pages.ts — layer 3, extend từ uiTest
import { uiTest } from './ui';
export const test = uiTest.extend<PageFixtures>({ dashboardPage: ..., ordersPage: ... });

Mỗi layer extend từ layer dưới — dependency chain được giữ nguyên mà không cần mergeTests() (dùng mergeTests khi cần kết hợp fixture từ nhiều nhánh độc lập).

11

Limitation

  • Debug khó khi chain dài. Khi fixture ở layer 3 fail vì lý do từ layer 1, stack trace thường trỏ vào layer 3. Cần mở trace viewer hoặc thêm log vào từng fixture để xác định tầng nào fail.
  • Không có dynamic dependency. Dependency phải khai báo tĩnh trong destructuring — không thể decide fixture nào cần depend dựa trên runtime condition. Nếu fixture chỉ thỉnh thoảng cần dependency, vẫn phải khai báo dependency đó; Playwright luôn khởi tạo nó.
  • Fixture không thể depend fixture chưa được khai báo trong generic. Nếu TypeScript type không liệt kê fixture, không destructure được — compiler báo lỗi ngay.
  • Thứ tự giữa hai fixture không depend nhau là không xác định. Nếu logic cần A được tạo trước B trong cùng layer, phải thêm dependency tường minh thay vì dựa vào thứ tự khai báo trong object.
12

Pitfall Thường Gặp

Pitfall 1 — Depend fixture không khai báo trong generic

// LỖI: apiClient không được khai báo trong generic type
export const test = base.extend<{ authedPage: Page }>({
  authedPage: async ({ page, apiClient }, use) => {
  //                          ^^^^^^^^^
  // TypeScript error: Property 'apiClient' does not exist on type...
    await use(page);
  },
});

Sửa: thêm apiClient: APIClient vào generic type.

Pitfall 2 — Worker fixture depend test fixture

// LỖI: worker fixture depend test fixture
export const test = base.extend<
  { authedPage: Page },
  { sharedBrowser: Browser }
>({
  sharedBrowser: [
    async ({ authedPage }, use) => {
    //         ^^^^^^^^^^
    // Runtime error: worker-scoped fixture "sharedBrowser"
    // cannot use test-scoped fixture "authedPage"
      await use(authedPage.context().browser()!);
    },
    { scope: 'worker' },
  ],

  authedPage: async ({ page }, use) => { await use(page); },
});

Sửa: chuyển sharedBrowser sang test scope, hoặc chuyển authedPage sang worker scope (nếu logic cho phép).

Pitfall 3 — Circular dependency ngầm

// Trông bình thường nhưng circular:
// userSession depend authToken
// authToken depend userSession
export const test = base.extend<{
  authToken: string;
  userSession: Session;
}>({
  authToken: async ({ userSession }, use) => {
    await use(userSession.token);
  },
  userSession: async ({ authToken }, use) => {
    const session = await Session.fromToken(authToken);
    await use(session);
  },
});
// → Runtime error: Fixtures "authToken" and "userSession" are circular.

Trong trường hợp này, authTokenuserSession là cùng một thứ — chọn một, bỏ một, hoặc tách phần login ra fixture riêng (credentials) rồi cả hai cùng depend credentials.

Pitfall 4 — Over-engineering dependency chain

// Quá nhiều layer cho test đơn giản
// Fixture chain: config → httpClient → sessionManager →
//   tokenCache → authState → authedPage → adminPage → dashboardPage
// Để viết 1 test: expect(dashboardPage.getByText('Hi')).toBeVisible()

Nếu test chỉ cần check 1 element trên dashboard, chain 7 layer là over-engineering. Mỗi layer thêm overhead setup/teardown và điểm fail tiềm ẩn. Rule of thumb: không quá 3-4 layer. Nếu chain dài hơn, xem lại xem layer nào có thể merge hoặc đơn giản hoá.

13

Quiz

Câu 1. Fixture authedPage khai báo: async ({ page, testUser }, use) => { ... }. Playwright xác định dependency của fixture này bằng cách nào?

Đáp án

Playwright đọc tên property trong destructuring tham số đầu — pagetestUser — để xác định fixture nào cần được khởi tạo trước authedPage. Không phải qua phân tích runtime mà qua phân tích static khi build dependency graph.

Câu 2. Test A dùng fixtureAfixtureB, cả hai đều depend apiClient (test scope). Bao nhiêu instance apiClient được tạo cho test A?

Đáp án

1 instance. Trong một test, mỗi test-scope fixture chỉ được tạo một lần, dù có bao nhiêu fixture khác depend vào nó. fixtureAfixtureB dùng chung cùng một instance apiClient.

Câu 3. Worker fixture dbPool có thể depend test fixture authedPage không? Tại sao?

Đáp án

Không. Worker fixture được khởi tạo một lần per worker trước khi bất kỳ test nào chạy — tại thời điểm đó chưa có test nào, nên không có test fixture nào tồn tại để inject. Playwright throw runtime error nếu cấu hình như vậy.

Câu 4. Trong fixture function, testInfo được inject bằng cách nào — destructuring từ tham số đầu hay tham số riêng?

Đáp án

Tham số riêng — tham số thứ ba của fixture function: async ({ page }, use, testInfo) => { ... }. Không destructure từ tham số đầu như fixture thông thường. workerInfo cũng tương tự nhưng dùng cho worker-scope fixture.

Câu 5. Fixture A depend B, B depend A. Playwright phát hiện vấn đề này lúc nào — compile time hay runtime?

Đáp án

Runtime — Playwright build dependency graph khi bắt đầu chạy test và detect circular dependency tại thời điểm đó. TypeScript không phát hiện được circular fixture dependency ở compile time vì dependency được xác định qua destructuring tên property.

14

Bài Tiếp Theo

Bài 28: Fixture Cleanup Sau use() — code sau await use() chạy như thế nào, thứ tự cleanup giữa các fixture phụ thuộc nhau, và các pattern teardown an toàn.