Mục lục
- Mục Tiêu Bài Học
- Cú Pháp Tag Array
- Use Case Multi-Tag
- Filter CLI Với Tag Array
- AND Logic Bằng --grep-invert
- Complex Filter Pattern
- Tag Inheritance Từ Describe
- Dimension Tagging
- testInfo.tags — Programmatic Access
- beforeEach Với testInfo.tags
- Reporter Handling Array Tag
- Static Tag vs Runtime Annotation
- 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:
- Cú pháp tag array và sự khác biệt với tag single về merge behavior.
- Cách Playwright xử lý --grep khi test có nhiều tag.
- Giới hạn OR-only của --grep và cách mô phỏng AND logic bằng --grep-invert.
- Tag inheritance khi nest describe với tag riêng.
- Dimension tagging — phân loại test theo nhiều chiều độc lập.
- Truy cập tag runtime qua
testInfo.tagsđể điều chỉnh behavior. - Behavior của từng reporter khi test có nhiều tag.
Cú Pháp Tag Array
Thay vì truyền một string, truyền array vào field tag của options object:
import { test, expect } from '@playwright/test';
// Tag single (bài 44) — 1 tag
test('homepage load', { tag: '@smoke' }, async ({ page }) => {
await page.goto('/');
});
// Tag array — nhiều tag trong 1 khai báo
test('payment flow', {
tag: ['@smoke', '@critical', '@p1', '@payment'],
}, 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 tag là string | string[] — cả hai dạng đều hợp lệ. Không cần ép về một dạng nhất quán trong cùng file.
Mỗi phần tử trong array phải bắt đầu bằng @. Playwright không enforce ký tự này ở runtime nhưng toàn bộ convention filter CLI, tooling, và reporter đều dựa trên prefix @.
Use Case Multi-Tag
Tag array hữu ích khi 1 test thuộc nhiều category độc lập cùng lúc:
// Test này cần chạy trong 3 pipeline khác nhau
test('user login', {
tag: ['@smoke', '@critical', '@auth'],
}, async ({ page }) => {
// @smoke → chạy trong PR gate (nhanh, coverage cơ bản)
// @critical → chạy trong regression ban đêm với priority cao
// @auth → chạy khi team Auth deploy thay đổi
});
Các trường hợp điển hình:
- PR gate + regression cùng lúc:
['@smoke', '@regression']— test vừa nhẹ đủ cho PR, vừa cần có trong full regression suite. - Priority + domain:
['@p0', '@payment']— test payment quan trọng nhất, cần xuất hiện trong report lọc theo domain và theo priority. - Môi trường:
['@staging', '@prod']— test chạy được trên cả hai môi trường, không cần viết lại. - Owner + type:
['@team-checkout', '@api']— tag team để assign alert và tag type để phân loại report.
Nguyên tắc cốt lõi: mỗi tag phục vụ 1 mục đích filter độc lập. Nếu 2 tag luôn xuất hiện cùng nhau và không bao giờ được filter riêng lẻ, gộp thành 1 tag mới.
Filter CLI Với Tag Array
--grep khớp theo regex với toàn bộ chuỗi tên test. Playwright nối tất cả tag của test vào tên trước khi so khớp — vì vậy filter theo bất kỳ tag nào trong array đều hoạt động.
// spec file
test('payment flow', {
tag: ['@smoke', '@critical', '@payment'],
}, async ({ page }) => { /* ... */ });
# Match vì test có @smoke
npx playwright test --grep "@smoke"
# Match vì test có @critical
npx playwright test --grep "@critical"
# Match vì test có @payment
npx playwright test --grep "@payment"
# Match vì test có cả 2 — --grep là OR theo regex
npx playwright test --grep "@smoke|@critical"
Hành vi quan trọng: test có bất kỳ tag nào khớp với pattern là sẽ được chọn. Không có cơ chế AND native ở cấp --grep.
# Chạy tất cả test có @smoke HOẶC @critical HOẶC @payment
npx playwright test --grep "@smoke|@critical|@payment"
# Kết quả: union của tất cả test có ít nhất 1 trong 3 tag
AND Logic Bằng --grep-invert
Playwright không có flag --grep-and native. AND logic phải được mô phỏng bằng cách kết hợp --grep (include) và --grep-invert (exclude):
# Chạy @smoke AND NOT @slow
npx playwright test --grep "@smoke" --grep-invert "@slow"
Điều này hoạt động vì:
--grep "@smoke": chọn tất cả test có tag@smoke.--grep-invert "@slow": loại bỏ khỏi tập đó bất kỳ test nào có tag@slow.- Kết quả: test có
@smokeVÀ KHÔNG có@slow.
Giả lập AND 2 tag dương (phải có cả A và B) cần thêm bước:
# Muốn: @smoke AND @critical
# Không thể dùng --grep trực tiếp
# Cách 1: tạo tag gộp @smoke-critical khi khai báo
# Cách 2: dùng grep-invert để loại bỏ không-critical khỏi @smoke set
# Loại bỏ test chỉ có @smoke nhưng không có @critical
# (Phức tạp — cần biết tag nào không phải @critical trong set @smoke)
npx playwright test --grep "@smoke" --grep-invert "@p2|@p3"
Nếu AND logic phức tạp xuất hiện thường xuyên, cân nhắc thêm tag gộp vào taxonomy thay vì dựa vào shell trick.
Complex Filter Pattern
Vì --grep nhận regex, có thể viết pattern phức hơn:
# OR: @smoke hoặc @critical
npx playwright test --grep "@smoke|@critical"
# OR với regex character class: @p0 hoặc @p1
npx playwright test --grep "@p[01]"
# Match @team-checkout hoặc @team-payment
npx playwright test --grep "@team-(checkout|payment)"
# Chạy test có @smoke nhưng không phải @slow hoặc @flaky
npx playwright test --grep "@smoke" --grep-invert "@slow|@flaky"
Lưu ý khi dùng regex đặc biệt trên shell: ký tự như (, ), | có thể cần escape tùy shell. Đặt toàn bộ pattern trong dấu nháy đơn để tránh shell interpolation:
# Sử dụng nháy đơn để tránh shell xử lý ký tự đặc biệt
npx playwright test --grep '@team-(checkout|payment)'
Trong CI (GitHub Actions, GitLab CI), define pattern trong env variable để tránh escape phức tạp:
# .github/workflows/test.yml
- name: Run smoke tests
run: npx playwright test --grep "$TAG_FILTER"
env:
TAG_FILTER: "@smoke|@critical"
Tag Inheritance Từ Describe
Playwright merge tag từ test.describe vào từng test con. Test nhận tất cả tag của describe block chứa nó, cộng với tag riêng của test:
test.describe('Auth', { tag: '@auth' }, () => {
// Test này có: @auth + @smoke
test('login', { tag: '@smoke' }, async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL('/dashboard');
});
// Test này có: @auth + @regression + @critical
test('logout', { tag: ['@regression', '@critical'] }, async ({ page }) => {
await page.getByRole('button', { name: 'Logout' }).click();
await expect(page).toHaveURL('/login');
});
// Test này có: @auth (chỉ inherit, không tag riêng)
test('forgot password', async ({ page }) => {
await page.getByText('Forgot password?').click();
await expect(page.getByRole('heading', { name: 'Reset Password' })).toBeVisible();
});
});
Nested describe cũng accumulate:
test.describe('E-commerce', { tag: '@ecom' }, () => {
test.describe('Checkout', { tag: ['@checkout', '@p0'] }, () => {
// Tag cuối cùng: @ecom + @checkout + @p0 + @smoke
test('complete order', { tag: '@smoke' }, async ({ page }) => {
// ...
});
});
});
Không có cơ chế override hay loại bỏ tag từ describe. Nếu 1 test trong group không nên có tag của describe, cần đưa test đó ra ngoài describe block hoặc dùng describe riêng không có tag đó.
Dimension Tagging
Multi-tag hữu ích nhất khi được tổ chức theo dimension — mỗi tag thuộc đúng 1 chiều phân loại, và mỗi test có đúng 1 tag từ mỗi dimension:
// Taxonomy 4 dimension:
// Speed: @smoke | @regression
// Priority: @p0 | @p1 | @p2
// Owner: @team-checkout | @team-auth | @team-catalog
// Type: @ui | @api | @visual
test('checkout complete order', {
tag: ['@smoke', '@p0', '@team-checkout', '@ui'],
}, async ({ page }) => {
// Orthogonal: có thể filter theo bất kỳ dimension nào
});
test('get product list', {
tag: ['@regression', '@p1', '@team-catalog', '@api'],
}, async ({ page, request }) => {
// ...
});
Với taxonomy này, mỗi câu hỏi filter trả về kết quả chính xác:
# Chạy tất cả smoke test
npx playwright test --grep "@smoke"
# Chạy tất cả test của team-checkout
npx playwright test --grep "@team-checkout"
# Chạy smoke test của team-checkout (AND)
npx playwright test --grep "@smoke" --grep-invert "@team-auth|@team-catalog"
# → giả lập: @smoke AND @team-checkout
# Chạy tất cả p0 test
npx playwright test --grep "@p0"
# Chạy p0 UI test (không chạy p0 API)
npx playwright test --grep "@p0" --grep-invert "@api"
Document taxonomy trong TAGS.md ở root project. Nếu team đủ lớn, enforce bằng custom ESLint rule hoặc script audit tag usage từ JSON report.
testInfo.tags — Programmatic Access
testInfo.tags là array của tất cả tag (đã merge từ describe) tại thời điểm test chạy. Có thể đọc để điều chỉnh behavior trong test body:
test('data-heavy flow', {
tag: ['@regression', '@slow', '@p1'],
}, async ({ page }, testInfo) => {
// Đọc tags — đã bao gồm cả inherited tags từ describe
console.log(testInfo.tags);
// Ví dụ output: ['@auth', '@regression', '@slow', '@p1']
// Điều chỉnh timeout nếu có @slow
if (testInfo.tags.includes('@slow')) {
test.setTimeout(120_000);
}
// Skip trên môi trường nhất định dựa trên tag
if (testInfo.tags.includes('@staging-only') && process.env.ENV !== 'staging') {
test.skip(true, 'This test only runs on staging');
}
await page.goto('/reports');
// ...
});
testInfo.tags là read-only array — không thể push vào để thêm tag sau khi khai báo. Để gắn metadata runtime, dùng testInfo.annotations.push() (xem bài 46).
So sánh nhanh:
// testInfo.tags — chỉ đọc static tags được khai báo
testInfo.tags // ['@smoke', '@p0'] — read-only
// testInfo.annotations — ghi runtime metadata
testInfo.annotations.push({ type: 'env', description: 'staging' });
beforeEach Với testInfo.tags
testInfo.tags cũng khả dụng trong hook — hữu ích để áp dụng logic chung cho toàn bộ test file mà không cần lặp điều kiện trong từng test:
import { test } from '@playwright/test';
test.beforeEach(async ({ page }, testInfo) => {
// Log tag để debug
console.log(`[${testInfo.title}] tags: ${testInfo.tags.join(', ')}`);
// Điều chỉnh timeout toàn file dựa trên tag của test hiện tại
if (testInfo.tags.includes('@slow')) {
test.setTimeout(120_000);
}
// Mọi test có @needs-auth đều cần login trước
if (testInfo.tags.includes('@needs-auth')) {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER!);
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('/dashboard');
}
});
test('view orders', { tag: ['@smoke', '@needs-auth'] }, async ({ page }) => {
// beforeEach đã login vì có @needs-auth
await page.goto('/orders');
});
test('homepage', { tag: '@smoke' }, async ({ page }) => {
// beforeEach không login (không có @needs-auth)
await page.goto('/');
});
Pattern này thay thế fixture phức tạp trong trường hợp đơn giản. Với project lớn, fixture vẫn là lựa chọn maintainable hơn vì có typing rõ ràng và dependency graph tường minh.
Reporter Handling Array Tag
Mỗi reporter xử lý tag array khác nhau:
List Reporter
Tag được hiển thị inline trong tên test, cách nhau bởi space:
✓ payment flow @smoke @critical @p1 @payment (1.2s)
HTML Reporter
Mỗi tag render thành filter chip riêng biệt trong UI. Có thể click để lọc test theo tag ngay trong report. Với nhiều tag (>4), chips có thể bị wrap sang dòng mới.
JUnit XML Reporter
Tag được concatenate vào classname hoặc test name trong XML output:
<testcase name="payment flow @smoke @critical @p1 @payment"
classname="tests/checkout.spec.ts"
time="1.2"/>
Khi CI parse JUnit XML để generate report, tag xuất hiện trong test name — cần xử lý nếu muốn hiển thị riêng.
JSON Reporter
Tag có trong field riêng biệt, giữ nguyên array structure:
{
"title": "payment flow",
"tags": ["@smoke", "@critical", "@p1", "@payment"],
"outcome": "passed"
}
JSON reporter thích hợp nhất cho post-processing: audit tag usage, generate tag coverage matrix, hoặc tích hợp với dashboard tùy chỉnh.
Static Tag vs Runtime Annotation
Có 2 cơ chế gắn metadata cho test — bổ sung lẫn nhau, không thay thế:
// Static tag — khai báo tại thời điểm viết code
// Dùng cho: taxonomy cố định, filter CLI, reporter
test('checkout', {
tag: ['@smoke', '@p0', '@payment'],
}, async ({ page }, testInfo) => {
// Runtime annotation — gắn dựa trên điều kiện trong lúc chạy
// Dùng cho: ghi nhận env, feature flag, dữ liệu test, nguyên nhân lỗi
const featureFlag = await getFeatureFlag('new-checkout');
if (featureFlag) {
testInfo.annotations.push({
type: 'feature-flag',
description: 'new-checkout=enabled',
});
}
// ...
});
Phân biệt rõ:
- Static tag: biết tại compile time, xuất hiện trong
testInfo.tags, được CLI dùng để filter trước khi chạy. - Runtime annotation: xuất hiện trong
testInfo.annotations, chỉ có sau khi test bắt đầu chạy, không thể dùng để filter trước.
Limitations
- Không có AND grep native:
--grepchỉ OR theo regex. AND phải dùng--grep-invertworkaround, phức tạp khi số điều kiện tăng. - Reporter cluttered với nhiều tag: quá 5 tag làm HTML report chip row bị wrap, JUnit test name dài khó đọc. Nên giới hạn 3-4 tag mỗi test từ taxonomy cố định.
- Không có tag validation tích hợp: Playwright không báo lỗi khi tag sai format, thiếu
@, hay không thuộc taxonomy. Cần ESLint rule hoặc CI check riêng. - Describe tag không thể override: test con không thể bỏ tag mà describe cha đã gắn. Phải cấu trúc lại describe hierarchy nếu cần.
- testInfo.tags read-only: không thể thêm tag sau khai báo. Dynamic metadata phải dùng
testInfo.annotations.
Pitfalls
Pitfall 1: Quên @ Prefix Ở 1 Phần Tử Trong Array
// SAI — phần tử thứ 3 thiếu @
test('checkout', { tag: ['@smoke', '@critical', 'payment'] }, ...);
// Hậu quả:
// --grep "@payment" sẽ không match vì tag là 'payment' không phải '@payment'
// Nhưng code không báo lỗi — bug âm thầm
// ĐÚNG
test('checkout', { tag: ['@smoke', '@critical', '@payment'] }, ...);
Pitfall 2: Tag Trùng Trong Array
// Tag @smoke xuất hiện 2 lần
test('checkout', { tag: ['@smoke', '@smoke', '@critical'] }, ...);
// Playwright không báo lỗi, nhưng reporter có thể hiển thị @smoke 2 lần
// HTML report: 2 chip @smoke cho cùng 1 test → confusing
// JUnit: "checkout @smoke @smoke @critical" trong test name
Pitfall 3: Regex Special Characters Trong Tag Chưa Escape
# Muốn filter tag @p[0] (nếu tag tên thật là @p[0])
# Nhưng --grep "@p[0]" là regex: "ký tự p theo sau bởi 0"
# → match @p0, @p10, bất kỳ string nào có 'p' rồi '0'
# Nếu cần match literal, escape:
npx playwright test --grep "@p\[0\]"
# Hoặc dùng convention tag không có ký tự đặc biệt trong regex: @p0, @p1
Pitfall 4: Tag Từ Describe Và Test Trùng Nhau — Duplicate Không Nhận Ra
test.describe('Auth', { tag: '@smoke' }, () => {
// Test này cũng tự gắn @smoke → duplicate
test('login', { tag: ['@smoke', '@critical'] }, async ({ page }) => {
// testInfo.tags: ['@smoke', '@smoke', '@critical'] ← 2 lần @smoke
});
});
// Playwright merge nhưng KHÔNG deduplicate — cả 2 @smoke giữ nguyên
// Dùng audit script để phát hiện
Tổng Kết
- Tag array
{ tag: ['@a', '@b'] }gắn nhiều tag cho 1 test trong 1 khai báo. --grepmatch nếu test có bất kỳ tag nào khớp pattern — OR logic.- AND logic cần kết hợp
--grep+--grep-invert. - Describe tag được merge vào tất cả test con — accumulate qua nhiều cấp nest.
- Dimension tagging: 1 tag per dimension per test — cho phép filter orthogonal.
testInfo.tagslà read-only array, đọc được trong test body và hook.- Runtime metadata dùng
testInfo.annotations.push(), không phải tag. - Giới hạn 3-4 tag mỗi test theo taxonomy cố định, document trong
TAGS.md.
Quiz
Câu 1. Test được khai báo với tag: ['@smoke', '@p0'] nằm trong test.describe('Auth', { tag: '@auth' }). Giá trị của testInfo.tags khi test chạy là gì?
Đáp án
['@auth', '@smoke', '@p0'] — describe tag được đặt trước, sau đó là tag của test. Nếu describe có thêm nested describe có tag, tất cả đều được accumulate theo thứ tự outer → inner.
Câu 2. Để chạy test có @smoke nhưng không có @slow, lệnh CLI nào đúng?
Đáp án
npx playwright test --grep "@smoke" --grep-invert "@slow". Cú pháp --grep "@smoke" --grep "@slow" không tương đương AND — flag --grep cuối sẽ ghi đè flag trước, chỉ còn filter @slow.
Câu 3. Test có tag: ['@smoke', '@smoke'] (trùng). Điều gì xảy ra?
Đáp án
Playwright không báo lỗi và không deduplicate. testInfo.tags sẽ là ['@smoke', '@smoke']. HTML reporter có thể hiển thị 2 chip @smoke, JUnit name chứa @smoke @smoke. Cần audit script để phát hiện.
Câu 4. Muốn chạy tất cả test có tag @p0 hoặc @p1 bằng 1 lệnh duy nhất, pattern nào ngắn nhất?
Đáp án
npx playwright test --grep "@p[01]". Regex character class [01] match @p0 hoặc @p1. Cần đảm bảo tag không có tên như @p01 hay @p100 để tránh false match — đây là lý do taxonomy nên nhất quán.
Câu 5. testInfo.tags.push('@new-tag') bên trong test body có hoạt động không? Nếu không, cách thêm metadata runtime là gì?
Đáp án
Không hoạt động — testInfo.tags là read-only array, runtime push sẽ throw hoặc bị bỏ qua. Để gắn metadata runtime, dùng testInfo.annotations.push({ type: 'key', description: 'value' }). Annotation xuất hiện trong report nhưng không thể dùng cho --grep filter.
