Mục lục
- Mục Tiêu Bài Học
- Recap Nhanh — Và Bài Này Thêm Gì
- Cú Pháp Và Yêu Cầu Version
- Behavior Chi Tiết — Two Modifiers Cùng Lúc
- Output Trong Reporter
- Use Case 1: TDD Red Phase — Focus Debug
- Use Case 2: Track Bug Đã Confirm
- Combine Với Annotation + Tag (v1.42+)
- So Sánh fail.only Với --grep
- CI Gate — forbidOnly Và Pre-commit Hook
- 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ẽ:
- Hiểu
test.fail.only()là chain modifier — kết hợp hai behavior độc lập trong một khai báo — và yêu cầu Playwright v1.49+. - Phân biệt chính xác
test.fail()đơn thuần vớitest.fail.only()về scope test bị ảnh hưởng. - Biết reporter hiển thị trạng thái gì khi test fail như expected và khi test pass không như mong đợi trong context
fail.only. - Áp dụng được
test.fail.only()cho hai use case chính: TDD red phase và track confirmed bug. - Tránh 4 pitfall làm
fail.onlygây rối thay vì có ích.
Recap Nhanh — Và Bài Này Thêm Gì
Series 1 đã cover hai API riêng biệt:
- Series 1 bài 404 —
test.fail(): Đảo ngược expected status. Test fail như expected → xanh; test pass khi không expected → đỏ. Toàn bộ test trong file vẫn chạy bình thường, chỉ đảo kết quả của test được đánh dấu. - Series 1 bài 406 —
test.only(): Focus mode per-file. Khi file có ít nhất mộttest.only(), các test không cóonlytrong cùng file bị skip.
Bài này không lặp lại hai nội dung trên. Điểm khác biệt duy nhất cần nắm: từ Playwright v1.49, hai modifier này có thể chain trực tiếp — test.fail.only() — kích hoạt cả hai behavior trong một khai báo. Đây là điều không làm được trước v1.49 (phải dùng hai call riêng hoặc dùng test.only() kết hợp test.fail() trong body).
Các chain khác trong v1.49+ như test.skip.only() hay test.fixme.only() tồn tại về mặt kỹ thuật nhưng không có giá trị thực tiễn: skip và fixme đã không chạy body — thêm only vào chỉ skip thêm test khác mà không thêm thông tin gì. test.fail.only() là chain có ý nghĩa nhất vì body vẫn chạy (cần chạy để xác nhận fail) đồng thời cần focus.
Cú Pháp Và Yêu Cầu Version
Yêu cầu tối thiểu: Playwright v1.49. Trước phiên bản này, test.fail không phải là object có property only — gọi test.fail.only() sẽ throw TypeError: test.fail.only is not a function.
import { test, expect } from '@playwright/test';
// Cú pháp cơ bản
test.fail.only('reproduce bug X', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
// assertion này fail do bug → expected
});
// Cú pháp với options object (v1.42+)
test.fail.only(
'reproduce bug X',
{
tag: '@known-bug',
annotation: { type: 'issue', description: 'JIRA-789' },
},
async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
}
);
Từ góc độ TypeScript, test.fail trong v1.49+ là callable function và object với property only. Compiler type-check đầy đủ tham số của chain — nếu truyền options không hợp lệ, TypeScript báo lỗi tại compile time.
Kiểm tra version trước khi dùng
npx playwright --version
# Cần >= 1.49.0
Nếu project vẫn cần hỗ trợ Playwright < 1.49, dùng workaround tương đương:
// Workaround cho Playwright < 1.49
test.only('reproduce bug X', async ({ page }) => {
test.fail(); // gọi trong body
await page.goto('/');
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
});
Workaround này có hành vi giống test.fail.only() nhưng ít rõ ràng hơn về intent ở dòng khai báo.
Behavior Chi Tiết — Two Modifiers Cùng Lúc
Hiểu test.fail.only() bằng cách nhìn hai modifier riêng và behavior khi chúng kết hợp:
| API | Test khác trong file | Test được đánh dấu — body fail | Test được đánh dấu — body pass |
|---|---|---|---|
test.fail() |
Chạy bình thường | Expected — xanh | Unexpected — đỏ |
test.only() |
Skip | Fail thật — đỏ | Pass bình thường — xanh |
test.fail.only() |
Skip | Expected — xanh | Unexpected — đỏ |
Nói cách khác: test.fail.only() = only (skip mọi test khác) + fail (đảo expected status của test này).
Một điểm quan trọng về scope của only: giống test.only() đơn thuần, scope là per-file. Các file spec khác không bị ảnh hưởng — test trong file khác vẫn chạy bình thường. fail.only chỉ skip test trong cùng file.
Phân biệt fail.only với fail() rồi only() riêng
Một cách nhầm lẫn phổ biến: viết test.fail() trong body của test.only() và nghĩ đó là giống nhau. Về behavior thì đúng — nhưng có sự khác biệt về readability và intent:
// Cách cũ — intent phân tán
test.only('reproduce bug X', async ({ page }) => {
test.fail(); // đọc dòng khai báo không thấy intent fail
await page.goto('/');
await expect(page.getByRole('button')).toBeEnabled();
});
// Cách mới v1.49 — intent rõ ngay từ khai báo
test.fail.only('reproduce bug X', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('button')).toBeEnabled();
});
Ưu điểm của chain: ai đọc dòng khai báo biết ngay test này vừa được focus vừa được kỳ vọng fail — không cần đọc vào body để tìm test.fail().
Output Trong Reporter
Khi test fail như expected (bug còn đó)
✓ 1 [chromium] › cart.spec.ts:5:5 › reproduce bug X (expected to fail)
- 2 [chromium] › cart.spec.ts:12:5 › checkout flow (skipped)
- 3 [chromium] › cart.spec.ts:20:5 › product listing (skipped)
1 expected to fail
2 skipped (1.3s)
Test fail như expected hiển thị màu xanh với chú thích (expected to fail). Các test còn lại trong file xuất hiện ở status skipped — đây là effect của only. Stack trace của assertion lỗi không xuất hiện trong terminal vì fail là trạng thái được kỳ vọng.
Khi test pass (bug đã được fix, marker chưa bỏ)
✗ 1 [chromium] › cart.spec.ts:5:5 › reproduce bug X
Error: Test was expected to fail, but passed.
This is likely a bug — remove test.fail() annotation.
1 failed
2 skipped (1.1s)
Đây là trạng thái CI cần bắt: test pass khi expected fail → đỏ, exit code khác 0 → pipeline dừng. Thông báo "This is likely a bug — remove test.fail() annotation" nhắc team bỏ marker và confirm fix.
HTML report
Trong HTML report, test fail.only khi fail như expected xuất hiện trong section "Expected Failures" (khác với "Passed", "Failed", "Skipped"). Các test bị skip do only xuất hiện trong "Skipped" với lý do "skipped due to test.only". Annotation và tag (nếu có) hiển thị trong section Annotations của test entry.
JUnit XML
<!-- fail như expected — không có failure element -->
<testcase name="reproduce bug X" classname="cart.spec.ts" time="0.8">
</testcase>
<!-- test khác bị skip do only -->
<testcase name="checkout flow" classname="cart.spec.ts" time="0">
<skipped message="skipped due to test.only"/>
</testcase>
Use Case 1: TDD Red Phase — Focus Debug
TDD red phase: viết test trước khi implement, test fail là expected. Khi cùng file có nhiều test đã xanh, chỉ muốn focus vào test đang viết mà không bị nhiễu bởi output của các test khác.
import { test, expect } from '@playwright/test';
// Các test đã implement và pass — không muốn skip chúng permanently
test('product listing loads', async ({ page }) => {
await page.goto('/products');
await expect(page.getByRole('list')).toBeVisible();
});
test('add to cart', async ({ page }) => {
await page.goto('/products/1');
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
// Feature chưa implement — đang debug, muốn focus
test.fail.only('user can apply promo code', async ({ page }) => {
await page.goto('/cart');
await page.getByTestId('promo-input').fill('SAVE20');
await page.getByRole('button', { name: 'Apply' }).click();
// Promo feature chưa có → button chưa tồn tại → assertion fail như expected
await expect(page.getByText('Promo applied: 20% off')).toBeVisible();
});
Trong session debug, chỉ test thứ ba chạy. Sau khi implement xong promo feature:
- Test
user can apply promo codepass → CI báo "unexpected pass" (đỏ). - Dev bỏ
.fail.only, đổi lại thànhtest()thường. - Chạy lại: cả 3 test xanh.
Cơ chế này enforce kỷ luật: không thể "quên" bỏ marker sau khi implement xong vì CI sẽ đỏ ngay.
Use Case 2: Track Bug Đã Confirm
Bug đã được xác nhận, cần viết test để tái hiện và debug. File có nhiều test đang xanh — muốn tập trung vào test bug này mà không làm chậm session debug bằng cách chờ các test khác chạy.
import { test, expect } from '@playwright/test';
test('cart shows correct subtotal', async ({ page }) => {
// test đang xanh, không cần chạy trong session debug
});
test('discount badge displays', async ({ page }) => {
// test đang xanh, không cần chạy trong session debug
});
// Bug JIRA-789 đã confirm: tổng tiền sai khi áp 2 discount liên tiếp
test.fail.only('regression: total wrong with double discount — JIRA-789', async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Add discount' }).click();
await page.getByRole('button', { name: 'Add discount' }).click();
// Assertion check kết quả đúng — hiện tại bug nên assertion này fail như expected
await expect(page.getByTestId('cart-total')).toHaveText('$0.00');
});
Quy trình sau khi debug xong:
- Bug fix xong → test pass → CI báo "unexpected pass" (đỏ).
- Dev bỏ
.only(giữ lại.failhoặc bỏ luôn tùy quy trình). - Nếu bỏ luôn
.fail: test trở thành regression test bình thường — xanh khi code đúng, đỏ khi bug quay lại. - Nếu giữ
.fail: CI tiếp tục track trạng thái bug theo cơ chế expected failure.
Với trường hợp bug chỉ tái hiện ở một điều kiện cụ thể (browser, env), có thể dùng conditional fail trong body kết hợp only ở khai báo:
// Khi bug chỉ xảy ra trên WebKit — muốn focus debug trên WebKit
test.only('regression: print dialog — JIRA-800', async ({ page, browserName }) => {
test.fail(browserName === 'webkit', 'Print dialog broken on WebKit — JIRA-800');
await page.goto('/invoice');
await page.getByRole('button', { name: 'Print' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
});
Đây là workaround khi cần conditional fail (chỉ test.fail(condition) trong body) mà vẫn muốn focus — vì test.fail.only() không nhận tham số condition ở khai báo.
Combine Với Annotation + Tag (v1.42+)
Từ v1.42, tham số options object { tag, annotation } có thể dùng trong chain. test.fail.only() chấp nhận cùng signature:
test.fail.only(
'regression: total wrong with double discount',
{
tag: '@known-bug',
annotation: { type: 'issue', description: 'JIRA-789' },
},
async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Add discount' }).click();
await page.getByRole('button', { name: 'Add discount' }).click();
await expect(page.getByTestId('cart-total')).toHaveText('$0.00');
}
);
Tag và annotation có giá trị thực tế trong ngữ cảnh này:
- Tag
@known-bug: khi bỏ.onlyvà commit vào suite thường, tag cho phép lọc tất cả test theo dõi bug:npx playwright test --grep @known-bug. - Annotation
issuevới JIRA link: hiển thị trong HTML report, giúp reviewer biết ngay context mà không cần đọc test name hay comment.
Cả tag lẫn annotation đều chấp nhận array — có thể gán nhiều tag hoặc nhiều annotation entry:
test.fail.only(
'checkout with 3DS — JIRA-900',
{
tag: ['@known-bug', '@high-priority'],
annotation: [
{ type: 'issue', description: 'https://jira.company.com/JIRA-900' },
{ type: 'severity', description: 'P1' },
],
},
async ({ page }) => {
// ...
}
);
So Sánh fail.only Với --grep
Cả test.fail.only() và flag --grep đều có thể dùng để focus vào một test cụ thể khi debug. Điểm khác biệt quan trọng:
| Tiêu chí | test.fail.only() |
--grep <pattern> + test.fail() |
|---|---|---|
| Cần sửa code? | Có — phải thêm .fail.only vào test |
Không — flag CLI, không chạm file test |
| Nguy cơ commit nhầm | Cao — only nếu lọt vào CI sẽ skip test khác |
Không có — flag chỉ có trong lệnh chạy |
| Expected fail semantics | Có — fail trong chain |
Cần giữ test.fail() riêng trong khai báo |
| Persist qua nhiều lần chạy | Có — in-code | Phải nhớ thêm flag mỗi lần chạy |
| Khi nào phù hợp | Debug ngắn hạn, cần persist focus; team biết quy trình bỏ marker | Muốn focus mà không sửa code; CI dùng --grep có chủ đích |
Ví dụ dùng --grep thay thế:
# Chạy đúng test có tên chứa "JIRA-789", không sửa code
npx playwright test --grep "JIRA-789"
# Nếu test đã có tag @known-bug
npx playwright test --grep "@known-bug"
--grep filter theo tên test — không loại trừ test khác bằng cách đặt skipped (chúng đơn giản không được load vào run). Trong reporter, chỉ test match pattern xuất hiện — không có hàng dài "skipped" như khi dùng only. Với số lượng test lớn, output gọn hơn đáng kể.
CI Gate — forbidOnly Và Pre-commit Hook
test.fail.only() chứa only — nghĩa là forbidOnly: true trong config sẽ bắt được nó, giống test.only() thường:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
forbidOnly: !!process.env.CI, // bật trên CI, tắt local
});
Khi forbidOnly: true, Playwright fail ngay trước khi chạy bất kỳ test nào nếu phát hiện only trong code:
Error: focused item found in the --forbid-only mode:
cart.spec.ts:8:0 › regression: total wrong with double discount — JIRA-789
Đây là cách CI tự động chặn fail.only lọt vào pipeline — không cần review thủ công.
Pre-commit hook bắt cả fail.only
Để chặn sớm hơn ngay từ máy local, hook grep cần cover fail.only riêng:
#!/bin/sh
# .git/hooks/pre-commit
if grep -rn "test\.only\|test\.describe\.only\|test\.fail\.only" tests/; then
echo "Error: .only found (test.only / describe.only / fail.only). Remove before commit."
exit 1
fi
Hoặc dùng regex gọn hơn:
if grep -rn "\.only(" tests/; then
echo "Error: .only() found. Remove before commit."
exit 1
fi
Pattern \.only( bắt được tất cả: test.only, describe.only, test.fail.only. Đây là cách đơn giản và an toàn nhất.
Limitation
- Yêu cầu Playwright v1.49+: Dự án dùng version cũ không compile được chain này —
test.fail.onlylà undefined. Phải dùng workaroundtest.only+test.fail()trong body. - Không hỗ trợ conditional fail ở khai báo:
test.fail.only(condition, reason)không tồn tại. Khi cần conditional fail (ví dụ chỉ fail trên WebKit), phải gọitest.fail(condition, reason)trong body củatest.only()— không thể viết gọn thành một chain. - Semantic phức tạp khi đọc code: Developer mới chưa quen với chain notation có thể nhầm
test.fail.onlylà gọi methodonly()trên kết quả củatest.fail()— đây là chain property trên objecttest.fail, không phải call chain theo nghĩa thông thường. Cần document rõ trong team. - Không phải là trạng thái commit được:
fail.onlylà công cụ local debug. Không có tình huống nào phù hợp để commitfail.onlyvào main branch — luôn phải bỏ.only(giữ hoặc bỏ.failtùy quy trình). - forbidOnly ở version cũ không catch fail.only: Nếu project dùng Playwright < 1.49 nhưng có ai đó upgrade lên 1.49+ cục bộ và viết
test.fail.only(),forbidOnlytrên CI cũ có thể không parse được pattern mới. Nên đảm bảo version Playwright đồng nhất giữa local và CI.
Pitfall Thường Gặp
1. Quên xóa .only trước khi commit — CI skip mọi test khác trong file
// Nếu dòng này lọt vào CI:
test.fail.only('regression: JIRA-789', async ({ page }) => {
// CI chỉ chạy test này, bỏ qua toàn bộ test còn lại trong file
// Pipeline có thể báo "pass" nhưng coverage đã bị hổng
});
Fix: bật forbidOnly: !!process.env.CI trong config và thêm pre-commit hook grep \.only(.
2. Dùng Playwright version cũ — TypeError tại runtime
TypeError: test.fail.only is not a function
at Object.<anonymous> (cart.spec.ts:8:5)
Xảy ra khi Playwright < v1.49. Fix: upgrade Playwright hoặc dùng workaround test.only + test.fail() trong body.
3. Nhầm fail.only (chain) với fail() rồi only() riêng — confuse về API
// KHÔNG phải cú pháp hợp lệ — fail() không trả về object có only
test.fail()
test.only('test name', async ({ page }) => { ... }); // hai câu riêng, không phải chain
// Đúng — chain trên object test.fail (property của test)
test.fail.only('test name', async ({ page }) => { ... });
Hai câu riêng trên có hành vi khác: test.fail() gọi không tham số sẽ kích hoạt conditional fail theo context, và test.only() là test hoàn toàn tách biệt. Không phải là "tạo test với cả fail và only".
4. Kỳ vọng fail.only ảnh hưởng cross-file
Developer dùng test.fail.only() trong cart.spec.ts với kỳ vọng mọi test trong các file khác cũng bị skip. Hành vi thực tế: chỉ test trong cart.spec.ts không có only mới bị skip — các file khác (checkout.spec.ts, auth.spec.ts, ...) chạy bình thường. Scope là per-file, không phải toàn suite.
Nếu muốn chạy đúng 1 test trong toàn bộ suite (cross-file), dùng: npx playwright test cart.spec.ts --grep "regression: JIRA-789".
Quiz
Câu 1. File order.spec.ts có 5 test. Test thứ 3 được khai báo test.fail.only(). Khi chạy toàn bộ suite (npx playwright test), bao nhiêu test được thực thi (body chạy) và bao nhiêu test bị skip?
Đáp án
1 test được thực thi (test thứ 3 có fail.only), 4 test bị skip (do only per-file). Các file spec khác trong suite không bị ảnh hưởng — test trong file khác chạy bình thường. Scope only là per-file.
Câu 2. Test được khai báo test.fail.only(). Khi chạy, assertion trong body pass hết (bug đã được fix). Kết quả CI là gì?
Đáp án
CI fail (đỏ). Reporter ghi nhận "Test was expected to fail, but passed" (unexpected pass). Exit code khác 0, pipeline dừng. Đây là cơ chế enforce bỏ .fail marker sau khi bug được fix. Developer cần bỏ .fail.only (hoặc ít nhất bỏ .only) rồi commit lại.
Câu 3. Giải thích tại sao test.fail.only() cần Playwright v1.49+ và không hoạt động ở version cũ hơn.
Đáp án
Trước v1.49, test.fail là một callable function thuần túy — không phải object có property only. Khi code truy cập test.fail.only, JavaScript nhận undefined. Gọi undefined() throw TypeError: test.fail.only is not a function. Từ v1.49, Playwright thiết kế lại test.fail là dual-purpose: vừa callable, vừa là object với property only — tương tự cách test vừa là hàm vừa có các property như test.skip, test.only.
Câu 4. Bạn cần debug bug chỉ xảy ra trên WebKit. File có 10 test, muốn chỉ chạy test bug đó với expected fail semantics chỉ trên WebKit. Viết khai báo đúng.
Đáp án
test.only('bug: print dialog on WebKit — JIRA-800', async ({ page, browserName }) => {
test.fail(browserName === 'webkit', 'Print dialog broken on WebKit — JIRA-800');
await page.goto('/invoice');
await page.getByRole('button', { name: 'Print' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
});
Không dùng test.fail.only() vì chain đó không nhận conditional fail ở khai báo. Thay vào đó: test.only() để focus (skip test khác), test.fail(condition) trong body để conditional fail chỉ trên WebKit. Trên Chromium/Firefox, test chạy bình thường — fail = đỏ thật, pass = xanh thật.
