Danh sách bài viết

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

title là option trong fixture tuple cho phép đặt tên hiển thị human-friendly trong reporter và Trace Viewer, thay vì dùng tên field TypeScript mặc định. Bài này cover cú pháp, sự khác biệt giữa field name và display title, hành vi trong HTML report và Trace Viewer, 3 use case (readable name, cross-team, group fixtures), pattern kết hợp với box, so sánh với test.step, limitation, 4 pitfall và quiz 5 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 title option làm gì và tại sao nó chỉ ảnh hưởng đến phần display của reporter.
  • Phân biệt được field name (TypeScript identifier) và display title (free string) — hai khái niệm hoàn toàn tách biệt.
  • Biết cụ thể title hiển thị ở đâu trong HTML report và Trace Viewer, và ở đâu không hiển thị.
  • Biết khi nào nên đặt title — readable name, cross-team readability, group fixtures cùng category.
  • Tránh được 4 pitfall: trùng title, quên title cho fixture critical, combine với box gây confusion, và sensitive info trong title.
2

title Là Gì

Khi Playwright chạy test và tạo report, mỗi fixture được nhận dạng theo tên field trong object truyền vào base.extend(). Mặc định, reporter hiển thị chính xác tên đó — ví dụ Fixture "db", Fixture "_authedAdminPage", Fixture "apiClient".

Field name là TypeScript identifier — bị ràng buộc bởi quy tắc đặt tên biến: không có khoảng trắng, không có ký tự đặc biệt, không có dấu tiếng Việt. Tên ngắn, dùng camelCase hoặc underscore prefix là phổ biến.

title là option trong fixture tuple cho phép chỉ định một chuỗi hiển thị riêng — không bị ràng buộc bởi quy tắc identifier. Khi có title, reporter dùng giá trị đó thay vì tên field.

Kết quả: field name trong code vẫn là _authedAdminPage, nhưng trong HTML report và Trace Viewer xuất hiện là Fixture "Authenticated Admin Page" hoặc bất cứ chuỗi nào bạn đặt.

Quan trọng: title chỉ là display — không thay đổi cách fixture hoạt động, không thay đổi cách fixture được inject vào test, không thay đổi thứ tự thực thi.

3

Cú Pháp

Tương tự boxauto, title dùng dạng tuple: phần tử thứ nhất là async function, phần tử thứ hai là options object.

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

type DBClient = {
  query: (sql: string) => Promise;
  disconnect: () => Promise;
};

export const test = base.extend<{ db: DBClient }>({
  db: [
    async ({}, use) => {
      const client = await connectDB();
      await use(client);
      await client.disconnect();
    },
    { title: 'Database Connection' },
  ],
});

Không có title: reporter hiển thị Fixture "db".

title: 'Database Connection': reporter hiển thị Fixture "Database Connection".

Field name db vẫn được dùng khi inject vào test:

// product.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('truy vấn danh sách sản phẩm', async ({ db }) => {
  // db vẫn inject qua tên field "db" — title không ảnh hưởng ở đây
  const products = await db.query('SELECT * FROM products LIMIT 10');
  expect(products.length).toBe(10);
});

title nhận bất kỳ chuỗi hợp lệ JavaScript nào — có khoảng trắng, dấu gạch ngang, ký tự đặc biệt, thậm chí emoji (dù emoji trong title có thể gây vấn đề display ở một số CI reporter).

4

Field Name Vs Display Title

Hai khái niệm này độc lập hoàn toàn:

Khía cạnh Field name Display title
Dùng để làm gì TypeScript identifier — inject vào test qua destructuring Chuỗi hiển thị trong reporter và Trace Viewer
Ràng buộc Quy tắc đặt tên biến JS/TS: không space, không ký tự đặc biệt Free string — bất kỳ ký tự hợp lệ nào
Thay đổi được không Phải refactor toàn bộ test đang dùng fixture nếu đổi Đổi tự do trong options — không ảnh hưởng test code
Scope hiển thị Trong source code TypeScript Trong HTML report, Trace Viewer — không thấy trong source code
Ví dụ _authedAdminPage 'Authenticated Admin Page'

Tách biệt này có giá trị thực tế: code có thể dùng tên ngắn (db, api, auth) để gõ nhanh trong test, trong khi report hiển thị tên đầy đủ ('Database Connection', 'REST API Client', 'Admin Auth Session') cho người đọc report — thường là QA lead, BA, hay người không biết tên biến internal.

