Danh sách bài viết

Bài 41: test.fail.only() — Combo Expected Fail + Only [v1.49]

Nhóm A.5 tiếp tục với test.fail.only() — chain modifier xuất hiện từ v1.49, kết hợp đồng thời expected failure semantics (test.fail) và focus mode (test.only). Bài này không lặp nội dung test.fail() cơ bản (Series 1 bài 404) hay test.only() cơ bản (Series 1 bài 406) — thay vào đó tập trung vào behavior của chain, use case TDD red phase và track bug, cách reporter hiển thị, và các ràng buộc CI.

28/05/2026
0 lượt xem
1

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ới test.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.only gây rối thay vì có ích.
2

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ột test.only(), các test không có only trong 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.

3

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 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.

4

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().

5

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>
6

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:

  1. Test user can apply promo code pass → CI báo "unexpected pass" (đỏ).
  2. Dev bỏ .fail.only, đổi lại thành test() thường.
  3. 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.

7

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:

  1. Bug fix xong → test pass → CI báo "unexpected pass" (đỏ).
  2. Dev bỏ .only (giữ lại .fail hoặc bỏ luôn tùy quy trình).
  3. 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.
  4. 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.

8

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ỏ .only và 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 issue vớ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 }) => {
    // ...
  }
);
9

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ể.

10

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.

11

Limitation

  • Yêu cầu Playwright v1.49+: Dự án dùng version cũ không compile được chain này — test.fail.only là undefined. Phải dùng workaround test.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ọi test.fail(condition, reason) trong body của test.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.only là gọi method only() trên kết quả của test.fail() — đây là chain property trên object test.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.only là công cụ local debug. Không có tình huống nào phù hợp để commit fail.only vào main branch — luôn phải bỏ .only (giữ hoặc bỏ .fail tù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(), forbidOnly trê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.
12

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".

13

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.