Danh sách bài viết

Bài 46: Annotation { type, description } — Metadata Có Cấu Trúc

Tag (bài 44-45) là flag string để filter. Annotation là cặp key-value gắn metadata có cấu trúc vào test — hiển thị chi tiết trong reporter, tích hợp được với issue tracker, và đọc được bởi custom reporter để tự động hóa workflow. Bài này cover cú pháp annotation object, multi-annotation array, apply cho describe block, combine với tag, các pattern phổ biến, và giới hạn cần biết.

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

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

Sau bài này bạn sẽ nắm được:

  • Annotation object { type, description } khác tag ở điểm nào về mục đích và hành vi.
  • Cú pháp khai báo single annotation và multi-annotation array.
  • Cách annotation được kế thừa khi áp dụng cho test.describe.
  • Cách combine tag và annotation trong cùng 1 khai báo test.
  • Behavior của từng reporter (list, HTML, JUnit, custom) với annotation.
  • Pattern issue tracking và severity classification tái sử dụng.
  • Giới hạn của annotation tĩnh và điểm khác biệt với runtime push (bài 47).
2

Annotation Là Gì — Khác Tag Ở Đâu

Tag là flag string đơn thuần: '@smoke', '@critical'. Giá trị của tag là chính tên nó — không có cấu trúc thêm. Tag được dùng để filter qua --grep trước khi chạy.

Annotation là cặp type / description: một key mô tả loại metadata và một value mô tả nội dung. Annotation không được dùng để filter CLI — mục đích là gắn thông tin có ngữ nghĩa để reporter hiển thị và tool tích hợp đọc.

// Tag: flag phân loại, dùng cho filter
test('checkout', { tag: '@smoke' }, ...);

// Annotation: key-value metadata, dùng cho reporter + integration
test('checkout', {
  annotation: { type: 'issue', description: 'JIRA-456' },
}, ...);

Tóm tắt sự khác biệt:

  • Tag: classify và filter — biết trước khi chạy, dùng cho --grep.
  • Annotation: metadata và reporting — gắn context, xuất hiện trong reporter detail, tích hợp với external system.
  • Hai cơ chế bổ sung lẫn nhau, không thay thế.
3

Cú Pháp Annotation Object

Truyền annotation vào options object của test():

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

test('payment flow', {
  annotation: { type: 'issue', description: 'JIRA-456' },
}, async ({ page }) => {
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay Now' }).click();
  await expect(page.getByText('Payment successful')).toBeVisible();
});

TypeScript type của field annotation{ type: string; description?: string } | { type: string; description?: string }[]. Cả object đơn lẫn array đều hợp lệ.

Field description là optional — có thể khai báo annotation chỉ với type:

// Chỉ có type, không có description
test('login', {
  annotation: { type: 'wip' },
}, async ({ page }) => {
  // ...
});

Tuy nhiên trong thực tế, annotation không có description ít hữu ích hơn vì reporter chỉ hiển thị type mà không có context. Nên luôn khai báo cả hai trừ khi type đã đủ tự mô tả (vd 'wip', 'skip-reason' không cần thêm gì).

4

Multi-Annotation Array

Khi cần gắn nhiều piece of metadata, truyền array thay vì object đơn:

test('payment flow', {
  annotation: [
    { type: 'issue', description: 'JIRA-456' },
    { type: 'severity', description: 'P1' },
    { type: 'owner', description: 'team-payment' },
  ],
}, async ({ page }) => {
  await page.goto('/checkout');
  // ...
});

Playwright giữ nguyên thứ tự array khi truyền vào test.annotations. Reporter hiển thị theo thứ tự khai báo — đặt annotation quan trọng nhất (ví dụ issue) lên đầu để dễ scan trong report.

Không có giới hạn số lượng annotation trong array về mặt type, nhưng nhiều hơn 4-5 annotation thì HTML report section sẽ dài — cân nhắc chỉ khai báo annotation có giá trị thực sự.

5

Type Phổ Biến Và Ý Nghĩa

Field type là free string — Playwright không enforce giá trị nào. Nhưng trong thực tế, một số type đã trở thành convention phổ biến:

// issue — link đến JIRA/GitHub issue
{ type: 'issue', description: 'JIRA-456' }
{ type: 'issue', description: 'https://github.com/org/repo/issues/789' }

// bug — bug ID liên quan
{ type: 'bug', description: 'BUG-123' }

// feature — feature flag hoặc feature ticket
{ type: 'feature', description: 'FEAT-99' }
{ type: 'feature', description: 'new-checkout-flow' }  // feature flag name

// severity — mức ưu tiên
{ type: 'severity', description: 'P0' }
{ type: 'severity', description: 'P1' }

// owner — team chịu trách nhiệm
{ type: 'owner', description: 'team-payment' }
{ type: 'owner', description: 'team-auth' }

// slow — performance note, cảnh báo timeout cao
{ type: 'slow', description: 'external API, up to 30s' }

// flaky — instability note kèm lý do
{ type: 'flaky', description: 'Race condition in CI, tracked BUG-99' }

// env — environment-specific note
{ type: 'env', description: 'staging-only' }
{ type: 'env', description: 'requires-feature-flag: dark-mode' }

type là free string, team nên document danh sách type chính thức (ví dụ trong ANNOTATIONS.md ở root project) để tránh drift: 'bug' vs 'Bug' vs 'BUG' là 3 giá trị khác nhau.

6

Apply Annotation Cho Describe Block

Annotation có thể khai báo ở test.describe. Tất cả test con trong block đó sẽ inherit annotation:

test.describe('Payment suite', {
  annotation: { type: 'domain', description: 'payment' },
}, () => {

  test('checkout complete', async ({ page }) => {
    // Inherit: annotation [{ type: 'domain', description: 'payment' }]
    await page.goto('/checkout');
  });

  test('refund flow', async ({ page }) => {
    // Inherit: annotation [{ type: 'domain', description: 'payment' }]
    await page.goto('/refund');
  });

});

Test con có annotation riêng sẽ merge với annotation từ describe:

test.describe('Payment suite', {
  annotation: { type: 'domain', description: 'payment' },
}, () => {

  test('checkout complete', {
    annotation: { type: 'issue', description: 'JIRA-456' },
  }, async ({ page }) => {
    // Annotations cuối: [
    //   { type: 'domain', description: 'payment' },  // từ describe
    //   { type: 'issue', description: 'JIRA-456' },  // của test
    // ]
  });

});

Annotation từ describe được đặt trước annotation của test — hành vi này nhất quán với tag inheritance. Nested describe cũng accumulate theo thứ tự outer → inner → test.

7

Combine Tag Và Annotation

Tag và annotation có thể khai báo cùng nhau trong options object — không xung đột:

test('payment flow', {
  tag: '@critical',
  annotation: [
    { type: 'issue', description: 'JIRA-789' },
    { type: 'severity', description: 'P1' },
  ],
}, async ({ page }) => {
  await page.goto('/checkout');
  await expect(page.getByText('Payment successful')).toBeVisible();
});

Cả tag array và annotation array cũng hoạt động:

test('payment flow', {
  tag: ['@smoke', '@critical'],
  annotation: [
    { type: 'issue', description: 'JIRA-789' },
    { type: 'severity', description: 'P1' },
    { type: 'owner', description: 'team-payment' },
  ],
}, async ({ page }) => {
  // tag: dùng cho filter CLI
  // annotation: dùng cho reporter detail + integration
});

Đây là cách dùng đầy đủ nhất: tag phân loại test theo dimension để filter, annotation gắn context cụ thể để báo cáo và tích hợp.

8

Reporter Handling

List Reporter

Annotation không hiển thị inline trong output terminal như tag. List reporter chỉ hiển thị trạng thái và tên test. Annotation xuất hiện ở verbose output khi test thất bại.

HTML Reporter

Khi mở playwright-report/index.html, click vào test detail, sẽ có section Annotations liệt kê tất cả annotation dạng bảng type / description:

Annotations
───────────────────────────────
issue       JIRA-456
severity    P1
owner       team-payment