5

Hành Vi Trong Reporter

HTML Report

Trong HTML report, mỗi test có section "Before Hooks" liệt kê fixture setup. Khi không có title:

Before Hooks
  ├── Fixture "db"
  └── Fixture "_authedAdminPage"

Sau khi thêm title:

Before Hooks
  ├── Fixture "Database Connection"
  └── Fixture "Authenticated Admin Page"

Tên trong "Before Hooks" đổi thành giá trị title. Khi expand từng entry, action bên trong fixture vẫn hiển thị bình thường (trừ khi kết hợp box: true).

Trace Viewer

Action log ở panel trái Trace Viewer có entry riêng cho từng fixture. Khi có title, entry đó hiển thị tên title thay vì tên field. Khi expand entry fixture trong Trace Viewer, action bên trong fixture vẫn thấy đầy đủ.

List Reporter (CLI Terminal)

Output terminal khi chạy npx playwright test không hiển thị fixture name (với hoặc không có title). Terminal chỉ hiện test name và pass/fail — fixture không xuất hiện ở đây. title không ảnh hưởng output terminal.

After Hooks

Phần teardown (code sau await use(...)) cũng hiển thị dưới tên title trong "After Hooks" của report. Nếu fixture có cả setup và teardown, cả hai đều được label bằng title đã đặt.

6

Use Case Thực Tế

1. Readable Name Cho Fixture Code Khó Đọc

Fixture field name đôi khi theo convention riêng của team — underscore prefix để phân biệt fixture nội bộ với fixture public, hoặc abbreviation ngắn để gõ nhanh. Trong report, những tên này trở nên khó đọc.

export const test = base.extend<{
  _authedAdminPage: Page;
  _apiCtx: APIRequestContext;
  _dbConn: DBConnection;
}>({
  _authedAdminPage: [
    async ({ browser }, use) => {
      const ctx = await browser.newContext({ storageState: 'admin-auth.json' });
      const page = await ctx.newPage();
      await use(page);
      await ctx.close();
    },
    { title: 'Authenticated Admin Page' },
  ],

  _apiCtx: [
    async ({ playwright }, use) => {
      const ctx = await playwright.request.newContext({
        baseURL: process.env.API_BASE_URL,
      });
      await use(ctx);
      await ctx.dispose();
    },
    { title: 'REST API Context' },
  ],

  _dbConn: [
    async ({}, use) => {
      const conn = await DBConnection.connect(process.env.DB_URL!);
      await use(conn);
      await conn.close();
    },
    { title: 'Database Connection' },
  ],
});

Trong code TypeScript, vẫn dùng { _authedAdminPage, _apiCtx, _dbConn } để destructure (ngắn, tiện). Trong report, QA thấy "Authenticated Admin Page", "REST API Context", "Database Connection" — rõ ràng không cần giải thích thêm.

2. Cross-Team Readability

Trong dự án có nhiều team — dev viết fixture, QA viết test, BA đọc report — tên biến internal không mang nhiều ý nghĩa với người không quen codebase. title giúp report tự giải thích mà không cần doc thêm.

// Fixture tên kỹ thuật, dùng trong code TypeScript
seedInvoicesForCurrentMonth: [
  async ({ request }, use) => {
    // Tạo 12 invoice tháng hiện tại
    for (let i = 1; i <= 12; i++) {
      await request.post('/api/invoices', {
        data: {
          amount: i * 100,
          month: new Date().getMonth() + 1,
          year: new Date().getFullYear(),
        },
      });
    }
    await use();
    await request.delete('/api/invoices/test-cleanup');
  },
  // QA và BA đọc report thấy ngay đây là gì
  { title: 'Seed 12 Invoices — Current Month' },
],

3. Nhóm Fixtures Cùng Category

Khi nhiều fixture phục vụ cùng một domain, đặt title theo pattern 'Category: Detail' giúp report nhóm chúng về mặt visual (dù Playwright không tự sort theo title).

export const test = base.extend<{
  apiClient: APIClient;
  apiToken: string;
  apiTestUser: UserPayload;
}>({
  apiClient: [
    async ({ playwright }, use) => {
      const client = new APIClient(playwright.request, process.env.API_BASE_URL!);
      await use(client);
    },
    { title: 'API Client' },
  ],

  apiToken: [
    async ({ request }, use) => {
      const res = await request.post('/api/auth/token', {
        data: { clientId: 'test', secret: process.env.API_SECRET },
      });
      const { token } = await res.json();
      await use(token);
    },
    { title: 'API Token' },
  ],

  apiTestUser: [
    async ({ request }, use) => {
      const res = await request.post('/api/users', {
        data: { name: 'Test User', role: 'viewer' },
      });
      const user = await res.json();
      await use(user);
      await request.delete(`/api/users/${user.id}`);
    },
    { title: 'API Test User' },
  ],
});

