Danh sách bài viết

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

box: true là option fixture option đánh dấu toàn bộ action bên trong fixture function sẽ không hiển thị trong reporter và Trace Viewer. Code vẫn chạy đầy đủ — chỉ có phần hiển thị UI bị ẩn. Bài này cover cú pháp, behavior trong HTML report và Trace Viewer, các use case thực tế (login internals, DB setup, POM helper), pattern combine với auto, so sánh với test.step box, debug consideration, limitation, 5 pitfall và quiz 4 câu.

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ẽ:

  • Hiểu box: true làm gì trong fixture options và tại sao code vẫn chạy khi dùng nó.
  • Viết được fixture có box: true cho các use case login, DB setup, POM helper.
  • Biết behavior cụ thể trong HTML report và Trace Viewer khi fixture được box.
  • Phân biệt được box ở fixture với boxtest.step.
  • Biết khi nào nên và không nên dùng box — đặc biệt trong bối cảnh debug.
2

box: true Là Gì

Khi viết custom fixture bằng test.extend(), fixture nhận hai dạng định nghĩa:

  • Dạng ngắn: Chỉ là async function — fixture chạy, action hiển thị trong reporter.
  • Dạng tuple: [async function, options] — options là object chứa các flag điều chỉnh hành vi fixture.

box: true là một trong các flag đó. Khi được bật, Playwright đánh dấu fixture là "boxed" — toàn bộ action bên trong fixture function (click, fill, goto, waitForURL, v.v.) sẽ bị ẩn khỏi Action log trong reporter và Trace Viewer.

Điều quan trọng: code vẫn chạy 100%. box: true chỉ ảnh hưởng đến phần hiển thị UI. Fixture không bị skip, không bị tắt, không bị thay đổi thứ tự thực thi.

Mục đích: giảm noise trong report khi fixture chứa nhiều action nội bộ mà người đọc report không cần thấy. Thay vì 15 action setup tràn vào Action log, report chỉ hiển thị các action thực sự thuộc test logic.

3

Cú Pháp

Fixture với box: true dùng dạng tuple: phần tử thứ nhất là async function, phần tử thứ hai là options object có box: true.

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

export const test = base.extend<{ adminLogin: void }>({
  adminLogin: [
    async ({ page }, use) => {
      // Tất cả action bên trong đây sẽ bị ẩn trong reporter
      await page.goto('/login');
      await page.getByLabel('Email').fill('[email protected]');
      await page.getByLabel('Password').fill('secret');
      await page.getByRole('button', { name: 'Login' }).click();
      await page.waitForURL('/admin');
      await use();
      // teardown nếu cần — cũng bị ẩn khi box: true
    },
    { box: true },
  ],
});
// admin.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('admin có thể xem dashboard', async ({ page, adminLogin }) => {
  // adminLogin đã chạy xong (login hoàn tất) nhưng không hiện action trong report
  await expect(page.getByRole('heading', { name: 'Admin Dashboard' })).toBeVisible();
});

Khi test chạy, adminLogin fixture thực thi đầy đủ 4 action goto/fill/fill/click. Nhưng Action log trong Trace Viewer và HTML report chỉ thấy action từ body test: expect heading Admin Dashboard.

So sánh với dạng không có box — cú pháp ngắn:

// Không có box — toàn bộ action fixture hiển thị trong reporter
export const test = base.extend<{ adminLogin: void }>({
  adminLogin: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Password').fill('secret');
    await page.getByRole('button', { name: 'Login' }).click();
    await page.waitForURL('/admin');
    await use();
  },
});
4

Behavior Trong Reporter

HTML Report

Trong HTML report (npx playwright show-report), mỗi test hiển thị section "Actions" (hoặc Action log). Với fixture không có box:

Before Hooks
  ├── adminLogin fixture
  │   ├── goto /login
  │   ├── fill [label=Email]
  │   ├── fill [label=Password]
  │   ├── click button[name=Login]
  │   └── waitForURL /admin

Test body
  └── expect heading Admin Dashboard

Với box: true trên fixture adminLogin:

