Mục lục
- Mục Tiêu Bài Học
- Recap Nhanh Từ Series 1 — Và Bài Này Khác Gì
- Skip + Tag + Annotation Trong Cùng Khai Báo (v1.42+)
- Skip Nhiều Tag Và Nhiều Annotation
- testInfo.skip() — Skip Qua Fixture
- Phân Biệt test.skip() vs testInfo.skip()
- Conditional Skip Kết Hợp Browser + OS
- Multiple Skip Conditions Trong Một Test
- Skip Kết Hợp Async Lookup
- Programmatic Annotation Push Lúc Runtime
- Skip Trong Reporter — HTML, JUnit, List
- ESM / CJS Edge Case
- Limitation
- Pitfall Thường Gặp
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Dùng được
test.skip()chaining với{ tag, annotation }object trong cùng một khai báo (v1.42+). - Phân biệt
test.skip(condition, reason)trong body vàtestInfo.skip(condition, reason)từ fixture — biết khi nào dùng cái nào. - Viết được pattern multiple skip conditions kết hợp async data fetch.
- Push annotation vào
testInfo.annotationslúc runtime và hiểu output trong các reporter. - Tránh 4 pitfall điển hình liên quan đến skip nâng cao.
Recap Nhanh Từ Series 1 — Và Bài Này Khác Gì
Series 1 bài 402 đã cover 3 hình thức cơ bản:
- Static skip —
test.skip('name', fn)dùngtest.skipthay chotesttại khai báo. - Conditional skip trong body —
test.skip(condition, reason)bên trong hàm async. - Group skip —
test.describe.skip(name, fn)skip cả nhóm.
Bài này không lặp lại những phần đó. Thay vào đó, bài tập trung vào:
- New signature
test.skip('name', { tag, annotation }, fn)giới thiệu ở v1.42 — skip, tag, annotate trong 1 dòng khai báo. testInfo.skip()— cách skip qua fixturetestInfo, hoạt động ở mọi nơi kể cảbeforeEachvà custom fixtures.- Multiple skip conditions kết hợp async lookup (feature flags, database state...).
- Programmatic annotation push lúc runtime qua
testInfo.annotations.push().
Bài 44 (trong nhóm A.5 này) sẽ đi sâu hơn vào new signature { tag, annotation } với tất cả option. Bài 41 sẽ cover test.fail.only(). Bài này chỉ dùng new signature để minh họa skip chaining.
Skip + Tag + Annotation Trong Cùng Khai Báo (v1.42+)
Trước v1.42, muốn skip một test và đánh tag và attach annotation phải viết ít nhất 2 bước riêng:
// Cách cũ — trước v1.42
test(
'checkout flow',
{ annotation: { type: 'issue', description: 'JIRA-123' } },
async ({ page }) => {
test.skip(true, 'BUG-123 — checkout bị broken');
// ...
}
);
Từ v1.42, test.skip chấp nhận cùng options object như test:
import { test, expect } from '@playwright/test';
test.skip(
'checkout flow',
{
tag: '@known-bug',
annotation: { type: 'issue', description: 'JIRA-123' },
},
async ({ page }) => {
await page.goto('/cart');
// body không bao giờ chạy, nhưng TypeScript vẫn type-check
}
);
Kết quả: test này skip tại load time, đồng thời được tag @known-bug (dùng được với --grep) và có annotation issue hiển thị trong HTML report.
TypeScript inference hoạt động đầy đủ — { tag, annotation } object có type TestDetails. Compiler báo lỗi nếu truyền key không hợp lệ.
Tại sao "chaining"?
Tên "skip chaining" xuất phát từ ý tưởng: một test vừa bị skip, vừa mang theo metadata (tag + annotation) như một chain. Trong Playwright source (v1.42+), test.skip, test.fail, test.fixme, test.slow đều nhận cùng overloaded signature — điều này cho phép kết hợp annotation với annotation modifiers một cách đồng nhất.
Skip Nhiều Tag Và Nhiều Annotation
Cả tag và annotation đều chấp nhận single value hoặc array:
test.skip(
'payment gateway integration',
{
tag: ['@known-bug', '@high-priority'],
annotation: [
{ type: 'issue', description: 'JIRA-456' },
{ type: 'severity', description: 'P1' },
],
},
async ({ page }) => {
await page.goto('/checkout/payment');
// ...
}
);
Trong HTML report, mỗi annotation entry hiển thị như một dòng riêng với type làm label. Tag array được flatten thành danh sách, mỗi tag dùng được với --grep độc lập.
Lưu ý: Tag convention của Playwright yêu cầu prefix @ (ví dụ @smoke, @known-bug). Không có prefix, giá trị vẫn được lưu nhưng CLI filter --grep @tag sẽ không match.
testInfo.skip() — Skip Qua Fixture
testInfo.skip(condition, reason) là method trên object testInfo — fixture built-in của Playwright Test. Gọi nó có hiệu lực tương đương test.skip(condition, reason) trong body, nhưng truy cập qua testInfo thay vì biến test global.
import { test, expect } from '@playwright/test';
test('feature flag check', async ({ page }, testInfo) => {
const features = await fetchFeatureFlags(); // async call
testInfo.skip(!features.darkMode, 'Dark Mode feature not enabled in this env');
await page.goto('/settings/appearance');
await expect(page.getByTestId('dark-mode-toggle')).toBeVisible();
});
Khi testInfo.skip() được gọi với condition true, Playwright throw một internal signal để dừng test. Các dòng sau không chạy, fixture teardown vẫn diễn ra bình thường.
Dùng trong custom fixture
testInfo.skip() đặc biệt hữu ích bên trong custom fixture — nơi bạn không thể truy cập biến test global:
// fixtures/db.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{ db: DatabaseClient }>({
db: async ({}, use, testInfo) => {
const client = new DatabaseClient();
const isReady = await client.ping();
// Skip test ngay nếu DB không sẵn sàng
testInfo.skip(!isReady, 'Database not reachable — skipping test');
await use(client);
await client.close();
},
});
Trong ví dụ này, bất kỳ test nào dùng fixture db đều tự động skip nếu database không ping được — không cần đặt test.skip() trong từng test riêng lẻ.
Phân Biệt test.skip() vs testInfo.skip()
| Tiêu chí | test.skip(condition, reason) |
testInfo.skip(condition, reason) |
|---|---|---|
| Truy cập qua | Biến test import từ @playwright/test |
Fixture testInfo trong function signature |
| Dùng được trong test body | Có | Có |
| Dùng được trong beforeEach | Có | Có |
| Dùng được trong custom fixture | Không khuyến nghị (closure phức tạp) | Có — qua tham số testInfo của fixture fn |
| Kết quả cuối | Status skipped |
Status skipped (giống nhau) |
| Khi nào nên dùng | Trong test body hoặc beforeEach — đơn giản, quen thuộc hơn | Trong custom fixture hoặc khi cần truyền qua layer |
Về output, hai cách hoàn toàn tương đương — reason đều xuất hiện trong HTML report và JUnit <skipped message="...">. Lựa chọn chỉ là về ergonomics.
testInfo.skip() không có tham số
Tương tự test.skip(), có thể gọi không tham số để skip unconditional:
test('wip test', async ({ page }, testInfo) => {
testInfo.skip(); // skip ngay, không điều kiện, không lý do
// ...
});
Bỏ reason là có thể nhưng không nên — xem Pitfall mục 14.
Conditional Skip Kết Hợp Browser + OS
Một pattern phổ biến trong CI matrix: skip khi browser X chạy trên OS Y — vì bug chỉ tái hiện ở kết hợp cụ thể đó, không phải browser hay OS riêng lẻ.
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ browserName }, testInfo) => {
testInfo.skip(
browserName === 'webkit' && process.platform === 'win32',
'WebKit trên Windows: file dialog API bị broken — JIRA-789'
);
});
test('native file picker', async ({ page }) => {
await page.goto('/upload');
await page.getByRole('button', { name: 'Choose file' }).click();
// ...
});
Vì skip được đặt trong beforeEach, mọi test trong block này sẽ bị skip khi đúng điều kiện. Bên ngoài điều kiện đó (WebKit/Linux, Chromium/Windows, v.v.), test chạy bình thường.
Kết hợp nhiều giá trị OS
test('clipboard API', async ({ page, browserName }, testInfo) => {
const isLinuxWebKit = browserName === 'webkit' && process.platform === 'linux';
const isWin32Firefox = browserName === 'firefox' && process.platform === 'win32';
testInfo.skip(
isLinuxWebKit || isWin32Firefox,
'Clipboard API không ổn định trên WebKit/Linux và Firefox/Windows'
);
await page.goto('/editor');
await page.keyboard.press('Control+V');
// ...
});
process.platform trả về 'linux', 'darwin' (macOS), hoặc 'win32' — đây là giá trị Node.js standard, có sẵn không cần import thêm.
Multiple Skip Conditions Trong Một Test
Một test có thể có nhiều lý do độc lập để bị skip. Thay vì kết hợp tất cả vào một boolean expression phức tạp, viết từng testInfo.skip() riêng — mỗi cái có reason riêng, dễ đọc và dễ bỏ từng điều kiện khi không còn cần:
test('end-to-end payment flow', async ({ page, browserName }, testInfo) => {
testInfo.skip(browserName === 'firefox', 'Firefox: payment iframe không load được trên CI');
testInfo.skip(process.env.SKIP_E2E === 'true', 'E2E test bị tắt trong môi trường này');
testInfo.skip(process.env.TEST_ENV === 'staging', 'Staging không có payment gateway thực');
await page.goto('/checkout');
// ...
});
Playwright dừng tại testInfo.skip() đầu tiên có condition true. Các lệnh sau không evaluate. Reason của condition đó được ghi vào report.
Thứ tự quan trọng: đặt condition có khả năng true cao nhất và rẻ nhất (như env check) lên trước để tránh gọi code đắt (network call, DB query) một cách thừa khi đã biết sẽ skip.
Skip Kết Hợp Async Lookup
Một số skip condition cần kết quả từ async call — ví dụ feature flags từ API, trạng thái service health, hoặc dữ liệu seed trong database. Kỹ thuật là await trước rồi truyền kết quả vào testInfo.skip():
import { test, expect } from '@playwright/test';
async function fetchFeatureFlags(): Promise<Record<string, boolean>> {
const res = await fetch('https://flags.internal/api/flags');
return res.json();
}
async function isServiceHealthy(service: string): Promise<boolean> {
try {
const res = await fetch(`https://health.internal/${service}`);
return res.ok;
} catch {
return false;
}
}
test('loyalty dashboard', async ({ page }, testInfo) => {
// Async lookups trước
const [flags, paymentHealthy] = await Promise.all([
fetchFeatureFlags(),
isServiceHealthy('payment'),
]);
testInfo.skip(!flags.loyaltyV2, 'Feature loyaltyV2 chưa được bật trong env này');
testInfo.skip(!paymentHealthy, 'Payment service không healthy — skip test phụ thuộc');
await page.goto('/loyalty');
await expect(page.getByTestId('loyalty-points')).toBeVisible();
});
Dùng Promise.all khi các async call độc lập với nhau để không chờ tuần tự không cần thiết.
Đặt async skip trong beforeAll
Nếu nhiều test trong cùng một describe đều cần cùng async check, tránh gọi lại mỗi test — đặt vào beforeAll và cache kết quả:
test.describe('Loyalty V2 tests', () => {
let loyaltyEnabled = false;
test.beforeAll(async () => {
const flags = await fetchFeatureFlags();
loyaltyEnabled = flags.loyaltyV2;
});
test.beforeEach(async ({}, testInfo) => {
testInfo.skip(!loyaltyEnabled, 'Feature loyaltyV2 not enabled');
});
test('show loyalty points', async ({ page }) => {
// chỉ chạy nếu loyaltyEnabled = true
});
test('redeem loyalty points', async ({ page }) => {
// chỉ chạy nếu loyaltyEnabled = true
});
});
Với cách này, fetchFeatureFlags() chỉ được gọi 1 lần cho cả nhóm thay vì N lần.
Programmatic Annotation Push Lúc Runtime
testInfo.annotations là mutable array — có thể push annotation vào bất kỳ lúc nào trong quá trình test chạy, kể cả sau khi đã skip. Annotation này sẽ xuất hiện trong HTML report cùng với kết quả test.
test('data export flow', async ({ page }, testInfo) => {
// Ghi lại environment metadata vào annotation
testInfo.annotations.push({
type: 'env',
description: process.env.TEST_ENV ?? 'unknown',
});
const buildId = process.env.CI_BUILD_ID;
if (buildId) {
testInfo.annotations.push({
type: 'ci-build',
description: buildId,
});
}
await page.goto('/export');
// ...
});
Dùng case phổ biến hơn — ghi lý do skip động rồi skip:
test('notification flow', async ({ page }, testInfo) => {
const flags = await fetchFeatureFlags();
if (!flags.notifications) {
testInfo.annotations.push({
type: 'skip-reason',
description: `Feature 'notifications' off — checked at ${new Date().toISOString()}`,
});
testInfo.skip(true, 'Feature notifications not enabled');
}
await page.goto('/notifications');
// ...
});
Khác với reason string trong testInfo.skip(), annotation push cho phép lưu thêm metadata có cấu trúc (type + description) thay vì chỉ plain text. HTML report render annotation như một bảng, dễ đọc hơn khi có nhiều trường.
Push annotation trong fixture teardown
Annotation push không giới hạn ở test body — có thể push trong fixture teardown để ghi lại state sau cleanup:
export const test = base.extend<{ cleanup: void }>({
cleanup: async ({}, use, testInfo) => {
await use();
// Sau khi test chạy xong, ghi lại thông tin cleanup
testInfo.annotations.push({
type: 'cleanup',
description: 'DB seed data removed',
});
},
});
Skip Trong Reporter — HTML, JUnit, List
HTML Report
Test skip với annotation và tag hiển thị trong HTML report như sau:
- Status badge màu xám —
skipped. - Section "Annotations" liệt kê từng annotation entry với
typelàm label vàdescriptionlàm nội dung. - Tag list hiển thị bên cạnh tên test, dùng được để filter trong report UI.
- Reason từ
test.skip(condition, reason)hoặctestInfo.skip(condition, reason)hiển thị dưới dạng annotationtype: 'skip'tự động.
JUnit XML
<testcase name="payment gateway integration" classname="checkout.spec.ts" time="0">
<skipped message="JIRA-456 — Payment service broken"/>
<properties>
<property name="issue" value="JIRA-456"/>
<property name="severity" value="P1"/>
</properties>
</testcase>
Annotation entries được map thành <property> elements. Các CI tool như Jenkins JUnit plugin đọc được các property này.
List reporter (terminal)
- 1 [chromium] › checkout.spec.ts:12:5 › payment gateway integration (skipped)
✓ 2 [chromium] › checkout.spec.ts:28:5 › product listing (passed)
1 skipped
1 passed (1.8s)
Reason không hiển thị trong list reporter — chỉ có status. Để xem reason, mở HTML report hoặc dùng --reporter=line verbose.
Tag filter
Skip test với tag vẫn có thể được lọc qua --grep:
# Chỉ chạy (và skip) test có tag @known-bug
npx playwright test --grep @known-bug
# Chạy tất cả, bỏ qua test có tag @known-bug
npx playwright test --grep-invert @known-bug
Chi tiết về tag filter sẽ được cover trong bài sau của nhóm A.5 này.
ESM / CJS Edge Case
Một edge case ít gặp nhưng gây ra behavior bất ngờ liên quan đến test.skip() static skip (dạng khai báo) trong ESM module.
ESM top-level await và static skip
Nếu file test dùng ESM (type: "module" trong package.json hoặc file .mts) và có top-level await trước khai báo test:
// CAUTION: ESM với top-level await
const config = await loadConfig(); // top-level await
test.skip('some test', async ({ page }) => {
// ...
});
Playwright có thể không nhận ra skip tại load time như dự kiến vì module load là async — behavior phụ thuộc vào version runtime. Với CJS (mặc định trong hầu hết Playwright project hiện tại), top-level await không tồn tại nên không có vấn đề này.
Dynamic import trong fixture
Tương tự, nếu fixture dùng dynamic import() để load conditional dependency:
export const test = base.extend({
specialClient: async ({}, use, testInfo) => {
// Dynamic import — chỉ load khi cần
const { SpecialClient } = await import('./special-client.js');
const client = new SpecialClient();
const available = await client.isAvailable();
testInfo.skip(!available, 'SpecialClient service not available');
await use(client);
await client.close();
},
});
Pattern này hoạt động bình thường vì skip được gọi qua testInfo trong async context — không phải load-time static skip.
Kết luận về ESM
Nếu project dùng ESM: ưu tiên dùng testInfo.skip() trong fixture hoặc test body thay vì test.skip() dạng static declaration phụ thuộc vào load-time evaluation. CJS project không có vấn đề này.
Limitation
- Không có audit trail giữa các run: Playwright không tích hợp sẵn cơ chế theo dõi "test này đã skip bao nhiêu run liên tiếp". Để biết skip kéo dài bao lâu, phải parse report history từ CI artifacts bên ngoài.
- Reason không được enforce: Không có cơ chế bắt buộc truyền reason vào
test.skip()haytestInfo.skip(). Cần convention + review để đảm bảo team không bỏ reason. - Conditional skip phụ thuộc env không reproducible: Khi skip condition là runtime state (feature flags, service health), reproduce lại behavior "tại sao bài này skip trên CI hôm qua" trở nên khó nếu env đã thay đổi.
- Annotation trong declaration không có ở report terminal: Annotation push và tag metadata chỉ xuất hiện đầy đủ trong HTML report và JUnit XML. List reporter terminal chỉ hiện status
skipped— không có tag, không có annotation details.
Pitfall Thường Gặp
1. Bỏ reason khi dùng testInfo.skip() — khó audit sau
// BAD: không có reason
test('flow', async ({ page }, testInfo) => {
testInfo.skip(!process.env.ENABLE_FEATURE);
// ...
});
// GOOD: reason rõ ràng
test('flow', async ({ page }, testInfo) => {
testInfo.skip(!process.env.ENABLE_FEATURE, 'Set ENABLE_FEATURE=1 để chạy test này');
// ...
});
2. Nhầm test.skip() declaration với testInfo.skip() trong body
// BAD: test.skip() không phải là method của testInfo
test('flow', async ({ page }, testInfo) => {
testInfo.test.skip(); // TypeError — testInfo không có property test
// ...
});
// GOOD
test('flow', async ({ page }, testInfo) => {
testInfo.skip(condition, 'reason'); // đúng
// hoặc
test.skip(condition, 'reason'); // cũng đúng, nhưng dùng biến test global
});
3. Đặt testInfo.skip() trong afterEach — không có tác dụng
// BAD: test đã chạy xong, skip không có hiệu lực
test.afterEach(async ({}, testInfo) => {
if (somePostTestCondition) {
testInfo.skip(true, 'Try to skip after test'); // quá muộn
}
});
// GOOD: đặt trong beforeEach hoặc đầu test body
test.beforeEach(async ({}, testInfo) => {
testInfo.skip(someCondition, 'reason');
});
4. Conditional skip với condition luôn true — tương đương static skip nhưng che giấu intent
// BAD: hardcoded true — nhìn có vẻ conditional nhưng không bao giờ chạy
test('some test', async ({ page }, testInfo) => {
testInfo.skip(true, 'Temporarily disabled'); // luôn skip
// ...
});
// GOOD: nếu muốn skip toàn bộ, dùng static skip tại khai báo
test.skip('some test', async ({ page }) => {
// ...
});
// hoặc gắn annotation rõ ràng:
test.skip(
'some test',
{ annotation: { type: 'wip', description: 'Not implemented yet' } },
async ({ page }) => {
// ...
}
);
Quiz
Câu 1. Khai báo sau đây có hợp lệ với Playwright v1.42+ không? Nếu có, test sẽ xuất hiện như thế nào trong HTML report?
test.skip(
'payment integration',
{ tag: '@known-bug', annotation: { type: 'issue', description: 'JIRA-100' } },
async ({ page }) => { ... }
);
Đáp án
Hợp lệ với v1.42+. Test xuất hiện trong HTML report với status skipped, tag @known-bug hiển thị cạnh tên test, và annotation issue: JIRA-100 trong section Annotations. Tag cũng dùng được để filter: --grep @known-bug.
Câu 2. Sự khác biệt về nơi sử dụng giữa test.skip(condition, reason) và testInfo.skip(condition, reason) là gì? Khi nào bắt buộc phải dùng testInfo.skip()?
Đáp án
Cả hai đều dùng được trong test body và beforeEach. Tuy nhiên, testInfo.skip() là lựa chọn bắt buộc (hoặc ít nhất là tự nhiên nhất) khi cần skip bên trong custom fixture — nơi bạn không truy cập được biến test global nhưng có testInfo trong function signature của fixture.
Câu 3. Code sau có hoạt động như mong đợi không? Nếu không, vấn đề là gì?
test('async skip test', async ({ page }, testInfo) => {
const flags = await fetchFeatureFlags();
testInfo.skip(!flags.newFeature, 'Feature not enabled');
testInfo.skip(browserName === 'firefox', 'Firefox not supported');
await page.goto('/new-feature');
});
Đáp án
Code có lỗi TypeScript: browserName không phải fixture tự có trong test signature — phải destructure từ fixture object: async ({ page, browserName }, testInfo). Nếu sửa lại như vậy thì logic hoạt động đúng: dừng tại testInfo.skip() đầu tiên có condition true.
Câu 4. Bạn có nhiều test trong cùng một describe đều cần check feature flag từ API. Cách tốt nhất để tránh gọi API nhiều lần là gì?
Đáp án
Dùng test.beforeAll để gọi API 1 lần và lưu kết quả vào biến outer-scope. Sau đó dùng test.beforeEach với testInfo.skip(!cachedResult, reason) để mỗi test được skip dựa trên giá trị đã cached. Với cách này, N test trong nhóm chỉ trigger 1 API call thay vì N calls.
Câu 5. Khi dùng testInfo.annotations.push({ type: 'env', description: 'staging' }), annotation này xuất hiện ở đâu trong output? Có xuất hiện trong terminal list reporter không?
Đáp án
Annotation push xuất hiện đầy đủ trong HTML report (section Annotations của test) và JUnit XML (dưới dạng <property> element). Terminal list reporter không hiển thị annotation details — chỉ hiện status pass/fail/skip. Để xem annotation trên terminal cần dùng reporter khác (JSON reporter hoặc custom reporter).