Report hiển thị ba entry có prefix API — người đọc nhận ra ngay đây là nhóm fixture liên quan đến API setup.

7

Pattern Combine Với box

titlebox là hai option độc lập và có thể dùng cùng nhau. Kết hợp: report hiển thị tên title trong "Before Hooks", nhưng khi expand, không có action nào bên trong (vì box: true).

export const test = base.extend<{ _internalAuth: void }>({
  _internalAuth: [
    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();
    },
    {
      title: 'Login as Admin',
      box: true,  // action ẩn — nhưng title vẫn hiển thị
    },
  ],
});

Report với fixture trên:

Before Hooks
  └── Fixture "Login as Admin"   ← title hiển thị, không có action bên trong khi expand

Thay vì Fixture "_internalAuth" (field name khó hiểu) với danh sách action bị ẩn, người đọc thấy Fixture "Login as Admin" — rõ ràng mục đích dù không thấy chi tiết.

Lưu ý khi combine: người đọc report thấy tên fixture nhưng không thấy action. Nếu team không biết box được dùng, họ có thể không hiểu tại sao expand fixture rỗng. Nên document convention này ở README hoặc comment trong file fixtures.

8

Group Fixtures Cùng Category

Khi project có nhiều fixture phức tạp, đặt title theo pattern nhất quán giúp report dễ scan hơn. Một số pattern hay dùng:

Pattern "Category: Detail"

// DB category
dbConnection: [async (...) => {...}, { title: 'DB: Connection' }],
dbSeedUsers:  [async (...) => {...}, { title: 'DB: Seed Users' }],
dbSeedOrders: [async (...) => {...}, { title: 'DB: Seed Orders' }],

// Auth category
authAdmin:    [async (...) => {...}, { title: 'Auth: Admin Session' }],
authViewer:   [async (...) => {...}, { title: 'Auth: Viewer Session' }],

// API category
apiClient:    [async (...) => {...}, { title: 'API: Client' }],
apiMockUser:  [async (...) => {...}, { title: 'API: Mock User' }],

Trong report, khi test dùng nhiều fixture, các entry trong Before Hooks sẽ thể hiện rõ chúng thuộc nhóm nào.

Pattern Title Phản Ánh Trạng Thái

Thay vì mô tả loại fixture, title mô tả trạng thái mà fixture đưa test vào:

loggedInAsAdmin:    [async (...) => {...}, { title: 'State: Logged in as Admin' }],
emptyShoppingCart:  [async (...) => {...}, { title: 'State: Empty shopping cart' }],
cartWithTwoItems:   [async (...) => {...}, { title: 'State: Cart with 2 items' }],
checkoutInProgress: [async (...) => {...}, { title: 'State: Checkout in progress' }],

Pattern này hữu ích khi viết test cho các luồng phụ thuộc vào trạng thái ban đầu — QA đọc report biết ngay test bắt đầu từ trạng thái nào.

9

So Sánh Với test.step

Dễ nhầm title fixture với tên được đặt trong test.step() vì cả hai đều là chuỗi tên hiển thị. Nhưng chúng hoạt động ở hai cấp khác nhau:

Tiêu chí test.step('name', fn) Fixture { title: 'name' }
Vị trí khai báo Bên trong body của test() hoặc helper function Options của fixture trong test.extend()
Phase chạy Trong test execution phase Trong fixture setup/teardown phase (Before/After Hooks)
Xuất hiện ở đâu trong report Trong "Test body" của Action log Trong "Before Hooks" / "After Hooks"
Scope hiển thị Mỗi lần test.step được gọi tạo 1 entry Mỗi fixture có 1 entry duy nhất per test
Khi nào dùng Nhóm các action trong test body thành bước logic Đặt tên dễ đọc cho fixture trong report

Ví dụ minh họa cả hai trong cùng một test:

// fixtures.ts — fixture có title
export const test = base.extend<{ db: DBClient }>({
  db: [
    async ({}, use) => {
      const client = await connectDB();
      await use(client);
      await client.disconnect();
    },
    { title: 'Database Connection' },  // ← hiển thị ở Before Hooks
  ],
});