Before Hooks
  └── adminLogin fixture   ← chỉ thấy tên fixture, không thấy action bên trong

Test body
  └── expect heading Admin Dashboard

Tên fixture vẫn xuất hiện trong "Before Hooks" — nhưng khi expand, không có action nào bên trong. Report sạch hơn.

Trace Viewer

Action log ở panel trái của Trace Viewer không liệt kê các action bên trong box fixture. Nếu test pass, người review trace chỉ thấy action test logic. Nếu test fail trong body test (không phải trong fixture), trace vẫn dẫn đến action fail cụ thể.

Nếu fixture fail (vd: goto /login timeout), Trace Viewer vẫn báo test fail và cho biết fixture nào fail — nhưng không thấy action cụ thể nào bên trong fixture đã gây ra lỗi.

List Reporter (CLI)

Output terminal không bị ảnh hưởng theo cách đáng kể — test pass/fail vẫn hiển thị bình thường. box: true không thay đổi trạng thái kết quả test.

Khi Fixture Fail

Nếu action bên trong box fixture throw (ví dụ element không tìm thấy), test fail như bình thường. Reporter báo fixture adminLogin fail nhưng không show action nào bên trong đã fail. Đây là trade-off chính — xem mục Limitation.

5

Use Case Thực Tế

1. Hide Login Internals

Fixture login thường là fixture phổ biến nhất trong một project — được dùng bởi hàng chục test. Bên trong có goto, fill email, fill password, click submit, waitForURL. Dev viết test không cần thấy 5 action này mỗi lần mở report.

export const test = base.extend<{
  userLogin: void;
  adminLogin: void;
}>({
  userLogin: [
    async ({ page }, use) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill('[email protected]');
      await page.getByLabel('Password').fill('userpass');
      await page.getByRole('button', { name: 'Sign in' }).click();
      await page.waitForURL('/dashboard');
      await use();
    },
    { box: true },
  ],

  adminLogin: [
    async ({ page }, use) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill('[email protected]');
      await page.getByLabel('Password').fill('adminpass');
      await page.getByRole('button', { name: 'Sign in' }).click();
      await page.waitForURL('/admin');
      await use();
    },
    { box: true },
  ],
});

Report chỉ hiện "userLogin fixture" hoặc "adminLogin fixture" trong Before Hooks — không có 5 action lặp lại.

2. Hide DB Setup Actions

Fixture seed database thường gọi API nhiều lần để tạo dữ liệu test — hàng chục request. Dev viết test quan tâm đến data đã có, không phải cách tạo data.

export const test = base.extend<{ seedProducts: void }>({
  seedProducts: [
    async ({ request }, use) => {
      // Tạo 10 sản phẩm test — không cần thấy 10 POST request trong report
      for (let i = 0; i < 10; i++) {
        await request.post('/api/products', {
          data: { name: `Product ${i}`, price: i * 10 },
        });
      }
      await use();
      // Cleanup — cũng bị ẩn
      await request.delete('/api/products/test-data');
    },
    { box: true },
  ],
});

3. POM Action Wrappers

Khi fixture trả về một helper object (kiểu Page Object Model), fixture setup thường khởi tạo các class và navigate đến trang ban đầu. Các action khởi tạo này không cần thấy trong report.

export const test = base.extend<{ checkoutPage: CheckoutPage }>({
  checkoutPage: [
    async ({ page }, use) => {
      // Setup: navigate và đợi page load — ẩn khỏi report
      await page.goto('/checkout');
      await page.waitForLoadState('networkidle');
      const checkout = new CheckoutPage(page);
      await use(checkout);
      // teardown
    },
    { box: true },
  ],
});
6

Pattern POM Helper Với box

Một pattern khác: fixture trả về một hàm thay vì một object. Hàm này có thể được gọi nhiều lần trong test với tham số khác nhau. Đây phù hợp cho trường hợp cần login với role khác nhau trong cùng một test.

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

type LoginFn = (email: string, password: string) => Promise<void>;

