Mục lục
- Mục Tiêu Bài Học
- testInfo.tags Là Gì
- Cú Pháp Cơ Bản
- Pattern: Conditional Timeout
- Pattern: Conditional Skip
- Pattern: Conditional Logging
- Pattern: Fixture Branch
- Tag Từ Describe — Merge Behavior
- Flexible Matching
- testInfo.tags vs --grep
- Combine Fixture Option Và Tag
- Tags Trong Reporter
- 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:
- Bản chất read-only của
testInfo.tagsvà tại sao nó khácannotations.push(). - Cách đọc tags trong
beforeEach, test body, và custom fixture để branch logic. - 4 pattern thực tế: conditional timeout, conditional skip, conditional logging, fixture branch.
- Cách tag từ
describeđược merge vàotestInfo.tags. - Flexible matching bằng
includes()vàsome(). - Sự khác biệt giữa
testInfo.tagsruntime branch và--greppre-run filter. - 4 pitfall phổ biến khi dùng tags để branch.
testInfo.tags Là Gì
testInfo.tags là string[] read-only, chứa toàn bộ tag của test đang chạy — bao gồm tag khai báo trực tiếp trên test và tag inherited từ các describe block bao ngoài.
Đây là property được populate trước khi test body bắt đầu, nghĩa là nó có mặt ngay từ dòng đầu tiên của test callback, trong beforeEach, và trong fixture.
Điểm cốt lõi phân biệt với testInfo.annotations:
- testInfo.tags: read-only, từ khai báo tĩnh (
{ tag: [...] }), dùng để đọc và branch logic. - testInfo.annotations: mutable array, ghi runtime metadata bằng
push(), không ảnh hưởng đến logic test.
Type trong Playwright API (v1.33+):
interface TestInfo {
readonly tags: string[];
// ...
}
Keyword readonly là thực sự — runtime push vào array này không có hiệu lực (hoặc throw tùy environment).
Cú Pháp Cơ Bản
Truy cập testInfo bằng tham số thứ hai của callback test:
import { test, expect } from '@playwright/test';
test('flow', { tag: ['@smoke', '@critical'] }, async ({ page }, testInfo) => {
console.log(testInfo.tags); // ['@smoke', '@critical']
if (testInfo.tags.includes('@smoke')) {
// smoke-specific logic
await page.goto('/quick-check');
} else {
await page.goto('/full-flow');
}
});
Tham số testInfo cũng khả dụng trong các hook và fixture — không chỉ trong test body.
// Trong beforeEach
test.beforeEach(async ({ page }, testInfo) => {
console.log('Running:', testInfo.title, '| Tags:', testInfo.tags);
});
// Trong custom fixture
const myFixtures = base.extend({
myService: async ({}, use, testInfo) => {
console.log('Fixture sees tags:', testInfo.tags);
await use(someService);
},
});
Pattern: Conditional Timeout
Thay vì set timeout riêng lẻ cho từng test slow, tập trung logic vào beforeEach:
import { test } from '@playwright/test';
test.beforeEach(async ({}, testInfo) => {
if (testInfo.tags.includes('@slow')) {
test.setTimeout(120_000);
}
});
test('quick check', { tag: '@smoke' }, async ({ page }) => {
// timeout mặc định (30s hoặc theo config)
await page.goto('/');
});
test('full data export', { tag: ['@regression', '@slow'] }, async ({ page }) => {
// timeout được set thành 120s bởi beforeEach vì có @slow
await page.goto('/export');
await page.getByRole('button', { name: 'Export All' }).click();
await page.waitForSelector('[data-status="done"]', { timeout: 110_000 });
});
Lợi ích: timeout policy được manage ở 1 chỗ. Thêm test mới chỉ cần gắn @slow, không cần nhớ set timeout thủ công.
Pattern này cũng dùng được để điều chỉnh actionTimeout và navigationTimeout riêng biệt nếu cần:
test.beforeEach(async ({ page }, testInfo) => {
if (testInfo.tags.includes('@slow')) {
test.setTimeout(120_000);
// Nếu page đã được tạo, có thể set riêng:
page.setDefaultTimeout(30_000);
page.setDefaultNavigationTimeout(60_000);
}
});
Pattern: Conditional Skip
Gắn tag mang ý nghĩa "browser constraint" hoặc "environment constraint" rồi skip tập trung ở hook:
test.beforeEach(async ({ browserName }, testInfo) => {
if (testInfo.tags.includes('@chromium-only') && browserName !== 'chromium') {
test.skip();
}
});
test('drag-and-drop', { tag: ['@chromium-only', '@ui'] }, async ({ page }) => {
// Chỉ chạy trên chromium — bị skip tự động trên firefox và webkit
await page.goto('/kanban');
await page.dragAndDrop('[data-card="1"]', '[data-column="done"]');
});
Tương tự, skip theo environment:
test.beforeEach(async ({}, testInfo) => {
const env = process.env.TEST_ENV ?? 'local';
if (testInfo.tags.includes('@staging-only') && env !== 'staging') {
test.skip(true, `Tag @staging-only: skipped on ${env}`);
}
if (testInfo.tags.includes('@no-prod') && env === 'production') {
test.skip(true, 'Tag @no-prod: destructive test, blocked on production');
}
});
Truyền message vào test.skip(true, reason) sẽ hiển thị trong HTML report — reviewer biết ngay lý do skip mà không cần đọc code.
Pattern: Conditional Logging
Test được đánh tag @flaky thường khó debug vì không biết chính xác điều gì thay đổi giữa các lần chạy. Verbose logging giúp thu thập thêm tín hiệu mà không làm chậm toàn bộ suite:
test.beforeEach(async ({ page }, testInfo) => {
if (testInfo.tags.includes('@flaky')) {
page.on('console', msg => console.log('PAGE:', msg.text()));
page.on('request', req => console.log('REQ:', req.url()));
page.on('response', res => console.log('RES:', res.status(), res.url()));
}
});
test('payment status poll', { tag: ['@payment', '@flaky'] }, async ({ page }) => {
// Nếu chạy mà có @flaky → tất cả console, request, response được log
await page.goto('/payment-status/123');
await expect(page.getByTestId('status')).toHaveText('confirmed', { timeout: 60_000 });
});
Sau khi test ổn định và bỏ tag @flaky, logging tự tắt mà không cần sửa code trong test body.
Có thể kết hợp với testInfo.attachments để ghi network log ra file đính kèm report:
test.afterEach(async ({}, testInfo) => {
if (testInfo.tags.includes('@flaky') && testInfo.status !== 'passed') {
// Đính kèm log vào report khi test fail
await testInfo.attach('network-log', {
body: Buffer.from(networkLogs.join('\n')),
contentType: 'text/plain',
});
}
});
Pattern: Fixture Branch
Custom fixture có thể đọc testInfo.tags để quyết định provide instance nào:
import { test as base } from '@playwright/test';
import { mockService, realService } from './services';
type Fixtures = {
apiService: typeof realService;
};
export const test = base.extend<Fixtures>({
apiService: async ({}, use, testInfo) => {
if (testInfo.tags.includes('@offline')) {
await use(mockService);
} else {
await use(realService);
}
},
});
// Trong spec file
import { test, expect } from './fixtures';
// Dùng mock — không cần network
test('list products offline', { tag: ['@offline', '@smoke'] }, async ({ apiService }) => {
const products = await apiService.getProducts();
expect(products).toHaveLength(3); // mock trả về data cố định
});
// Dùng real service
test('list products live', { tag: '@integration' }, async ({ apiService }) => {
const products = await apiService.getProducts();
expect(products.length).toBeGreaterThan(0);
});
So với cách truyền flag qua env variable, fixture branch dựa trên tag rõ ràng hơn vì tag visible ngay tại khai báo test — reviewer đọc spec file biết ngay test nào dùng mock.
Tag Từ Describe — Merge Behavior
Như đã nói ở bài 45, testInfo.tags chứa tag merged từ tất cả describe bao ngoài cộng với tag của chính test. Điều này có nghĩa là branch logic trong fixture hoặc beforeEach sẽ kích hoạt cho toàn bộ test trong describe mà không cần khai báo lại.
test.describe('Offline Suite', { tag: '@offline' }, () => {
// Cả 3 test dưới đây đều có @offline trong testInfo.tags
// → fixture apiService của chúng đều dùng mockService
test('get products', async ({ apiService }) => {
const list = await apiService.getProducts();
expect(list).toHaveLength(3);
});
test('get product detail', async ({ apiService }) => {
const product = await apiService.getProduct('p1');
expect(product.name).toBe('Mock Product 1');
});
test('create order', { tag: '@smoke' }, async ({ apiService }) => {
// Tags: @offline + @smoke
const order = await apiService.createOrder({ productId: 'p1' });
expect(order.id).toBeDefined();
});
});
Thứ tự trong testInfo.tags không được đảm bảo — không nên dựa vào index. Luôn dùng includes() hoặc some() để kiểm tra sự có mặt của tag.
Flexible Matching
Hai cách match tag phổ biến:
Exact Match
// Kiểm tra 1 tag cụ thể
if (testInfo.tags.includes('@smoke')) { ... }
// Kiểm tra nhiều tag — test có bất kỳ tag nào
if (testInfo.tags.some(t => ['@smoke', '@critical'].includes(t))) { ... }
// Kiểm tra test có cả 2 tag
const hasSmoke = testInfo.tags.includes('@smoke');
const hasCritical = testInfo.tags.includes('@critical');
if (hasSmoke && hasCritical) { ... }
Prefix Match
// Match bất kỳ tag nào bắt đầu bằng @p (priority tags: @p0, @p1, @p2)
const hasPriority = testInfo.tags.some(t => t.startsWith('@p'));
// Lấy priority level cụ thể
const priorityTag = testInfo.tags.find(t => t.startsWith('@p'));
const priorityLevel = priorityTag ? parseInt(priorityTag.slice(2)) : null;
// Áp dụng: p0 và p1 được retry thêm
if (priorityLevel !== null && priorityLevel <= 1) {
// Tăng retry cho test critical
test.setTimeout(testInfo.timeout * 1.5);
}
Prefix match hữu ích khi taxonomy có nhiều giá trị trong 1 dimension (priority, team, môi trường) và muốn check dimension mà không cần liệt kê từng giá trị.
testInfo.tags vs --grep
Hai cơ chế liên quan đến tag nhưng hoạt động hoàn toàn khác nhau:
--grep |
testInfo.tags |
|
|---|---|---|
| Thời điểm | Trước khi chạy (pre-run filter) | Trong khi chạy (runtime branch) |
| Tác động | Test không match → không được schedule | Mọi test vẫn chạy, code path khác nhau |
| Mục đích | Chọn subset test để chạy | Điều chỉnh behavior bên trong test |
| Ai dùng | CLI, CI pipeline | Test code, fixture, hook |
Ví dụ minh họa sự khác biệt:
# --grep: chỉ test có @slow được chạy, test khác bị bỏ qua hoàn toàn
npx playwright test --grep "@slow"
// testInfo.tags: tất cả test đều chạy
// test nào có @slow thì timeout được tăng lên
test.beforeEach(async ({}, testInfo) => {
if (testInfo.tags.includes('@slow')) {
test.setTimeout(120_000);
}
});
Hai cơ chế có thể kết hợp: dùng --grep "@slow" để chọn đúng subset cần debug, đồng thời testInfo.tags trong beforeEach tự động set timeout phù hợp cho chúng.
Combine Fixture Option Và Tag
Fixture option và tag có thể được kết hợp trong cùng 1 fixture để đạt granularity cao hơn:
type Fixtures = {
variant: 'default' | 'mock';
apiService: typeof realService;
};
export const test = base.extend<Fixtures>({
variant: ['default', { option: true }],
apiService: async ({ variant }, use, testInfo) => {
// Branch dựa trên cả option lẫn tag
if (testInfo.tags.includes('@offline') || variant === 'mock') {
await use(mockService);
} else {
await use(realService);
}
},
});
Sử dụng:
// Dùng mock vì tag @offline
test('product list offline', { tag: '@offline' }, async ({ apiService }) => {
// ...
});
// Dùng mock vì option variant=mock (không cần tag)
test.use({ variant: 'mock' });
test('product list mock variant', async ({ apiService }) => {
// ...
});
// Dùng real (không có @offline, không có variant override)
test('product list live', async ({ apiService }) => {
// ...
});
Pattern này giữ nguyên tính linh hoạt của fixture option (override per file) trong khi vẫn cho phép per-test override bằng tag.
Tags Trong Reporter
Đọc testInfo.tags bên trong test không ảnh hưởng đến cách reporter hiển thị tags. Reporter lấy tag từ khai báo tĩnh — không phụ thuộc vào việc code có đọc tags hay không.
Điều này có nghĩa:
- Gắn tag
@slowđể tăng timeout trongbeforeEach→ tag@slowvẫn hiển thị bình thường trong HTML report như các tag khác. - Nếu test skip do
testInfo.tags.includes('@chromium-only')→ report ghi nhận là skipped, tag vẫn thấy. - Tags được đọc hay không đọc, reporter behavior không thay đổi.
Không có cách "ẩn" 1 tag khỏi reporter trong khi vẫn đọc nó ở runtime. Nếu tag chỉ phục vụ internal logic và không muốn xuất hiện trong report, cân nhắc dùng env variable hoặc fixture option thay vì tag.
Limitations
- Read-only: không thể push tag mới vào
testInfo.tagsruntime. Dynamic metadata phải dùngtestInfo.annotations.push(). - Tag inherit describe không thể override: test con không thể loại bỏ tag của describe cha khỏi
testInfo.tags. Nếu describe có@offline, mọi test con đều bị fixture branch về mock dù muốn hay không. - Thứ tự không được đảm bảo:
testInfo.tags[0]không nhất thiết là tag đầu tiên khai báo. Luôn dùngincludes()/some(). - Không có validation tích hợp: tag typo không được phát hiện —
testInfo.tags.includes('@flakey')không match test có tag@flaky, và Playwright không cảnh báo.
Pitfalls
Pitfall 1: Tag Typo Gây Silent Skip Branch
// Test khai báo @flaky
test('poll status', { tag: '@flaky' }, async ({ page }, testInfo) => {
// ...
});
// beforeEach check sai tên tag
test.beforeEach(async ({ page }, testInfo) => {
if (testInfo.tags.includes('@flakey')) { // typo: flakey != flaky
page.on('console', msg => console.log(msg.text()));
}
// Log không bao giờ bật → debug infra "hoạt động" nhưng không làm gì
});
Cách phòng: define tag constant thay vì dùng literal string trực tiếp.
// tags.ts
export const Tags = {
FLAKY: '@flaky',
SLOW: '@slow',
OFFLINE: '@offline',
CHROMIUM_ONLY: '@chromium-only',
} as const;
// Trong test
import { Tags } from './tags';
if (testInfo.tags.includes(Tags.FLAKY)) { ... }
Pitfall 2: Push Tag Runtime Trực Tiếp
// KHÔNG làm — read-only có nghĩa là vậy
testInfo.tags.push('@new-tag');
// Có thể throw TypeError hoặc không có hiệu lực
// Tag không xuất hiện trong report, --grep sau không thấy
// ĐÚNG nếu muốn metadata runtime:
testInfo.annotations.push({ type: 'dynamic', description: 'extra-info' });
Pitfall 3: Confuse tags vs titlePath
// testInfo.tags — chứa tag gắn qua { tag: [...] }
testInfo.tags // ['@smoke', '@auth']
// testInfo.titlePath — chứa tên file, describe, test theo path
testInfo.titlePath // ['auth.spec.ts', 'Auth Suite', 'login']
// Không dùng titlePath.includes('@smoke') để check tag
// Không dùng tags.includes('Auth Suite') để check describe name
Pitfall 4: Quên @ Prefix Trong includes Check
// SAI — check 'slow' thay vì '@slow'
if (testInfo.tags.includes('slow')) {
test.setTimeout(120_000); // Không bao giờ chạy
}
// ĐÚNG
if (testInfo.tags.includes('@slow')) {
test.setTimeout(120_000);
}
Tag luôn được lưu với @ prefix trong testInfo.tags. Nếu khai báo tag thiếu @ ({ tag: 'slow' }), thì tag trong array cũng không có @ — và filter CLI sẽ không nhận ra nó.
Tổng Kết
testInfo.tagslàreadonly string[]— đọc được nhưng không thêm được runtime.- Khả dụng trong
beforeEach,afterEach, test body, và custom fixture. - Chứa cả tag khai báo trực tiếp lẫn inherited từ
describe— thứ tự không guaranteed. - 4 pattern thực tế: conditional timeout (beforeEach), conditional skip (beforeEach + browserName), conditional logging (beforeEach + page events), fixture branch (base.extend).
- Dùng
includes()cho exact match,some(t => t.startsWith(prefix))cho prefix match. testInfo.tagsbranch logic IN test — mọi test vẫn chạy.--grepfilter BEFORE run — test không match không chạy.- Đọc tags không ảnh hưởng reporter — reporter luôn hiển thị tags từ khai báo tĩnh.
- Tag typo là silent bug — dùng constant object để tránh.
Quiz
Câu 1. testInfo.tags.push('@extra') bên trong beforeEach có hoạt động không? Giải thích lý do và nêu cách đúng nếu muốn gắn thêm metadata runtime.
Đáp án
Không hoạt động. testInfo.tags là readonly — runtime push không có hiệu lực (hoặc throw). Để gắn metadata runtime, dùng testInfo.annotations.push({ type: 'extra', description: '...' }). Annotation xuất hiện trong HTML report nhưng không thể dùng cho --grep.
Câu 2. Test nằm trong test.describe('Suite', { tag: '@offline' }) và được khai báo { tag: '@smoke' }. Giá trị testInfo.tags khi test chạy chứa những gì? Fixture branch theo @offline có kích hoạt không?
Đáp án
testInfo.tags chứa cả @offline lẫn @smoke (thứ tự không guaranteed). Fixture kiểm tra testInfo.tags.includes('@offline') sẽ trả về true → fixture branch về mock sẽ kích hoạt.
Câu 3. Muốn kiểm tra test có bất kỳ priority tag nào (@p0, @p1, @p2, @p3) mà không liệt kê từng giá trị, viết điều kiện nào?
Đáp án
const hasPriority = testInfo.tags.some(t => t.startsWith('@p'));
Hoặc nếu cần giới hạn chỉ @p0–@p3 (không match @payment): testInfo.tags.some(t => /^@p\d+$/.test(t)).
Câu 4. Sự khác biệt chính giữa testInfo.tags và --grep khi cùng liên quan đến tag là gì?
Đáp án
--grep hoạt động trước khi test chạy — test không match pattern sẽ không được schedule. testInfo.tags hoạt động trong khi test chạy — mọi test đều được chạy, nhưng code path bên trong test có thể khác nhau tùy tag. Hai cơ chế phục vụ mục đích khác nhau và có thể kết hợp.
Câu 5. beforeEach check testInfo.tags.includes('@slow') và gọi test.setTimeout(120_000). Test không có tag @slow nhưng nằm trong describe có tag @slow. Timeout có được set thành 120s không?
Đáp án
Có. testInfo.tags bao gồm cả tag inherited từ describe — test không cần khai báo @slow trực tiếp. Nếu describe cha có { tag: '@slow' }, tất cả test con đều có @slow trong testInfo.tags và điều kiện trong beforeEach sẽ kích hoạt.