// checkout.spec.ts
test('tạo đơn hàng thành công', async ({ page, db }) => {
  // test.step hiển thị ở Test body
  await test.step('Điều hướng đến trang checkout', async () => {
    await page.goto('/checkout');
  });

  await test.step('Điền thông tin giao hàng', async () => {
    await page.getByLabel('Địa chỉ').fill('123 Main St');
    await page.getByLabel('Số điện thoại').fill('0900000000');
  });

  await test.step('Xác nhận đơn', async () => {
    await page.getByRole('button', { name: 'Đặt hàng' }).click();
    await expect(page.getByText('Đặt hàng thành công')).toBeVisible();
  });
});

Report sẽ có:

Before Hooks
  └── Fixture "Database Connection"   ← từ title fixture

Test body
  ├── Điều hướng đến trang checkout   ← từ test.step
  ├── Điền thông tin giao hàng
  └── Xác nhận đơn
10

Limitation

Title chỉ display, không thay đổi behavior

Đã nhắc ở trên nhưng đáng nhấn mạnh: title không ảnh hưởng thứ tự chạy, scope, timeout, hay bất kỳ behavior nào của fixture. Mọi thứ chỉ là cosmetic. Nếu cần thay đổi behavior, dùng các option khác (scope, timeout, auto, box).

Title quá dài

Reporter wrap title khi nó quá dài. Không có giới hạn cứng từ Playwright, nhưng thực tế title trên 60 ký tự thường bị wrap trong HTML report — gây khó đọc ở một số layout. Giữ title ngắn gọn, súc tích.

Không có i18n

title là giá trị string cố định tại compile time. Không có cơ chế đổi ngôn ngữ title theo locale. Nếu team dùng nhiều ngôn ngữ khi đọc report, phải chọn một ngôn ngữ duy nhất cho title — thường là tiếng Anh để nhất quán với tool và doc.

Không ảnh hưởng list reporter (CLI)

Như đã đề cập ở mục 5, terminal output khi chạy test không hiển thị fixture name — có title hay không đều không tác động đến output CLI. title chỉ có ý nghĩa trong HTML report và Trace Viewer.

11

Pitfall Thường Gặp

1. Hai fixture dùng cùng title

// NGUY HIỂM — hai fixture có cùng title
export const test = base.extend<{
  adminLogin: void;
  superAdminLogin: void;
}>({
  adminLogin: [
    async ({ page }, use) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill('[email protected]');
      await page.getByRole('button', { name: 'Login' }).click();
      await use();
    },
    { title: 'Admin Login' },  // ← title này
  ],

  superAdminLogin: [
    async ({ page }, use) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill('[email protected]');
      await page.getByRole('button', { name: 'Login' }).click();
      await use();
    },
    { title: 'Admin Login' },  // ← trùng title!
  ],
});

Playwright không throw error khi hai fixture trùng title. Nhưng trong report, khi test dùng cả hai fixture, xuất hiện hai entry "Admin Login" trong Before Hooks — không phân biệt được cái nào là cái nào. Đặt title khác biệt: 'Admin Login''Super Admin Login'.

2. Quên đặt title cho fixture critical

Khi project có 10 fixture và chỉ 7 cái được đặt title, 3 cái còn lại hiển thị tên field trong report. Nếu tên field là _xyzHelper hay tmpSetup, người đọc report (đặc biệt QA không quen codebase) không biết fixture đó làm gì.

// Fixture quan trọng nhưng tên field không tự giải thích
_xyzSetup: [
  async ({ request }, use) => {
    // Setup phức tạp để chuẩn bị state cho luồng XYZ
    await request.post('/api/xyz/init');
    await use();
    await request.delete('/api/xyz/cleanup');
  },
  // Thiếu title → report hiển thị Fixture "_xyzSetup"
],

Nếu fixture này fail trên CI, người đọc thấy "_xyzSetup fixture failed" — không rõ ngay đây là setup gì. Thêm { title: 'XYZ Flow Initialization' } để rõ ràng.

3. Combine với box gây confusion cho team

Khi fixture có cả titlebox: true, report hiển thị tên title rõ ràng nhưng không có action nào bên trong khi expand. Developer mới trong team có thể không biết fixture box là gì và nghĩ report bị lỗi.