export const test = base.extend<{ loginAs: LoginFn }>({
  loginAs: [
    async ({ page }, use) => {
      const login: LoginFn = async (email, password) => {
        await page.goto('/login');
        await page.getByLabel('Email').fill(email);
        await page.getByLabel('Password').fill(password);
        await page.getByRole('button', { name: 'Login' }).click();
        await page.waitForURL('/dashboard');
      };
      await use(login);
    },
    { box: true },
  ],
});
// user-profile.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('user chỉ thấy profile của mình', async ({ page, loginAs }) => {
  await loginAs('[email protected]', 'pass1');
  await page.goto('/profile');
  await expect(page.getByText('[email protected]')).toBeVisible();
});

test('admin thấy profile của mọi user', async ({ page, loginAs }) => {
  await loginAs('[email protected]', 'adminpass');
  await page.goto('/admin/users');
  await expect(page.getByText('[email protected]')).toBeVisible();
});

Cả hai test trên: fixture setup action (goto /login, fill, click) bị ẩn. Trong report, chỉ thấy action từ test body: goto /profile, expect text [email protected].

Lưu ý: box: true ẩn action trong fixture function — không ẩn action trong hàm login khi được gọi từ test body. Nếu test body gọi loginAs(), action từ lần gọi đó vẫn bị ẩn vì nó chạy bên trong closure của fixture function đã được box.

7

Combine box Với auto

boxauto là hai option độc lập — có thể dùng cùng nhau. auto: true khiến fixture chạy cho mọi test trong scope mà không cần khai báo trong destructuring. box: true ẩn action của fixture đó.

export const test = base.extend<{ testLogger: void }>({
  testLogger: [
    async ({}, use, testInfo) => {
      // Log test bắt đầu — không cần thấy trong report
      console.log(`[START] ${testInfo.title}`);
      const start = Date.now();

      await use();

      // Log sau khi test kết thúc
      const duration = Date.now() - start;
      console.log(`[END] ${testInfo.title} — ${duration}ms`);
    },
    { auto: true, box: true },  // Tự chạy + ẩn action
  ],
});

Fixture testLogger chạy tự động cho mọi test, nhưng không để lại dấu vết trong Action log của report. Test vẫn thấy log trong console khi chạy locally, nhưng HTML report và Trace Viewer không có entry nào từ fixture này.

Pattern này hữu ích cho: logging, metrics collection, side-effect setup không liên quan đến flow test.

8

So Sánh Với test.step box

Playwright có hai chỗ dùng box: true — cùng từ khóa, nhưng khác hoàn toàn về scope và hành vi:

Tiêu chí test.step(..., { box: true }) Fixture { box: true }
Vị trí dùng Bên trong body của test() Options của fixture trong test.extend()
Điều gì bị ẩn Tên step wrapper, action được flatten lên cùng cấp test Toàn bộ action bên trong fixture function
Tên trong report Step tên không hiển thị, action vẫn thấy Fixture name vẫn thấy trong Before Hooks, action bên trong ẩn
Phạm vi ẩn Chỉ ẩn wrapper name — action vẫn flatten ra ngoài Ẩn hoàn toàn action — không thấy trong log
Khi nào dùng Khi tên step không có ý nghĩa với người đọc report Khi toàn bộ nội dung fixture là implementation detail

Ví dụ để thấy sự khác biệt: với test.step box, các action goto/fill/click từ bên trong step vẫn xuất hiện trong Action log (chỉ ẩn tên step). Với fixture box: true, không có action nào từ fixture xuất hiện trong log.

Nếu muốn ẩn toàn bộ action setup, dùng fixture box: true. Nếu chỉ muốn ẩn tên một wrapper step (nhưng vẫn thấy action con), dùng test.step(..., { box: true }).

9

Debug Consideration

box: true phù hợp khi fixture stable và hiếm khi fail. Nhưng trong giai đoạn phát triển fixture mới, hoặc khi CI báo fixture fail và cần tìm action nào gây ra lỗi, box: true cản trở debug.

Một cách xử lý là điều khiển box qua biến môi trường:

