Danh sách bài viết

Bài 43: test('name', { tag, annotation }, fn) — Signature Mới [v1.42]

Từ v1.42, Playwright thêm tham số options vào giữa name và fn cho cả test() lẫn các modifier của nó. Options object gồm tag và annotation — cho phép khai báo metadata ngay tại chỗ định nghĩa test thay vì gán runtime qua testInfo. Bài này tập trung vào cú pháp options object, cách apply lên test.describe và các modifier, phân biệt declarative vs runtime annotation, hiển thị trong reporter, và 4 pitfall phổ biến.

28/05/2026
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 cú pháp test(name, options, fn) và type TestDetails của options object trong v1.42+.
  • Dùng được tag (string hoặc array) và annotation (object hoặc array) ngay tại khai báo test.
  • Apply options object lên test.describe, test.skip, test.fail, test.fixme.
  • Phân biệt rõ declarative annotation (options object) và runtime annotation (testInfo.annotations.push()) — biết khi nào dùng cái nào.
  • Dùng CLI filter --grep--grep-invert với tag.
  • Tránh 4 pitfall điển hình khi dùng signature mới.
2

Bối Cảnh — Signature Cũ Và Vấn Đề

Trước v1.42, test() chỉ nhận 2 tham số: namefn. Muốn gắn tag hoặc annotation cho một test, cần gọi testInfo.annotations.push() hoặc dựa vào convention tên test:

// Cách cũ — annotation chỉ có thể gắn tại runtime
test('checkout flow', async ({ page }, testInfo) => {
  testInfo.annotations.push({ type: 'feature', description: 'Critical revenue path' });
  testInfo.annotations.push({ type: 'tag', description: '@smoke' });

  await page.goto('/cart');
  // ...
});

Cách này có 3 hạn chế:

  • Annotation nằm trong body — phải chạy test mới biết test đó được annotate gì. Không đọc được lúc collect.
  • Tag không có field riêng — phải giả lập qua annotation type: 'tag', không dùng được với --grep CLI chính thức.
  • Muốn lọc trước khi chạy (ví dụ chỉ collect smoke tests) không thực hiện được qua CLI tag filter.

Từ v1.42, Playwright giới thiệu tham số thứ ba options (type TestDetails) vào giữa namefn, giải quyết cả 3 vấn đề trên.

3

Signature Mới: Options Object Ở Giữa

Signature đầy đủ với options object:

test(name: string, options: TestDetails, fn: TestFunction): void

Trong đó TestDetails được định nghĩa:

interface TestDetails {
  tag?: string | string[];
  annotation?: { type: string; description?: string }
             | { type: string; description?: string }[];
}

Ví dụ cơ bản — single tag, single annotation:

import { test, expect } from '@playwright/test';

test('checkout flow', {
  tag: '@smoke',
  annotation: { type: 'feature', description: 'Critical revenue path' },
}, async ({ page }) => {
  await page.goto('/cart');
  await page.getByRole('button', { name: 'Checkout' }).click();
  await expect(page).toHaveURL('/checkout');
});

Options object là tham số tùy chọn — không truyền vào vẫn hợp lệ (backward compatible với signature 2-tham-số). Nếu truyền vào, Playwright enforce type TestDetails qua TypeScript: key không nằm trong interface sẽ báo compile error.

Thứ tự tham số

Options object phải nằm giữa name và fn — không đổi chỗ được. Playwright dùng vị trí tham số, không dùng named arguments:

// ĐÚNG: name → options → fn
test('name', { tag: '@smoke' }, async ({ page }) => { ... });

// SAI: fn không thể đứng trước options
test('name', async ({ page }) => { ... }, { tag: '@smoke' }); // TypeError
4

Tag — Cú Pháp Và Convention

tag nhận giá trị là string hoặc array of string. Giá trị được lưu nguyên bản — Playwright không tự động thêm hay bỏ prefix.

Convention @prefix

Convention là bắt đầu bằng @ (ví dụ '@smoke', '@critical'). Convention này không bị enforce bởi runtime — tag không có @ vẫn được lưu. Tuy nhiên CLI filter --grep và UI Mode dùng pattern matching, và convention @ giúp tránh match nhầm với text trong tên test.