// fixtures.ts — combine title + box
_internalAuth: [
  async ({ page }, use) => {
    // Nhiều action login — bị ẩn bởi box
    await page.goto('/login');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Password').fill('pass');
    await page.getByRole('button', { name: 'Login' }).click();
    await use();
  },
  {
    title: 'User Login',  // Hiển thị rõ
    box: true,            // Nhưng action ẩn
  },
],

Thêm comment rõ ràng trong file fixtures giải thích convention dùng box. Hoặc document trong README của fixtures folder.

4. Đặt sensitive info trong title

// SAI — title chứa thông tin nhạy cảm
apiToken: [
  async ({}, use) => {
    const token = process.env.API_TOKEN;
    await use(token!);
  },
  // Title xuất hiện trong HTML report — có thể bị lưu, share, upload CI artifact
  { title: `API Token: ${process.env.API_TOKEN}` },  // ← TUYỆT ĐỐI KHÔNG LÀM
],

HTML report thường được lưu làm CI artifact và có thể được share cho nhiều người. Title xuất hiện plaintext trong report HTML. Đừng bao giờ đặt token, password, secret, hay bất kỳ credential nào vào title. Chỉ mô tả loại credential, không phải giá trị:

// ĐÚNG — title mô tả chức năng, không chứa giá trị
apiToken: [
  async ({}, use) => {
    const token = process.env.API_TOKEN;
    await use(token!);
  },
  { title: 'API Token (from env)' },
],
12

Quiz

Câu 1

Fixture bên dưới có field name dbtitle: 'Database Connection'. Trong test, dev destructure fixture bằng tên nào?

export const test = base.extend<{ db: DBClient }>({
  db: [
    async ({}, use) => {
      const client = await connectDB();
      await use(client);
      await client.disconnect();
    },
    { title: 'Database Connection' },
  ],
});
Đáp án

Dev vẫn dùng field name db để destructure: async ({ db }) => { ... }. title chỉ thay đổi tên hiển thị trong reporter và Trace Viewer — không thay đổi cách fixture được inject vào test. Tên field TypeScript (db) và display title ('Database Connection') là hai thứ hoàn toàn độc lập.

Câu 2

Sau khi thêm title: 'Admin Auth Session' vào fixture, output terminal khi chạy npx playwright test thay đổi như thế nào?

Đáp án

Không thay đổi gì. List reporter (CLI terminal) không hiển thị fixture name — có hay không có title đều không ảnh hưởng output terminal. title chỉ hiển thị trong HTML report (section "Before Hooks") và Trace Viewer (action log). Terminal chỉ hiện test name và kết quả pass/fail.

Câu 3

Đoạn code sau có vấn đề gì về bảo mật?

jwtToken: [
  async ({}, use) => {
    const token = await fetchServiceToken(
      process.env.SERVICE_ID!,
      process.env.SERVICE_SECRET!,
    );
    await use(token);
  },
  { title: `JWT: ${process.env.SERVICE_SECRET}` },
],
Đáp án

Title chứa giá trị của process.env.SERVICE_SECRET — secret này sẽ xuất hiện plaintext trong HTML report. HTML report thường được lưu làm CI artifact, có thể download bởi nhiều người trong team hoặc bị lộ nếu artifact không có access control đúng. Title nên chỉ mô tả chức năng: { title: 'JWT Token (Service Account)' } — không nhúng giá trị credential vào title.

Câu 4

Sự khác biệt về vị trí trong report giữa title fixture và tên của test.step() là gì?

Đáp án

Title fixture xuất hiện trong section "Before Hooks" (và "After Hooks" nếu có teardown) của report — đây là phần setup/teardown trước và sau test body. Tên test.step() xuất hiện trong "Test body" — đây là phần execution chính của test. Hai cấp khác nhau: fixture chạy trong setup phase, test.step chạy trong execution phase.

Câu 5

Fixture dưới đây có title và box cùng lúc. Khi QA mở HTML report và click vào entry "User Login" trong Before Hooks, họ thấy gì bên trong?

_auth: [
  async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByRole('button', { name: 'Login' }).click();
    await page.waitForURL('/home');
    await use();
  },
  { title: 'User Login', box: true },
],
Đáp án

Entry "User Login" hiển thị trong Before Hooks nhờ title. Nhưng khi expand entry đó, không có action nào bên trong — vì box: true ẩn toàn bộ action (goto, fill, click, waitForURL) khỏi report. Entry fixture tồn tại nhưng rỗng khi expand. Code vẫn chạy đầy đủ — chỉ phần hiển thị bị ẩn.