NỘI DUNG BÀI HỌC
🧠 Phần 1: Giải mã testInfo - "Thư ký hiện trường"
🧬 Phần 2: Nguồn gốc & Vòng đời (Lifecycle)
🗺️ Phần 3: Tư duy Phạm vi (Scope Strategy)
⚔️ Phần 4: Quy tắc Ghi đè (Override Rules)
🏷️ Phần 5: Nghệ thuật sử dụng Tags (@)
🎓 Phần 1: LÀM CHỦ testInfo TỪ A ĐẾN Z
Nếu page là "đôi tay" để bạn thao tác với trang web, thì testInfo chính là "Bộ não quản lý" của bài test đó. Nó nắm giữ mọi thông tin: Tên bài test là gì? Đang chạy lần thứ mấy? File nào? Có cần skip không?...
Cách gọi testInfo ra dùng
testInfo là tham số thứ 2 trong hàm test (sau Object { page }).
import { test } from '@playwright/test';
// 👇 Khai báo testInfo ở đây
test('Demo testInfo', async ({ page }, testInfo) => {
console.log(testInfo.title); // In ra tên bài test
});
Nhóm chức năng: LẤY THÔNG TIN (Read Only)
Dùng để log, debug hoặc viết logic rẽ nhánh (if/else).
| Thuộc tính | Ý nghĩa | Ví dụ thực tế |
title |
Tên bài test | In log: Đang chạy bài: ${testInfo.title} |
project.name |
Tên Project (Chrome, iPhone...) | if (testInfo.project.name === 'Mobile Safari') { ... } |
file |
Đường dẫn file test hiện tại | Debug xem bài test này nằm ở file nào. |
retry |
Số lần chạy lại (0, 1, 2...) | Logic: Nếu retry > 0 thì chờ lâu hơn chút. |
status |
Trạng thái (passed, failed, timedOut...) |
Thường dùng trong test.afterEach để check kết quả cuối cùng. |
duration |
Thời gian chạy (ms) | Check xem bài test chạy nhanh hay chậm. |
Nhóm chức năng: ĐIỀU KHIỂN LUỒNG (Control Flow)
Dùng để thay đổi số phận của bài test ngay trong lúc chạy.
A. testInfo.skip(condition, reason) - Bỏ qua động
Khác với test.skip() (bỏ qua cứng), lệnh này cho phép bạn chạy code kiểm tra trước rồi mới quyết định có skip hay không.
test('Test thanh toán Momo', async ({ page }, testInfo) => {
const isMomoMaintenance = await checkMomoStatus(); // Hàm tự viết
if (isMomoMaintenance) {
console.log('Momo đang bảo trì, nghỉ test!');
testInfo.skip(); // 🛑 Dừng ngay lập tức, đánh dấu là Skipped
}
// Code bên dưới sẽ không chạy nếu bị skip
await page.click('#pay-momo');
});
B. testInfo.setTimeout(ms) - Gia hạn thời gian
Mặc định bài test có 30s. Nhưng có một bài upload file nặng cần 2 phút. Đừng sửa config chung, hãy sửa riêng cho nó.
test('Upload file 1GB', async ({ page }, testInfo) => {
// Xin thêm thời gian riêng cho bài này (120 giây)
testInfo.setTimeout(120000);
await page.setInputFiles('#upload', 'big-file.zip');
});
C. testInfo.fail() - Ép phải Fail
Dùng khi bạn test một trường hợp "Mong đợi nó phải thất bại" (Negative Test). Ví dụ: Bug này Dev bảo chưa fix được, test chạy mà Pass là sai (vì đáng lẽ phải lỗi).
Nhóm chức năng: QUẢN LÝ DỮ LIỆU & SONG SONG (Parallel)
Đây là phần "ăn tiền" nhất của testInfo khi chạy CI/CD nhiều luồng.
A. testInfo.parallelIndex (Số thứ tự luồng)
Khi bạn chạy 4 workers song song. Làm sao để tạo dữ liệu không trùng nhau?
-
Worker A đang chạy bài 1.
-
Worker B đang chạy bài 2.
Nếu cả 2 cùng tạo user admin@gmail.com -> Lỗi trùng lặp (Duplicate).
👉 Giải pháp: Dùng parallelIndex (0, 1, 2, 3...) để gắn đuôi cho dữ liệu.
test('Đăng ký user song song', async ({ page }, testInfo) => {
// Worker 0 -> user_0@test.com
// Worker 1 -> user_1@test.com
const uniqueEmail = `user_${testInfo.parallelIndex}@test.com`;
await page.fill('#email', uniqueEmail);
});
B. testInfo.outputDir (Thư mục chứa rác của bài test này)
Mỗi bài test khi chạy sẽ được Playwright cấp cho một thư mục riêng trong test-results/... để chứa video, trace. Bạn có thể lấy đường dẫn này nếu muốn lưu thêm file riêng vào đó.
console.log(`Video của bài này đang nằm ở: ${testInfo.outputDir}`);
5. Nhóm chức năng: BÁO CÁO (Report Enhancements)
Làm đẹp cho HTML Report.
A. testInfo.attach() - Đính kèm bằng chứng
test('Attach bằng chứng', async ({ page }, testInfo) => {
const screenshot = await page.screenshot();
// Đính kèm ảnh vào Report
await testInfo.attach('Ảnh lỗi giỏ hàng', {
body: screenshot,
contentType: 'image/png'
});
// Đính kèm Text/Log/JSON
await testInfo.attach('API Response', {
body: JSON.stringify({ id: 1, status: 'error' }),
contentType: 'application/json'
});
});
B. testInfo.annotations - Gắn nhãn (Note)
Gắn link Jira, link Bug, hoặc ghi chú quan trọng.
test('Test Login', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://jira.company.com/browse/BUG-123'
});
testInfo.annotations.push({
type: 'author',
description: 'Anh Tester'
});
});
👉 Kết quả: Trong Report sẽ hiện các dòng này rất đẹp, bấm vào link được luôn.
🎯 TỔNG KẾT
| Bạn muốn làm gì? | Dùng lệnh testInfo... |
| Lấy tên bài test | .title |
| Lấy tên trình duyệt (Chrome/Safari) | .project.name |
| Bỏ qua bài test (Logic động) | .skip() |
| Xin thêm thời gian chạy | .setTimeout(ms) |
| Tạo dữ liệu Unique khi chạy song song | .parallelIndex |
| Đưa ảnh/log vào Report | .attach() |
| Gắn link Jira/Bug vào Report | .annotations.push() |
| Xem bài test này chạy lại lần mấy | .retry |
Phần 2: Nguồn cấp cho testInfo
testInfo không tự nhiên sinh ra. Nó được Playwright Test Runner (Bộ điều khiển trung tâm) tổng hợp từ 3 Nguồn Chính và "tiêm" (inject) vào hàm test của bạn ngay trước khi test chạy.
Dưới đây là sơ đồ "giải phẫu" nguồn gốc của testInfo:
Nguồn 1: Từ Code bạn viết (Static Analysis)
Trước khi chạy test, Playwright sẽ quét (scan) qua file .spec.ts của bạn để đọc chữ.
-
testInfo.title: Lấy từ dòngtest('Tên bài test', ...)bạn viết. -
testInfo.file: Lấy từ tên file.spec.tsđang chứa đoạn code đó. -
testInfo.line: Lấy số dòng code (Line number) nơi bài test bắt đầu. -
testInfo.fn: Lấy chính cái hàm (function) chứa code test của bạn.
👉 Ví dụ:
// file: login.spec.ts
test('Đăng nhập', async () => {});
// Playwright đọc dòng này -> Gán 'Đăng nhập' vào title, 'login.spec.ts' vào file.
Nguồn 2: Từ File Config (playwright.config.ts)
Đây là những thông tin "Môi trường" mà bạn đã cài đặt từ trước.
-
testInfo.project: Lấy từ mảngprojects: [...]. Nếu Worker đang chạy job cho 'Chrome', nó sẽ điền thông tin project Chrome vào đây. -
testInfo.timeout: Lấy từ settingtimeouttrong config (hoặc mặc định 30s). -
testInfo.repeatEachIndex: Nếu bạn config chạy lặp lại 5 lần (repeatEach: 5), nó sẽ đếm xem đây là lần lặp thứ mấy.
Nguồn 3: Từ "Thời gian thực" lúc chạy (Runtime)
Đây là những thông tin chỉ khi nào chạy mới biết được. Playwright Test Runner tính toán và cập nhật liên tục.
-
testInfo.retry:-
Lần đầu chạy: Runner gán = 0.
-
Nếu Fail, Runner quyết định chạy lại: Nó tăng số này lên = 1 rồi đưa lại cho bạn.
-
-
testInfo.workerIndex¶llelIndex:-
Khi bạn gõ lệnh chạy test, Playwright khởi động ví dụ 4 processes (Workers).
-
Nó sẽ đánh số áo cho từng worker: "Mày là số 0", "Mày là số 1"... và gán vào biến này.
-
-
testInfo.duration: Tính từ lúc bắt đầu (start time) đến lúc hiện tại. -
testInfo.status:-
Lúc bắt đầu: Trạng thái là "đang chạy".
-
Lúc kết thúc: Nếu bắt được lỗi (
Errorthrown) -> Cập nhật thành'failed'.
-
🔄 Quy trình "Đóng gói" và "Giao hàng"
Hãy tưởng tượng quy trình như sau:
-
Bước 1 (Chuẩn bị): Bạn gõ
npx playwright test. -
Bước 2 (Tổng hợp): Playwright Runner đọc Config + Đọc File Test.
-
Bước 3 (Phân công): Runner gọi Worker lên: "Ê Worker số 1, mày chạy bài test 'Login' trên trình duyệt 'Chrome' cho tao".
-
Bước 4 (Tạo Hồ Sơ): Runner tạo ra một cái Object
testInfo, điền hết thông tin vào:-
Title: "Login"
-
Project: "Chrome"
-
Worker: 1
-
Retry: 0
-
-
Bước 5 (Tiêm thuốc - Injection): Runner đưa cái
testInfonày vào tham số thứ 2 của hàm test.test('Login', async ({ page }, testInfo) => { // Lúc này biến testInfo đã đầy đủ dữ liệu để bạn dùng });
🎯 Tóm lại
testInfo là một bản "Hồ sơ nhiệm vụ" (Mission Dossier) được Bộ chỉ huy (Runner) soạn sẵn và đưa cho người lính (Test Function) trước khi ra trận.
-
Nó biết tên nhiệm vụ (từ code bạn viết).
-
Nó biết vũ khí sử dụng (từ config).
-
Nó biết số hiệu người lính (từ runtime).
🌐 Phần 3: Bản đồ phạm vi
Hãy tưởng tượng cấu trúc bài test của bạn có 3 tầng lớp:
-
Tầng File: (Ngoài cùng file
.spec.ts). -
Tầng Group: (Bên trong
test.describe). -
Tầng Test Function: (Bên trong
test('...', async () => {})).
Dưới đây là bảng phân bố quyền lực:
| Tầng Phạm Vi | test.* (Ví dụ: test.skip()) | testInfo.* (Ví dụ: testInfo.skip()) |
| 1. Toàn bộ File | ✅ DÙNG ĐƯỢC (Skip cả file) | ❌ CHẾT (Undefined) |
2. Trong Group (describe) |
✅ DÙNG ĐƯỢC (Skip cả nhóm) | ❌ CHẾT (Undefined) |
| 3. Trong Test Function | ✅ DÙNG ĐƯỢC | ✅ DÙNG ĐƯỢC |
| Tính chất | TĨNH (Static): Khai báo luật chơi từ đầu. | ĐỘNG (Dynamic): Xử lý tình huống khi đang chạy. |
Phân tích chi tiết từng tầng
🏢 Tầng 1: Phạm vi FILE (Global File Scope)
Ở tầng này, bạn muốn ra lệnh: "File này chưa viết xong, đừng chạy bài nào trong này cả!".
-
Chỉ
test.*làm được. -
testInfochưa được sinh ra ở đây.
import { test } from '@playwright/test';
// ✅ CÁCH ĐÚNG: Skip toàn bộ file này
test.skip(true, 'File này đang bảo trì');
// hoặc test.fixme();
// hoặc test.slow();
// ❌ CÁCH SAI: Báo lỗi ngay lập tức
// testInfo.skip(); // -> Error: testInfo is not defined
test('Bài test A', async ({ page }) => { ... });
test('Bài test B', async ({ page }) => { ... });
📂 Tầng 2: Phạm vi GROUP (Describe Block)
Bạn muốn gom nhóm: "Chỉ skip nhóm test liên quan đến Thanh toán, nhóm Đăng nhập vẫn chạy bình thường".
-
Chỉ
test.*làm được.
import { test } from '@playwright/test';
test.describe('Nhóm Thanh Toán', () => {
// ✅ CÁCH ĐÚNG: Chỉ ảnh hưởng các test nằm trong ngoặc {} này
test.skip();
test('Thanh toán thẻ', ...); // Bị skip
test('Thanh toán ví', ...); // Bị skip
});
test.describe('Nhóm Đăng Nhập', () => {
test('Login Gmail', ...); // Vẫn chạy bình thường
});
⚡ Tầng 3: Phạm vi TEST FUNCTION (Runtime)
Đây là nơi cả hai cùng tồn tại. Tuy nhiên, cách dùng và tư duy khác nhau.
A. Đối với test.* (Ưu tiên dùng để Check Môi trường)
Thường dùng ở dòng đầu tiên của hàm test để check các điều kiện "cứng" (OS, Browser).
test('Chụp ảnh màn hình', async ({ page, isMobile }) => {
// ✅ Viết tắt (Shorthand): Rất gọn
// Nếu là Mobile thì Skip luôn
test.skip(isMobile, 'Mobile không hỗ trợ chụp full page');
// ... code
});
B. Đối với testInfo.* (Ưu tiên dùng để Check Logic Nghiệp Vụ)
Dùng khi bạn phải chạy một đoạn code (crawl data, check API) rồi mới biết là có nên skip/fail hay không.
test('Check kho hàng', async ({ page }, testInfo) => { // 👈 Phải khai báo testInfo
await page.goto('/kho-hang');
const soLuong = await page.innerText('#stock');
// Logic nghiệp vụ phức tạp
if (soLuong === '0') {
console.log('Hết hàng, không test mua nữa');
// ✅ Dùng testInfo nhìn code sẽ tường minh hơn (theo phong cách OOP)
testInfo.skip();
}
await page.click('#buy-btn');
});
Tổng hợp các cặp lệnh tương đương
Dưới đây là danh sách các lệnh bạn có thể dùng ở cả 2 hệ (khi ở trong hàm test):
| Mục đích | Dùng test (Static/Shorthand) | Dùng testInfo (Object Method) |
| Bỏ qua | test.skip(condition, 'Lý do') |
if (cond) testInfo.skip('Lý do') |
| Đang sửa | test.fixme(condition, 'Lý do') |
if (cond) testInfo.fixme('Lý do') |
| Mong Fail | test.fail(condition, 'Lý do') |
if (cond) testInfo.fail('Lý do') |
| Chạy chậm | test.slow(condition, 'Lý do') |
if (cond) testInfo.slow('Lý do') |
| Gia hạn giờ | test.setTimeout(120000) |
testInfo.setTimeout(120000) |
⚠️ Lưu ý đặc biệt về setTimeout
-
test.setTimeout(ms): Set thời gian chết (Timeout) cho chính bài test đó. -
testInfo.setTimeout(ms): Tác dụng y hệt.
Tuy nhiên, test.setTimeout() cũng có thể dùng ở cấp File hoặc cấp Describe (điều mà testInfo không làm được).
// Cấp Group
test.describe('Nhóm Upload nặng', () => {
test.setTimeout(60000); // Tất cả test con đều có 60s
test('Upload 1', ...);
test('Upload 2', ...);
});
Lời khuyên "Vàng" để chọn lựa
Để code của bạn nhất quán và chuyên nghiệp, hãy tuân theo quy tắc này:
-
Luôn ưu tiên dùng
test.*:-
Cho việc cấu hình ở đầu file (
test.slow()). -
Cho việc cấu hình cả nhóm (
test.describe -> test.skip()). -
Cho việc check điều kiện môi trường đơn giản (
test.skip(isMobile)). -
Lý do: Code ngắn hơn, dễ đọc hơn ("Declarative" - Nhìn là biết ý định).
-
-
Chỉ dùng
testInfo.*khi:-
Bạn cần Skip/Fail dựa trên kết quả chạy của code (ví dụ: gọi API xong thấy server bảo trì -> skip).
-
Bạn đang viết Helper Function tách biệt (như ví dụ bài trước mình nói), nơi mà bạn chỉ truyền biến
testInfovào.
-
🔍 Phần 4: ĐIỂM KHÁC NHAU (VỀ BẢN CHẤT & CÁCH DÙNG)
Sự khác biệt không chỉ nằm ở tên gọi, mà nằm ở Tư duy lập trình (Paradigm).
Về Bản Chất (Nature)
-
test(Module Import): Là "NHÀ LẬP PHÁP" (Declarative)-
Nó dùng để định nghĩa cấu trúc và luật lệ của bài test ngay từ khi chưa chạy.
-
Nó trả lời câu hỏi: "Bài test này tên gì? Thuộc nhóm nào? Luật chơi là gì (Skip/Slow/Fixme)?"
-
Nó là TĨNH (Static).
-
-
testInfo(Object Instance): Là "THƯ KÝ HIỆN TRƯỜNG" (Runtime)-
Nó đại diện cho trạng thái hiện tại của bài test đang chạy.
-
Nó trả lời câu hỏi: "Hiện tại đang chạy đến đâu rồi? Đã chạy bao lâu? Có lỗi gì không? Có cần đính kèm bằng chứng gì không?"
-
Nó là ĐỘNG (Dynamic).
-
Về Vị Trí Đứng (Scope)
Đây là điểm chết người nếu dùng sai.
-
test: Có quyền lực bao trùm TOÀN BỘ FILE. -
testInfo: Chỉ sống sót trong HÀM TEST.
Ví dụ minh họa:
import { test } from '@playwright/test';
// ✅ KHU VỰC 1: Global Scope (Toàn File)
// Chỉ "Nhà lập pháp" test.* mới có quyền đứng đây.
test.slow();
// testInfo.slow(); ❌ SAI LẦM! (Lỗi: testInfo is not defined)
test.describe('Nhóm Admin', () => {
// ✅ KHU VỰC 2: Group Scope (Trong nhóm)
// Chỉ "Nhà lập pháp" test.* mới có quyền đứng đây.
test.skip();
test('Login', async ({ page }, testInfo) => {
// ✅ KHU VỰC 3: Test Function Scope (Trong hàm)
// Cả hai cùng tồn tại ở đây.
test.slow(); // OK
testInfo.slow(); // OK
});
});
Về Độ Linh Hoạt (Flexibility)
-
test(Viết tắt - Shorthand):-
Hỗ trợ cú pháp ngắn gọn, truyền điều kiện trực tiếp vào hàm.
-
Ví dụ:
test.skip(isMobile, 'Lý do');
-
-
testInfo(Tường minh - Explicit):-
Không hỗ trợ viết tắt. Bạn phải dùng cấu trúc
ifcủa ngôn ngữ lập trình. -
Ví dụ:
if (isMobile) { testInfo.skip(); }
-
TẠI SAO LẠI SINH RA 2 CÁI? (TRIẾT LÝ THIẾT KẾ)
Nếu test đã làm được gần hết mọi việc (Skip, Slow, Timeout...), tại sao đội ngũ Playwright lại đẻ thêm testInfo làm gì cho rắc rối?
Dưới đây là 3 lý do cốt lõi:
Lý do 1: Để "Đính kèm dữ liệu" (Data Attachment)
Thằng test chỉ là công cụ ra lệnh, nó không có chỗ chứa dữ liệu.
Khi bạn muốn chụp ảnh, lưu video, log text vào báo cáo, bạn cần một cái "Túi chứa" -> Đó là testInfo.
-
test.attach()❌ -> Không tồn tại lệnh này. -
testInfo.attach()✅ -> Có.
Lý do 2: Để hỗ trợ viết "Helper Functions" (Tách hàm) - QUAN TRỌNG NHẤT
Đây là lý do khiến dân chuyên nghiệp thích testInfo.
Giả sử bạn viết một hàm chung checkServerHealth ở file utils.ts.
-
Nếu dùng
test.skip(): Bạn phải importtestvào file utils. Điều này tạo ra sự phụ thuộc chặt chẽ (Coupling). -
Nếu dùng
testInfo: Bạn chỉ cần truyền objecttestInfovào như một tham số. Hàm utils không cần biếttestlà cái quái gì, nó chỉ tương tác với "hồ sơ nhiệm vụ".
Ví dụ:
// file: utils.ts
// Không cần import { test } from '@playwright/test'; -> Code sạch hơn
export async function checkHealth(testInfo) {
if (serverDead) {
testInfo.skip(); // Tự động skip bài test đang gọi nó
}
}
Lý do 3: Truy cập Metadata (Siêu dữ liệu)
Bạn không thể hỏi thằng test là: "Ê, tao đang chạy bài số mấy? Tên bài test là gì?". Thằng test không biết, nó chỉ biết định nghĩa thôi.
Chỉ có testInfo (Thư ký hiện trường) mới biết những thông tin runtime này (title, retry, duration, status...).
🎯 TỔNG KẾT
Để dễ nhớ, hãy tưởng tượng:
-
test(Sếp):-
Ra quy định từ đầu: "Dự án này làm chậm thôi (slow)", "Nhóm này nghỉ (skip)".
-
Đứng ở trên cao (File/Group level).
-
-
testInfo(Nhân viên hiện trường):-
Cầm hồ sơ đi làm: "Tên nhiệm vụ là X, làm lần thứ 2 (retry)".
-
Báo cáo tình hình: "Sếp ơi, em thấy server chết rồi, em tự nghỉ nhé (skip)".
-
Thu thập bằng chứng: "Đây là ảnh em chụp (attach)".
-
Vì vậy, Playwright cần cả hai: Một kẻ để Ra Luật (Rule) và một kẻ để Quản Lý Trạng Thái (State).
Phần 5: Quy tắc ghi giữa test và testInfo
"Quy tắc ghi đè" (Override Rules) của Playwright. Đây là một quy tắc vàng bạn cần nhớ:
👑 Quy tắc: "Càng vào sâu (Scope hẹp hơn) thì càng có quyền lực cao hơn."
Và "Lệnh chạy sau cùng sẽ chiến thắng".
Dưới đây là Bảng Xếp Hạng Quyền Lực từ thấp đến cao (Số 5 là yếu nhất, Số 1 là mạnh nhất).
🏆 Bảng Xếp Hạng Độ Ưu Tiên Timeout
| Thứ hạng | Vị trí đặt lệnh | Lệnh sử dụng | Độ ưu tiên |
| 5 (Yếu nhất) | playwright.config.ts |
timeout: 30000 |
Mặc định (Global). |
| 4 | Đầu file .spec.ts |
test.setTimeout() |
Ghi đè config chung cho file này. |
| 3 | Trong test.describe() |
test.setTimeout() |
Ghi đè file cho nhóm này. |
| 2 | Trong hàm test() |
test.setTimeout() |
Ghi đè nhóm cho bài test cụ thể này. |
| 1 (Mạnh nhất) | Trong hàm test() (Runtime) |
testInfo.setTimeout() |
Trùm cuối. Ghi đè tất cả mọi thứ trước đó. |
⚔️ Ví dụ thực chiến: "Đại Chiến Timeout"
Hãy xem đoạn code dưới đây. Mình đã cài đặt 5 tầng Timeout khác nhau. Theo bạn, cuối cùng bài test sẽ chết sau bao nhiêu giây?
import { test } from '@playwright/test';
// -----------------------------------------------------
// TẦNG 4: Cấp File (Ví dụ: 60 giây)
// Nó đã ghi đè cái config 30s mặc định rồi.
// -----------------------------------------------------
test.setTimeout(60000);
test.describe('Nhóm Upload Nặng', () => {
// ---------------------------------------------------
// TẦNG 3: Cấp Group (Ví dụ: 90 giây)
// Nó ghi đè cái 60s ở trên cho các test trong nhóm này.
// ---------------------------------------------------
test.setTimeout(90000);
test('Cuộc chiến vương quyền', async ({ page }, testInfo) => {
// -------------------------------------------------
// TẦNG 2: Cấp Hàm Test (Ví dụ: 120 giây)
// Nó ghi đè cái 90s của Group.
// -------------------------------------------------
test.setTimeout(120000);
console.log(`⏳ Timeout hiện tại (Sau tầng 2): ${testInfo.timeout}`);
// -> In ra 120000
// ... Chạy một lúc ...
if (true) {
// -----------------------------------------------
// TẦNG 1: Runtime (Ví dụ: 5 giây)
// "Tao đổi ý rồi, tao muốn kết thúc nhanh!"
// -----------------------------------------------
testInfo.setTimeout(5000);
console.log(`⏳ Timeout chốt hạ (Sau tầng 1): ${testInfo.timeout}`);
// -> In ra 5000
}
// Kết quả: Bài test này sẽ Time Out sau 5 GIÂY (kể từ lúc bắt đầu test)
// Bất chấp các ông lớn ở trên set bao nhiêu.
await page.waitForTimeout(10000);
});
});
💡 Tại sao testInfo lại thắng?
-
Scope hẹp nhất: Nó nằm trong cùng, cụ thể nhất cho từng tình huống chạy (Runtime).
-
Thời điểm thực thi:
-
test.setTimeout()ở Config/File/Describe được đọc TRƯỚC khi test chạy (lúc Playwright quét file). -
test.setTimeout()trong hàm test được chạy LÚC BẮT ĐẦU. -
testInfo.setTimeout()thường được gọi KHI ĐANG CHẠY (dựa trên logic if/else). Vì nó chạy sau cùng, nên nó chốt đơn cuối cùng.
-
⚠️ Lưu ý đặc biệt về "Cộng dồn" vs "Ghi đè"
Timeout trong Playwright là GHI ĐÈ (Override), không phải cộng dồn.
-
Describe set 30s.
-
Test set 10s.
-
=> Kết quả là 10s (chứ không phải 40s).
-
Nếu bài test chạy được 5s rồi, bạn gọi
testInfo.setTimeout(20000). Tổng thời gian sống của bài test sẽ được set lại thành 20s (tính từ lúc bắt đầu).
🎯 Tóm lại
Nếu có cả hai (hoặc nhiều hơn), thằng nào cụ thể hơn và nằm sâu hơn sẽ thắng.
-
test.describethắngtest.config. -
testfunction thắngtest.describe. -
testInfothắng tất cả.
Phần 6: Ví dụ về sử dụng testInfo với các config
🛠 Phần 1: Cấu hình playwright.config.ts
Trước tiên, phải có cấu hình thì testInfo mới có dữ liệu để đọc.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
retries: 1, // 👈 Để test thử tính năng testInfo.retry
timeout: 30000, // 👈 Nguồn cung cấp cho testInfo.timeout
use: {
baseURL: 'https://crm.anhtester.com',
screenshot: 'off', // Tắt chụp tự động để mình demo chụp thủ công bằng testInfo
},
projects: [
{
name: 'Google Chrome PC', // 👈 Nguồn cung cấp cho testInfo.project.name
use: { ...devices['Desktop Chrome'] }
},
],
});
🧪 Phần 2: File Test demo-test-info.spec.ts
Đây là nơi testInfo tỏa sáng. Hãy đọc kỹ các comment giải thích nguồn gốc dữ liệu nhé.
import { test, expect } from '@playwright/test';
// Mình dùng test.afterEach để check trạng thái cuối cùng (Status)
test.afterEach(async ({}, testInfo) => {
console.log(`\n🏁 [KẾT THÚC] Trạng thái: ${testInfo.status}`);
console.log(`⏱ [THỜI GIAN] Chạy mất: ${testInfo.duration}ms`);
});
test('Đăng ký tài khoản VIP (Full Demo testInfo)', async ({ page }, testInfo) => {
// ============================================================
// 1️⃣ NGUỒN 1: TỪ CODE BẠN VIẾT (Static Analysis)
// ============================================================
console.log('--- 📂 THÔNG TIN TĨNH ---');
console.log(`📝 Tên bài test: ${testInfo.title}`); // "Đăng ký tài khoản VIP..."
console.log(`📄 File chứa test: ${testInfo.file}`); // ".../demo-test-info.spec.ts"
// ============================================================
// 2️⃣ NGUỒN 2: TỪ CONFIG (Môi trường)
// ============================================================
console.log('--- ⚙️ CẤU HÌNH ---');
console.log(`🌍 Project: ${testInfo.project.name}`); // "Google Chrome PC"
console.log(`⏳ Timeout cho phép: ${testInfo.timeout}ms`); // 30000
// ============================================================
// 3️⃣ NGUỒN 3: TỪ RUNTIME (Thời gian thực)
// ============================================================
console.log('--- 🏃 THÔNG TIN CHẠY THỰC TẾ ---');
console.log(`🔄 Đây là lần chạy thứ: ${testInfo.retry + 1} (Retry Index: ${testInfo.retry})`);
console.log(`👷 Worker xử lý: Số ${testInfo.workerIndex}`);
// 💡 ỨNG DỤNG THỰC TẾ 1: Dùng parallelIndex để tạo Data không trùng
// Dù chạy song song 10 luồng, email này cũng không bao giờ bị trùng
const uniqueEmail = `vip_user_${testInfo.parallelIndex}_${Date.now()}@anhtester.com`;
console.log(`📧 Email đăng ký: ${uniqueEmail}`);
// 💡 ỨNG DỤNG THỰC TẾ 2: Logic Retry
// Nếu đây là lần chạy lại (do lần 1 bị fail), ta chờ lâu hơn xíu cho mạng ổn định
if (testInfo.retry > 0) {
console.log('⚠️ Đang chạy lại (Retry), chờ thêm 2 giây cho chắc ăn...');
await page.waitForTimeout(2000);
}
// 💡 ỨNG DỤNG THỰC TẾ 3: Dynamic Skip (Dựa trên Project)
// Giả sử tính năng này chưa làm xong trên Mobile
if (testInfo.project.name.includes('Mobile')) {
console.log('⛔️ Mobile chưa hỗ trợ -> Skip!');
testInfo.skip();
}
// --- BẮT ĐẦU CHẠY TEST ---
await page.goto('/admin/authentication');
await page.fill('input[name="email"]', uniqueEmail);
await page.fill('input[name="password"]', '123456');
// ============================================================
// 4️⃣ BÁO CÁO (REPORTING) - Ghi thêm vào hồ sơ
// ============================================================
// 🔗 1. Gắn link Jira vào Report
testInfo.annotations.push({
type: 'Jira Ticket',
description: 'https://jira.anhtester.com/browse/DEV-999'
});
// 📸 2. Chụp ảnh thủ công và ĐÍNH KÈM vào Report (Không lưu rác ở root)
const screenshotBuffer = await page.screenshot();
await testInfo.attach('Ảnh bằng chứng điền form', {
body: screenshotBuffer,
contentType: 'image/png'
});
// Giả sử check tiêu đề (Để Pass)
await expect(page).toHaveTitle(/Login/);
});
📊 Kết quả in ra Console (Mô phỏng)
Khi bạn chạy lệnh npx playwright test, đây là những gì bạn thấy trên màn hình đen (Console):
--- 📂 THÔNG TIN TĨNH ---
📝 Tên bài test: Đăng ký tài khoản VIP (Full Demo testInfo)
📄 File chứa test: C:\Project\tests\demo-test-info.spec.ts
--- ⚙️ CẤU HÌNH ---
🌍 Project: Google Chrome PC
⏳ Timeout cho phép: 30000ms
--- 🏃 THÔNG TIN CHẠY THỰC TẾ ---
🔄 Đây là lần chạy thứ: 1 (Retry Index: 0)
👷 Worker xử lý: Số 0
📧 Email đăng ký: vip_user_0_1699999999@anhtester.com
🏁 [KẾT THÚC] Trạng thái: passed
⏱ [THỜI GIAN] Chạy mất: 1245ms
📑 Kết quả trong HTML Report
Khi mở báo cáo lên (npx playwright show-report), bạn sẽ thấy bài test này cực kỳ giàu thông tin:
-
Header: Tên bài test, Project Chrome.
-
Annotations: Một dòng màu xanh ghi "Jira Ticket: https://jira..." (Bấm vào được).
-
Attachments: Một mục ảnh tên là "Ảnh bằng chứng điền form" (Bấm vào xem ảnh được).
-
Parameters: Email đã dùng (
vip_user_0...) nếu bạn log nó ra output.
Đây chính là sức mạnh của việc tận dụng triệt để "Hồ sơ nhiệm vụ" testInfo!
Phần 7: Tags là gì
Đây là tính năng cực kỳ quan trọng để quản lý hàng trăm bài test. Hãy tưởng tượng nó giống như Hashtag (#) trên Facebook/TikTok vậy. Nó giúp bạn lọc và chỉ chạy những bài test bạn muốn (ví dụ: chỉ chạy test chức năng Login, hoặc chỉ chạy test quan trọng smoke).
📁 BƯỚC 1: Chuẩn bị file Test
Tạo file: tests/demo-tags.spec.ts
import { test } from '@playwright/test';
// 1️⃣ Cách Cổ điển: Gắn thẳng vào tên (@smoke)
test('Đăng nhập thành công @smoke', async () => {
console.log('✅ [RUNNING] Test Đăng nhập (@smoke)');
});
// 2️⃣ Cách Hiện đại: Dùng object (tag: '@regression')
test('Thêm vào giỏ hàng', {
tag: '@regression'
}, async () => {
console.log('✅ [RUNNING] Test Giỏ hàng (@regression)');
});
// 3️⃣ Gắn nhiều tag cùng lúc (['@smoke', '@slow'])
test('Thanh toán thẻ Visa', {
tag: ['@smoke', '@slow']
}, async () => {
console.log('✅ [RUNNING] Test Thanh toán (@smoke + @slow)');
});
// 4️⃣ Gắn tag cho cả một nhóm (@auth)
test.describe('Nhóm Quản lý User', {
tag: '@auth'
}, () => {
test('Đổi mật khẩu', async () => {
console.log('✅ [RUNNING] Test Đổi mật khẩu (Thừa kế @auth)');
});
});
💻 BƯỚC 2: Chạy lệnh CLI (Phân biệt Windows vs Mac)
Đây là phần quan trọng nhất. Hãy chọn lệnh đúng với máy bạn đang dùng.
1. Chạy test Smoke (Lọc đơn giản)
Trường hợp này đơn giản, thường cả 2 đều chạy được không cần ngoặc, nhưng để an toàn tuyệt đối thì nên thêm vào.
-
Windows (PowerShell/CMD):
npx playwright test tests/demo-tags.spec.ts --grep "@smoke" -
Mac / Linux:
npx playwright test tests/demo-tags.spec.ts --grep @smokeKết quả mong đợi:
-
Chạy 2 bài:
-
Đăng nhập thành công(Vì có @smoke trên tên). -
Thanh toán thẻ Visa(Vì trong mảng tag có @smoke).
-
-
Các bài khác bị bỏ qua.
-
2. Chạy test KHÔNG CHẬM (Lọc ngược)
Dùng cờ --grep-invert.
-
Windows (PowerShell/CMD):
npx playwright test tests/demo-tags.spec.ts --grep-invert "@slow" -
Mac / Linux:
npx playwright test tests/demo-tags.spec.ts --grep-invert @slowKết quả mong đợi:
-
Chạy 3 bài.
-
Bỏ qua bài
Thanh toán thẻ Visa(Vì nó có tag@slow).
-
3. Chạy Smoke HOẶC Auth (Logic OR - Quan Trọng ⚠️)
Đây là chỗ hay lỗi nhất. Ký tự | (hoặc) rất nhạy cảm.
-
Windows: BẮT BUỘC phải bao toàn bộ regex trong ngoặc kép
"". -
Mac/Linux: Nên bao trong ngoặc đơn
''hoặc ngoặc kép đều được (để tránh shell hiểu nhầm). -
Windows (PowerShell/CMD):
# Bắt buộc có dấu ngoặc kép "..." bao quanh npx playwright test tests/demo-tags.spec.ts --grep "@smoke|@auth" -
Mac / Linux:
# Khuyên dùng ngoặc đơn '...' npx playwright test tests/demo-tags.spec.ts --grep '@smoke|@auth'Kết quả mong đợi:
-
Chạy 3 bài:
Đăng nhập,Thanh toán(smoke) vàĐổi mật khẩu(auth). -
Bỏ qua bài
Thêm vào giỏ hàng(vì nó là@regression).
-
⚙️ BƯỚC 3: Cấu hình trong playwright.config.ts
Cách tốt nhất để quên đi nỗi đau Windows vs Mac là đưa hết logic vào file config. Vì file config là JavaScript, nó chạy giống nhau trên mọi máy.
Sửa file playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Project 1: Chỉ chạy test Smoke
{
name: 'Smoke Tests',
grep: /@smoke/, // 👈 Regex lọc tag
use: { ...devices['Desktop Chrome'] },
},
// Project 2: Chạy toàn bộ hệ thống trừ bài chậm
{
name: 'Fast Regression',
grepInvert: /@slow/, // 👈 Regex lọc bỏ tag
use: { ...devices['Desktop Chrome'] },
},
],
});
Cách chạy (Giống hệt nhau cả 2 máy):
npx playwright test --project "Smoke Tests"
🧠 BƯỚC 4: Logic động trong Code (testInfo.tags)
Phần này dùng để xử lý logic bên trong bài test.
// file: tests/test-logic-tags.spec.ts
import { test } from '@playwright/test';
// 🔴 BÀI 1: Có Tags (@debug, @api)
test('Test tính năng Debug', {
tag: ['@debug', '@api']
}, async ({ page }, testInfo) => {
console.log('\n--- 🟢 BẮT ĐẦU BÀI TEST CÓ TAG ---');
// 1. Logic xử lý tag @debug
if (testInfo.tags.includes('@debug')) {
console.log('🐞 [DEBUG MODE] Đã phát hiện tag @debug!');
console.log(' -> Sẽ in ra nhiều log hơn bình thường...');
// Ví dụ: Tăng timeout lên 2 phút nếu đang debug
test.setTimeout(120000);
console.log(' -> Đã tăng Timeout lên 120s.');
}
// 2. Logic xử lý tag @api
if (testInfo.tags.includes('@api')) {
console.log('🌐 [API MODE] Đây là test API, chặn load ảnh CSS...');
// Ví dụ thực tế: Chặn request ảnh để chạy cho nhanh
await page.route('**/*.{png,jpg,jpeg,css}', route => route.abort());
}
// Chạy thử để xem có load ảnh không
await page.goto('https://example.com');
console.log('--- KẾT THÚC BÀI TEST CÓ TAG ---\n');
});
// 🔵 BÀI 2: Không có Tags (Để so sánh)
test('Test bình thường', async ({ page }, testInfo) => {
console.log('\n--- ⚪ BẮT ĐẦU BÀI TEST THƯỜNG ---');
if (testInfo.tags.includes('@debug')) {
console.log('🐞 Dòng này sẽ KHÔNG BAO GIỜ hiện ra ở bài này');
} else {
console.log('✅ Bài này không có tag @debug, chạy như thường.');
}
console.log('--- KẾT THÚC BÀI TEST THƯỜNG ---\n');
});
KẾT QUẢ MONG ĐỢI TRÊN MÀN HÌNH:
Running 2 tests using 1 worker
--- 🟢 BẮT ĐẦU BÀI TEST CÓ TAG ---
🐞 [DEBUG MODE] Đã phát hiện tag @debug!
-> Sẽ in ra nhiều log hơn bình thường...
-> Đã tăng Timeout lên 120s.
🌐 [API MODE] Đây là test API, chặn load ảnh CSS...
--- KẾT THÚC BÀI TEST CÓ TAG ---
--- ⚪ BẮT ĐẦU BÀI TEST THƯỜNG ---
✅ Bài này không có tag @debug, chạy như thường.
--- KẾT THÚC BÀI TEST THƯỜNG ---
2 passed (2.5s)
💡 Giải thích tại sao nó chạy đúng?
-
Khai báo: Khi bạn viết
{ tag: ['@debug'] }, Playwright sẽ lưu danh sách này vào metadata của bài test. -
Truy cập: Khi bài test chạy,
testInfođược sinh ra và nó copy danh sách tag kia vàotestInfo.tags. -
Logic: Code
ifcủa bạn kiểm tra mảng này, nếu có thì chạy code bên trong.
