NỘI DUNG BÀI HỌC
Phần 1: Debug lỗi hiệu quả với Trace Viewer 🐞
Phần 2: Các loại Report mặc định của Playwright 📊
Phần 3: Báo cáo nâng cao với Allure Report 💎
Phần 4: Custom Reporter (Tùy chỉnh Báo cáo) 🛠️
Nếu Parallel là "Động cơ Ferrari" giúp xe chạy nhanh, thì Trace Viewer chính là cái "Camera Hành Trình 360 độ" (hoặc Hộp Đen máy bay).
Khi test bị Fail, thay vì đoán mò, bạn mở Trace Viewer lên để "tua lại" quá khứ xem chuyện quái gì đã xảy ra.
🕵️Phần 1: PLAYWRIGHT TRACE VIEWER: "CỖ MÁY THỜI GIAN" ĐỂ DEBUG
1. Trace Viewer Là Gì? (Khác gì với Video?)
Nhiều bạn nhầm Trace Viewer với quay Video màn hình.
-
Video: Chỉ là các điểm ảnh (Pixels). Bạn thấy lỗi nhưng không thể tương tác, không thể F12 (Inspect Element) vào cái nút trong video được.
-
Trace Viewer: Là tập hợp các SNAPSHOT DOM (Mã nguồn trang web) tại từng thời điểm.
-
Nó lưu lại cả Network (API), Console Log, Source Code.
-
Bạn có thể "sờ mó", inspect element ngay trong quá khứ!
-
👉 Ví dụ: Test fail lúc 8:00. Bạn mở Trace lên, tua lại lúc 7:59, hover chuột vào nút Login xem lúc đó CSS của nó đang bị lỗi gì mà không click được.
2. Chiến Lược Cấu Hình (Khi nào thì bật?)
Trace Viewer rất nặng (file zip có thể lên tới vài MB). Nếu bật 100% cho hàng nghìn test case thì ổ cứng nổ tung.
Chúng ta có các chiến thuật trong playwright.config.ts:
🛡️ Chiến thuật 1: 'on-first-retry' (Khuyên Dùng cho CI/CD)
Đây là chiến thuật thông minh nhất.
-
Lần 1: Chạy test KHÔNG lưu Trace (cho nhanh).
-
Nếu Fail: Playwright tự động chạy lại (Retry). Lần này nó BẬT Trace lên.
-
Kết quả: Bạn chỉ nhận được file Trace của những bài bị lỗi. Test xanh thì không tốn dung lượng lưu trace.
🐛 Chiến thuật 2: 'retain-on-failure' (Dùng khi Dev/Debug)
Lưu Trace cho bất kỳ bài nào bị Fail (ngay lần chạy đầu tiên).
⚙️ Cách Cấu Hình:
Mở playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// 1. Chụp màn hình khi lỗi
screenshot: 'only-on-failure',
// 2. Quay video khi retry (để xem cho vui mắt)
video: 'on-first-retry',
// 🌟 3. TRACE: "Vũ khí tối thượng"
// 'on-first-retry': Chỉ lưu khi test fail và chạy lại.
// 'retain-on-failure': Lưu ngay khi fail.
trace: 'on-first-retry',
},
});
Code ví dụ
Đây là một ví dụ cơ chế "Lần 1 tiết kiệm, Lần 2 tất tay" của Playwright.
Để làm được demo này, chúng ta cần 2 mảnh ghép:
-
Cấu hình: Bật
retries(để có cơ hội chạy lại) vàtrace: 'on-first-retry'. -
Code test: Dùng biến
testInfo.retryđể viết một đoạn code "Gài bẫy" -> Cố tình cho Fail ở lần chạy đầu tiên.
🛠️ Bước 1: Cấu hình playwright.config.ts
Bắt buộc phải set retries ít nhất là 1 thì mới có chuyện "First Retry" xảy ra.
import { defineConfig } from '@playwright/test';
export default defineConfig({
// 1. Cho phép chạy lại 1 lần nếu fail
retries: 1,
use: {
// 2. Chỉ quay Trace ở lần chạy lại (Lần đầu fail thì kệ, không lưu)
trace: 'on-first-retry',
// Các config khác...
baseURL: 'https://crm.anhtester.com',
screenshot: 'only-on-failure',
},
reporter: 'html', // Dùng HTML report để xem cho dễ
});
💻 Bước 2: Viết Code "Gài Bẫy" (tests/demo-trace.spec.ts)
Chúng ta sẽ dùng testInfo.retry. Giá trị của nó bắt đầu từ 0.
-
testInfo.retry === 0: Lần chạy đầu tiên. -
testInfo.retry === 1: Lần chạy lại thứ nhất.
import { test, expect } from '@playwright/test';
test('Demo Trace on-first-retry: Fail lần 1, Pass lần 2', async ({ page }, testInfo) => {
// --- GIAI ĐOẠN 1: TRUY CẬP ---
await page.goto('/admin/authentication');
await expect(page).toHaveTitle(/Login/);
// --- GIAI ĐOẠN 2: GÀI BẪY (CỐ TÌNH FAIL) ---
console.log(`🔄 Đang chạy lần thứ: ${testInfo.retry + 1} (Retry index: ${testInfo.retry})`);
if (testInfo.retry === 0) {
console.log('🔥 [Lần 1] Phát hiện chạy lần đầu -> Ép cho Fail để kích hoạt Retry!');
// Cố tình expect sai để test fail ngay lập tức
// Lần này Playwright sẽ KHÔNG lưu Trace (do config on-first-retry)
expect(true, 'Cố tình fail lần 1').toBe(false);
}
// --- GIAI ĐOẠN 3: LOGIC CHÍNH (CHỈ CHẠY KHI RETRY) ---
console.log('✅ [Lần 2] Đây là lần Retry -> Code sẽ chạy mượt mà và LƯU TRACE.');
await page.locator('input[name="email"]').fill('admin@example.com');
await page.locator('input[name="password"]').fill('123456');
await page.locator('button[type="submit"]').click();
await expect(page).toHaveURL('/admin/');
});
🎥 Bước 3: Chạy và Xem Kết Quả
Bạn chạy lệnh:
npx playwright test tests/demo-trace.spec.ts
Diễn biến sẽ như sau:
-
Terminal báo:
×(Fail 1 cái) rồi sau đó±(Flaky/Retry thành công). -
Mở báo cáo lên:
npx playwright show-report
Trong HTML Report, bạn sẽ thấy sự khác biệt thần kỳ:
-
Tab "Attempt 1" (Lần chạy đầu):
-
Trạng thái: Failed.
-
Phần Trace: KHÔNG CÓ (Hoặc chỉ có screenshot nếu config). Vì chúng ta bảo "Lần đầu fail thì đừng có lưu trace cho tốn chỗ".
-
-
Tab "Attempt 2" (Lần Retry):
-
Trạng thái: Passed.
-
Phần Trace: CÓ FILE TRACE.ZIP! 🎁
-
- Lý do: Vì đây là "First Retry", điều kiện đã thỏa mãn -> Playwright bật camera ghi hình lại toàn bộ quá trình lần 2 này.
Công thức của "Flaky Test" (Test Chập Chờn)
Trong thế giới Automation, trạng thái test được định nghĩa như sau:
-
PASSED (Xanh): Chạy lần 1 -> ✅ Qua luôn.
-
FAILED (Đỏ): Chạy lần 1 ❌ -> Retry lần 2 ❌ -> Retry lần n ❌. (Chết hẳn).
-
FLAKY (Vàng/Cam): Chạy lần 1 ❌ (Fail) -> Retry lần 2 ✅ (Pass).
Giống như bóng đèn bị lỏng dây: Lúc sáng lúc tắt.
Trong trường hợp ví dụ của chúng ta: Do code mình ép nó Fail lần 1 và Pass lần 2, nên Playwright dán nhãn nó là Flaky.
2. Tại sao Trace lại nằm ở lần Flaky (Retry)?
Bây giờ bạn hãy mở Report lên (npx playwright show-report) và bấm vào cái bài test Flaky đó. Bạn sẽ thấy giao diện chia làm các Tab (Attempts):
-
Tab "Attempt 1" (Lần thử 1):
-
Trạng thái: Failed.
-
Trace: KHÔNG CÓ. (Vì config
on-first-retrybảo là: "Lần đầu tao chưa quan tâm").
-
-
Tab "Attempt 2" (Lần thử 2):
-
Trạng thái: Passed.
-
Trace: CÓ! 🎁 (Vì đây là lần Retry đầu tiên).
-
👉 Kết luận:
Dù kết quả cuối cùng là Pass (Flaky), nhưng Playwright VẪN LƯU TRACE của lần chạy thứ 2.
Điều này cực kỳ giá trị! Nó giúp bạn trả lời câu hỏi: "Tại sao lần 1 chết mà lần 2 lại sống? Do mạng lag hay do server?". Bạn mở Trace lần 2 lên sẽ thấy mọi thứ hoạt động trơn tru (hoặc hơi chậm chút) để so sánh.
" Tại sao không để Fail luôn mà phải Retry làm gì cho ra cái màu Vàng (Flaky)?"
Câu trả lời là: Để cứu quy trình CI/CD (Deployment).
-
Tưởng tượng công ty đang cần Deploy gấp code lên Production.
-
Có 1 bài test bị Fail do... mạng bị lag 1 giây.
-
Nếu không có Retry: Toàn bộ quá trình Deploy bị hủy bỏ (Block). Cả team Backend/Frontend phải ngồi chờ chạy lại từ đầu mất 30 phút. Rất lãng phí!
-
Nếu có Retry: Playwright tự chạy lại -> Thấy Pass -> Đánh dấu Flaky (Vàng). Quy trình Deploy vẫn tiếp tục.
-
Dev sẽ nhìn thấy màu vàng và biết: "À, bài này hơi chập chờn, để mai mình xem lại trace sau, nhưng ít nhất code hôm nay vẫn lên được Production."
-
=> Flaky (Retry) là cơ chế "Tự chữa lành" tạm thời để hệ thống vận hành trơn tru.
3. Cách Xem Trace (Mổ Xẻ Hộp Đen)
Có 2 cách để mở file Trace (đuôi .zip).
Cách 1: Xem qua HTML Report (Phổ biến nhất)
Sau khi chạy test xong, bạn mở report lên:
npx playwright show-report
-
Bấm vào bài test bị Fail.
-
Cuộn xuống dưới cùng, bấm vào icon Trace.
Cách 2: Xem file Zip độc lập (Trace Online)
Nếu đồng nghiệp gửi cho bạn file trace.zip qua Slack.
-
Truy cập: trace.playwright.dev
-
Kéo thả file zip vào.
-
(Hoặc dùng lệnh:
npx playwright show-trace trace.zip)
4. Giải Phẫu Giao Diện Trace Viewer 🏥
Khi mở lên, bạn sẽ thấy một bảng điều khiển như phi thuyền không gian. Hãy chú ý 4 khu vực chính:
1️⃣ The Timeline (Dòng Thời Gian - Trên cùng)
Dải màu sắc cầu vồng trên cùng.
-
Mỗi vạch màu là một hành động.
-
Bạn di chuột qua lại để xem màn hình thay đổi như thế nào theo thời gian thực (như tua phim).
2️⃣ The Actions (Danh Sách Hành Động - Bên Trái)
Liệt kê từng bước code đã chạy:
-
Goto /login -
Fill #email -
Click button -
❌
Expect(Dòng đỏ lòm là chỗ chết).
👉 Mẹo: Bấm vào một dòng Action bất kỳ (ví dụ dòng Click), màn hình bên phải sẽ nhảy về đúng thời điểm TRƯỚC và SAU khi click.
3️⃣ The DOM Snapshot (Màn Hình Chính - Ở Giữa)
Đây là "bóng ma" của trang web trong quá khứ.
-
Nó không phải ảnh chụp! Nó là HTML thật.
-
Bạn có thể thử bôi đen text, F12 để Inspect Element ngay trên cái snapshot này để xem tại sao nút đó bị ẩn.
4️⃣ The Details (Chi Tiết Kỹ Thuật - Bên Phải)
Đây là nơi chứa bằng chứng phạm tội:
-
Call Tab: Xem dòng code nào thực hiện hành động này.
-
Console Tab: Xem lúc đó trình duyệt có báo lỗi JS đỏ lòm nào không?
-
Network Tab: (Quan trọng nhất) Xem lúc bấm nút, API gửi đi đâu? Server trả về 200 hay 500? Body trả về là gì?
5. Thực Hành: Bắt Lỗi "Bóng Ma" (Flaky Test)
Giả sử bạn có 1 test thỉnh thoảng fail (Flaky) do mạng lag.
-
Cấu hình
trace: 'on-first-retry'. -
Chạy test trên CI. Nó fail ở lần retry.
-
Tải file Trace về.
-
Mở lên, bấm vào bước
Click Submit. -
Nhìn sang tab Network bên phải.
-
Phát hiện: Thấy API
/logintrả về500 Internal Server Error. -
Kết luận: Không phải do code automation sai, mà do Server lúc đó bị sập! -> Chụp ảnh Trace gửi cho đội Backend fix.
🎯 Tóm lại
Trace Viewer biến bạn từ một người "đoán mò" thành một Thám tử đại tài.
-
Đừng bao giờ debug chỉ bằng log text.
-
Hãy tập thói quen mở Trace Viewer đầu tiên khi thấy test fail.
Cấu hình chuẩn: trace: 'on-first-retry'.
🕵️ Tại sao Trace không kịp lưu trang Admin?
Hãy tưởng tượng Trace Viewer là một người quay phim.
-
Dòng code
await expect(page).toHaveURL('/admin/')chạy xong. -
Playwright thấy URL đã đổi sang
/admin/-> Test Passed ngay lập tức. -
Ngay khoảnh khắc đó, đạo diễn hô "CẮT!". Máy quay (Trace) tắt cái rụp.
-
Lúc này, trình duyệt vừa mới đổi URL, nhưng nội dung (HTML/CSS) bên trong chưa kịp hiển thị (render) xong.
👉 Kết quả: Trong Trace Viewer, bạn thấy URL đúng là /admin/ nhưng màn hình vẫn trắng trơn hoặc đang loading, chưa kịp hiện Dashboard.
🛠️ Cách khắc phục (Cho nó "sống chậm lại" 1 chút)
Để Trace Viewer kịp chụp lại khoảnh khắc huy hoàng khi vào được Admin, bạn cần bắt nó chờ thêm một nhịp nữa trước khi kết thúc bài test.
Có 2 cách sửa, bạn chọn cách nào cũng được:
Cách 1: Thêm verify một phần tử trên trang Admin (Chuẩn nhất) ✅
Thay vì chỉ check URL, hãy bắt nó đợi đến khi nhìn thấy chữ "Dashboard" hoặc menu nào đó. Lúc này trang web chắc chắn đã hiện ra.
// Thay vì chỉ expect URL, hãy expect thêm nội dung
await expect(page).toHaveURL('/admin/');
// 👉 Bắt buộc Playwright phải chờ Render xong cái Menu thì mới được Pass
Cách 2: Dùng waitForTimeout (Dùng cho Demo/Học tập) ⚡
Vì bạn đang dạy học/demo tính năng Trace, nên dùng cách này cho nhanh và trực quan. Ép nó chờ 2-3 giây để Trace quay đủ phim.
👨🏫
*"Các bạn nhớ nhé, Automation Test chạy nhanh hơn mắt thường rất nhiều.
Khi lệnh kiểm tra URL (
expect URL) thành công, Playwright sẽ đóng bài test ngay lập tức (trong tích tắc). Lúc đó trang web có thể còn chưa kịp tải xong giao diện.Vì vậy, để có một bản báo cáo đẹp (hoặc file Trace đẹp), chúng ta nên kiểm tra sự xuất hiện của một cái nút hoặc dòng chữ cụ thể trên trang đích, thay vì chỉ kiểm tra mỗi đường link URL."*
📦Phần 2: CÁCH XEM FILE TRACE ĐÃ DOWNLOAD (.zip)
Khi test chạy trên CI/CD (Jenkins, GitLab CI) bị lỗi, hệ thống sẽ trả về một file Artifact tên là trace.zip.
Nhiều bạn mới học thường quen tay... Giải nén (Extract) file này ra.
👉 SAI LẦM! Trace Viewer đọc trực tiếp file .zip. Đừng giải nén nó!
Dưới đây là 3 cách để mở "Hộp đen" này ra xem:
Cách 1: Dùng trang web chính chủ (Dễ nhất - Không cần cài đặt) 🌐
Cách này tiện nhất khi bạn muốn gửi file cho Sếp, PM hoặc Tester khác (những người không cài Node.js/Playwright) xem bằng chứng lỗi.
-
Truy cập: trace.playwright.dev
-
Mở thư mục chứa file
.zipvừa tải về. -
Kéo và thả (Drag & Drop) file
.zipvào giữa màn hình trình duyệt. -
Xong! Giao diện Trace Viewer hiện ra ngay lập tức.
💡 Lưu ý bảo mật: Trang web này là PWA (Progressive Web App). Khi bạn kéo file vào, nó được xử lý ngay trên trình duyệt của bạn (Local), không hề upload dữ liệu lên server của Microsoft. Nên rất an toàn cho dữ liệu dự án.
Cách 2: Dùng dòng lệnh (Chuẩn Dev) 💻
Nếu bạn là Developer và đã cài Playwright trên máy, dùng lệnh này là nhanh nhất và mượt nhất (đặc biệt với file nặng > 50MB).
-
Mở Terminal (CMD/PowerShell) tại thư mục chứa file.
-
Gõ lệnh:
npx playwright show-trace <đường-dẫn-đến-file-zip>Ví dụ:
npx playwright show-trace ./Downloads/trace.zip -
Playwright sẽ tự bật cửa sổ Trace Viewer cục bộ lên.
Phần 3. Reporter
1. List Reporter (ListReporterOptions)
Loại này dùng để in kết quả ra Terminal (màn hình đen).
-
printSteps(boolean):-
false(Mặc định): Chỉ in ra tên bài test khi nó chạy xong. -
true: In chi tiết từng bước (test.step) bên trong bài test.
-
Ví dụ cấu hình:
reporter: [
['list', {
printSteps: true // 💡 Bật lên để thấy nó đang click vào đâu, fill cái gì ngay trên terminal
}]
],
2. HTML Reporter (HtmlReporterOptions)
Đây là loại có nhiều option nhất vì nó sinh ra trang web báo cáo.
-
outputFolder(string):-
Thư mục chứa báo cáo. Mặc định là
playwright-report. Bạn có thể đổi thànhmy-reportnếu thích.
-
-
open('always' | 'never' | 'on-failure'):-
'always': Chạy xong tự động bật trình duyệt mở báo cáo lên (Phiền nếu chạy CI). -
'never': Không bao giờ tự bật (Khuyên dùng cho CI/CD). -
'on-failure': Chỉ tự bật nếu có lỗi (Khuyên dùng cho máy Local).
-
-
host&port:-
Cấu hình địa chỉ IP và cổng để serve báo cáo (ít dùng, trừ khi bạn setup server riêng).
-
-
title(string):-
Đổi tiêu đề tab trình duyệt của báo cáo. Mặc định là "Playwright Test Report". Bạn có thể đổi thành "Dự án A Report".
-
-
attachmentsBaseURL(string):-
Dùng khi bạn muốn tách file ảnh/video ra server khác (CDN) để file HTML nhẹ hơn. (Nâng cao).
-
Ví dụ cấu hình chuẩn:
reporter: [
['html', {
outputFolder: 'bao-cao-test',
open: 'on-failure', // 💡 Chỉ mở khi có lỗi để đỡ phiền
title: 'Báo cáo Kiểm thử Dự án X', // 💡 Branding cho đẹp
host: '0.0.0.0', // 💡 Để đồng nghiệp cùng mạng LAN xem được (nếu cần)
port: 9323
}]
],
3. JUnit Reporter (JUnitReporterOptions)
Dùng cho hệ thống CI/CD (Jenkins, GitLab, Azure DevOps).
-
outputFile(string):-
Bắt buộc phải có nếu dùng loại này. Đường dẫn file XML muốn xuất ra (VD:
results.xml).
-
-
stripANSIControlSequences(boolean):-
true: Xóa bỏ các ký tự màu mè (mã màu của Terminal) khỏi file XML. -
Tại sao cần? Vì Jenkins đôi khi không đọc được mã màu, gây lỗi parse XML. Nên bật
true.
-
-
includeProjectInTestName(boolean):-
true: Thêm tên Project vào tên Test case. -
Ví dụ:
[chromium] Login Casethay vì chỉLogin Case. -
Tác dụng: Giúp phân biệt khi bạn chạy test trên nhiều trình duyệt (Chrome, Safari, Firefox).
-
Ví dụ cấu hình chuẩn cho CI:
reporter: [
['junit', {
outputFile: 'test-results/results.xml',
stripANSIControlSequences: true, // 💡 Xóa mã màu để XML sạch sẽ
includeProjectInTestName: true // 💡 Để biết lỗi ở Chrome hay Firefox
}]
],
4. JSON Reporter (JsonReporterOptions)
Dùng để xuất dữ liệu thô cho các tool thống kê tự viết.
-
outputFile(string):-
Đường dẫn file JSON muốn xuất ra.
-
Ví dụ cấu hình:
reporter: [
['json', { outputFile: 'test-results/data.json' }]
],
5. Blob Reporter (BlobReporterOptions)
Đây là hàng "cao cấp", dùng cho kỹ thuật Sharding (Chia nhỏ test chạy trên nhiều máy) rồi Gộp (Merge) lại sau.
-
outputDir(string):-
Thư mục chứa file blob (dạng binary). Mặc định là
blob-report.
-
-
fileName(string):-
Tên file cụ thể. Thường không cần set, để Playwright tự sinh tên ngẫu nhiên để tránh trùng lặp giữa các máy.
-
Khi nào dùng?
Khi bạn chạy test trên 5 máy khác nhau (Sharding 1/5 -> 5/5). Mỗi máy sẽ xuất ra 1 file Blob. Sau đó bạn gom 5 file Blob này lại để tạo ra 1 báo cáo HTML tổng hợp duy nhất.
Ví dụ cấu hình (Trên CI):
reporter: [
['blob', { outputDir: 'blob-reports' }] // 💡 Chỉ cần thế này là đủ
],
🎯 TỔNG HỢP CẤU HÌNH
Đây là file playwright.config.ts mẫu mực, tận dụng tối đa các options trên:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
// 1. List: Để Dev nhìn terminal cho sướng mắt
['list', { printSteps: true }],
// 2. HTML: Để xem chi tiết lỗi (chỉ mở khi fail)
['html', {
outputFolder: 'html-report',
open: 'on-failure',
title: 'Automation Report'
}],
// 3. JUnit: Để gửi cho Jenkins đọc
['junit', {
outputFile: 'results.xml',
stripANSIControlSequences: true,
includeProjectInTestName: true
}],
// 4. Blob: Nếu dự án lớn cần chạy nhiều máy (Sharding)
// ['blob', { outputDir: 'my-blobs' }]
],
});
Phần 4: ALLURE REPORT NÂNG CAO - "BÁO CÁO BIẾT NÓI"
1. Tư duy: Báo cáo không chỉ là Pass/Fail
Nếu HTML Report chỉ cho ta biết "Kết quả", thì Allure Report với allure-js-commons cho ta biết "Câu chuyện".
-
Parameters: Sếp hỏi "Test case này nhập email gì mà lỗi?". Nhìn báo cáo thấy ngay, không cần soi code.
-
Attachments: Dev hỏi "Lúc lỗi màn hình trông thế nào?". Có ngay ảnh chụp đính kèm đúng bước đó.
2. Cài đặt & Cấu hình
npm install -g allurenpm install --save-dev allure-playwright allure-js-commons
allure-playwright: Cấu hình trong playwright.config.ts.npm install -g allure: cài đặt allure globalallure-js-commons: Dùng để viết code trong file test.3. Thực hành: Viết Test Case "Full Option" 🛠️
Chúng ta sẽ viết một bài test Login thất bại (Negative Case). Đây là trường hợp cần nhiều thông tin nhất để debug.
💻 Code Mẫu (tests/login-rich-allure.spec.ts)
import { test, expect } from '@playwright/test';
// 👇 Thư viện "thần thánh" giúp report xịn xò
import * as allure from "allure-js-commons";
test('Login Negative Case: Sai Password', async ({ page }) => {
// 1️⃣ METADATA (Trang điểm cho bài test)
await allure.epic("Authentication Module");
await allure.feature("Login Feature");
await allure.story("Login với mật khẩu sai");
await allure.severity("critical");
await allure.owner("Anh Tester");
await allure.description("Kiểm tra hệ thống ngăn chặn đăng nhập khi sai pass.");
// Dữ liệu test
const testData = {
email: "admin@example.com",
password: "wrong_password_123"
};
// 2️⃣ STEP 1: Truy cập và Chụp ảnh bằng chứng
// Sử dụng allure.step để bọc code lại
await allure.step("Bước 1: Truy cập trang Login", async () => {
await page.goto('https://crm.anhtester.com/admin/authentication');
await expect(page).toHaveTitle(/Login/);
// 📸 KỸ THUẬT: Đính kèm ảnh chụp màn hình vào NGAY BƯỚC NÀY
const screenshot = await page.screenshot();
await allure.attachment("Ảnh màn hình trang Login", screenshot, "image/png");
});
// 3️⃣ STEP 2: Điền form (Có Log Parameter)
// stepContext giúp ta thêm tham số vào báo cáo
await allure.step("Bước 2: Điền thông tin đăng nhập", async (stepContext) => {
// 📝 KỸ THUẬT: Log lại dữ liệu đã nhập (Parameters)
// Giúp người đọc báo cáo biết ta đã nhập cái gì mà không cần đoán
await stepContext.parameter("Email Input", testData.email);
await stepContext.parameter("Password Input", "******"); // Che mật khẩu thật
await page.locator('input[name="email"]').fill(testData.email);
await page.locator('input[name="password"]').fill(testData.password);
// Chụp ảnh form sau khi điền
await allure.attachment("Form sau khi điền", await page.screenshot(), "image/png");
});
// 4️⃣ STEP 3: Submit và Kiểm tra lỗi
await allure.step("Bước 3: Click Login và Check lỗi", async () => {
await page.locator('button[type="submit"]').click();
// Verify thông báo lỗi hiện ra
const alertLocator = page.locator('div.alert-danger'); // Ví dụ locator
await expect(alertLocator).toBeVisible();
// Đính kèm text nội dung lỗi lấy được
const textContent = await alertLocator.textContent();
await allure.attachment("Nội dung thông báo lỗi", textContent || "No text", "text/plain");
});
});
4. Giải thích các "Vũ khí hạng nặng" trong code 🔫
Hãy giải thích kỹ cho học sinh 3 hàm mới này:
📸 allure.attachment(name, content, type)
-
Tác dụng: Ghim một file (ảnh, text, log, json) vào báo cáo.
-
Ví dụ:
await allure.attachment('Screenshot', pngBuffer, 'image/png'); -
Tại sao dùng? Thay vì chụp ảnh tự động chỉ khi Fail (screenshot: 'only-on-failure'), ta chủ động chụp ảnh ở những bước quan trọng (như điền form xong) để làm bằng chứng (Evidence).
📝 stepContext.parameter(name, value)
-
Tác dụng: Hiển thị một bảng thông số nhỏ ngay trong dòng Step đó.
-
Tại sao dùng? Khi chạy Data Driven Testing (chạy 100 user khác nhau), nếu test fail, nhìn vào parameter biết ngay là fail ở user nào (ví dụ:
Email: user_bi_khoa@gmail.com).
🪜 allure.step(name, callback)
-
Tương tự
test.stepcủa Playwright nhưng tương thích tốt hơn với các hàmattachmentvàparametercủa thư việnallure-js-commons.
Cấu hình (playwright.config.ts)
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['list'], // Xem ở terminal
['html'], // Xem debug nhanh
// 👇 Cấu hình Allure ở đây
['allure-playwright']
],
});
Chạy và Xem Kết Quả
Quy trình chuẩn (như script bạn đã viết):
-
Chạy test:
npx playwright test(Lúc này folder
allure-resultssẽ đầy ắp file JSON). -
Tạo báo cáo và mở lên:
allure generate ./allure-results -o ./allure-report
allure open ./allure-report
📚 Phần 5: TẠO BÁO CÁO ALLURE GIỮ LẠI LỊCH SỬ (HISTORY)
"Tại sao chạy xong npx playwright test rồi mà không xem được báo cáo ngay? Tại sao phải chạy thêm cái script dài dòng này?"
Câu trả lời nằm ở 2 vấn đề lớn sau đây:
1. Vấn đề "Nguyên Liệu thô" vs "Món Ăn" 🥩 ➡️ 🍱
Khi Playwright chạy xong, nó chỉ sinh ra các file .json hoặc .txt chứa kết quả (nằm trong thư mục allure-results). Đây giống như Thịt sống, Rau sống. không thể "ăn" (xem) được.
👉 Chúng ta cần một "Đầu bếp" (lệnh allure generate) để nấu đống nguyên liệu đó thành một trang web HTML đẹp đẽ.
2. Vấn đề "Mất Trí Nhớ" (Quan trọng nhất) 🧠
Mặc định, mỗi lần các tạo báo cáo mới, Allure sẽ XÓA SẠCH báo cáo cũ đi để làm lại từ đầu.
-
Hậu quả: Biểu đồ lịch sử (Trend Chart) sẽ luôn chỉ có 1 điểm duy nhất. Các em không biết được test case này hôm qua Pass hay Fail, tính ổn định ra sao.
-
Giải pháp: Script này đóng vai trò là "Người nhắc tuồng".
-
Trước khi nấu món mới, nó Copy lịch sử của lần chạy trước bỏ vào nồi nấu chung với kết quả mới.
-
Kết quả: Biểu đồ sẽ nối tiếp nhau (Lần 1 -> Lần 2 -> Lần 3...).
-
🚀 HƯỚNG DẪN CÁCH CHẠY (CHO TỪNG HỆ ĐIỀU HÀNH)
Trước tiên, đảm bảo trong thư mục scripts/ cđã có 2 file này :
allure-history.ps1 (Dành cho Windows)
$ResultsDir = "allure-results"
$ReportDir = "allure-report"
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " ALLURE REPORT GENERATOR" -ForegroundColor White
Write-Host "============================================" -ForegroundColor Cyan
# BƯỚC 1: Copy history từ report cũ
if (Test-Path "$ReportDir\history") {
Write-Host "[1/3] Copying history..." -ForegroundColor Yellow
if (-not (Test-Path $ResultsDir)) {
New-Item -ItemType Directory -Path $ResultsDir | Out-Null
}
if (Test-Path "$ResultsDir\history") {
Remove-Item -Recurse -Force "$ResultsDir\history"
}
Copy-Item -Recurse "$ReportDir\history" "$ResultsDir\history"
Write-Host " Done!" -ForegroundColor Green
} else {
Write-Host "[1/3] No history (first run)" -ForegroundColor Gray
}
# BƯỚC 2: Xóa report cũ trước khi generate mới (thay cho --clean)
if (Test-Path $ReportDir) {
Write-Host "[2/3] Cleaning old report..." -ForegroundColor Yellow
Remove-Item -Recurse -Force $ReportDir
Write-Host " Done!" -ForegroundColor Green
} else {
Write-Host "[2/3] No old report to clean" -ForegroundColor Gray
}
# BƯỚC 3: Generate report (Allure 3 syntax)
Write-Host "[3/3] Generating report..." -ForegroundColor Yellow
# dành cho phiên bản ver 2.0
# npx allure-commandline generate $ResultsDir -o $ReportDir --clean
allure generate $ResultsDir -o $ReportDir
if ($LASTEXITCODE -eq 0) {
Write-Host " Done!" -ForegroundColor Green
} else {
Write-Host " Failed!" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Open: allure open $ReportDir" -ForegroundColor White
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
allure-history.sh (Dành cho Mac/Linux)
#!/bin/bash
RESULTS_DIR="allure-results"
REPORT_DIR="allure-report"
echo ""
echo "============================================"
echo " ALLURE REPORT GENERATOR"
echo "============================================"
# BƯỚC 1: Copy history từ report cũ
if [ -d "$REPORT_DIR/history" ]; then
echo "[1/3] Copying history..."
mkdir -p "$RESULTS_DIR"
rm -rf "$RESULTS_DIR/history"
cp -r "$REPORT_DIR/history" "$RESULTS_DIR/history"
echo " Done!"
else
echo "[1/3] No history (first run)"
fi
# BƯỚC 2: Xóa report cũ trước khi generate mới (thay cho --clean)
if [ -d "$REPORT_DIR" ]; then
echo "[2/3] Cleaning old report..."
rm -rf "$REPORT_DIR"
echo " Done!"
else
echo "[2/3] No old report to clean"
fi
# BƯỚC 3: Generate report (Allure 3 syntax)
echo "[3/3] Generating report..."
# dành cho phiên bản ver 2.0
# npx allure-commandline generate $ResultsDir -o $ReportDir --clean
allure generate "$RESULTS_DIR" -o "$REPORT_DIR"
if [ $? -eq 0 ]; then
echo " Done!"
else
echo " Failed!"
exit 1
fi
echo ""
echo "============================================"
echo " Open: allure open $REPORT_DIR"
echo "============================================"
echo ""
🖥️ DÀNH CHO TEAM WINDOWS
Windows có cơ chế bảo mật chặn chạy script lạ, nên chúng ta cần lệnh đặc biệt để "vượt rào".
Cách 1: Chạy thủ công (Terminal)
Mở Terminal (PowerShell) và gõ lệnh sau:
powershell -ExecutionPolicy Bypass -File ./scripts/allure-history.ps1
-
Giải thích:
-
powershell: Gọi phần mềm PowerShell. -
-ExecutionPolicy Bypass: Chìa khóa vạn năng để Windows cho phép chạy script mà không hỏi quyền Admin. -
-File: Đường dẫn file script.
-
Cách 2: Tích hợp package.json (Khuyên dùng)
Thêm dòng này vào package.json:
"scripts": {
"report:win": "powershell -ExecutionPolicy Bypass -File ./scripts/allure-history.ps1"
}
👉 Cách chạy: npm run report:win
🍎 DÀNH CHO TEAM MACBOOK / LINUX
Mac/Linux khắt khe về quyền thực thi file (Permission), nên chúng ta cần cấp quyền trước.
Bước 0: Cấp quyền (Chỉ làm 1 lần đầu tiên)
Mở Terminal, chạy lệnh sau để cấp quyền thực thi cho file script:
chmod +x ./scripts/allure-history.sh
(Nếu không làm bước này, khi chạy sẽ bị báo lỗi Permission denied).
Cách 1: Chạy thủ công (Terminal)
Gõ lệnh:
./scripts/allure-history.sh
Cách 2: Tích hợp package.json (Khuyên dùng)
Thêm dòng này vào package.json:
"scripts": {
"report:mac": "./scripts/allure-history.sh"
}
👉 Cách chạy: npm run report:mac
📸 Kết quả mong đợi (Khi chạy thành công)
Dù là Win hay Mac, khi chạy xong màn hình sẽ hiện các bước màu xanh/vàng rất đẹp:
============================================
ALLURE REPORT GENERATOR
============================================
[1/3] Copying history...
Done!
[2/3] Cleaning old report...
Done!
[3/3] Generating report...
Report successfully generated to allure-report
Done!
============================================
Open: allure open allure-report
============================================
Cuối cùng, copy dòng lệnh allure open allure-report để xem thành quả nhé!
🛠️Phần 6: CUSTOM REPORTER: TỰ TAY VIẾT "THƯ KÝ" RIÊNG
1. Tại sao cần Custom Reporter?
Mặc dù Playwright đã có sẵn HTML, JUnit, JSON... nhưng đôi khi nhu cầu thực tế rất "dị":
-
Gửi thông báo Chat: Test xong thì bắn tin nhắn vào Slack / Microsoft Teams / Discord.
-
Lưu vào Database: Ghi kết quả vào InfluxDB, MongoDB để vẽ biểu đồ riêng bằng Grafana.
-
Logic riêng: "Nếu test fail quá 10% thì tự động gọi API restart server".
-
Console Log kiểu "Hổ Báo": In ra màn hình console theo màu sắc và format riêng của team.
👉 Custom Reporter cho phép bạn can thiệp vào từng nhịp thở của quá trình chạy Test.
2. Vòng đời (Lifecycle) - "Nhịp thở" của Reporter
Để viết được Custom Reporter, học sinh cần hiểu Playwright sẽ gọi các hàm nào và theo thứ tự nào.
Hãy tưởng tượng Reporter là một Thư ký ngồi trong phòng thi:
-
onBegin(): Giám thị hô "Bắt đầu làm bài!" (Playwright bắt đầu chạy).-
Làm gì? Khởi tạo biến đếm (pass = 0, fail = 0), in dòng "Start Testing...".
-
-
onTestBegin(): Học sinh bắt đầu làm câu 1.-
Làm gì? In ra "Đang chạy test A...".
-
-
onTestEnd(): Học sinh làm xong câu 1.-
Làm gì? Kiểm tra xem Pass hay Fail? Cộng điểm vào biến đếm.
-
-
onEnd(): Giám thị hô "Hết giờ! Thu bài".-
Làm gì? Tính tổng điểm. Gửi báo cáo đi (Slack/Email).
-
Thực hành: Viết "MySimpleReporter"
Chúng ta sẽ viết một reporter đơn giản: In ra console kết quả tóm tắt, nhưng có màu mè và đếm số lượng.
Bước 1: Tạo file my-reporter.ts
Tạo file này ở thư mục gốc (hoặc trong folder reporters/).
import { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
class MySimpleReporter implements Reporter {
// Biến dùng để đếm số lượng
private passed = 0;
private failed = 0;
private skipped = 0;
// 1. KHI BẮT ĐẦU CHẠY (Giám thị hô Start)
onBegin(config: FullConfig, suite: Suite) {
console.log(`🚀 BẮT ĐẦU CHẠY TEST SUITE: ${suite.allTests().length} bài test.`);
// Reset bộ đếm
this.passed = 0;
this.failed = 0;
this.skipped = 0;
}
// 2. KHI MỖI BÀI TEST BẮT ĐẦU
onTestBegin(test: TestCase, result: TestResult) {
// console.log(`▶️ Đang chạy: ${test.title}`);
}
// 3. KHI MỖI BÀI TEST KẾT THÚC (Quan trọng nhất)
onTestEnd(test: TestCase, result: TestResult) {
// Check trạng thái
if (result.status === 'passed') {
this.passed++;
console.log(`✅ [PASS] ${test.title}`);
} else if (result.status === 'failed') {
this.failed++;
console.log(`❌ [FAIL] ${test.title}`);
// Có thể in thêm lỗi nếu muốn: console.log(result.error?.message);
} else if (result.status === 'skipped') {
this.skipped++;
console.log(`⚠️ [SKIP] ${test.title}`);
}
}
// 4. KHI KẾT THÚC TOÀN BỘ (Giám thị thu bài)
onEnd(result: FullResult) {
console.log('\n====================================');
console.log('📊 TỔNG KẾT KẾT QUẢ:');
console.log(`✅ Passed: ${this.passed}`);
console.log(`❌ Failed: ${this.failed}`);
console.log(`⚠️ Skipped: ${this.skipped}`);
console.log('====================================');
if (this.failed > 0) {
console.log('😭 Có lỗi rồi đại vương ơi!');
} else {
console.log('🎉 Full xanh! Đi nhậu thôi!');
}
}
}
export default MySimpleReporter;
Bước 2: Cấu hình playwright.config.ts
Khai báo đường dẫn đến file reporter vừa tạo.
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Dùng Custom Reporter kết hợp với HTML
reporter: [
['html'],
['./my-reporter.ts'] // 👈 Trỏ vào file vừa tạo
],
});
Bước 3: Chạy thử
Chạy lệnh test bình thường: npx playwright test.
Console in ra dòng log do chính tay mình custom, cảm giác rất "quyền lực".
Code demo custom Report của mình
/**
* ═══════════════════════════════════════════════════════════════════════════
* CUSTOM PLAYWRIGHT REPORTER
* ═══════════════════════════════════════════════════════════════════════════
*
* File này định nghĩa một Custom Reporter cho Playwright Test Runner.
* Reporter là "người giám sát" theo dõi và báo cáo kết quả test.
*
* CÁC METHODS CỦA REPORTER:
* ─────────────────────────────────────────────────────────────────────────────
* | Method | Khi nào được gọi? |
* |---------------|-----------------------------------------------------------|
* | onBegin | Bắt đầu chạy test (1 lần duy nhất) |
* | onTestBegin | Mỗi khi 1 test case bắt đầu |
* | onStepBegin | Mỗi khi 1 step trong test bắt đầu (test.step()) |
* | onStepEnd | Mỗi khi 1 step kết thúc |
* | onTestEnd | Mỗi khi 1 test case kết thúc |
* | onStdOut | Mỗi khi test gọi console.log() |
* | onStdErr | Mỗi khi test gọi console.error() |
* | onError | Khi có lỗi toàn cục (worker crash, unhandled exception) |
* | onEnd | Kết thúc toàn bộ test run |
* | onExit | Ngay trước khi test runner thoát (cleanup) |
* | printsToStdio | Báo Playwright rằng reporter này ghi ra console |
* ─────────────────────────────────────────────────────────────────────────────
*
* CÁCH SỬ DỤNG:
* Trong playwright.config.ts, set: reporter: './custom.ts'
*/
import type {
FullConfig,
FullResult,
Reporter,
Suite,
TestCase,
TestResult,
TestStep,
TestError,
} from '@playwright/test/reporter';
// ═══════════════════════════════════════════════════════════════════════════
// ANSI ESCAPE CODES - Mã màu cho terminal
// ═══════════════════════════════════════════════════════════════════════════
// ANSI Escape Codes là chuỗi ký tự đặc biệt giúp thay đổi màu sắc text
// trong terminal. Format: '\x1b[<code>m' với code là số tương ứng màu.
// Ví dụ: '\x1b[31m' = màu đỏ, '\x1b[0m' = reset về mặc định
const c = {
reset: '\x1b[0m', // Reset về màu mặc định
bold: '\x1b[1m', // In đậm
dim: '\x1b[2m', // Mờ nhạt
italic: '\x1b[3m', // In nghiêng
underline: '\x1b[4m', // Gạch chân
// Màu chữ (Foreground)
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
// Màu nền (Background)
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
};
// ═══════════════════════════════════════════════════════════════════════════
// HÀM TIỆN ÍCH (HELPER FUNCTIONS)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Chuyển đổi milliseconds thành format dễ đọc
* Ví dụ: 1500 → "1.50s", 65000 → "1m 5s"
*/
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`; // Dưới 1 giây
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`; // Dưới 1 phút
// Trên 1 phút: hiện dạng "Xm Ys"
return `${Math.floor(ms / 60000)}m ${((ms % 60000) / 1000).toFixed(0)}s`;
}
/**
* Tạo thanh progress bar dạng text
* Ví dụ: [########........] 50%
* |--passed--|--failed--|--remaining--|
*/
function progressBar(passed: number, failed: number, total: number): string {
const width = 25; // Độ rộng thanh progress
const p = Math.round((passed / Math.max(total, 1)) * width);
const f = Math.round((failed / Math.max(total, 1)) * width);
return (
`${c.green}${'#'.repeat(p)}${c.reset}` + // Phần passed: ####
`${c.red}${'x'.repeat(f)}${c.reset}` + // Phần failed: xxxx
`${c.dim}${'.'.repeat(Math.max(0, width - p - f))}${c.reset}` // Còn lại: ....
);
}
/**
* Lấy thời gian hiện tại dạng HH:MM:SS
*/
function timestamp(): string {
return new Date().toLocaleTimeString('en-US', { hour12: false });
}
// ═══════════════════════════════════════════════════════════════════════════
// CUSTOM REPORTER CLASS
// ═══════════════════════════════════════════════════════════════════════════
// Implement interface Reporter của Playwright
// Tất cả methods đều là optional, chỉ cần implement những gì bạn cần
class CustomReporter implements Reporter {
// ─────────────────────────────────────────────────────────────────────────
// BIẾN LƯU TRẠNG THÁI
// ─────────────────────────────────────────────────────────────────────────
private startTime = 0; // Thời điểm bắt đầu test run
private passed = 0; // Số test PASSED
private failed = 0; // Số test FAILED
private skipped = 0; // Số test SKIPPED
private total = 0; // Tổng số test
private current = 0; // Test đang chạy (thứ mấy)
private failures: { title: string; error: string }[] = []; // Danh sách test lỗi
private globalErrors: string[] = []; // Lỗi toàn cục (worker crash)
private logs: Map<string, string[]> = new Map(); // Console logs theo test ID
private currentTestId = ''; // ID của test đang chạy
// ─────────────────────────────────────────────────────────────────────────
// onBegin - Được gọi KHI BẮT ĐẦU chạy test
// ─────────────────────────────────────────────────────────────────────────
// Tham số:
// - config: Cấu hình Playwright (workers, retries, v.v.)
// - suite: Cây chứa toàn bộ test cases
onBegin(config: FullConfig, suite: Suite) {
this.startTime = Date.now(); // Ghi nhận thời điểm bắt đầu
this.total = suite.allTests().length; // Đếm tổng số test
// In header đẹp
console.log('');
console.log(`${c.cyan}${'='.repeat(65)}${c.reset}`);
console.log(` ${c.bold}PLAYWRIGHT TEST RUNNER${c.reset} ${c.dim}v${config.version}${c.reset}`);
console.log(`${c.cyan}${'='.repeat(65)}${c.reset}`);
console.log(` ${c.gray}Started:${c.reset} ${timestamp()}`);
console.log(` ${c.gray}Tests:${c.reset} ${c.bold}${this.total}${c.reset} ${c.gray}Workers:${c.reset} ${c.bold}${config.workers}${c.reset}`);
console.log(`${c.dim}${'─'.repeat(65)}${c.reset}`);
console.log('');
}
// ─────────────────────────────────────────────────────────────────────────
// onTestBegin - Được gọi MỖI KHI 1 TEST BẮT ĐẦU
// ─────────────────────────────────────────────────────────────────────────
// Tham số:
// - test: Thông tin test case (title, file, location, v.v.)
onTestBegin(test: TestCase) {
this.current++; // Tăng bộ đếm test
this.currentTestId = test.id; // Lưu ID test hiện tại
this.logs.set(test.id, []); // Khởi tạo mảng logs cho test này
// Lấy tên file (bỏ phần đường dẫn)
const file = test.location.file.split(/[\\\/]/).pop();
const progress = `${c.gray}[${this.current}/${this.total}]${c.reset}`;
// In dòng thông báo test bắt đầu
console.log(`${progress} ${c.blue}>>>${c.reset} ${c.dim}${file}${c.reset} ${c.bold}>${c.reset} ${test.title}`);
}
// ─────────────────────────────────────────────────────────────────────────
// onStepBegin - Được gọi MỖI KHI 1 STEP BẮT ĐẦU
// ─────────────────────────────────────────────────────────────────────────
// Step = các bước trong test, được định nghĩa bằng test.step()
// Lưu ý: Playwright cũng tạo các "internal steps" (click, fill, v.v.)
// nên cần filter chỉ lấy category === 'test.step'
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
// Không log ở đây để tránh output bị lặp (sẽ log trong onStepEnd)
}
// ─────────────────────────────────────────────────────────────────────────
// onStepEnd - Được gọi MỖI KHI 1 STEP KẾT THÚC
// ─────────────────────────────────────────────────────────────────────────
// Tham số:
// - step.category: Loại step ('test.step' cho custom, 'pw:api' cho Playwright)
// - step.duration: Thời gian chạy (ms)
// - step.error: Lỗi nếu step fail
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
// Chỉ hiện các step do user định nghĩa (test.step()), bỏ qua internal steps
if (step.category === 'test.step') {
const duration = formatDuration(step.duration);
const icon = step.error ? `${c.red}x${c.reset}` : `${c.green}+${c.reset}`;
console.log(` ${c.dim}|${c.reset} ${icon} ${c.dim}${step.title}${c.reset} ${c.gray}(${duration})${c.reset}`);
}
}
// ─────────────────────────────────────────────────────────────────────────
// onTestEnd - Được gọi MỖI KHI 1 TEST KẾT THÚC
// ─────────────────────────────────────────────────────────────────────────
// Tham số:
// - result.status: 'passed' | 'failed' | 'skipped' | 'timedOut'
// - result.duration: Thời gian chạy test (ms)
// - result.error: Thông tin lỗi nếu test fail
// - result.retry: Số lần retry
onTestEnd(test: TestCase, result: TestResult) {
const duration = formatDuration(result.duration);
const file = test.location.file.split(/[\\\/]/).pop();
// Xác định status và màu sắc
let status: string;
switch (result.status) {
case 'passed':
this.passed++;
status = `${c.bgGreen}${c.bold} PASS ${c.reset}`;
break;
case 'failed':
this.failed++;
status = `${c.bgRed}${c.bold} FAIL ${c.reset}`;
// Lưu lại thông tin test fail để hiện ở cuối
this.failures.push({
title: `${file} > ${test.title}`,
error: result.error?.message?.split('\n')[0] || 'Unknown',
});
break;
case 'skipped':
this.skipped++;
status = `${c.bgYellow}${c.bold} SKIP ${c.reset}`;
break;
case 'timedOut':
this.failed++;
status = `${c.bgRed}${c.bold} TIME ${c.reset}`;
break;
default:
status = `${c.gray}[${result.status}]${c.reset}`;
}
// In kết quả test
console.log(` ${c.dim}└${c.reset} ${status} ${c.dim}(${duration})${c.reset}`);
// Thông báo nếu đây là retry
if (result.retry > 0) {
console.log(` ${c.yellow}^ Retry #${result.retry}${c.reset}`);
}
// Hiện console logs nếu có (giới hạn 3 dòng đầu)
const testLogs = this.logs.get(test.id) || [];
if (testLogs.length > 0) {
console.log(` ${c.magenta}Logs:${c.reset}`);
testLogs.slice(0, 3).forEach(log => {
// Cắt ngắn log nếu quá dài
console.log(` ${c.dim}| ${log.substring(0, 60)}${log.length > 60 ? '...' : ''}${c.reset}`);
});
}
console.log('');
}
// ─────────────────────────────────────────────────────────────────────────
// onStdOut - BẮT console.log() TỪ TEST
// ─────────────────────────────────────────────────────────────────────────
// Mỗi khi test gọi console.log(), method này được trigger
// Dùng để capture và lưu logs để hiện trong report
onStdOut(chunk: string | Buffer, test?: TestCase) {
const text = chunk.toString().trim();
if (test && text) {
const logs = this.logs.get(test.id) || [];
logs.push(text);
this.logs.set(test.id, logs);
}
}
// ─────────────────────────────────────────────────────────────────────────
// onStdErr - BẮT console.error() TỪ TEST
// ─────────────────────────────────────────────────────────────────────────
// Tương tự onStdOut nhưng cho stderr
onStdErr(chunk: string | Buffer, test?: TestCase) {
const text = chunk.toString().trim();
if (test && text) {
const logs = this.logs.get(test.id) || [];
logs.push(`[ERR] ${text}`); // Đánh dấu đây là error log
this.logs.set(test.id, logs);
}
}
// ─────────────────────────────────────────────────────────────────────────
// onError - ĐƯỢC GỌI KHI CÓ LỖI TOÀN CỤC
// ─────────────────────────────────────────────────────────────────────────
// Các lỗi này không thuộc về test cụ thể nào, ví dụ:
// - Worker process crash
// - Unhandled exception
// - Lỗi trong fixture
onError(error: TestError) {
this.globalErrors.push(error.message || 'Unknown global error');
console.log('');
console.log(`${c.bgRed}${c.bold} GLOBAL ERROR ${c.reset}`);
console.log(`${c.red}${error.message}${c.reset}`);
console.log('');
}
// ─────────────────────────────────────────────────────────────────────────
// onEnd - ĐƯỢC GỌI KHI TOÀN BỘ TEST KẾT THÚC
// ─────────────────────────────────────────────────────────────────────────
// Tham số:
// - result.status: 'passed' | 'failed' | 'timedout' | 'interrupted'
onEnd(result: FullResult) {
const duration = formatDuration(Date.now() - this.startTime);
const total = this.passed + this.failed + this.skipped;
const rate = total > 0 ? ((this.passed / total) * 100).toFixed(1) : '0';
// In summary
console.log(`${c.cyan}${'='.repeat(65)}${c.reset}`);
console.log(` ${c.bold}TEST RESULTS${c.reset} ${c.dim}Completed at ${timestamp()}${c.reset}`);
console.log(`${c.dim}${'─'.repeat(65)}${c.reset}`);
console.log(` [${progressBar(this.passed, this.failed, total)}] ${rate}%`);
console.log('');
console.log(` ${c.green}Passed:${c.reset} ${this.passed} ${c.red}Failed:${c.reset} ${this.failed} ${c.yellow}Skipped:${c.reset} ${this.skipped}`);
console.log(` ${c.gray}Duration:${c.reset} ${c.bold}${duration}${c.reset}`);
console.log(`${c.cyan}${'='.repeat(65)}${c.reset}`);
// Liệt kê các test fail
if (this.failures.length > 0) {
console.log('');
console.log(`${c.red}${c.bold}FAILED TESTS:${c.reset}`);
this.failures.forEach((t, i) => {
console.log(` ${c.red}${i + 1}. ${t.title}${c.reset}`);
console.log(` ${c.dim}${t.error.substring(0, 80)}${t.error.length > 80 ? '...' : ''}${c.reset}`);
});
}
// Liệt kê các lỗi toàn cục
if (this.globalErrors.length > 0) {
console.log('');
console.log(`${c.red}${c.bold}GLOBAL ERRORS:${c.reset}`);
this.globalErrors.forEach((err, i) => {
console.log(` ${c.red}${i + 1}. ${err}${c.reset}`);
});
}
// Banner kết quả cuối cùng
console.log('');
if (result.status === 'passed') {
console.log(`${c.bgGreen}${c.bold}${c.white} ALL TESTS PASSED ${c.reset}`);
} else if (result.status === 'timedout') {
console.log(`${c.bgYellow}${c.bold} TIMED OUT ${c.reset}`);
} else {
console.log(`${c.bgRed}${c.bold}${c.white} SOME TESTS FAILED ${c.reset}`);
}
console.log('');
}
// ─────────────────────────────────────────────────────────────────────────
// onExit - ĐƯỢC GỌI NGAY TRƯỚC KHI PLAYWRIGHT THOÁT
// ─────────────────────────────────────────────────────────────────────────
// Đây là cơ hội cuối cùng để:
// - Upload report lên server
// - Gửi thông báo Slack/Teams
// - Cleanup temp files
// - Ghi log vào database
async onExit() {
console.log(`${c.dim}Reporter cleanup complete.${c.reset}`);
}
// ─────────────────────────────────────────────────────────────────────────
// printsToStdio - BÁO PLAYWRIGHT RẰNG REPORTER NÀY GHI RA CONSOLE
// ─────────────────────────────────────────────────────────────────────────
// Nếu return true, Playwright biết không cần ghi thêm output mặc định
// Giúp tránh conflict với các built-in reporters
printsToStdio(): boolean {
return true;
}
}
// Export class để Playwright có thể sử dụng
export default CustomReporter;
