Mục lục
- Mục Tiêu Bài Học
- Annotation Là Gì — Khác Tag Ở Đâu
- Cú Pháp Annotation Object
- Multi-Annotation Array
- Type Phổ Biến Và Ý Nghĩa
- Apply Annotation Cho Describe Block
- Combine Tag Và Annotation
- Reporter Handling
- Pattern Issue Tracking
- Pattern Severity Classification
- Custom Reporter Consume Annotation
- Annotation Static vs Runtime Push
- Limitations
- Pitfalls
- Tổng Kết
- Quiz
- Bài Tiếp Theo
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).
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ế.
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 là { 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ì).
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ự.
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' }
Vì 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.
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.
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.
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.
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.
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.
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.
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.
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.
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!
});
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 quatest.annotations. - Export annotation constant từ factory file để đảm bảo
typenhấ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.
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.