export const test = base.extend<{ adminLogin: void }>({
  adminLogin: [
    async ({ page }, use) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill('[email protected]');
      await page.getByLabel('Password').fill('secret');
      await page.getByRole('button', { name: 'Login' }).click();
      await page.waitForURL('/admin');
      await use();
    },
    // Tắt box khi set DEBUG_FIXTURES=1 — thấy action để debug
    { box: !process.env.DEBUG_FIXTURES },
  ],
});
# Chạy bình thường — fixture bị box, report sạch
npx playwright test

# Chạy với debug — fixture không box, thấy tất cả action
DEBUG_FIXTURES=1 npx playwright test

Cách này cho phép toggle nhanh mà không cần sửa code. Phù hợp khi cần điều tra lỗi fixture trong CI.

Ngoài ra, có thể tạm thời tắt box khi viết fixture mới — chạy vài lần để xác nhận action sequence đúng, sau đó bật lại.

10

Limitation

Một số giới hạn thực tế khi dùng box: true:

Khó debug khi fixture fail

Khi fixture fail trong CI, report báo tên fixture fail nhưng không có action nào để tra. Dev phải tắt box và chạy lại để thấy action sequence — tốn thời gian CI thêm một lần.

Box không ảnh hưởng đến console.log

box: true chỉ ẩn action trong Action log của reporter. Các console.log bên trong fixture vẫn in ra terminal khi chạy --debug hoặc khi stdout được capture. Nếu cần tắt cả log, phải xử lý riêng trong code.

Box không hoạt động với fixture scope worker

Fixture có scope: 'worker' chạy một lần cho toàn bộ worker, không chạy trước mỗi test. box: true kết hợp với scope: 'worker' không được hỗ trợ — Playwright sẽ throw error tại runtime. Chỉ dùng box với scope mặc định ('test').

Best practice

Chỉ box fixture khi fixture đó đã stable — đã chạy ổn định trong một thời gian, ít thay đổi, và khi fail thì nguyên nhân thường là môi trường test chứ không phải fixture logic. Fixture mới hoặc phức tạp nên để không box cho đến khi ổn định.

11

Pitfall Thường Gặp

1. Box fixture fail — không biết action nào gây ra lỗi

// Fixture box — trông ổn trong normal run
adminLogin: [
  async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('[email protected]');    // ← Label thay đổi thành "Email Address"
    await page.getByLabel('Password').fill('secret');
    await page.getByRole('button', { name: 'Login' }).click();
    await use();
  },
  { box: true },
],

CI báo adminLogin fixture failed nhưng report không có action nào để nhìn. Dev mất thêm thời gian tắt box và chạy lại để phát hiện label đã thay đổi. Lesson: fixture với UI selector nên được monitor kỹ trước khi box.

2. Box mọi fixture — mất visibility CI

// Box toàn bộ fixture → CI fail → không trace được gì
export const test = base.extend<{
  login: void;
  seedData: void;
  setupPermissions: void;
  mockThirdParty: void;
}>({
  login: [async ({ page }, use) => { /* ... */ }, { box: true }],
  seedData: [async ({ request }, use) => { /* ... */ }, { box: true }],
  setupPermissions: [async ({ request }, use) => { /* ... */ }, { box: true }],
  mockThirdParty: [async ({ page }, use) => { /* ... */ }, { box: true }],
});

Khi test fail mà không rõ do test logic hay do fixture setup, report không cung cấp đủ thông tin. Box nên áp dụng cho fixture thực sự stable và đơn giản — không nên áp dụng hàng loạt.

3. Nhầm box với test.step.skip

test.step.skip khiến step không chạy (bỏ qua hoàn toàn). box: true trong fixture khiến fixture vẫn chạy đầy đủ nhưng ẩn action. Nhầm lẫn hai khái niệm dẫn đến expect fixture đã chạy nhưng thực ra không có action nào.

// NHẦM — dev nghĩ box nghĩa là skip
adminLogin: [
  async ({ page }, use) => {
    await page.goto('/login');
    // ...
    await use();
  },
  { box: true },  // Fixture vẫn CHẠY — chỉ ẩn khỏi report
],