Ký tự hợp lệ

Playwright không giới hạn ký tự trong tag string — về mặt kỹ thuật có thể dùng bất kỳ string nào. Thực tế nên giới hạn ở alphanumeric, dash, underscore để tránh vấn đề với shell escaping khi truyền qua CLI:

// Khuyến nghị
tag: '@smoke'
tag: '@critical-path'
tag: '@payment_flow'

// Tránh — có thể gây vấn đề với shell
tag: '@test (smoke)'
tag: '@tag with spaces'

Single tag

test('add to cart', {
  tag: '@smoke',
}, async ({ page }) => {
  await page.goto('/products');
  await page.getByRole('button', { name: 'Add to cart' }).first().click();
  await expect(page.getByTestId('cart-count')).toHaveText('1');
});

Bài 44 sẽ đi sâu hơn vào tag single vs array và các pattern filter phức tạp. Bài này chỉ cover đủ để hiểu options object.

5

Annotation — Cú Pháp Và Các Trường

Annotation là một object với 2 trường:

  • type (string, bắt buộc): nhãn phân loại. Playwright không validate giá trị — team tự quy ước. Ví dụ: 'bug', 'feature', 'issue', 'severity', 'owner'.
  • description (string, tùy chọn): nội dung chi tiết. Ví dụ: ticket ID, mô tả, tên người phụ trách.
// Single annotation — object trực tiếp
test('user profile update', {
  annotation: { type: 'feature', description: 'User settings — phase 2' },
}, async ({ page }) => {
  await page.goto('/profile/edit');
  // ...
});

Annotation không có description

description là optional — có thể bỏ khi chỉ cần đánh dấu loại:

test('slow rendering benchmark', {
  annotation: { type: 'slow' },
}, async ({ page }) => {
  // ...
});

Type là free string — cần team convention

type không được validate, team dễ dùng không nhất quán: người dùng 'Bug', người dùng 'bug', người dùng 'BUG'. Hệ quả là query annotation trong custom reporter hoặc export ra JIRA/Slack bị phức tạp hơn. Nên định nghĩa một tập type cố định trong team convention (ví dụ: chỉ lowercase, danh sách cho phép).

6

Multi-tag Và Multi-annotation

Cả tagannotation đều chấp nhận array:

test('checkout flow', {
  tag: ['@smoke', '@critical', '@payment'],
  annotation: [
    { type: 'issue', description: 'JIRA-456' },
    { type: 'severity', description: 'P1' },
    { type: 'owner', description: 'team-payment' },
  ],
}, async ({ page }) => {
  await page.goto('/cart');
  await page.getByRole('button', { name: 'Checkout' }).click();
  await expect(page).toHaveURL('/checkout');
  // ...
});

Behavior với multiple tags

Mỗi tag trong array hoạt động độc lập với CLI filter:

  • --grep "@smoke" — test này match (có tag @smoke).
  • --grep "@critical" — test này cũng match.
  • --grep "@payment" — match.
  • --grep "@regression" — không match (test không có tag này).

HTML report hiển thị tất cả tags như danh sách chip dùng để filter trong report UI. Mỗi annotation entry trong array được render thành một dòng riêng trong section "Annotations".

Kết hợp single và array tùy ý

// Một tag, nhiều annotation
test('login flow', {
  tag: '@smoke',
  annotation: [
    { type: 'feature', description: 'Auth system' },
    { type: 'priority', description: 'high' },
  ],
}, async ({ page }) => { ... });

// Nhiều tag, không annotation
test('navigation bar', {
  tag: ['@smoke', '@ui'],
}, async ({ page }) => { ... });
7

Apply Lên test.describe

test.describe cũng chấp nhận options object với cú pháp tương tự. Tag và annotation đặt ở describe level sẽ được kế thừa bởi tất cả test bên trong:

import { test, expect } from '@playwright/test';