HTML report không có filter theo annotation (khác với tag có filter chip). Annotation chỉ hiển thị trong detail view của từng test.

JUnit XML Reporter

Annotation xuất hiện dưới dạng <property> trong <properties> block của từng <testcase>:

<testcase name="payment flow" classname="tests/checkout.spec.ts" time="1.5">
  <properties>
    <property name="issue" value="JIRA-456"/>
    <property name="severity" value="P1"/>
    <property name="owner" value="team-payment"/>
  </properties>
</testcase>

Format này cho phép CI system (Jenkins, TeamCity) parse annotation thành metadata test case riêng biệt.

Custom Reporter

Custom reporter đọc annotation qua test.annotations — array các object { type, description }. Xem chi tiết ở mục 11.

9

Pattern Issue Tracking

Annotation phù hợp nhất cho việc link test với issue tracker — tạo audit trail rõ ràng giữa test case và ticket:

test('cart quantity regression', {
  annotation: [
    { type: 'issue', description: 'https://jira.company.com/browse/PROJ-456' },
    { type: 'regression', description: 'Found in v2.3.1' },
  ],
}, async ({ page }) => {
  await page.goto('/cart');
  await page.getByLabel('Quantity').fill('0');
  await expect(page.getByText('Quantity must be at least 1')).toBeVisible();
});

Dùng full URL thay vì chỉ ticket ID khi description sẽ được render thành link trong custom reporter hoặc dashboard. Nếu reporter chỉ hiển thị text, ticket ID ngắn gọn ('PROJ-456') dễ scan hơn.

Pattern này cho phép:

  • Khi mở HTML report thấy test fail → annotation chỉ thẳng ticket để debug.
  • Custom reporter có thể comment lên ticket khi test fail trong CI.
  • Audit: test nào đang cover issue nào — extract từ JSON report.
10

Pattern Severity Classification

Vì annotation là plain object, có thể export constant để tái sử dụng — tránh typo và đảm bảo consistency:

// factories/annotations.ts
export const P0 = { type: 'severity', description: 'P0' } as const;
export const P1 = { type: 'severity', description: 'P1' } as const;
export const P2 = { type: 'severity', description: 'P2' } as const;

export const owner = (team: string) => ({ type: 'owner', description: team } as const);
export const issue = (id: string) => ({ type: 'issue', description: id } as const);
// tests/checkout.spec.ts
import { P0, P1, owner, issue } from '../factories/annotations';

test('critical checkout flow', {
  annotation: [P0, owner('team-payment'), issue('JIRA-456')],
}, async ({ page }) => {
  // annotation: [
  //   { type: 'severity', description: 'P0' },
  //   { type: 'owner', description: 'team-payment' },
  //   { type: 'issue', description: 'JIRA-456' },
  // ]
});

test('optional upsell flow', {
  annotation: [P2, owner('team-catalog')],
}, async ({ page }) => {
  // ...
});

Factory function như owner('team-payment') cũng tiện hơn literal object khi value thay đổi thường xuyên. Constant như P0, P1 với as const giúp TypeScript inference tốt hơn.

11

Custom Reporter Consume Annotation

Custom reporter nhận TestCase object có field annotations: { type: string; description?: string }[]. Đây là cách tích hợp annotation với external system:

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

class AnnotationReporter implements Reporter {
  onTestEnd(test: TestCase, result: TestResult) {
    // Tìm annotation có type 'issue'
    const issue = test.annotations.find(a => a.type === 'issue');

    if (issue && result.status === 'failed') {
      // Gửi notification khi test fail và có issue annotation
      slackNotify(
        `Test "${test.title}" failed — ref: ${issue.description}`
      );
    }

    // Collect severity để generate report
    const severity = test.annotations.find(a => a.type === 'severity');
    if (severity) {
      this.severityStats[severity.description] =
        (this.severityStats[severity.description] ?? 0) + 1;
    }
  }

  private severityStats: Record = {};

  onEnd() {
    console.log('Severity breakdown:', this.severityStats);
    // Output: { P0: 3, P1: 12, P2: 24 }
  }
}

