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òng test('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ảng projects: [...]. 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ừ setting timeout trong 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 & parallelIndex:

    • 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 (Error thrown) -> 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:

  1. Bước 1 (Chuẩn bị): Bạn gõ npx playwright test.

  2. Bước 2 (Tổng hợp): Playwright Runner đọc Config + Đọc File Test.

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

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

  5. Bước 5 (Tiêm thuốc - Injection): Runner đưa cái testInfo nà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:

  1. Tầng File: (Ngoài cùng file .spec.ts).

  2. Tầng Group: (Bên trong test.describe).

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

  • testInfo chư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:

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

  2. 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 testInfo và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úcluậ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 if củ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 import test và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 object testInfo vào như một tham số. Hàm utils không cần biết test là 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:

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

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

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

  2. 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ơnnằm sâu hơn sẽ thắng.

  • test.describe thắng test.config.

  • test function thắng test.describe.

  • testInfo thắ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:

  1. Header: Tên bài test, Project Chrome.

  2. Annotations: Một dòng màu xanh ghi "Jira Ticket: https://jira..." (Bấm vào được).

  3. Attachments: Một mục ảnh tên là "Ảnh bằng chứng điền form" (Bấm vào xem ảnh được).

  4. 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 @smoke

    Kế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 @slow

    Kế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?

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

  2. Truy cập: Khi bài test chạy, testInfo được sinh ra và nó copy danh sách tag kia vào testInfo.tags.

  3. Logic: Code if của bạn kiểm tra mảng này, nếu có thì chạy code bên trong.



Teacher

Teacher

Nguyên Hoàng

Automation Engineer

With 7+ years of hands-on experience across multiple languages and frameworks. I'm here to share knowledge, helping you turn complex processes into simple and effective solutions.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học