test.describe('Smoke suite', {
  tag: '@smoke',
}, () => {
  test('homepage loads', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveTitle(/My App/);
    // Test này tự nhận tag @smoke từ describe
  });

  test('login page accessible', async ({ page }) => {
    await page.goto('/login');
    await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
    // Test này cũng có tag @smoke
  });
});

Override ở test level

Test con có thể thêm tag riêng — không override tag của describe, hai tập tag được merge:

test.describe('Checkout suite', {
  tag: '@smoke',
  annotation: { type: 'feature', description: 'Checkout system' },
}, () => {
  test('add to cart', async ({ page }) => {
    // Nhận @smoke và annotation 'feature' từ describe
    await page.goto('/products');
    // ...
  });

  test('payment step', {
    tag: '@critical',
    annotation: { type: 'severity', description: 'P0' },
  }, async ({ page }) => {
    // Nhận @smoke (từ describe) + @critical (từ test riêng)
    // Nhận cả 2 annotation: 'feature' + 'severity'
    await page.goto('/checkout/payment');
    // ...
  });
});

Nested describe

Trong trường hợp describe lồng nhau, tags từ tất cả các lớp được merge — không có priority override:

test.describe('Outer', { tag: '@regression' }, () => {
  test.describe('Inner', { tag: '@smoke' }, () => {
    test('case', async ({ page }) => {
      // test này có cả @regression và @smoke
    });
  });
});
8

Apply Lên Các Modifier: test.skip, test.fail, test.fixme

Từ v1.42, các modifier test.skip, test.fail, test.fixme đều nhận cùng options object. Đây là cách gắn metadata ngay tại điểm khai báo modifier — không cần body của test chạy mới biết.

test.skip với options

// Skip với tag và annotation
test.skip('payment with 3DS', {
  tag: '@known-bug',
  annotation: { type: 'issue', description: 'JIRA-789' },
}, async ({ page }) => {
  await page.goto('/checkout/payment');
  // body này không chạy, nhưng tag và annotation vẫn được ghi
});

test.fail với options

// Kỳ vọng fail — test phải fail thì mới pass
test.fail('search returns wrong order', {
  tag: '@flaky',
  annotation: [
    { type: 'issue', description: 'JIRA-101' },
    { type: 'severity', description: 'P2' },
  ],
}, async ({ page }) => {
  await page.goto('/search?q=playwright');
  // Nếu test này pass thì Playwright báo unexpected pass
  // Nếu fail thì được tính là expected — kết quả pass trong report
});

test.fixme với options

// fixme = skip + annotation rõ ràng là "cần fix"
test.fixme('image upload flow', {
  tag: '@wip',
  annotation: { type: 'owner', description: 'team-media' },
}, async ({ page }) => {
  await page.goto('/upload');
  // ...
});

Về behavior runtime: test.skip dừng test ngay, không chạy body. test.fail chạy body nhưng đảo ngược kết quả pass/fail. test.fixme skip tương tự test.skip nhưng với intent khác nhau về mặt semantic — giúp team phân biệt "skip tạm" (skip) và "cần sửa sau" (fixme).

Lưu ý: bài 40 đã cover chi tiết test.skip() chaining. Bài này không lặp lại phần đó mà tập trung vào options object như một shared pattern cho tất cả modifier.

9

Declarative vs Runtime (testInfo.annotations.push)

Có hai cách gắn annotation cho một test — declarative (options object) và runtime (testInfo.annotations.push()). Cả hai đều hợp lệ, cùng tồn tại trong một test, nhưng phục vụ mục đích khác nhau.

Tiêu chí Declarative (options object) Runtime (testInfo.annotations.push)
Thời điểm gắn Collect phase — trước khi test chạy Trong quá trình test chạy
Phụ thuộc runtime state Không — giá trị phải biết trước Có — ví dụ env variable, kết quả API call
Dùng với CLI filter (--grep) Có (tag field) Không (chỉ là data trong report)
Readable từ code review Tốt hơn — metadata thấy ngay tại khai báo Phải đọc body để biết
Phù hợp với Tag cố định, annotation biết trước (ticket ID, owner, severity) Annotation dynamic (build ID, env state, kết quả query)

Kết hợp cả hai trong cùng test