// test vẫn cần login mới pass — box không skip fixture
test('admin dashboard', async ({ page, adminLogin }) => {
  await expect(page.getByRole('heading', { name: 'Admin' })).toBeVisible();
});

4. Box fixture với scope: 'worker'

// SAI — box không hợp lệ với worker scope
dbConnection: [
  async ({}, use) => {
    const db = await createConnection();
    await use(db);
    await db.close();
  },
  { scope: 'worker', box: true },  // ← Runtime error
],

// ĐÚNG — bỏ box, hoặc chuyển về scope test
dbConnection: [
  async ({}, use) => {
    const db = await createConnection();
    await use(db);
    await db.close();
  },
  { scope: 'worker' },  // Worker scope không hỗ trợ box
],

5. Expect fixture fail rõ ràng trong test critical

Test critical (như test luồng thanh toán, luồng tạo tài khoản) khi fail cần trace đầy đủ để báo cáo incident. Nếu fixture setup dùng box: true, trace không đầy đủ — khó xác định setup hay business logic là nguồn gốc lỗi. Với test critical, cân nhắc bỏ box hoặc giữ box chỉ cho fixture phụ trợ không ảnh hưởng luồng chính.

12

Quiz

Câu 1

Fixture sau đây có box: true. Khi test chạy, điều gì xảy ra với page.goto('/login') bên trong?

export const test = base.extend<{ loginFixture: void }>({
  loginFixture: [
    async ({ page }, use) => {
      await page.goto('/login');
      await page.fill('#email', '[email protected]');
      await use();
    },
    { box: true },
  ],
});
Đáp án

page.goto('/login') vẫn thực thi bình thường — browser thật sự navigate đến /login. box: true không skip hay tắt action, chỉ ẩn action đó khỏi Action log trong reporter và Trace Viewer. Test vẫn ở trạng thái đã login sau khi fixture chạy xong.

Câu 2

Sự khác biệt giữa box: true trong fixture và box: true trong test.step là gì về những gì hiển thị trong Action log?

Đáp án

Với test.step('name', fn, { box: true }): tên step bị ẩn, nhưng các action bên trong fn được flatten lên cùng cấp test — vẫn thấy trong Action log, chỉ không có wrapper step tên. Với fixture { box: true }: toàn bộ action bên trong fixture function không xuất hiện trong Action log — fixture chỉ hiện tên trong Before Hooks, không có action nào bên trong khi expand.

Câu 3

Fixture seedDatabox: true. Trong CI, test fail với thông báo "seedData fixture failed". Dev mở HTML report nhưng không thấy action nào để trace. Cách xử lý nhanh nhất để tìm nguyên nhân là gì?

Đáp án

Tạm thời tắt box trên fixture seedData — chuyển { box: true } thành {} hoặc dùng env variable: { box: !process.env.DEBUG_FIXTURES } rồi chạy lại với DEBUG_FIXTURES=1 npx playwright test. Lần chạy này reporter sẽ hiện đầy đủ action sequence bên trong fixture, giúp xác định action nào throw. Sau khi fix xong, bật lại box: true.

Câu 4

Đoạn code dưới đây có vấn đề gì không? Nếu có, sửa thế nào?

export const test = base.extend<{ sharedDb: DatabaseConnection }>({
  sharedDb: [
    async ({}, use) => {
      const db = await DatabaseConnection.create();
      await use(db);
      await db.close();
    },
    { scope: 'worker', box: true },
  ],
});
Đáp án

Vấn đề: scope: 'worker'box: true không thể dùng cùng nhau — Playwright không hỗ trợ box fixture với worker scope và sẽ throw runtime error. Có hai cách sửa: (1) Bỏ box: true để giữ worker scope: { scope: 'worker' }. (2) Chuyển về test scope nếu muốn dùng box: { box: true } (không có scope: 'worker') — nhưng fixture sẽ chạy lại mỗi test thay vì một lần per worker.

13

Bài Tiếp Theo

Bài tiếp theo trong nhóm Custom Fixtures: title option — đặt tên hiển thị cho fixture trong reporter thay vì dùng tên biến fixture.

Bài 26: Fixture title — Đặt Tên Hiển Thị Trong Reporter