export default AnnotationReporter;
// playwright.config.ts
export default {
  reporter: [
    ['list'],
    ['./custom-reporter.ts'],
  ],
};

Custom reporter có thể đọc tất cả annotation, filter theo type, aggregate thống kê, hoặc post lên external API. Đây là lý do annotation dùng structured object thay vì free string như tag — consumer dễ parse hơn.

12

Annotation Static vs Runtime Push

Annotation khai báo trong options object là static — biết tại thời điểm viết code, không phụ thuộc vào runtime. Đây là nội dung bài này.

Ngoài ra, Playwright còn cho phép push annotation runtime trong test body qua testInfo.annotations.push():

test('checkout with feature flag', {
  // Static — khai báo cố định
  annotation: { type: 'issue', description: 'JIRA-456' },
}, async ({ page }, testInfo) => {

  // Runtime push — dựa trên điều kiện lúc chạy
  const flag = await getFeatureFlag('new-checkout');
  testInfo.annotations.push({
    type: 'feature-flag',
    description: `new-checkout=${flag ? 'on' : 'off'}`,
  });

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

Cả static và runtime annotation đều xuất hiện trong cùng 1 testInfo.annotations array. Static annotation được thêm trước khi test bắt đầu, runtime push thêm trong quá trình chạy. Chi tiết testInfo.annotations.push() được cover ở bài 47.

13

Limitations

  • Type không được enforce: 'bug', 'Bug', 'BUG' là 3 giá trị khác nhau. Không có schema validation tích hợp — dễ inconsistent khi team lớn.
  • Description free string: không validate format. URL malformed, ticket ID sai convention, hay text dài sẽ không bị bắt lỗi.
  • Không filter được qua CLI: annotation không thể dùng với --grep. Chỉ có thể filter annotation ở post-processing (JSON report, custom reporter) sau khi test đã chạy.
  • HTML reporter không filter theo annotation: khác với tag có filter chip trong HTML report, annotation chỉ hiển thị trong detail view từng test — không thể nhóm hay lọc test theo annotation trực tiếp trong UI.
  • Describe override không hỗ trợ: test con không thể loại bỏ annotation từ describe cha. Nếu cần, phải đưa test ra ngoài describe block.
14

Pitfalls

Pitfall 1: Type Không Nhất Quán Giữa Các Thành Viên

// Developer A
test('login fail', {
  annotation: { type: 'bug', description: 'BUG-123' },
}, ...);

// Developer B — cùng loại nhưng type khác
test('payment fail', {
  annotation: { type: 'Bug', description: 'BUG-456' },
}, ...);

// Developer C
test('logout fail', {
  annotation: { type: 'BUG', description: 'BUG-789' },
}, ...);

// Custom reporter filter: test.annotations.find(a => a.type === 'bug')
// → Chỉ match Developer A, bỏ sót B và C âm thầm

Giải pháp: export constant từ factories/annotations.ts thay vì type string trực tiếp, hoặc CI script audit annotation.type từ JSON report.

Pitfall 2: Description Quá Dài Làm Reporter Rối

// Tránh: description dài hàng trăm ký tự
test('flow', {
  annotation: {
    type: 'context',
    description: 'This test covers the entire payment flow including card validation, 3DS authentication, webhook processing, and refund edge cases in v2.3 with the new checkout UI',
  },
}, ...);

// Nên: ngắn gọn, link đến tài liệu nếu cần chi tiết
test('flow', {
  annotation: [
    { type: 'context', description: 'Full payment flow v2.3' },
    { type: 'doc', description: 'https://wiki.company.com/payment-tests' },
  ],
}, ...);

Pitfall 3: Nhầm Annotation Với Tag — Dùng Sai Mục Đích

// SAI — dùng annotation để filter, nhưng --grep không đọc annotation
test('smoke test', {
  annotation: { type: 'category', description: 'smoke' },
}, ...);
// npx playwright test --grep "smoke" → KHÔNG match qua annotation

// ĐÚNG — dùng tag để filter
test('smoke test', {
  tag: '@smoke',
  annotation: { type: 'owner', description: 'team-qa' },  // metadata thêm
}, ...);

Pitfall 4: Sensitive Data Trong Description

// NGUY HIỂM — URL chứa token trong description
test('api flow', {
  annotation: {
    type: 'endpoint',
    description: 'https://api.company.com/v1/pay?token=secret-api-key-here',
  },
}, ...);
// → token lộ qua HTML report, JUnit XML, CI artifact, custom reporter log

Description không được encrypt hay redact trong bất kỳ reporter nào. Không đặt API key, password, hay secret token vào description. Nếu cần link đến resource cần auth, dùng internal URL không chứa credential.

Pitfall 5: Nhầm Annotation Static Với testInfo.annotations.push()

// Annotation static (options object) — set trước khi test chạy
test('flow', {
  annotation: { type: 'issue', description: 'JIRA-456' },
}, async ({ page }, testInfo) => {

  // testInfo.annotations lúc này ĐÃ có { type: 'issue', description: 'JIRA-456' }
  // Không cần push lại — sẽ tạo duplicate
  testInfo.annotations.push({ type: 'issue', description: 'JIRA-456' }); // duplicate!
});
15

Tổng Kết

  • Annotation { type, description } là cặp key-value gắn structured metadata — không dùng để filter CLI, dùng cho reporter detail và integration.
  • Multi-annotation dùng array; Playwright giữ nguyên thứ tự khai báo.
  • Annotation trên test.describe được inherit bởi tất cả test con — accumulate theo thứ tự outer → inner → test.
  • Tag và annotation có thể combine trong cùng 1 options object.
  • HTML reporter: section "Annotations" trong detail view. JUnit: <property>. Custom reporter: đọc qua test.annotations.
  • Export annotation constant từ factory file để đảm bảo type nhất quán.
  • Không đặt sensitive data (token, password) trong description — lộ qua mọi reporter.
  • Annotation static (options object) và runtime push (testInfo.annotations.push()) bổ sung lẫn nhau — bài 47 cover phần push.
16

Quiz

Câu 1. Test có annotation: { type: 'smoke', description: 'PR gate' }. Lệnh npx playwright test --grep "smoke" có chạy test này không?

Đáp án

Không. --grep match theo tên test và tag, không đọc annotation. Annotation không thể dùng để filter CLI. Muốn filter theo "smoke" phải dùng tag: '@smoke'.

Câu 2. Test con nằm trong test.describe('Suite', { annotation: { type: 'domain', description: 'checkout' } }) và tự khai báo thêm annotation: { type: 'issue', description: 'JIRA-1' }. Giá trị của testInfo.annotations khi test chạy là gì?

Đáp án

[{ type: 'domain', description: 'checkout' }, { type: 'issue', description: 'JIRA-1' }]. Annotation describe đặt trước annotation của test, theo thứ tự outer → inner → test.

Câu 3. Tại sao nên export annotation constant từ factory file thay vì inline literal trực tiếp trong từng test?

Đáp án

Field type là free string — không có validation tích hợp. Nếu mỗi người tự viết literal, dễ xuất hiện 'bug' / 'Bug' / 'BUG' không nhất quán. Custom reporter filter theo type sẽ bỏ sót kết quả âm thầm. Export constant đảm bảo toàn team dùng cùng 1 giá trị.

Câu 4. Trong custom reporter, làm sao đọc tất cả annotation của 1 test?

Đáp án

Qua test.annotations trong callback onTestEnd(test, result). Field này là array { type: string; description?: string }[], bao gồm cả annotation static (khai báo trong options) lẫn annotation được push runtime qua testInfo.annotations.push().

Câu 5. Điều gì xảy ra nếu đặt API token vào annotation: { type: 'endpoint', description: 'https://api.com?token=secret' }?

Đáp án

Token sẽ lộ trong tất cả reporter output: HTML report (có thể public), JUnit XML (CI artifact), custom reporter log, và JSON report. Không có mechanism redact hay encrypt description. Không đặt credential vào annotation — dùng internal URL không chứa secret, hoặc chỉ ghi ticket ID/doc link.