test('export CSV flow', {
  tag: '@smoke',
  annotation: { type: 'feature', description: 'Data export' },
}, async ({ page }, testInfo) => {
  // Declarative annotation đã có ở trên (static metadata)

  // Runtime annotation — thêm thông tin từ môi trường đang chạy
  testInfo.annotations.push({
    type: 'env',
    description: process.env.TEST_ENV ?? 'local',
  });

  const buildId = process.env.CI_BUILD_ID;
  if (buildId) {
    testInfo.annotations.push({
      type: 'build',
      description: buildId,
    });
  }

  await page.goto('/export');
  await page.getByRole('button', { name: 'Export CSV' }).click();
  // ...
});

Trong HTML report, tất cả annotation — cả declarative lẫn runtime — đều xuất hiện trong cùng section "Annotations", theo thứ tự: declarative trước, runtime push sau.

Quy tắc chọn giữa hai cách

  • Metadata biết tại thời điểm code (ticket ID, owner, severity, feature name) → dùng options object declarative.
  • Metadata phụ thuộc runtime (env, build ID, feature flag state, kết quả lookup) → dùng testInfo.annotations.push().
  • Không dùng runtime push để thay thế hoàn toàn declarative — mất khả năng filter trước khi chạy.
10

Tag Filter Qua CLI

Tag trong options object là field chính thức, dùng được với --grep--grep-invert CLI flag — không phải pattern matching trên tên test.

--grep: chỉ chạy test có tag khớp

# Chạy tất cả test có tag @smoke
npx playwright test --grep "@smoke"

# Chạy test có tag @critical (trong dự án cụ thể)
npx playwright test --grep "@critical" --project=chromium

--grep-invert: bỏ qua test có tag khớp

# Bỏ qua tất cả test có tag @slow
npx playwright test --grep-invert "@slow"

# Kết hợp: chạy @smoke nhưng bỏ @flaky
npx playwright test --grep "@smoke" --grep-invert "@flaky"

--grep với regex

--grep nhận regex string — có thể dùng để match nhiều tag cùng lúc:

# Chạy test có @smoke HOẶC @critical
npx playwright test --grep "@smoke|@critical"

# Chạy test có @smoke VÀ @critical (cả 2)
# Không có cú pháp AND trong --grep — phải filter 2 bước hoặc dùng custom reporter

Tag trong playwright.config.ts

Có thể đặt grep filter cố định trong config cho từng project:

// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'smoke',
      grep: /@smoke/,
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'full',
      grepInvert: /@slow/,
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

Với config này, project smoke chỉ chạy tests có tag @smoke, project full chạy tất cả ngoại trừ @slow.

--grep match tên test hay tag?

Trước v1.42, --grep match trên chuỗi tiêu đề test (bao gồm cả describe chain). Từ v1.42, khi test có tag, --grep kiểm tra cả tag field. Nghĩa là nếu test name không chứa @smoke nhưng tag có @smoke, --grep "@smoke" vẫn match — hành vi này mới và quan trọng cần nắm.

11

Hiển Thị Trong Reporter

List reporter (terminal)

Tag hiển thị trong tên test ở terminal — Playwright append tag vào sau tiêu đề:

  ✓  1 [chromium] › checkout.spec.ts:5:1 › checkout flow @smoke @critical (1.2s)
  ✓  2 [chromium] › checkout.spec.ts:18:1 › add to cart @smoke (0.5s)

Annotation không hiển thị trong list reporter terminal — chỉ có tag.

HTML report

  • Mỗi tag hiển thị như một "chip" có thể click để filter — chỉ hiện tests có tag đó.
  • Section "Annotations" bên dưới tên test liệt kê từng annotation entry: type làm label (bold), description làm content.
  • Nếu annotation có URL trong description, HTML report không tự động render thành link — vẫn là plain text.

JUnit XML

<testcase name="checkout flow" classname="checkout.spec.ts" time="1.2">
  <properties>
    <property name="feature" value="Critical revenue path"/>
    <property name="severity" value="P1"/>
    <property name="playwright:tag" value="@smoke"/>
    <property name="playwright:tag" value="@critical"/>
  </properties>
