Mục lục
- Mục Tiêu Bài Học
- test.step.skip() Là Gì?
- Cú Pháp
- Hành Vi Khi Chạy
- Phân Biệt test.skip() vs test.step.skip()
- Hiển Thị Trong Reporter Và Trace Viewer
- Use Case Thực Tế
- Pattern Conditional Step Skip
- So Sánh Với if/else Thuần
- Không Có test.step.fixme() Và test.step.only()
- Limitation
- Pitfall Thường Gặp
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi đọc xong bài này, bạn sẽ:
- Giải thích được
test.step.skip()hoạt động thế nào và kháctest.skip()ở điểm gì. - Biết step bị skip ảnh hưởng như thế nào đến test status, reporter, và Trace Viewer.
- Áp dụng pattern conditional step skip thay cho
if/elsethuần để giữ audit trail trong report. - Nhận biết được 4 pitfall phổ biến khi dùng
test.step.skip(). - Hiểu giới hạn của API này ở v1.49: không có
test.step.fixme()haytest.step.only().
test.step.skip() Là Gì?
test.step.skip(name, fn) là variant của test.step(), được thêm từ Playwright v1.49. Khi gọi, Playwright đăng ký step với tên đã chỉ định nhưng không thực thi function fn bên trong. Tên step vẫn xuất hiện trong reporter với trạng thái skipped.
Điểm mấu chốt: step bị skip nhưng test không dừng. Các step tiếp theo sau test.step.skip() vẫn chạy bình thường. Đây là điểm khác biệt cốt lõi so với test.skip() — vốn dừng toàn bộ test ngay lập tức.
API này thuộc cùng nhóm v1.49 với annotation box option trong test.step(). Cả hai đều mở rộng cách kiểm soát step ở level chi tiết hơn trước.
Cú Pháp
Cú pháp giống hệt test.step(), chỉ khác ở chỗ gọi test.step.skip thay vì test.step:
import { test, expect } from '@playwright/test';
test('checkout', async ({ page }) => {
await test.step('add to cart', async () => {
await page.getByRole('button', { name: 'Add' }).click();
});
await test.step.skip('apply discount', async () => {
// Code bên trong KHÔNG chạy
await page.getByRole('button', { name: 'Discount' }).click();
});
await test.step('confirm', async () => {
await page.getByRole('button', { name: 'Confirm' }).click();
});
});
Lưu ý: vẫn phải await trước test.step.skip(). Bỏ await gây vấn đề tương tự như với test.step() — xem mục Pitfall.
test.step.skip() cũng hỗ trợ return value theo signature. Tuy nhiên vì function bên trong không chạy, giá trị trả về sẽ là undefined — không nên dùng return value từ step bị skip.
Hành Vi Khi Chạy
Khi Playwright gặp test.step.skip() trong quá trình chạy:
- Đăng ký step với tên đã chỉ định vào danh sách step của test.
- Đánh dấu step đó là
skipped— không gọi functionfn. - Trả về ngay lập tức (Promise resolve với
undefined). - Tiếp tục thực thi các dòng code sau trong test body.
Test status cuối cùng không bị ảnh hưởng bởi step bị skip. Nếu tất cả step còn lại pass, test vẫn được báo là passed. Step skip không làm test fail và không làm test chuyển sang skipped.
Ví dụ output từ list reporter với test có một step bị skip:
✓ [chromium] › checkout.spec.ts:3:5 › checkout (2.1s)
✓ add to cart (0.7s)
⊘ apply discount (skipped)
✓ confirm (1.3s)
Step apply discount hiển thị với ký hiệu skip (ví dụ ⊘ tùy reporter theme) và thời gian gần bằng 0 vì không có code chạy bên trong.
Phân Biệt test.skip() vs test.step.skip()
Đây là điểm dễ nhầm nhất khi mới tiếp cận API này:
| Tiêu chí | test.skip() |
test.step.skip() |
|---|---|---|
| Phạm vi skip | Toàn bộ test | Chỉ step đó |
| Test có tiếp tục chạy không? | Không — dừng ngay | Có — step sau vẫn chạy |
| Test status cuối cùng | skipped |
Phụ thuộc step còn lại (thường passed) |
| Vị trí gọi | Đầu test hoặc ngoài test (declaration) | Bên trong body của test |
| Hiển thị trong reporter | Test status skipped |
Step đơn lẻ status skipped |
| Có sẵn từ version | Từ đầu | v1.49 |
Quy tắc đơn giản để nhớ: dùng test.skip() khi toàn bộ test không thể / không nên chạy; dùng test.step.skip() khi chỉ một bước trong flow cần bỏ qua nhưng các bước khác vẫn có giá trị kiểm tra.
Hiển Thị Trong Reporter Và Trace Viewer
HTML Report
Mở bằng npx playwright show-report. Step bị skip hiển thị inline trong danh sách step của test với icon và label skipped. Không có section riêng "Skipped Steps" — step skip nằm xen kẽ trong chronological flow của test, đúng vị trí nó được khai báo trong code.
Điều này giúp người review report thấy ngay: step nào bị skip, ở vị trí nào trong flow, và step nào xung quanh nó đã pass.
List Reporter (CLI)
Step skip xuất hiện giống như step pass/fail nhưng với ký hiệu khác. Duration của step skip thường hiển thị là 0ms hoặc rất nhỏ vì không có code thực sự chạy.
Trace Viewer
Trong Trace Viewer, step skip xuất hiện trong panel Action log ở phía trái với trạng thái skipped. Khi click vào step đó, phần action list bên trong rỗng — không có network request, không có screenshot, không có DOM interaction — vì function bên trong chưa bao giờ được gọi.
Đây là điểm khác biệt trực quan rõ ràng nhất giữa step skip và step pass có ít action.
JUnit / JSON Reporter
Với JUnit reporter, step skip xuất hiện trong XML output với attribute phản ánh trạng thái skipped. Hữu ích khi CI pipeline đọc JUnit XML để aggregate kết quả.
Use Case Thực Tế
1. Feature Flag — Bước Chỉ Chạy Khi Flag Enable
Khi một tính năng đang trong giai đoạn rollout, bước test tính năng đó chỉ có nghĩa trên môi trường bật flag. Thay vì xóa hoặc comment out step, dùng step.skip để giữ code nhưng bỏ qua khi flag tắt:
test('payment flow', async ({ page }) => {
const flagEnabled = process.env.FEATURE_NEW_PAYMENT === 'true';
await test.step('fill card details', async () => {
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/28');
await page.getByLabel('CVV').fill('123');
});
// Bước "save card" chỉ hiển thị khi feature flag bật
if (flagEnabled) {
await test.step('save card for future use', async () => {
await page.getByLabel('Save card').check();
});
} else {
await test.step.skip('save card for future use', async () => {
// step vẫn hiện trong report nhưng bị skip
});
}
await test.step('confirm payment', async () => {
await page.getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByText('Payment successful')).toBeVisible();
});
});
2. WIP Step — Step Chưa Implement Xong
Khi một bước trong flow chưa implement xong ở phía frontend (nhưng các bước khác đã ready để test), skip step đó để test pass mà không phải tách thành test riêng:
test('user profile update', async ({ page }) => {
await test.step('navigate to profile', async () => {
await page.goto('/profile');
});
await test.step.skip('update avatar', async () => {
// TODO: avatar upload chưa ready, skip tạm
await page.getByRole('button', { name: 'Upload avatar' }).click();
});
await test.step('update display name', async () => {
await page.getByLabel('Display name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Profile updated')).toBeVisible();
});
});
3. Conditional Flow — Skip Khi Điều Kiện Không Match
Một số flow có bước tuỳ chọn phụ thuộc vào state của hệ thống. Ví dụ: step "dismiss onboarding modal" chỉ cần chạy khi user mới lần đầu đăng nhập:
test('dashboard access', async ({ page }) => {
await test.step('login', async () => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Login' }).click();
});
const isFirstLogin = await page.getByRole('dialog', { name: 'Welcome' }).isVisible();
if (isFirstLogin) {
await test.step('dismiss onboarding modal', async () => {
await page.getByRole('button', { name: 'Get started' }).click();
});
} else {
await test.step.skip('dismiss onboarding modal', async () => {});
}
await test.step('verify dashboard loaded', async () => {
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
});
4. Debug Tạm Thời — Skip Để Isolate Vấn Đề
Khi debug một test phức tạp, tạm skip một số step để thu hẹp phạm vi tìm lỗi. Nhanh hơn comment out và dễ revert hơn:
test('e2e checkout', async ({ page }) => {
await test.step('add items', async () => { /* ... */ });
await test.step('apply coupon', async () => { /* ... */ });
await test.step.skip('verify cart total', async () => {
// Tạm skip để debug bước tiếp theo
});
await test.step('proceed to payment', async () => { /* ... */ });
});
Pattern Conditional Step Skip
Pattern phổ biến nhất: dùng if/else để quyết định giữa test.step() bình thường và test.step.skip(), giữ cùng tên step trong cả hai nhánh:
test('flow', async ({ page }) => {
const isFeatureEnabled = await checkFeature();
if (isFeatureEnabled) {
await test.step('new feature step', async () => {
// code chạy khi feature enable
await page.getByRole('button', { name: 'New Feature' }).click();
});
} else {
await test.step.skip('new feature step', async () => {
// không chạy, nhưng step xuất hiện trong report
});
}
});
Lý do giữ cùng tên step ở cả hai nhánh: Report sẽ nhất quán — dù môi trường nào, tên step đó luôn xuất hiện. Người đọc report biết ngay "step này bị skip ở môi trường này" thay vì phải thắc mắc tại sao step biến mất.
Pattern Tách Thành Helper
Khi logic conditional phức tạp hơn, tách ra helper function:
async function conditionalStep(
name: string,
condition: boolean,
fn: () => Promise
) {
if (condition) {
await test.step(name, fn);
} else {
await test.step.skip(name, fn);
}
}
test('checkout flow', async ({ page }) => {
const hasDiscount = await page.getByText('Promo applied').isVisible();
await conditionalStep('verify discount', hasDiscount, async () => {
await expect(page.getByText('10% off')).toBeVisible();
});
await test.step('confirm order', async () => {
await page.getByRole('button', { name: 'Place order' }).click();
});
});
Helper này tái sử dụng được và giữ logic test sạch hơn so với lặp đi lặp lại if/else trong mỗi test.
So Sánh Với if/else Thuần
Trước khi có test.step.skip(), cách duy nhất để bỏ qua một bước là dùng if condition:
// Cách cũ — if/else thuần
test('checkout', async ({ page }) => {
if (featureEnabled) {
await test.step('new feature step', async () => {
await page.getByRole('button', { name: 'Feature' }).click();
});
// Nếu featureEnabled = false: step không tồn tại trong report
}
});
// Cách mới — step.skip explicit
test('checkout', async ({ page }) => {
if (featureEnabled) {
await test.step('new feature step', async () => {
await page.getByRole('button', { name: 'Feature' }).click();
});
} else {
await test.step.skip('new feature step', async () => {});
// Step vẫn hiện trong report với status skipped
}
});
So sánh cụ thể:
| Tiêu chí | if không có step |
test.step.skip() |
|---|---|---|
| Xuất hiện trong reporter | Không — step "vô hình" | Có — hiển thị status skipped |
| Trace Viewer log | Không có entry | Có entry với status skipped |
| Khả năng audit | Phải đọc code mới biết step đó tồn tại | Thấy ngay từ report |
| Tính nhất quán report | Report thay đổi cấu trúc theo môi trường | Report luôn có cùng set step, chỉ khác status |
| Debug | Khó biết bước nào bị bỏ qua | Rõ ràng ngay trong report |
test.step.skip() có giá trị chính ở tính observability: người đọc report biết step đó tồn tại và có chủ đích bị skip, không phải bị xóa hay quên.
Không Có test.step.fixme() Và test.step.only()
Ở cấp test, Playwright có test.skip(), test.fixme(), test.only(), và test.fail(). Ở cấp step trong v1.49, chỉ có test.step.skip(). Các variant khác chưa tồn tại:
test.step.fixme(): Không có trong v1.49. Nếu cần đánh dấu step là "cần sửa", dùngtest.step.skip()kết hợp comment trong code.test.step.only(): Không có. Không thể chạy một step đơn lẻ tách khỏi test — step luôn nằm trong context của cả test.test.step.fail(): Không có. Không thể khai báo trước rằng một step dự kiến sẽ fail (không giốngtest.fail()ở cấp test).
Nếu Playwright thêm các variant này trong phiên bản sau v1.49, release notes sẽ ghi rõ. Luôn kiểm tra changelog tại playwright.dev/docs/release-notes trước khi giả định API đã có.
Limitation
-
Skip tĩnh = code chết. Nếu một step được khai báo là
test.step.skip()mà không có điều kiện nào, function bên trong không bao giờ được gọi. Đây là dead code — cần xóa hẳn hoặc chuyển thành conditional skip. -
Không có skip reason trong API.
test.step.skip()không nhận tham số reason như một số framework khác. Người đọc report thấy step bị skip nhưng không thấy lý do. Phải đặt lý do trong tên step hoặc comment trong code. - Conditional skip phụ thuộc runtime. Khi điều kiện skip được tính lúc runtime (ví dụ check DOM state, gọi API), logic skip gắn với trạng thái không thể đoán trước ở compile time. Điều này làm debug phức tạp hơn — cùng một test có thể có cấu trúc step khác nhau tùy lần chạy.
- Không có step-level only. Không thể chạy một bước đơn lẻ để debug — phải chạy cả test, chỉ có thể skip các bước khác bằng tay.
Pitfall Thường Gặp
1. Skip Step Thường Xuyên Dẫn Đến Code Dead
// SAI — skip tĩnh không có điều kiện: function bên trong không bao giờ chạy
test('flow', async ({ page }) => {
await test.step.skip('verify email sent', async () => {
await expect(page.getByText('Check your inbox')).toBeVisible();
});
});
Step skip tĩnh (không có điều kiện) là dead code. Nếu step này không cần nữa, xóa đi. Nếu vẫn cần trong tương lai, dùng comment thay thế hoặc giữ với điều kiện rõ ràng.
2. Nhầm Lẫn Giữa Skip Static Và Conditional
// NGUY HIỂM — trông như conditional nhưng thực ra luôn skip
const shouldSkip = false; // giá trị cố định
await test.step.skip('important step', async () => {
// Luôn bị skip dù shouldSkip = false
// Người đọc sau có thể nhầm nghĩ đây là conditional
});
// ĐÚNG — conditional rõ ràng
if (shouldSkip) {
await test.step.skip('important step', async () => {});
} else {
await test.step('important step', async () => {
// code thực sự chạy
});
}
3. Skip Step Không Có Reason — Khó Audit
// KHÓ AUDIT — không biết tại sao step bị skip
await test.step.skip('apply discount', async () => {
await page.getByRole('button', { name: 'Discount' }).click();
});
// TỐT HƠN — tên step mô tả lý do hoặc comment trong code
// Hoặc đặt tên có context:
await test.step.skip('apply discount [skip: coupon service down in CI]', async () => {
await page.getByRole('button', { name: 'Discount' }).click();
});
Tên step xuất hiện trong report — đặt context vào tên step giúp người đọc report hiểu lý do skip mà không cần mở code.
4. Skip Step Quan Trọng Làm Test Sau Fail Vô Lý
// NGUY HIỂM — skip auth step làm các step sau fail
test('admin action', async ({ page }) => {
await test.step.skip('authenticate as admin', async () => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Login' }).click();
});
// Step sau fail vì chưa đăng nhập — lý do fail không rõ ràng
await test.step('delete user', async () => {
await page.goto('/admin/users'); // redirect về /login
await page.getByRole('button', { name: 'Delete' }).click(); // fail
});
});
Skip step thiết lập tiền điều kiện (authentication, seed data, navigation) làm các step sau fail với lỗi không liên quan. Chỉ skip step khi bạn chắc chắn các step sau không phụ thuộc vào outcome của step đó.
Quiz
Câu 1
Đoạn code sau có vấn đề gì?
test('checkout', async ({ page }) => {
await test.step('add to cart', async () => {
await page.getByRole('button', { name: 'Add' }).click();
});
test.step.skip('apply discount', async () => {
await page.getByRole('button', { name: 'Discount' }).click();
});
await test.step('confirm', async () => {
await page.getByRole('button', { name: 'Confirm' }).click();
});
});
Đáp án
Dòng test.step.skip(...) thiếu await. Dù step bị skip không chạy code bên trong, test.step.skip() vẫn trả về một Promise. Thiếu await khiến Promise không được chờ — trong trường hợp này ít gây vấn đề thực tế hơn (vì không có code chạy), nhưng là thói quen xấu. Nếu sau này step được chuyển lại thành test.step() bình thường mà quên thêm await, sẽ gây race condition. Luôn await cả test.step.skip().
Câu 2
Nếu một step bị skip bằng test.step.skip(), test status cuối cùng sẽ là gì (giả sử tất cả step còn lại pass)?
Đáp án
Test status là passed. Step skip không làm test fail và không làm test status chuyển sang skipped. Chỉ khi dùng test.skip() (skip toàn bộ test) thì test status mới là skipped. test.step.skip() chỉ ảnh hưởng đến status của step đơn lẻ đó trong report.
Câu 3
Sự khác biệt về observability giữa hai cách bỏ qua step sau là gì?
// Cách A
if (condition) {
await test.step('verify total', async () => { /* ... */ });
}
// Cách B
if (condition) {
await test.step('verify total', async () => { /* ... */ });
} else {
await test.step.skip('verify total', async () => {});
}
Đáp án
Cách A: khi condition là false, step không xuất hiện trong reporter. Người đọc report không biết step này tồn tại — không có dấu hiệu nào cho thấy đây là bước có chủ đích bị bỏ qua. Cách B: khi condition là false, tên step vẫn xuất hiện trong reporter với status skipped. Người đọc report biết step này tồn tại và bị bỏ qua có chủ đích. Cách B có observability tốt hơn — phù hợp khi bước bị skip là một phần của flow chính thức, không phải code thử nghiệm.
Câu 4
Trong Trace Viewer, làm sao phân biệt một step bị skip với một step pass nhưng có rất ít action bên trong?
Đáp án
Khi click vào step trong Trace Viewer, panel action list bên trong step skip hoàn toàn rỗng — không có network request, không có DOM action, không có screenshot — vì function bên trong chưa bao giờ được gọi. Ngược lại, một step pass với ít action vẫn có ít nhất một entry trong action list (ví dụ một click hoặc một assertion). Ngoài ra, step skip được đánh dấu với status skipped trong UI — khác với icon pass màu xanh của step bình thường.
Câu 5
Đoạn code sau có lỗi logic không? Nếu có, hậu quả là gì?
test('admin dashboard', async ({ page }) => {
await test.step.skip('login as admin', async () => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Login' }).click();
});
await test.step('check admin panel', async () => {
await page.goto('/admin');
await expect(page.getByText('User Management')).toBeVisible();
});
});
Đáp án
Có lỗi logic. Step 'login as admin' bị skip tĩnh (không có điều kiện) nên không bao giờ chạy. Khi step 'check admin panel' chạy và navigate đến /admin, server redirect về trang login vì chưa có session. Step fail với lỗi không liên quan đến chức năng đang test. Hậu quả: report hiển thị test fail ở bước "check admin panel" — người đọc không biết nguyên nhân thực sự là skip step login. Đây là ví dụ của pitfall "skip step thiết lập tiền điều kiện".