</testcase>

Annotation được map thành <property> với name=typevalue=description. Tag được map thành <property name="playwright:tag">.

Custom reporter — đọc tag và annotation

Custom reporter có thể đọc tag và annotation qua testResult trong onTestEnd:

// custom-reporter.ts
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';

class MyReporter implements Reporter {
  onTestEnd(test: TestCase, result: TestResult) {
    const tags = test.tags; // string[]
    const annotations = result.annotations; // { type, description }[]

    if (tags.includes('@smoke')) {
      // gửi notification cho smoke test failures
    }
  }
}

export default MyReporter;
12

Backward Compatibility

Signature cũ test(name, fn) tiếp tục hoạt động bình thường — không cần migrate toàn bộ codebase cùng một lúc. Playwright detect tham số thứ hai là function hay object để phân biệt 2 signature.

// Cả hai cách đều hợp lệ trong cùng file
test('old style', async ({ page }) => {
  await page.goto('/');
});

test('new style with tag', {
  tag: '@smoke',
}, async ({ page }) => {
  await page.goto('/');
});

Khi nâng Playwright lên v1.42 từ version cũ hơn, toàn bộ test đang dùng signature cũ vẫn chạy được — không bị breaking change. Signature mới là additive, không thay thế.

Version guard

Nếu project có khả năng chạy trên nhiều môi trường với Playwright version khác nhau, cần biết: options object trong v1.41 hoặc cũ hơn sẽ throw error vì signature đó chưa tồn tại. Không có runtime check built-in — cần pin version hoặc check process.env.npm_package_version thủ công nếu thực sự cần guard.

13

Limitation

  • Tag typo không được detect lúc runtime: '@smok' thay vì '@smoke' vẫn compile và chạy được — nhưng --grep "@smoke" sẽ không match. TypeScript không có enum hay literal type enforcement cho tag value.
  • Annotation không validate schema: type là free string. Nếu một người dùng 'Bug' và người khác dùng 'bug', report và custom reporter sẽ thấy hai loại riêng biệt. Playwright không có annotation type registry.
  • Options object là static: Giá trị tag và annotation trong options phải là literal — không thể dùng kết quả từ function call hay async operation tại thời điểm khai báo test. Đây là giới hạn design của declarative approach.
  • v1.42+ only: Nếu dùng Playwright v1.41 hoặc cũ hơn, truyền object làm tham số thứ hai sẽ throw error vì không match overload nào. Cần update package hoặc dùng signature cũ.
  • --grep AND không native: Không có cú pháp CLI để filter "test có cả @smoke AND @critical". Chỉ có OR (regex @smoke|@critical) hoặc NOT (--grep-invert). AND filter phải thực hiện qua custom script hoặc parse test list.
14

Pitfall Thường Gặp

1. Quên @ prefix trong tag — filter CLI không match

// BAD: tag không có @ prefix
test('checkout flow', {
  tag: 'smoke', // thiếu @
}, async ({ page }) => { ... });

// CLI filter không match
// npx playwright test --grep "@smoke"  → 0 tests found

// GOOD
test('checkout flow', {
  tag: '@smoke',
}, async ({ page }) => { ... });

2. Tag và annotation dài gây reporter render rối

// BAD: tag quá dài, annotation description là đoạn văn
test('auth flow', {
  tag: '@this-is-a-very-long-tag-name-that-will-overflow-the-chip',
  annotation: {
    type: 'description',
    description: 'This test verifies the entire authentication flow including...(200 words)',
  },
}, async ({ page }) => { ... });

// GOOD: tag ngắn gọn, annotation description concise
test('auth flow', {
  tag: '@auth',
  annotation: { type: 'feature', description: 'Auth system — login + session' },
}, async ({ page }) => { ... });

3. Mix old và new signature ngẫu nhiên trong cùng file — confusing reader

// BAD: không nhất quán trong cùng file spec
test('case A', async ({ page }) => { ... }); // old

test('case B', { tag: '@smoke' }, async ({ page }) => { ... }); // new

test('case C', async ({ page }, testInfo) => { // old + runtime
  testInfo.annotations.push({ type: 'smoke', description: 'smoke tag' });
}); // inconsistent với case B

// GOOD: chọn một style và dùng nhất quán trong file

4. Annotation type không consistent giữa team — khó query

// BAD: mỗi người dùng type khác nhau cho cùng khái niệm
// Dev A:
annotation: { type: 'Bug', description: 'JIRA-123' }
// Dev B:
annotation: { type: 'bug', description: 'JIRA-456' }
// Dev C:
annotation: { type: 'issue', description: 'JIRA-789' }

// Custom reporter phải handle 3 case riêng → phức tạp

// GOOD: định nghĩa enum trong constants file và dùng chung
// annotation-types.ts
export const AnnotationType = {
  ISSUE: 'issue',
  SEVERITY: 'severity',
  OWNER: 'owner',
  FEATURE: 'feature',
} as const;

// test file
import { AnnotationType } from '../annotation-types';
test('checkout', {
  annotation: { type: AnnotationType.ISSUE, description: 'JIRA-123' },
}, async ({ page }) => { ... });
15

Quiz

Câu 1. Đoạn code sau có compile và chạy được với Playwright v1.42+ không? Nếu có, kết quả CLI filter --grep "@smoke" sẽ như thế nào?

test('search results', {
  tag: 'smoke',
  annotation: { type: 'feature', description: 'Search system' },
}, async ({ page }) => {
  await page.goto('/search');
});
Đáp án

Code compile và chạy được — không có TypeScript error vì tag chấp nhận string tùy ý. Tuy nhiên CLI filter --grep "@smoke" sẽ không match test này vì tag value là 'smoke' (thiếu @), không phải '@smoke'. Đây là pitfall #1 — typo tag không được detect tại compile time.

Câu 2. Test con trong test.describe có tag riêng có override tag của describe không? Giải thích behavior merge.

Đáp án

Không override — tag được merge. Nếu describe có tag: '@smoke' và test con có tag: '@critical', test đó sẽ có cả @smoke lẫn @critical. Playwright không có cơ chế "override" — chỉ có "thêm vào". Để loại bỏ tag từ describe, chỉ có cách không đặt tag đó ở describe level ngay từ đầu.

Câu 3. Sự khác biệt chính giữa đặt annotation trong options object và dùng testInfo.annotations.push() trong body là gì? Hãy cho ví dụ use case phù hợp với mỗi cách.

Đáp án

Options object là declarative — giá trị biết trước lúc code, được đọc tại collect phase (trước khi test chạy), dùng được với CLI tag filter. Thích hợp cho ticket ID, owner, severity, feature name — những gì không thay đổi theo runtime.

testInfo.annotations.push()runtime — có thể dùng kết quả từ async call, env variable, hay bất kỳ logic nào trong body. Thích hợp cho build ID, tên môi trường, trạng thái feature flag tại thời điểm chạy.

Câu 4. Có cú pháp CLI nào để filter "chỉ chạy test có CẢ @smoke VÀ @critical" không? Nếu không, giải pháp thay thế là gì?

Đáp án

Không có cú pháp AND native trong CLI. --grep "@smoke|@critical" là OR (match test có ít nhất một trong hai tag). Để filter AND, có 2 giải pháp: (1) Tạo tag gộp như @smoke-critical — rõ ràng nhưng cứng; (2) Dùng playwright test --list --grep "@smoke" để lấy danh sách rồi filter tiếp bằng script bên ngoài check tag @critical.

Câu 5. Code sau có lỗi gì? Sửa lại cho đúng.

const getTag = () => '@smoke';

test('product listing', {
  tag: getTag(),
}, async ({ page }) => {
  await page.goto('/products');
});
Đáp án

Code này không có lỗigetTag() là synchronous function call trả về string literal, hoàn toàn hợp lệ trong options object. Limitation chỉ áp dụng khi cần kết quả từ async operation (không thể await trong object literal). Nếu getTagasync function thì mới có vấn đề — await getTag() không thể dùng trực tiếp trong object literal ở top-level scope.