Playwright cho người mới
0% hoàn thành

QA Automation với Playwright + TypeScript

Khóa học dành cho người chưa từng viết một dòng code nào. Bạn sẽ đi từ “máy tính là gì khi lập trình” đến tự viết bộ test tự động chạy trên GitHub.

19bài học
16+hình minh họa
40câu hỏi ôn tập
1dự án thực tế

Bạn sẽ đi qua 5 chặng

🧭 Khởi động QA là gì, cài đặt 🧱 Lập trình cơ bản TypeScript từ số 0 🎭 Playwright cốt lõi Locator, Action, Assert 🏗️ Nâng cao POM, Fixture, API 🏆 Dự án & CI Sẵn sàng đi làm
Mỗi chặng xây trên chặng trước. Đừng nhảy cóc nhé!

Cách học hiệu quả nhất

📖

1. Đọc & xem hình

Mỗi khái niệm đều có ví dụ đời thường và hình minh họa.

⌨️

2. Tự gõ lại code

Đừng copy-paste ngay. Gõ tay giúp ngón tay và não cùng nhớ.

🧪

3. Làm bài thực hành

Khung viền đứt nét màu xanh là bài tập. Có lời giải để đối chiếu.

✅

4. Trả lời quiz

Cuối mỗi bài có câu hỏi. Sai cũng không sao, đọc giải thích là nhớ.

Kế hoạch gợi ý: 4 tuần, mỗi ngày 1 giờ

TuầnNội dungKết quả
Tuần 1Bài 0 → 5: QA, cài đặt, TypeScript cơ bản, async/awaitĐọc hiểu được code đơn giản
Tuần 2Bài 6 → 11: Web, test đầu tiên, Locator, Action, Assertion, DebugTự viết test cho 1 trang web
Tuần 3Bài 12 → 15: Tổ chức test, POM, Config, Fixture, APIViết test gọn gàng, dễ bảo trì
Tuần 4Bài 16 → 18: Dự án SauceDemo, CI/CD, ôn tậpCó dự án đưa vào CV
💾 Tiến độ được lưu tự động

Bấm “Đánh dấu hoàn thành” ở cuối mỗi bài. Thanh tiến độ trên cùng sẽ tăng dần, và lần sau mở lại file, bạn sẽ quay về đúng bài đang học (trên cùng trình duyệt).

🌱 Một lời động viên

Lập trình giống học lái xe: lúc đầu thấy rất nhiều thứ phải nhớ cùng lúc, nhưng sau vài tuần mọi thứ trở thành phản xạ. Ai cũng từng là người mới. Cứ đi chậm mà chắc.

QA, Testing & Automation là gì?

Trước khi viết code, hãy hiểu mình đang làm nghề gì và tại sao nghề này quan trọng.

🎯 Sau bài này bạn sẽ
  • Giải thích được QA, bug, test case là gì
  • Phân biệt kiểm thử thủ công và tự động
  • Biết Playwright là gì và vì sao dùng TypeScript

Bug là gì?

Bug (lỗi) là khi phần mềm hoạt động khác với mong đợi. Ví dụ:

  • Bấm nút “Thanh toán” nhưng không có gì xảy ra.
  • Nhập sai mật khẩu vẫn đăng nhập được.
  • Giỏ hàng có 2 món nhưng hiển thị số 3.
🍰 Ví dụ đời thường

Hãy tưởng tượng một nhà máy làm bánh. Developer là thợ làm bánh. QA (Quality Assurance – Đảm bảo chất lượng) là người kiểm tra từng chiếc bánh trước khi đến tay khách: đủ ngọt chưa, có bị cháy không, hộp có đúng nhãn không. Nếu bánh lỗi lọt ra ngoài, khách hàng sẽ phàn nàn và công ty mất uy tín.

Test case – kịch bản kiểm thử

Một test case là một kịch bản gồm: các bước làm + kết quả mong đợi.

MãBước thực hiệnKết quả mong đợi
TC011. Mở trang đăng nhập
2. Nhập email đúng, mật khẩu đúng
3. Bấm “Đăng nhập”
Chuyển đến trang chủ, hiện “Xin chào, An”
TC021. Mở trang đăng nhập
2. Nhập mật khẩu sai
3. Bấm “Đăng nhập”
Hiện thông báo lỗi “Sai mật khẩu”

Thủ công vs Tự động

🧑 Kiểm thử thủ công 🤖 Kiểm thử tự động 🖱️ 😓 ⏱ 30 phút cho 1 lượt kiểm tra 😴 Dễ mệt, dễ bỏ sót khi lặp lại Chrome ✓ Firefox ✓ Safari ✓ ⚡ 2 phút, chạy song song nhiều trình duyệt 🔁 Lặp lại chính xác 1000 lần, chạy cả đêm
Automation không thay thế con người – nó làm phần việc lặp đi lặp lại để QA tập trung vào việc cần suy nghĩ.
Thủ côngTự động
Phù hợp khiKhám phá tính năng mới, kiểm tra giao diện “có đẹp không”Kiểm tra lặp lại (regression), chạy mỗi lần có code mới
Chi phí ban đầuThấpCao hơn (phải viết code)
Chi phí về lâu dàiCao (tốn người, tốn giờ)Thấp (máy chạy miễn phí)
Độ chính xác khi lặp lạiGiảm dần vì mệtLuôn giống nhau

Kim tự tháp kiểm thử

E2E / UI API / Integration Unit test ← Playwright mạnh nhất ở đây ← Playwright cũng làm được ← Developer viết Chậm hơn, tốn hơn Nhanh, rẻ, nhiều
E2E (End-to-End) = kiểm tra từ đầu đến cuối như người dùng thật: mở trình duyệt, bấm, gõ, xem kết quả.

Playwright là gì?

Playwright là một công cụ miễn phí do Microsoft phát triển, cho phép bạn viết code để điều khiển trình duyệt giống hệt người thật: mở trang, bấm nút, gõ chữ, rồi kiểm tra xem kết quả có đúng không.

📝 Code test login.spec.ts 🎭 Playwright 🟦 Chromium (Chrome, Edge) 🟧 Firefox 🟪 WebKit (Safari)
Viết một lần, chạy trên cả 3 “họ” trình duyệt phổ biến nhất.
⏳

Tự động chờ

Tự đợi nút hiện ra rồi mới bấm – ít lỗi “chập chờn”.

🎥

Ghi lại mọi thứ

Ảnh chụp, video, “hộp đen” trace để xem lại lỗi.

🪄

Tự sinh code

Công cụ Codegen ghi lại thao tác của bạn thành code.

🔌

Test cả API

Không chỉ giao diện, còn gọi API trực tiếp.

Vì sao chọn TypeScript?

JavaScript là ngôn ngữ của web. TypeScript = JavaScript + “kiểu dữ liệu”. Nghe khó nhưng lợi ích rất dễ hiểu:

JavaScript

Bạn gõ nhầm page.clik(). Không ai nhắc. Chạy test mới biết lỗi.

TypeScript

Gõ nhầm page.clik() → VS Code gạch đỏ ngay và gợi ý click. Sửa trước khi chạy.

TypeScript giống như có một người kèm cặp ngồi cạnh, nhắc bạn mỗi khi gõ sai. Với người mới, đây là món quà lớn.

📝 Ôn tập

1. Một test case gồm những phần chính nào?

Test case = các bước làm + kết quả mong đợi. Thiếu kết quả mong đợi thì không biết test đúng hay sai.

2. Loại kiểm thử nào phù hợp nhất để tự động hóa?

Tự động hóa tỏa sáng ở các việc lặp lại nhiều lần với kết quả rõ ràng (regression test).

3. Playwright có thể chạy trên trình duyệt nào?

Playwright hỗ trợ cả 3 engine: Chromium (Chrome/Edge), Firefox, WebKit (Safari).

Cài đặt công cụ

Chuẩn bị “bộ đồ nghề”: Node.js, VS Code và Playwright. Chỉ mất khoảng 15 phút.

🎯 Sau bài này bạn sẽ
  • Cài xong Node.js, VS Code, Playwright
  • Biết dùng Terminal ở mức cơ bản
  • Chạy được bài test mẫu đầu tiên

Terminal là gì?

💬 Ví dụ đời thường

Thông thường bạn ra lệnh cho máy tính bằng chuột (bấm vào icon). Terminal là cách “nhắn tin” cho máy tính bằng chữ. Bạn gõ một lệnh, nhấn Enter, máy làm theo và trả lời lại bằng chữ.

Trong VS Code, mở Terminal bằng menu Terminal → New Terminal hoặc phím tắt Ctrl + ` (phím dưới Esc).

Các bước cài đặt

  1. Cài Node.js Vào nodejs.org, tải bản LTS (bản ổn định), cài như phần mềm bình thường (Next → Next → Finish). Node.js là “động cơ” để chạy code JavaScript/TypeScript ngoài trình duyệt. Kèm theo là npm – “cửa hàng ứng dụng” để tải thư viện như Playwright.
    # Kiểm tra đã cài thành công chưa (mở Terminal mới rồi gõ):
    node -v
    npm -v
    # Nếu hiện ra số phiên bản (ví dụ v22.x.x) là thành công 🎉
  2. Cài Visual Studio Code Tải tại code.visualstudio.com. Đây là “cuốn vở” để viết code. Sau khi cài, vào tab Extensions (biểu tượng 4 ô vuông) và cài extension “Playwright Test for VSCode” của Microsoft.
  3. Tạo thư mục dự án Tạo thư mục, ví dụ D:\hoc-playwright. Trong VS Code chọn File → Open Folder và mở thư mục đó.
  4. Cài Playwright Mở Terminal trong VS Code và gõ:
    npm init playwright@latest
    Máy sẽ hỏi vài câu. Cứ chọn như sau (dùng phím mũi tên và Enter):
    Câu hỏiChọn
    Do you want to use TypeScript or JavaScript?TypeScript
    Where to put your end-to-end tests?tests (Enter)
    Add a GitHub Actions workflow?true (dùng ở bài 17)
    Install Playwright browsers?true
  5. Chạy thử test mẫu
    npx playwright test          # chạy tất cả test
    npx playwright show-report   # mở báo cáo đẹp trên trình duyệt
    Nếu thấy dòng chữ màu xanh kiểu 6 passed là bạn đã thành công!

Cấu trúc thư mục sau khi cài

📁 hoc-playwright/ ├── 📁 .github/workflows/ ← file chạy test tự động trên GitHub (bài 17) ├── 📁 node_modules/ ← thư viện đã tải về. KHÔNG sửa, KHÔNG đụng vào ├── 📁 tests/ ← NƠI BẠN VIẾT TEST ⭐ │ └── example.spec.ts ← test mẫu ├── package.json ← “lý lịch” dự án: tên, thư viện đang dùng └── playwright.config.ts ← cấu hình: trình duyệt nào, chờ bao lâu… (bài 14)
💡 Quy ước đặt tên

File test luôn kết thúc bằng .spec.ts (ví dụ login.spec.ts). Playwright tự tìm các file này để chạy.

Lỗi thường gặp (Windows)

❌ 'node' is not recognized…

Bạn mở Terminal trước khi cài Node.js. Đóng VS Code hoàn toàn rồi mở lại. Nếu vẫn lỗi, khởi động lại máy.

❌ npx : File ... cannot be loaded because running scripts is disabled

PowerShell đang chặn chạy script. Chạy lệnh sau một lần là xong:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
🧪 Thực hành
  1. Chạy npx playwright test --ui. Một cửa sổ đẹp sẽ hiện ra – đây là UI Mode.
  2. Bấm nút ▶ cạnh một test để chạy. Xem các bước hiện ra bên phải.
  3. Chạy npx playwright test --headed để thấy trình duyệt thật mở lên và tự bấm.

📝 Ôn tập

1. Bạn nên viết file test vào thư mục nào?

Thư mục tests là nơi chứa test. node_modules là thư viện tải về – tuyệt đối không sửa.

2. Lệnh nào mở báo cáo kết quả test?

show-report mở báo cáo HTML với chi tiết từng test pass/fail.

TypeScript #1: Biến & kiểu dữ liệu

Bài học lập trình đầu tiên. Không cần giỏi toán, chỉ cần biết đọc và làm theo từng bước.

🎯 Sau bài này bạn sẽ
  • Hiểu “code” là gì và máy chạy code ra sao
  • Tạo biến với const, let
  • Biết 3 kiểu dữ liệu cơ bản: chữ, số, đúng/sai

Code là gì?

🍳 Ví dụ đời thường

Code giống một công thức nấu ăn cực kỳ chi tiết. Máy tính là đầu bếp rất nghe lời nhưng không biết tự suy nghĩ: nó đọc từ trên xuống dưới, từng dòng một, và làm đúng y như được viết – kể cả khi bạn viết sai.

Chạy code ở đâu?

Tạo file hoc.ts trong thư mục dự án, viết code vào, rồi chạy bằng lệnh:

npx tsx hoc.ts

Hoặc thử nhanh không cần cài gì tại TypeScript Playground (bấm Run, xem kết quả ở tab Logs).

console.log – “nói” ra màn hình

// Dòng bắt đầu bằng // là "chú thích" – máy bỏ qua, chỉ để người đọc
console.log('Xin chào Playwright!');
console.log(2 + 3);

Kết quả trong Terminal:

Xin chào Playwright!
5

Biến – chiếc hộp có dán nhãn

Biến là chỗ để lưu một giá trị, đặt tên để dùng lại sau.

'An' string (chữ) ten 25 number (số) tuoi true boolean (đúng/sai) daDangNhap
Tên biến là nhãn dán ngoài hộp. Giá trị là thứ nằm bên trong hộp.
const ten = 'An';          // hộp tên "ten", bên trong là chữ 'An'
const tuoi = 25;           // hộp "tuoi", bên trong là số 25
let daDangNhap = true;     // hộp "daDangNhap", bên trong là true (đúng)

console.log(ten);          // In ra: An
console.log(tuoi + 1);     // In ra: 26

const hay let?

🔒 const

Hộp niêm phong. Bỏ đồ vào rồi thì không đổi được nữa. Dùng mặc định.

🔓 let

Hộp có nắp mở. Có thể thay đồ bên trong. Chỉ dùng khi giá trị thực sự cần thay đổi.

let soLanThu = 1;
soLanThu = 2;      // ✅ được, vì là let

const matKhau = 'abc123';
matKhau = 'xyz';   // ❌ LỖI: không thể gán lại biến const

3 kiểu dữ liệu cơ bản

KiểuÝ nghĩaVí dụDùng trong test
stringChữ, đặt trong dấu nháy ' ''admin@shop.vn'Email, mật khẩu, tiêu đề trang
numberSố, không có dấu nháy3, 99.5Số lượng sản phẩm, giá tiền
booleanChỉ có true hoặc falsetrueĐã đăng nhập chưa? Có hiển thị không?
⚠️ Bẫy kinh điển

'5' (có nháy) là chữ, còn 5 là số. '5' + 1 cho ra '51' (ghép chữ), còn 5 + 1 cho ra 6.

TypeScript “dán nhãn kiểu” cho biến

const email: string = 'an@shop.vn';
const soLuong: number = 3;

const gia: number = 'một trăm';  // ❌ TypeScript báo lỗi ngay:
// Type 'string' is not assignable to type 'number'.

Phần : string nói với TypeScript: “hộp này chỉ đựng chữ”. Thường TypeScript tự đoán được nên bạn không cần viết, nhưng hiểu nó giúp bạn đọc thông báo lỗi.

Ghép chữ với template string

const ten = 'An';
const soDon = 3;
// Dùng dấu huyền ` (phím cạnh số 1) và ${...} để chèn biến vào
console.log(`Xin chào ${ten}, bạn có ${soDon} đơn hàng`);
// → Xin chào An, bạn có 3 đơn hàng

So sánh & logic

Ký hiệuÝ nghĩaVí dụKết quả
===bằng nhau3 === 3true
!==khác nhau'a' !== 'b'true
> < >= <=lớn hơn, nhỏ hơn…18 >= 18true
&&VÀ (cả hai đều đúng)true && falsefalse
||HOẶC (một trong hai đúng)true || falsetrue
!KHÔNG (đảo ngược)!truefalse
💡 Luôn dùng 3 dấu bằng

= là gán (bỏ đồ vào hộp). === là so sánh. Đừng nhầm hai cái này.

🧪 Thực hành

Tạo biến lưu: tên sản phẩm “Áo thun”, giá 150000, số lượng 2, còn hàng (true). In ra câu: Áo thun x2 = 300000đ.

Xem lời giải
const tenSP = 'Áo thun';
const gia = 150000;
const soLuong = 2;
const conHang = true;
console.log(`${tenSP} x${soLuong} = ${gia * soLuong}đ`);

📝 Ôn tập

1. Nên dùng gì để khai báo một biến không thay đổi?

const là lựa chọn mặc định. Chỉ dùng let khi cần gán lại.

2. '10' + 5 cho ra kết quả gì?

'10' là chữ nên dấu + sẽ ghép chữ: '105'.

3. true && false bằng?

VÀ (&&) chỉ đúng khi cả hai đều đúng.

TypeScript #2: Điều kiện, mảng, vòng lặp, object, hàm

5 “viên gạch” này đủ để bạn đọc hiểu 90% code test Playwright.

🎯 Sau bài này bạn sẽ
  • Cho máy “ra quyết định” với if/else
  • Lưu danh sách bằng mảng, lặp qua bằng for
  • Gom thông tin bằng object, đóng gói thao tác bằng hàm

1. if / else – Ra quyết định

Nhập tuổi tuoi >= 18? đúng sai ✅ “Mời vào” ⛔ “Chưa đủ tuổi”
Giống người bảo vệ ở cửa: kiểm tra một điều kiện rồi chọn đường đi.
const tuoi = 20;

if (tuoi >= 18) {
  console.log('Mời vào');        // chạy khi điều kiện ĐÚNG
} else {
  console.log('Chưa đủ tuổi');   // chạy khi điều kiện SAI
}

Dấu ngoặc nhọn { } bao một “khối lệnh” – nhóm các dòng đi cùng nhau.

2. Mảng – Danh sách có thứ tự

traiCay 🍎 'Táo' [0] 🍊 'Cam' [1] 🥭 'Xoài' [2] Toa đầu tiên có số thứ tự là 0, không phải 1!
Mảng giống đoàn tàu: mỗi toa chứa một giá trị và có số thứ tự (index) bắt đầu từ 0.
const traiCay = ['Táo', 'Cam', 'Xoài'];

console.log(traiCay[0]);       // Táo   (toa số 0)
console.log(traiCay[2]);       // Xoài
console.log(traiCay.length);   // 3     (có 3 toa)

traiCay.push('Dưa');            // gắn thêm toa vào cuối
console.log(traiCay);          // [ 'Táo', 'Cam', 'Xoài', 'Dưa' ]

3. Vòng lặp – Làm lại nhiều lần

const emails = ['a@shop.vn', 'b@shop.vn', 'c@shop.vn'];

// "Với mỗi email trong danh sách emails, làm việc sau:"
for (const email of emails) {
  console.log(`Đang kiểm tra: ${email}`);
}

// Lặp theo số lần: i chạy từ 0 đến 2
for (let i = 0; i < 3; i++) {
  console.log(`Lần thứ ${i + 1}`);
}

Trong test, vòng lặp giúp bạn chạy cùng một kịch bản với nhiều bộ dữ liệu (bài 12).

4. Object – Thẻ thông tin

🪪 Ví dụ đời thường

Một object giống thẻ căn cước: gồm nhiều cặp “tên trường : giá trị”, ví dụ Họ tên: An, Năm sinh: 2000.

const nguoiDung = {
  email: 'an@shop.vn',
  matKhau: 'Abc@123',
  tuoi: 25,
  laAdmin: false,
};

console.log(nguoiDung.email);     // an@shop.vn   (dùng dấu chấm để lấy giá trị)
console.log(nguoiDung.laAdmin);   // false
💡 Bạn sẽ thấy dấu chấm ở khắp nơi

page.goto(), page.getByRole()… đều là “lấy thứ gì đó bên trong object page”. Hiểu dấu chấm là hiểu một nửa Playwright!

5. Hàm – Cỗ máy tái sử dụng

2 3 Đầu vào (tham số) cong(a, b) return a + b 5 Đầu ra (return)
Bỏ nguyên liệu vào → máy xử lý → trả ra kết quả. Viết một lần, dùng bao nhiêu lần cũng được.
// Cách 1: function truyền thống
function cong(a: number, b: number): number {
  return a + b;   // return = "trả kết quả ra ngoài"
}

// Cách 2: arrow function (hàm mũi tên) – rất hay gặp trong Playwright
const chaoHoi = (ten: string) => {
  console.log(`Xin chào ${ten}`);
};

console.log(cong(2, 3));   // 5
chaoHoi('An');             // Xin chào An

Nhìn lại một test Playwright, bạn sẽ thấy async ({ page }) => { ... } – đó chính là một arrow function!

6. import / export – Chia sẻ giữa các file

// file: utils.ts
export function taoEmail(ten: string) {
  return `${ten}@test.vn`;
}

// file: login.spec.ts
import { taoEmail } from './utils';   // mượn hàm từ file khác
console.log(taoEmail('an'));           // an@test.vn
🧪 Thực hành

Viết hàm kiemTraMatKhau(mk: string) trả về true nếu mật khẩu dài từ 8 ký tự trở lên, ngược lại trả về false. Dùng vòng lặp để kiểm tra danh sách ['123', 'matkhau123', 'abc'].

Xem lời giải
function kiemTraMatKhau(mk: string): boolean {
  if (mk.length >= 8) {
    return true;
  } else {
    return false;
  }
  // Viết gọn hơn: return mk.length >= 8;
}

const danhSach = ['123', 'matkhau123', 'abc'];
for (const mk of danhSach) {
  console.log(`${mk}: ${kiemTraMatKhau(mk) ? 'Hợp lệ ✅' : 'Quá ngắn ❌'}`);
}

📝 Ôn tập

1. Với const a = ['x', 'y', 'z'], a[1] là gì?

Index bắt đầu từ 0: a[0] = 'x', a[1] = 'y'.

2. Từ khóa nào dùng để hàm trả kết quả ra ngoài?

return trả giá trị về chỗ gọi hàm. console.log chỉ in ra màn hình.

3. user.email nghĩa là gì?

Dấu chấm dùng để truy cập thuộc tính bên trong một object.

Async / Await – Nghệ thuật chờ đợi

Khái niệm quan trọng nhất để Playwright hoạt động đúng. Quên nó là test sẽ chạy loạn.

🎯 Sau bài này bạn sẽ
  • Hiểu tại sao thao tác với trình duyệt cần “chờ”
  • Biết Promise, async, await là gì
  • Tránh lỗi số 1 của người mới: quên await

Câu chuyện quán cà phê ☕

☕ Ví dụ đời thường

Bạn gọi một ly cà phê. Nhân viên không đưa cà phê ngay mà đưa bạn một tờ phiếu hẹn có số thứ tự. Tờ phiếu đó chính là Promise (lời hứa): “cà phê sẽ có, nhưng chưa phải bây giờ”.

await nghĩa là: “Tôi đứng chờ ở đây cho đến khi nhận được cà phê, rồi mới làm việc tiếp theo.”

❌ Không await 1. Gọi cà phê 2. Uống ngay 🥤 → Cốc rỗng! Chưa có gì để uống 😵 ✅ Có await 1. Gọi cà phê ⏳ await… đợi pha chế 2. Uống ☕ 😋
Trình duyệt cũng vậy: mở trang, bấm nút… đều cần thời gian. Phải đợi xong việc trước mới làm việc sau.

Trong code trông như thế nào?

// Hàm giả lập pha cà phê mất 2 giây
function phaCaPhe(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve('☕ Cà phê sữa'), 2000);
  });
}

// Muốn dùng await thì hàm phải có chữ async phía trước
async function buoiSang() {
  console.log('Gọi cà phê...');
  const ly = await phaCaPhe();   // đứng chờ 2 giây ở đây
  console.log(`Đã nhận: ${ly}`); // chỉ chạy sau khi có cà phê
}

buoiSang();
📏 Hai quy tắc vàng
  1. Muốn dùng await bên trong một hàm → hàm đó phải có chữ async.
  2. Trong Playwright, gần như mọi lệnh bắt đầu bằng page. hoặc expect(...) đều cần await phía trước.

Áp dụng vào Playwright

test('đăng nhập', async ({ page }) => {        // ← async ở đây
  await page.goto('https://shop.vidu.vn');     // ← đợi trang tải xong
  await page.getByLabel('Email').fill('an@shop.vn');  // ← đợi gõ xong
  await page.getByRole('button', { name: 'Đăng nhập' }).click();
  await expect(page.getByText('Xin chào')).toBeVisible();
});
🐛 Lỗi số 1 của người mới: quên await
page.goto('https://shop.vidu.vn');            // ❌ thiếu await
page.getByRole('button').click();             // chạy khi trang chưa tải xong!

Kết quả: test lúc pass lúc fail (gọi là flaky), hoặc báo lỗi khó hiểu. Khi gặp lỗi lạ, việc đầu tiên: kiểm tra xem có thiếu await không.

💡 Mẹo nhỏ

Không phải cái gì cũng cần await. page.getByRole(...) chỉ tạo một “địa chỉ” (locator), chưa làm gì cả nên không cần await. Chỉ khi bạn hành động (.click(), .fill()) hoặc kiểm tra (expect) mới cần await.

📝 Ôn tập

1. Promise giống với thứ gì trong ví dụ quán cà phê?

Promise là lời hứa “kết quả sẽ có sau”, giống phiếu hẹn.

2. Dòng nào viết đúng?

Hành động với trình duyệt cần await phía trước.

3. Để dùng await trong một hàm, hàm đó cần gì?

Chỉ được dùng await bên trong hàm async.

Trang web hoạt động thế nào?

Muốn “điều khiển” trang web, bạn cần biết trang web được làm từ gì.

🎯 Sau bài này bạn sẽ
  • Phân biệt HTML, CSS, JavaScript
  • Hiểu cây DOM và thuộc tính của phần tử
  • Dùng DevTools (F12) để “soi” trang web

Ngôi nhà web 🏠

HTML Khung nhà: tường, cửa, cửa sổ + CSS Sơn, màu sắc, trang trí + 💡 JavaScript Điện nước: bật đèn, mở cửa
Playwright làm việc chủ yếu với HTML: tìm “cánh cửa” (nút) rồi bấm vào.

HTML trông như thế nào?

HTML gồm các thẻ (tag) đóng mở như <button>...</button>. Đây là một form đăng nhập đơn giản:

<h1>Đăng nhập</h1>
<form>
  <label for="email">Email</label>
  <input id="email" type="text" placeholder="ban@vidu.vn">

  <label for="pass">Mật khẩu</label>
  <input id="pass" type="password">

  <button class="btn-primary" data-testid="login-btn">Đăng nhập</button>
</form>
PhầnVí dụÝ nghĩa
Tên thẻbutton, input, h1Loại phần tử: nút, ô nhập, tiêu đề
idid="email"Mã định danh, thường là duy nhất trên trang
classclass="btn-primary"Nhóm để trang trí CSS, có thể trùng nhau
placeholderplaceholder="ban@vidu.vn"Chữ mờ gợi ý trong ô nhập
data-testiddata-testid="login-btn"Mã dành riêng cho tester ❤️ – không bị đổi khi đổi giao diện

Cây DOM 🌳

Trình duyệt đọc HTML và dựng thành một cây gọi là DOM (Document Object Model). Mỗi thẻ là một “nhánh”.

html head body form h1 footer input#email input#pass button 🎯
Playwright “leo cây” DOM để tìm đúng phần tử bạn cần – ví dụ nút button ở dưới cùng.

Role – “vai trò” của phần tử

Mỗi phần tử có một vai trò (role) mà trình đọc màn hình cho người khiếm thị sử dụng. Playwright khuyên dùng role để tìm phần tử vì nó giống cách người dùng thật nhìn trang.

Bạn nhìn thấyThẻ HTMLRole
Nút bấm<button>button
Đường link<a href>link
Ô nhập chữ<input type="text">textbox
Ô tích<input type="checkbox">checkbox
Tiêu đề<h1> … <h6>heading
Danh sách thả xuống<select>combobox

DevTools – Kính lúp của QA 🔍

  1. Mở DevToolsTrên Chrome/Edge, nhấn F12 hoặc chuột phải vào phần tử → Inspect (Kiểm tra).
  2. Chọn phần tửBấm biểu tượng mũi tên ở góc trái DevTools (hoặc Ctrl+Shift+C), rồi rê chuột lên trang. Code HTML tương ứng sẽ sáng lên.
  3. Xem thuộc tínhNhìn xem phần tử có id, data-testid, chữ hiển thị là gì – đó là “manh mối” để viết locator.
  4. Xem roleTrong DevTools, tab Accessibility (bên phải tab Styles) hiển thị role và name của phần tử.
🧪 Thực hành

Mở saucedemo.com, nhấn F12, soi nút Login. Nó có những thuộc tính gì? (gợi ý: id, data-test, value)

Xem đáp án

Nút Login là <input type="submit" class="submit-button btn_action" data-test="login-button" id="login-button" name="login-button" value="Login">. Thuộc tính data-test là dành riêng cho tester – chúng ta sẽ dùng nó ở bài 16.

📝 Ôn tập

1. Thành phần nào tạo nên “khung” của trang web?

HTML là khung, CSS trang trí, JavaScript tạo hành vi.

2. Thuộc tính nào được tạo ra riêng cho việc test, ít bị thay đổi nhất?

class có thể đổi khi designer sửa giao diện. data-testid chỉ dành cho test nên ổn định.

3. Một thẻ <a href="..."> có role là gì?

Thẻ a có href là đường link → role link.

Viết bài test đầu tiên

Mổ xẻ từng dòng của một bài test Playwright. Sau bài này bạn sẽ không còn sợ code nữa.

🎯 Sau bài này bạn sẽ
  • Hiểu cấu trúc một file test
  • Biết mô hình Arrange – Act – Assert
  • Chạy test bằng nhiều cách, đọc báo cáo

Giải phẫu một bài test

Tạo file tests/dau-tien.spec.ts và gõ:

// ① Lấy 2 "công cụ" từ thư viện Playwright
import { test, expect } from '@playwright/test';

// ② Khai báo một test: test('tên test', hàm chứa các bước)
test('trang chủ có tiêu đề Playwright', async ({ page }) => {
  // ③ ARRANGE – chuẩn bị: mở trang web
  await page.goto('https://playwright.dev/');

  // ④ ASSERT – kiểm tra: tiêu đề tab có chứa chữ "Playwright"
  await expect(page).toHaveTitle(/Playwright/);
});

test('bấm Get started dẫn tới trang Installation', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // ⑤ ACT – hành động: bấm vào link "Get started"
  await page.getByRole('link', { name: 'Get started' }).click();

  // ⑥ ASSERT – kiểm tra: thấy tiêu đề "Installation"
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
#Đoạn codeGiải thích bằng lời
①import { test, expect }Mượn 2 công cụ: test để khai báo bài test, expect để kiểm tra kết quả.
②async ({ page }) => {...}Một hàm async. Playwright tự đưa cho bạn page – một tab trình duyệt mới tinh.
③page.goto(url)Mở địa chỉ web, giống gõ URL vào thanh địa chỉ.
④toHaveTitle(/Playwright/)/.../ là “mẫu tìm kiếm” (regex): tiêu đề chỉ cần chứa chữ Playwright.
⑤getByRole('link', {...}).click()Tìm đường link có chữ “Get started” rồi bấm vào.
⑥toBeVisible()Kiểm tra phần tử đang hiển thị trên màn hình.

Công thức AAA

🧰 Arrange Chuẩn bị: mở trang, đăng nhập 👆 Act Hành động: bấm, gõ, chọn ✅ Assert Kiểm tra: kết quả có đúng không?
Mọi bài test tốt đều theo 3 bước này. Test không có Assert = chỉ là “bấm cho vui”.

Các cách chạy test

LệnhTác dụng
npx playwright testChạy tất cả test, ẩn trình duyệt (headless – nhanh)
npx playwright test dau-tien.spec.tsChỉ chạy 1 file
npx playwright test -g "tiêu đề"Chỉ chạy test có tên chứa “tiêu đề”
npx playwright test --headedHiện trình duyệt để xem nó tự bấm
npx playwright test --project=chromiumChỉ chạy trên Chromium
npx playwright test --uiMở UI Mode – khuyên dùng khi học ⭐
npx playwright show-reportMở báo cáo HTML

UI Mode – Bảng điều khiển trực quan

Playwright UI Mode 📁 TESTS ✔ trang chủ có tiêu đề ✔ bấm Get started ✘ đăng nhập sai ○ thêm giỏ hàng ▶ Run all ← dòng thời gian: ảnh từng bước CÁC BƯỚC page.goto getByRole.click expect.toBeVisible Installation ← phần tử được tô sáng
Bấm vào từng bước bên trái để “tua lại” trang web tại đúng thời điểm đó.

Đọc báo cáo HTML

Sau khi chạy, lệnh npx playwright show-report mở báo cáo liệt kê mọi test, thời gian chạy, trình duyệt. Bấm vào test bị lỗi để xem: dòng code lỗi, ảnh chụp màn hình, và trace (bài 11).

🧪 Thực hành

Viết test cho trang demo.playwright.dev/todomvc: mở trang và kiểm tra tiêu đề tab chứa chữ “React”.

Xem lời giải
import { test, expect } from '@playwright/test';

test('trang todo có tiêu đề React', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc');
  await expect(page).toHaveTitle(/React/);
});

📝 Ôn tập

1. { page } trong async ({ page }) => là gì?

page là một “fixture” có sẵn: mỗi test nhận một tab trình duyệt sạch sẽ.

2. Thứ tự đúng của công thức AAA?

Chuẩn bị → Hành động → Kiểm tra.

3. Lệnh nào giúp xem test chạy từng bước trực quan nhất?

UI Mode cho xem từng bước, ảnh chụp, và chạy lại từng test.

Locator – Tìm đúng phần tử

Kỹ năng quan trọng nhất của một Automation QA: chỉ cho máy biết “cái nút nào”.

🎯 Sau bài này bạn sẽ
  • Biết các loại locator và thứ tự ưu tiên
  • Viết locator ổn định, dễ đọc
  • Lọc, thu hẹp khi có nhiều phần tử giống nhau
📍 Ví dụ đời thường

Locator giống cách bạn chỉ đường cho shipper. “Nhà màu xanh” (class/màu sắc) – dễ sai vì nhà có thể sơn lại. “Nhà số 12, đường Lê Lợi” (role + tên) – rõ ràng và ít thay đổi. Locator tốt = địa chỉ không bị lỗi thời.

Thứ tự ưu tiên

1. getByRole()⭐ Ưu tiên số 1 2. getByLabel()ô nhập có nhãn 3. getByPlaceholder() 4. getByText() 5. getByTestId() 6. locator(css) Giống ngườidùng thật Phụ thuộccode HTML
Càng ở trên càng giống cách người dùng nhìn trang → test càng bền khi giao diện thay đổi.
LocatorDùng khiVí dụ
getByRoleNút, link, ô nhập, tiêu đề, checkbox…page.getByRole('button', { name: 'Đăng nhập' })
getByLabelÔ nhập có nhãn <label>page.getByLabel('Email')
getByPlaceholderÔ nhập có chữ mờ gợi ýpage.getByPlaceholder('ban@vidu.vn')
getByTextĐoạn chữ, thông báopage.getByText('Đăng nhập thành công')
getByAltTextHình ảnhpage.getByAltText('Logo công ty')
getByTestIdCó data-testidpage.getByTestId('login-btn')
locatorPhương án cuối: CSS / XPathpage.locator('#email'), page.locator('.btn-primary')

🎮 Sân chơi: bấm vào phần tử để xem locator

Rê chuột và bấm vào từng phần tử trong “trình duyệt giả” dưới đây:

https://shop.vidu.vn/dang-nhap

Đăng nhập

Ghi nhớ tôi
Quên mật khẩu?
// 👆 Bấm vào một phần tử phía trên

Locator gợi ý và lý do sẽ hiện ở đây.

Khi có nhiều phần tử giống nhau

Trang danh sách sản phẩm có 10 nút “Thêm vào giỏ”. Làm sao chọn đúng nút của “Áo thun”?

// ❌ Mơ hồ: có 10 nút giống nhau → Playwright báo lỗi "strict mode violation"
await page.getByRole('button', { name: 'Thêm vào giỏ' }).click();

// ✅ Lọc: tìm thẻ sản phẩm có chữ "Áo thun", rồi tìm nút bên trong nó
await page
  .getByRole('listitem')
  .filter({ hasText: 'Áo thun' })
  .getByRole('button', { name: 'Thêm vào giỏ' })
  .click();

// Các cách khác (dùng khi thật cần):
page.getByRole('button').first();   // phần tử đầu tiên
page.getByRole('button').last();    // phần tử cuối cùng
page.getByRole('button').nth(2);    // phần tử thứ 3 (đếm từ 0)
⚠️ Strict mode – Playwright rất “khó tính”

Nếu locator khớp với nhiều hơn 1 phần tử mà bạn lại bấm vào, Playwright sẽ báo lỗi thay vì đoán bừa. Đây là điều tốt: nó bắt bạn viết locator chính xác. Hạn chế lạm dụng .first(), .nth() vì thứ tự có thể đổi.

CSS selector cơ bản (khi bắt buộc)

ViếtNghĩa
#emailPhần tử có id="email"
.btn-primaryPhần tử có class btn-primary
[data-test="username"]Phần tử có thuộc tính data-test bằng “username”
form buttonNút nằm bên trong form
🧪 Thực hành

Trên trang TodoMVC, viết locator cho: (1) ô nhập công việc, (2) tiêu đề “todos”.

Xem lời giải
const oNhap = page.getByPlaceholder('What needs to be done?');
const tieuDe = page.getByRole('heading', { name: 'todos' });

📝 Ôn tập

1. Locator nào được Playwright khuyên dùng nhất?

getByRole mô phỏng cách người dùng (và công nghệ hỗ trợ) nhìn trang – bền vững nhất.

2. “Strict mode violation” nghĩa là gì?

Locator mơ hồ, khớp nhiều phần tử → cần lọc thêm bằng filter hoặc viết cụ thể hơn.

3. Ô nhập có <label>Họ tên</label> – locator tốt nhất?

getByLabel tìm đúng ô nhập gắn với nhãn đó. getByText sẽ trả về chính cái nhãn chứ không phải ô nhập.

Action – Tương tác với trang

Tìm được phần tử rồi, giờ là lúc bấm, gõ, chọn, kéo thả… như người dùng thật.

🎯 Sau bài này bạn sẽ
  • Dùng thành thạo các hành động phổ biến
  • Xử lý bàn phím, dropdown, upload file, hộp thoại
  • Viết hoàn chỉnh một test thêm công việc

Bảng hành động phổ biến

Hành độngCode
🌐Mở trangawait page.goto('https://...')
👆Bấmawait locator.click()
👆👆Bấm đúpawait locator.dblclick()
⌨️Điền chữ (xóa cũ, ghi mới)await locator.fill('xin chào')
🧹Xóa nội dung ô nhậpawait locator.clear()
⏎Nhấn phímawait locator.press('Enter')
☑️Tích / bỏ tíchawait locator.check() / uncheck()
🔽Chọn trong dropdownawait locator.selectOption('Hà Nội')
🖱️Rê chuột lênawait locator.hover()
📎Tải file lênawait locator.setInputFiles('anh.png')
↔️Kéo thảawait a.dragTo(b)
📜Lấy chữ của phần tửconst t = await locator.textContent()

Ví dụ hoàn chỉnh: ứng dụng ghi chú công việc

import { test, expect } from '@playwright/test';

test('thêm và hoàn thành công việc', async ({ page }) => {
  // Arrange
  await page.goto('https://demo.playwright.dev/todomvc');
  const oNhap = page.getByPlaceholder('What needs to be done?');

  // Act: thêm 2 công việc
  await oNhap.fill('Mua sữa');
  await oNhap.press('Enter');
  await oNhap.fill('Học Playwright');
  await oNhap.press('Enter');

  // Assert: có đúng 2 dòng
  await expect(page.getByTestId('todo-item')).toHaveCount(2);

  // Act: tích hoàn thành "Mua sữa"
  await page
    .getByTestId('todo-item')
    .filter({ hasText: 'Mua sữa' })
    .getByRole('checkbox')
    .check();

  // Assert: dòng đó có class "completed"
  await expect(
    page.getByTestId('todo-item').filter({ hasText: 'Mua sữa' })
  ).toHaveClass(/completed/);

  // Assert: còn 1 việc chưa làm
  await expect(page.getByText('1 item left')).toBeVisible();
});

fill() hay pressSequentially()?

⚡ fill()

Dán cả đoạn chữ vào ngay lập tức. Nhanh. Dùng trong 95% trường hợp.

🐢 pressSequentially()

Gõ từng ký tự như người thật. Dùng khi ô nhập có gợi ý tự động (autocomplete) cần bắt từng phím.

Bàn phím

await page.keyboard.press('Tab');           // chuyển sang ô kế tiếp
await page.keyboard.press('Control+A');     // chọn tất cả
await page.keyboard.press('Escape');        // đóng popup

Hộp thoại (alert / confirm)

// Đăng ký TRƯỚC khi hành động làm hiện hộp thoại
page.on('dialog', (dialog) => dialog.accept());   // tự bấm "OK"
await page.getByRole('button', { name: 'Xóa' }).click();

Mở tab mới

// Bắt đầu "chờ tab mới" trước khi bấm, rồi mới bấm link
const tabMoiPromise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Chính sách' }).click();
const tabMoi = await tabMoiPromise;
await expect(tabMoi).toHaveTitle(/Chính sách/);
🧪 Thực hành

Trên TodoMVC: thêm 3 việc, xóa việc thứ 2 (rê chuột lên dòng đó sẽ hiện nút ×, có role button tên “Delete”), rồi kiểm tra chỉ còn 2 việc.

Xem lời giải
test('xóa công việc', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc');
  const oNhap = page.getByPlaceholder('What needs to be done?');
  for (const viec of ['Việc A', 'Việc B', 'Việc C']) {
    await oNhap.fill(viec);
    await oNhap.press('Enter');
  }
  const dongB = page.getByTestId('todo-item').filter({ hasText: 'Việc B' });
  await dongB.hover();
  await dongB.getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByTestId('todo-item')).toHaveCount(2);
});

📝 Ôn tập

1. Để điền email vào ô nhập, dùng hàm nào?

fill() xóa nội dung cũ và điền chữ mới vào ô nhập.

2. Chọn “Hà Nội” trong một <select> bằng?

selectOption dành riêng cho dropdown <select>.

Assertion & Auto-waiting

Kiểm tra kết quả là trái tim của test. Và Playwright có “siêu năng lực” tự chờ.

🎯 Sau bài này bạn sẽ
  • Dùng các assertion phổ biến với expect
  • Hiểu cơ chế tự chờ (auto-waiting) và tự thử lại (retry)
  • Tránh dùng waitForTimeout
📝 Ví dụ đời thường

expect giống giáo viên chấm bài: so sánh đáp án của học sinh (kết quả thực tế) với đáp án chuẩn (kết quả mong đợi). Khớp → ✅ pass. Không khớp → ❌ fail và ghi rõ sai ở đâu.

Các assertion hay dùng nhất

AssertionKiểm tra điều gì
await expect(page).toHaveURL(/dashboard/)Địa chỉ trang chứa “dashboard”
await expect(page).toHaveTitle('Trang chủ')Tiêu đề tab
await expect(loc).toBeVisible()Phần tử đang hiển thị
await expect(loc).toBeHidden()Phần tử bị ẩn / không có
await expect(loc).toHaveText('Xin chào An')Chữ khớp chính xác
await expect(loc).toContainText('Xin chào')Chữ có chứa một đoạn
await expect(loc).toHaveValue('an@shop.vn')Giá trị trong ô nhập
await expect(loc).toHaveCount(3)Số lượng phần tử
await expect(loc).toBeEnabled() / toBeDisabled()Nút bấm được hay bị khóa
await expect(loc).toBeChecked()Ô tích đã được tích
await expect(loc).toHaveAttribute('href', '/gio-hang')Thuộc tính HTML
expect(5).toBe(5)So sánh giá trị thường (không cần await)

Phủ định với .not

await expect(page.getByText('Lỗi')).not.toBeVisible();
await expect(page).not.toHaveURL(/login/);

Auto-waiting – Playwright tự biết chờ

Trang web thật thường tải từ từ: nút có thể hiện ra sau 1 giây, đang có hiệu ứng trượt… Trước khi bấm, Playwright tự động kiểm tra một loạt điều kiện:

.click() ✓ Có trong DOM ✓ Đang hiển thị ✓ Đứng yên ✓ Không bị che ✓ Bấm được (enabled) 👆 Bấm! 🔁 Chưa đạt? Thử lại liên tục cho đến khi đạt hoặc hết thời gian chờ (timeout)
Bạn không cần viết lệnh “chờ 3 giây”. Playwright tự kiểm tra và chỉ hành động khi phần tử đã sẵn sàng.

Web-first assertion: tự thử lại

Các assertion có await expect(locator) cũng tự thử lại liên tục trong tối đa 5 giây (mặc định). Ví dụ sau khi bấm “Đăng nhập”, chữ “Xin chào” mất 2 giây mới hiện – test vẫn pass.

❌ Không nên
await page.waitForTimeout(3000);
// chờ cứng 3 giây – chậm & vẫn có thể fail
const t = await loc.textContent();
expect(t).toBe('Xin chào');
// không tự thử lại!
✅ Nên
await expect(loc).toHaveText('Xin chào');
// tự chờ, tự thử lại
// pass ngay khi chữ xuất hiện

Soft assertion – kiểm tra hết rồi mới báo

// Bình thường: assertion đầu tiên fail → test dừng luôn
// Soft: ghi nhận lỗi nhưng vẫn chạy tiếp, cuối cùng báo tất cả lỗi
await expect.soft(page.getByTestId('ten')).toHaveText('An');
await expect.soft(page.getByTestId('email')).toHaveText('an@shop.vn');
await expect.soft(page.getByTestId('sdt')).toHaveText('0901234567');

Thông điệp lỗi tùy chỉnh

await expect(page.getByTestId('gio-hang'), 'Giỏ hàng phải có 2 sản phẩm').toHaveText('2');

📝 Ôn tập

1. Kiểm tra trang đã chuyển đến địa chỉ có chữ “cart”?

toHaveURL kiểm tra địa chỉ trang. Đừng quên await.

2. Vì sao không nên dùng waitForTimeout(3000)?

Chờ cứng lãng phí thời gian và không đảm bảo. Hãy để auto-waiting làm việc.

3. Mặc định expect trên locator thử lại trong bao lâu?

Mặc định 5 giây, có thể chỉnh trong config (expect.timeout).

Codegen, Debug & Trace Viewer

Những công cụ giúp bạn viết test nhanh gấp đôi và tìm lỗi trong vài phút.

🎯 Sau bài này bạn sẽ
  • Dùng Codegen để tự sinh code từ thao tác
  • Chạy test từng bước để gỡ lỗi
  • Đọc Trace Viewer – “hộp đen máy bay” của test

Codegen – Bạn bấm, máy viết code 🪄

npx playwright codegen https://demo.playwright.dev/todomvc
🌐 Trình duyệt What needs to be done? Add 👆 Bạn thao tác bình thường… ⏺ Playwright Inspector await page.goto('…/todomvc'); await page.getByPlaceholder( 'What needs…').fill('Mua sữa'); await page.getByPlaceholder( 'What needs…').press('Enter'); ▌
Codegen tự chọn locator tốt (ưu tiên getByRole). Copy code vào file test rồi bổ sung assertion.
⚠️ Codegen là trợ lý, không phải tác giả

Code sinh ra chưa có assertion có ý nghĩa và có thể dư thừa bước. Luôn đọc lại, đổi tên, thêm expect. Trong Inspector có nút “Pick locator” để bấm vào một phần tử bất kỳ và lấy locator của nó – rất tiện!

Debug – chạy chậm từng bước

npx playwright test dau-tien.spec.ts --debug

Trình duyệt mở ra cùng Playwright Inspector. Bấm Step over để chạy từng dòng, thấy rõ phần tử nào đang được tìm.

test('gỡ lỗi', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await page.pause();   // ⏸ dừng tại đây để bạn tự quan sát
  await page.getByRole('link', { name: 'Get started' }).click();
});
💡 Trong VS Code

Với extension Playwright, bấm vào biểu tượng ▶ xanh bên cạnh dòng test(...) để chạy, hoặc chuột phải → Debug Test. Đặt breakpoint (chấm đỏ cạnh số dòng) để dừng ở dòng bất kỳ.

Trace Viewer – Hộp đen của test ✈️

Trace ghi lại mọi thứ: từng hành động, ảnh chụp trước/sau, log mạng, console. Khi test fail trên máy chủ, bạn mở trace là biết chuyện gì đã xảy ra.

# Bật trace cho lần chạy này
npx playwright test --trace on

# Mở trace (hoặc bấm vào test trong report → tab Trace)
npx playwright show-trace test-results/.../trace.zip

Trong playwright.config.ts, mặc định là trace: 'on-first-retry' – chỉ ghi khi test fail và chạy lại, tiết kiệm dung lượng.

Đọc thông báo lỗi như dân chuyên

  1) tests/login.spec.ts:12:5 › đăng nhập thành công

    Error: Timed out 5000ms waiting for expect(locator).toBeVisible()

    Locator: getByText('Xin chào An')
    Expected: visible
    Received: <element(s) not found>

      14 |   await page.getByRole('button', { name: 'Đăng nhập' }).click();
    > 15 |   await expect(page.getByText('Xin chào An')).toBeVisible();
         |                                               ^
DòngÝ nghĩa
login.spec.ts:12:5Test bắt đầu ở file login.spec.ts, dòng 12
Timed out 5000msĐã chờ 5 giây mà không đạt
LocatorĐang tìm phần tử nào
Expected / ReceivedMong đợi “hiển thị” nhưng thực tế “không tìm thấy”
> 15 |Dòng code gây lỗi
🕵️ Quy trình điều tra lỗi 4 bước
  1. Đọc lỗi: dòng nào, locator nào, mong đợi gì, nhận được gì.
  2. Xem ảnh chụp/trace: lúc đó màn hình hiển thị gì?
  3. Phân loại: lỗi do test viết sai (locator sai, quên await) hay ứng dụng có bug?
  4. Sửa hoặc báo bug kèm bằng chứng (ảnh, trace, bước tái hiện).

📝 Ôn tập

1. Công cụ nào ghi lại thao tác của bạn thành code?

npx playwright codegen URL mở trình duyệt và ghi lại thao tác.

2. Test fail trên máy chủ CI, bạn không thấy màn hình. Nên xem gì?

Trace ghi lại toàn bộ diễn biến – công cụ điều tra số 1.

Tổ chức test: describe, hooks & dữ liệu

Khi có hàng trăm test, cần sắp xếp gọn gàng như tủ hồ sơ.

🎯 Sau bài này bạn sẽ
  • Nhóm test với test.describe
  • Dùng hooks beforeEach, afterEach để bỏ code lặp
  • Chạy một test với nhiều bộ dữ liệu, gắn tag

describe – nhóm test

import { test, expect } from '@playwright/test';

test.describe('Chức năng đăng nhập', () => {

  // Chạy TRƯỚC MỖI test trong nhóm này
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
  });

  test('đăng nhập thành công', async ({ page }) => {
    // không cần goto nữa – beforeEach đã làm
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page).toHaveURL(/inventory/);
  });

  test('sai mật khẩu báo lỗi', async ({ page }) => {
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('sai');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText('do not match');
  });
});

Các hook

HookChạy khi nàoVí dụ dùng
test.beforeEachTrước mỗi testMở trang, đăng nhập
test.afterEachSau mỗi testDọn dữ liệu, đăng xuất
test.beforeAllMột lần trước tất cảTạo dữ liệu dùng chung qua API
test.afterAllMột lần sau tất cảXóa dữ liệu dùng chung

Mỗi test là một thế giới riêng

🌐 Trình duyệt 🕶️ Context 1 Test: đăng nhập cookie riêng 🍪 🕶️ Context 2 Test: giỏ hàng cookie riêng 🍪 🕶️ Context 3 Test: thanh toán cookie riêng 🍪
Mỗi test chạy trong một “cửa sổ ẩn danh” riêng. Test A đăng nhập không ảnh hưởng test B.
⚠️ Nguyên tắc vàng: test độc lập

Đừng viết test B phụ thuộc vào kết quả test A (“test A tạo đơn, test B xóa đơn đó”). Các test có thể chạy song song, theo thứ tự bất kỳ. Mỗi test phải tự chuẩn bị mọi thứ nó cần.

Một kịch bản, nhiều bộ dữ liệu (data-driven)

const cacTaiKhoanLoi = [
  { user: 'locked_out_user', pass: 'secret_sauce', loi: 'locked out' },
  { user: 'standard_user',   pass: 'sai_mk',       loi: 'do not match' },
  { user: '',                pass: 'secret_sauce', loi: 'Username is required' },
];

for (const tk of cacTaiKhoanLoi) {
  test(`đăng nhập lỗi: "${tk.user}" → ${tk.loi}`, async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
    await page.getByPlaceholder('Username').fill(tk.user);
    await page.getByPlaceholder('Password').fill(tk.pass);
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText(tk.loi);
  });
}

3 bộ dữ liệu → Playwright tạo ra 3 test riêng biệt, mỗi cái có tên rõ ràng trong báo cáo.

Tag, skip, only

// Gắn tag để chạy theo nhóm
test('đăng nhập', { tag: '@smoke' }, async ({ page }) => { /* ... */ });

test.skip('tính năng chưa xong', async ({ page }) => { /* ... */ });  // bỏ qua
test.only('chỉ chạy test này', async ({ page }) => { /* ... */ });     // chỉ chạy cái này (nhớ xóa trước khi commit!)
test.fixme('đang có bug #123', async ({ page }) => { /* ... */ });    // đánh dấu cần sửa
npx playwright test --grep @smoke          # chỉ chạy test có tag @smoke
npx playwright test --grep-invert @slow    # bỏ qua test có tag @slow

test.step – báo cáo dễ đọc hơn

test('mua hàng', async ({ page }) => {
  await test.step('Đăng nhập', async () => { /* ... */ });
  await test.step('Thêm sản phẩm vào giỏ', async () => { /* ... */ });
  await test.step('Thanh toán', async () => { /* ... */ });
});

📝 Ôn tập

1. Muốn mở trang đăng nhập trước mỗi test trong nhóm, dùng gì?

beforeEach chạy trước mỗi test.

2. Điều nào ĐÚNG về các test?

Test độc lập = ổn định, chạy song song được, dễ tìm lỗi.

Page Object Model (POM)

Mẫu thiết kế phổ biến nhất trong automation. Hầu như mọi buổi phỏng vấn đều hỏi.

🎯 Sau bài này bạn sẽ
  • Hiểu vấn đề mà POM giải quyết
  • Biết class, constructor, this ở mức cần thiết
  • Tự viết một Page Object và dùng trong test

Vấn đề: locator bị lặp khắp nơi

Bạn có 50 test, test nào cũng đăng nhập bằng page.getByPlaceholder('Username'). Một ngày developer đổi chữ thành “Tên đăng nhập” → bạn phải sửa 50 chỗ 😱.

📺 Ví dụ đời thường

POM giống chiếc điều khiển TV. Bạn chỉ cần bấm nút “Tăng âm lượng”, không cần biết bên trong TV có mạch điện gì. Page Object đóng gói “mạch điện” (locator, các bước) vào bên trong, test chỉ việc “bấm nút” như loginPage.dangNhap(...).

📝 TEST 🧩 PAGE OBJECT 🌐 TRANG WEB login.spec.ts cart.spec.ts checkout.spec.ts LoginPage dangNhap(user, pass) InventoryPage themVaoGio(ten) 🔧 Giao diện đổi? Chỉ sửa 1 chỗ trong Page Object!
Test nói “làm gì”, Page Object biết “làm thế nào”.

Class – khuôn đúc đồ vật

Class là một “bản thiết kế”. Từ bản thiết kế đó, bạn tạo ra các “đồ vật” (object) bằng từ khóa new.

class ConCho {
  ten: string;                    // thuộc tính: con chó có tên

  constructor(ten: string) {      // hàm chạy khi tạo con chó mới
    this.ten = ten;               // this = "chính con chó này"
  }

  sua() {                         // hành động (method)
    console.log(`${this.ten}: Gâu gâu!`);
  }
}

const milu = new ConCho('Milu');  // đúc ra một con chó từ khuôn
milu.sua();                       // Milu: Gâu gâu!

Viết LoginPage

Tạo thư mục pages/ và file pages/LoginPage.ts:

import { type Page, type Locator, expect } from '@playwright/test';

export class LoginPage {
  // Khai báo các "linh kiện" của trang
  readonly page: Page;
  readonly oUsername: Locator;
  readonly oPassword: Locator;
  readonly nutLogin: Locator;
  readonly thongBaoLoi: Locator;

  constructor(page: Page) {
    this.page = page;
    this.oUsername = page.getByPlaceholder('Username');
    this.oPassword = page.getByPlaceholder('Password');
    this.nutLogin = page.getByRole('button', { name: 'Login' });
    this.thongBaoLoi = page.locator('[data-test="error"]');
  }

  // Các "nút bấm trên điều khiển"
  async moTrang() {
    await this.page.goto('https://www.saucedemo.com/');
  }

  async dangNhap(user: string, pass: string) {
    await this.oUsername.fill(user);
    await this.oPassword.fill(pass);
    await this.nutLogin.click();
  }

  async kiemTraLoi(noiDung: string) {
    await expect(this.thongBaoLoi).toContainText(noiDung);
  }
}

Dùng trong test – gọn như đọc tiếng Việt

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('đăng nhập thành công', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.moTrang();
  await loginPage.dangNhap('standard_user', 'secret_sauce');
  await expect(page).toHaveURL(/inventory/);
});

test('tài khoản bị khóa', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.moTrang();
  await loginPage.dangNhap('locked_out_user', 'secret_sauce');
  await loginPage.kiemTraLoi('locked out');
});
💡 Quy tắc viết Page Object tốt
  • Mỗi trang (hoặc mỗi khu vực lớn) = 1 class.
  • Locator khai báo trong constructor, đặt tên rõ ràng.
  • Method đặt tên theo hành động nghiệp vụ: dangNhap, themVaoGio, không phải clickButton1.
  • Assertion chính nên để trong test, giúp test dễ đọc “đang kiểm tra cái gì”.

📝 Ôn tập

1. Lợi ích lớn nhất của POM?

POM giảm trùng lặp và tăng khả năng bảo trì.

2. Từ khóa nào tạo object mới từ class?

new LoginPage(page) tạo một đối tượng LoginPage.

Cấu hình & Fixture

Điều chỉnh “bảng điều khiển” của Playwright và tạo đồ nghề chuẩn bị sẵn cho test.

🎯 Sau bài này bạn sẽ
  • Đọc hiểu và chỉnh playwright.config.ts
  • Chạy test trên nhiều trình duyệt và thiết bị di động
  • Tạo fixture riêng và đăng nhập một lần dùng nhiều lần

playwright.config.ts – giải thích từng dòng

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',            // thư mục chứa test
  timeout: 30_000,               // mỗi test tối đa 30 giây
  expect: { timeout: 5_000 },    // mỗi expect chờ tối đa 5 giây
  fullyParallel: true,           // chạy song song cho nhanh
  retries: process.env.CI ? 2 : 0,  // trên CI: fail thì chạy lại 2 lần
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',              // xuất báo cáo HTML

  use: {
    baseURL: 'https://www.saucedemo.com',  // goto('/') = goto(baseURL)
    trace: 'on-first-retry',     // ghi trace khi chạy lại
    screenshot: 'only-on-failure', // chụp ảnh khi fail
    video: 'retain-on-failure',  // giữ video khi fail
    testIdAttribute: 'data-test',  // getByTestId sẽ tìm thuộc tính data-test
  },

  // Mỗi project = một môi trường chạy
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
    { name: 'mobile',   use: { ...devices['iPhone 13'] } },   // giả lập điện thoại 📱
  ],
});
💡 baseURL giúp code ngắn gọn

Có baseURL, bạn chỉ cần viết await page.goto('/inventory.html'). Muốn chạy trên môi trường khác (staging, production) chỉ cần đổi 1 dòng.

Biến môi trường – giấu mật khẩu

Không bao giờ viết mật khẩu thật thẳng vào code rồi đưa lên GitHub. Dùng biến môi trường:

// Đọc từ biến môi trường, nếu không có thì dùng giá trị mặc định
const user = process.env.APP_USER ?? 'standard_user';
const pass = process.env.APP_PASS ?? 'secret_sauce';
# PowerShell
$env:APP_USER="admin"; npx playwright test
# macOS / Linux / Git Bash
APP_USER=admin npx playwright test

Fixture – đồ nghề chuẩn bị sẵn

🍽️ Ví dụ đời thường

Vào nhà hàng, bàn đã được bày sẵn bát đũa (setup), ăn xong nhân viên dọn dẹp (teardown). Bạn chỉ việc ngồi ăn. Fixture làm đúng điều đó cho test: chuẩn bị trước, dọn dẹp sau, test chỉ cần “xin” là có.

Bạn đã dùng fixture có sẵn rồi: { page }. Còn có context, browser, request (bài 15), browserName…

Tự tạo fixture: tự động có LoginPage

// file: fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

type MyFixtures = {
  loginPage: LoginPage;
};

export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);   // 🍽️ bày bàn
    await loginPage.moTrang();
    await use(loginPage);                    // 👉 đưa cho test dùng
    // (code sau use = dọn dẹp, nếu cần)
  },
});

export { expect } from '@playwright/test';
// file: tests/login.spec.ts
import { test, expect } from '../fixtures';   // ⚠️ import từ fixtures, không phải @playwright/test

test('đăng nhập', async ({ loginPage, page }) => {
  await loginPage.dangNhap('standard_user', 'secret_sauce');  // trang đã mở sẵn!
  await expect(page).toHaveURL(/inventory/);
});

Đăng nhập một lần, dùng cho mọi test

Nếu 100 test đều cần đăng nhập, mỗi lần mất 3 giây → lãng phí 5 phút. Giải pháp: đăng nhập 1 lần, lưu “phiên đăng nhập” (cookie) vào file, các test khác dùng lại.

// file: tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('đăng nhập và lưu phiên', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL(/inventory/);
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// trong playwright.config.ts
projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],   // chạy setup trước
  },
],
⚠️ Nhớ thêm vào .gitignore

File playwright/.auth/ chứa phiên đăng nhập – đừng đưa lên GitHub.

📝 Ôn tập

1. Muốn chạy test trên giả lập iPhone, cấu hình ở đâu?

Mỗi project trong config là một môi trường chạy, có thể là thiết bị di động.

2. Trong fixture, await use(x) làm gì?

Trước use = chuẩn bị, sau use = dọn dẹp.

API Testing với Playwright

Kiểm tra “phía sau hậu trường” – nhanh hơn test giao diện hàng chục lần.

🎯 Sau bài này bạn sẽ
  • Hiểu API, request, response, status code
  • Gửi GET, POST và kiểm tra kết quả
  • Kết hợp API + UI để test nhanh hơn

API là gì? 🍜

🧑 Khách hàng (App / Test của bạn) 🤵 Người phục vụ (API) 👨‍🍳 Nhà bếp (Server / Database) Request Response “Cho tôi 1 phở bò” → API chuyển vào bếp → mang ra “🍜 + hóa đơn 200 OK”
Bạn không vào bếp, chỉ nói chuyện với người phục vụ theo “thực đơn” (tài liệu API).

Các phương thức HTTP

Phương thứcÝ nghĩaVí dụ đời thường
GETLấy dữ liệuXem thực đơn
POSTTạo mớiGọi món mới
PUT / PATCHSửaĐổi món: “cho thêm hành”
DELETEXóaHủy món

Status code – mã trả lời

MãÝ nghĩa
200 OKThành công
201 CreatedTạo mới thành công
400 Bad RequestBạn gửi sai dữ liệu
401 / 403Chưa đăng nhập / không có quyền
404 Not FoundKhông tìm thấy
500 Server ErrorLỗi phía server (thường là bug!)

Dễ nhớ: 2xx = tốt 😊, 4xx = lỗi do người gửi 🙋, 5xx = lỗi do server 💥.

Viết test API

Dùng API miễn phí jsonplaceholder.typicode.com để luyện tập. Fixture request giúp gửi yêu cầu mà không cần mở trình duyệt.

import { test, expect } from '@playwright/test';

const API = 'https://jsonplaceholder.typicode.com';

test('GET: lấy bài viết số 1', async ({ request }) => {
  const res = await request.get(`${API}/posts/1`);

  expect(res.status()).toBe(200);          // kiểm tra mã trả lời
  const body = await res.json();           // đọc dữ liệu JSON
  expect(body.id).toBe(1);
  expect(body.userId).toBe(1);
  expect(body.title).toBeTruthy();         // có tiêu đề (không rỗng)
});

test('POST: tạo bài viết mới', async ({ request }) => {
  const res = await request.post(`${API}/posts`, {
    data: { title: 'Học Playwright', body: 'Rất vui!', userId: 1 },
  });

  expect(res.status()).toBe(201);
  const body = await res.json();
  expect(body).toMatchObject({ title: 'Học Playwright', userId: 1 });
  expect(body.id).toBeDefined();
});

test('GET: bài viết không tồn tại trả 404', async ({ request }) => {
  const res = await request.get(`${API}/posts/99999`);
  expect(res.status()).toBe(404);
});

JSON – ngôn ngữ của API

JSON trông giống hệt object trong TypeScript bạn đã học ở bài 4:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident...",
  "body": "quia et suscipit..."
}

Kết hợp API + UI 🚀

💡 Mẹo của dân chuyên

Muốn test “xóa đơn hàng” trên giao diện? Đừng tạo đơn hàng bằng cách bấm 10 bước trên UI. Hãy tạo nhanh bằng API (0.2 giây), rồi chỉ dùng UI cho phần thật sự cần kiểm tra.

test('xóa đơn hàng', async ({ page, request }) => {
  // Arrange bằng API – nhanh
  const res = await request.post('/api/orders', { data: { sanPham: 'Áo thun' } });
  const { id } = await res.json();

  // Act + Assert bằng UI
  await page.goto(`/orders/${id}`);
  await page.getByRole('button', { name: 'Xóa đơn' }).click();
  await expect(page.getByText('Đã xóa đơn hàng')).toBeVisible();
});

📝 Ôn tập

1. Tạo mới dữ liệu thường dùng phương thức nào?

POST = tạo mới.

2. Mã 500 thường có nghĩa là gì?

5xx = lỗi server. QA nên báo cho developer.

Dự án thực tế: Kiểm thử SauceDemo 🛒

Ghép tất cả lại: viết bộ test hoàn chỉnh cho một trang bán hàng – đủ để đưa vào CV.

🎯 Mục tiêu dự án
  • Lập kế hoạch test case
  • Xây dựng cấu trúc POM chuẩn
  • Viết test đăng nhập, thêm giỏ hàng, thanh toán

Trang saucedemo.com được tạo ra để luyện automation. Tài khoản: standard_user / secret_sauce. Các phần tử có thuộc tính data-test.

Bước 1: Lập kế hoạch test

MãKịch bảnMong đợiƯu tiên
LG-01Đăng nhập đúngVào trang ProductsCao
LG-02Sai mật khẩuBáo lỗi “do not match”Cao
LG-03Tài khoản bị khóaBáo lỗi “locked out”Trung bình
CT-01Thêm 2 sản phẩm vào giỏBiểu tượng giỏ hiện số 2Cao
CO-01Thanh toán đầy đủHiện “Thank you for your order!”Cao
CO-02Thanh toán thiếu họ tênBáo lỗi “First Name is required”Trung bình

Bước 2: Cấu trúc thư mục

📁 saucedemo-tests/ ├── 📁 pages/ │ ├── LoginPage.ts │ ├── InventoryPage.ts │ └── CheckoutPage.ts ├── 📁 tests/ │ ├── login.spec.ts │ └── checkout.spec.ts ├── fixtures.ts └── playwright.config.ts

Bước 3: Config

// playwright.config.ts (phần quan trọng)
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  reporter: 'html',
  use: {
    baseURL: 'https://www.saucedemo.com',
    testIdAttribute: 'data-test',     // SauceDemo dùng data-test
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});

Bước 4: Page Objects

// pages/LoginPage.ts
import { type Page, type Locator } from '@playwright/test';

export class LoginPage {
  readonly username: Locator;
  readonly password: Locator;
  readonly loginButton: Locator;
  readonly error: Locator;

  constructor(private readonly page: Page) {
    this.username = page.getByTestId('username');
    this.password = page.getByTestId('password');
    this.loginButton = page.getByTestId('login-button');
    this.error = page.getByTestId('error');
  }

  async goto() {
    await this.page.goto('/');
  }

  async login(user: string, pass: string) {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.loginButton.click();
  }
}
// pages/InventoryPage.ts
import { type Page, type Locator } from '@playwright/test';

export class InventoryPage {
  readonly title: Locator;
  readonly cartBadge: Locator;
  readonly cartLink: Locator;

  constructor(private readonly page: Page) {
    this.title = page.getByTestId('title');
    this.cartBadge = page.getByTestId('shopping-cart-badge');
    this.cartLink = page.getByTestId('shopping-cart-link');
  }

  // Thêm sản phẩm theo tên hiển thị, ví dụ "Sauce Labs Backpack"
  async addToCart(productName: string) {
    await this.page
      .getByTestId('inventory-item')
      .filter({ hasText: productName })
      .getByRole('button', { name: 'Add to cart' })
      .click();
  }

  async openCart() {
    await this.cartLink.click();
  }
}
// pages/CheckoutPage.ts
import { type Page, type Locator } from '@playwright/test';

export class CheckoutPage {
  readonly checkoutButton: Locator;
  readonly firstName: Locator;
  readonly lastName: Locator;
  readonly postalCode: Locator;
  readonly continueButton: Locator;
  readonly finishButton: Locator;
  readonly completeHeader: Locator;
  readonly error: Locator;

  constructor(private readonly page: Page) {
    this.checkoutButton = page.getByTestId('checkout');
    this.firstName = page.getByTestId('firstName');
    this.lastName = page.getByTestId('lastName');
    this.postalCode = page.getByTestId('postalCode');
    this.continueButton = page.getByTestId('continue');
    this.finishButton = page.getByTestId('finish');
    this.completeHeader = page.getByTestId('complete-header');
    this.error = page.getByTestId('error');
  }

  async fillInfo(first: string, last: string, zip: string) {
    await this.firstName.fill(first);
    await this.lastName.fill(last);
    await this.postalCode.fill(zip);
    await this.continueButton.click();
  }
}

Bước 5: Fixture gom tất cả

// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { InventoryPage } from './pages/InventoryPage';
import { CheckoutPage } from './pages/CheckoutPage';

type Pages = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
  checkoutPage: CheckoutPage;
};

export const test = base.extend<Pages>({
  loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
  inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
  checkoutPage: async ({ page }, use) => { await use(new CheckoutPage(page)); },
});

export { expect } from '@playwright/test';

Bước 6: Viết test

// tests/login.spec.ts
import { test, expect } from '../fixtures';

test.describe('Đăng nhập', () => {
  test.beforeEach(async ({ loginPage }) => {
    await loginPage.goto();
  });

  test('LG-01 đăng nhập đúng', { tag: '@smoke' }, async ({ loginPage, inventoryPage, page }) => {
    await loginPage.login('standard_user', 'secret_sauce');
    await expect(page).toHaveURL(/inventory/);
    await expect(inventoryPage.title).toHaveText('Products');
  });

  test('LG-02 sai mật khẩu', async ({ loginPage }) => {
    await loginPage.login('standard_user', 'sai_mat_khau');
    await expect(loginPage.error).toContainText('do not match');
  });

  test('LG-03 tài khoản bị khóa', async ({ loginPage }) => {
    await loginPage.login('locked_out_user', 'secret_sauce');
    await expect(loginPage.error).toContainText('locked out');
  });
});
// tests/checkout.spec.ts
import { test, expect } from '../fixtures';

test.describe('Giỏ hàng & Thanh toán', () => {
  test.beforeEach(async ({ loginPage }) => {
    await loginPage.goto();
    await loginPage.login('standard_user', 'secret_sauce');
  });

  test('CT-01 thêm 2 sản phẩm vào giỏ', async ({ inventoryPage }) => {
    await inventoryPage.addToCart('Sauce Labs Backpack');
    await inventoryPage.addToCart('Sauce Labs Bike Light');
    await expect(inventoryPage.cartBadge).toHaveText('2');
  });

  test('CO-01 thanh toán thành công', { tag: '@smoke' }, async ({ inventoryPage, checkoutPage }) => {
    await test.step('Thêm sản phẩm & mở giỏ', async () => {
      await inventoryPage.addToCart('Sauce Labs Backpack');
      await inventoryPage.openCart();
    });
    await test.step('Nhập thông tin giao hàng', async () => {
      await checkoutPage.checkoutButton.click();
      await checkoutPage.fillInfo('An', 'Nguyen', '700000');
    });
    await test.step('Hoàn tất đơn', async () => {
      await checkoutPage.finishButton.click();
      await expect(checkoutPage.completeHeader).toHaveText('Thank you for your order!');
    });
  });

  test('CO-02 thiếu họ tên báo lỗi', async ({ inventoryPage, checkoutPage }) => {
    await inventoryPage.addToCart('Sauce Labs Backpack');
    await inventoryPage.openCart();
    await checkoutPage.checkoutButton.click();
    await checkoutPage.fillInfo('', 'Nguyen', '700000');
    await expect(checkoutPage.error).toContainText('First Name is required');
  });
});

Bước 7: Chạy & kiểm tra

npx playwright test                 # chạy tất cả
npx playwright test --grep @smoke   # chỉ chạy smoke test
npx playwright show-report          # xem báo cáo
⚠️ Nếu test fail vì không tìm thấy phần tử

Trang web thật có thể thay đổi theo thời gian. Mở F12 kiểm tra lại giá trị data-test, hoặc dùng npx playwright codegen https://www.saucedemo.com → nút Pick locator. Đây chính là kỹ năng bảo trì test ngoài đời thực!

🧪 Thử thách mở rộng
  1. Thêm test: sắp xếp sản phẩm theo giá thấp → cao (dropdown có data-test="product-sort-container", dùng selectOption('lohi')), kiểm tra sản phẩm đầu tiên có giá $7.99.
  2. Thêm test: xóa sản phẩm khỏi giỏ, kiểm tra biểu tượng số trên giỏ biến mất.
  3. Chạy trên cả 3 trình duyệt bằng cách thêm project firefox, webkit.
  4. Đưa dự án lên GitHub kèm file README mô tả cách chạy → link vào CV.
Gợi ý thử thách 1
test('sắp xếp giá thấp → cao', async ({ page }) => {
  await page.getByTestId('product-sort-container').selectOption('lohi');
  await expect(page.getByTestId('inventory-item-price').first()).toHaveText('$7.99');
});

CI/CD, Best practices & con đường sự nghiệp

Để test tự chạy mỗi khi có code mới – và những thói quen của một Automation QA chuyên nghiệp.

🎯 Sau bài này bạn sẽ
  • Chạy test tự động trên GitHub Actions
  • Nắm các nguyên tắc viết test bền vững, tránh flaky
  • Biết lộ trình học tiếp và chuẩn bị phỏng vấn

CI/CD là gì?

CI (Continuous Integration) = mỗi khi developer đẩy code lên, một “robot” trên máy chủ tự động cài đặt và chạy toàn bộ test. Nếu có test fail → báo ngay, không cho code lỗi lọt vào sản phẩm.

👩‍💻git push ⚙️GitHub Actions 📦Cài đặt 🎭Chạy test ✅ Merge ❌ Báo lỗi
Hoàn toàn tự động – bạn chỉ cần đẩy code lên GitHub.

File workflow

Khi cài Playwright và chọn “Add GitHub Actions workflow”, file .github/workflows/playwright.yml đã được tạo sẵn:

name: Playwright Tests
on:                                  # khi nào chạy?
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest           # máy ảo Linux miễn phí của GitHub
    steps:
    - uses: actions/checkout@v4      # 1. lấy code về
    - uses: actions/setup-node@v4    # 2. cài Node.js
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci                    # 3. cài thư viện
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps   # 4. cài trình duyệt
    - name: Run Playwright tests
      run: npx playwright test       # 5. chạy test
    - uses: actions/upload-artifact@v4   # 6. lưu báo cáo để tải về xem
      if: ${{ !cancelled() }}
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Đẩy code lên GitHub → vào tab Actions của repository để xem kết quả. Báo cáo HTML nằm trong mục Artifacts.

Best practices – 10 thói quen vàng

✅ NÊN
  • Dùng locator hướng người dùng: getByRole, getByLabel
  • Dùng web-first assertion: await expect(loc)...
  • Mỗi test độc lập, tự chuẩn bị dữ liệu
  • Đặt tên test mô tả hành vi: “sai mật khẩu báo lỗi”
  • Dùng POM khi dự án lớn dần
  • Tạo dữ liệu bằng API cho nhanh
  • Giấu mật khẩu trong biến môi trường
❌ KHÔNG NÊN
  • waitForTimeout(5000) – chờ cứng
  • XPath dài kiểu //div[3]/div[2]/span
  • Quên await
  • Test phụ thuộc thứ tự chạy
  • Commit test.only lên GitHub
  • Một test kiểm tra quá nhiều thứ không liên quan
  • Bỏ qua test fail “vì chạy lại thì pass”

Flaky test – kẻ thù số 1

Flaky test là test lúc pass lúc fail dù code không đổi. Nguyên nhân thường gặp:

Nguyên nhânCách sửa
Quên awaitKiểm tra mọi dòng page. và expect(locator)
Kiểm tra giá trị “chụp” một lần (textContent() + toBe)Dùng await expect(loc).toHaveText() để tự thử lại
Locator mơ hồ, dùng .nth()Lọc bằng filter({ hasText }), dùng role/test id
Test dùng chung dữ liệu, chạy song song đè nhauMỗi test tạo dữ liệu riêng (tên ngẫu nhiên, timestamp)
Phụ thuộc dịch vụ bên ngoài chậmGiả lập phản hồi bằng page.route()

Lộ trình tiếp theo

1 Git & GitHub 2 Visual testingtoHaveScreenshot() 3 Mock APIpage.route() 4 Báo cáo AllureDocker, chạy phân tán 🏆 Senior SDET
SDET = Software Development Engineer in Test – kỹ sư phát triển kiểm thử.

Câu hỏi phỏng vấn thường gặp

1. Playwright khác Selenium thế nào?

Playwright có auto-waiting sẵn, hỗ trợ nhiều trình duyệt qua một API, chạy song song mặc định, có trace viewer, codegen, test API tích hợp, mỗi test có browser context riêng nên cô lập tốt và nhanh.

2. Locator nào nên ưu tiên? Vì sao?

getByRole → getByLabel → getByPlaceholder → getByText → getByTestId → CSS/XPath. Các locator hướng người dùng bền vững khi HTML/CSS thay đổi và đồng thời kiểm tra khả năng truy cập (accessibility).

3. Page Object Model là gì?

Mẫu thiết kế đóng gói locator và thao tác của mỗi trang vào một class. Giảm lặp code, giao diện đổi chỉ sửa một chỗ, test dễ đọc.

4. Xử lý flaky test như thế nào?

Xem trace để tìm nguyên nhân gốc; dùng web-first assertion thay vì chờ cứng; locator chính xác; test độc lập; mock dịch vụ ngoài. Retry chỉ là biện pháp tạm thời.

5. Fixture là gì? Khác gì beforeEach?

Fixture là đồ nghề chuẩn bị theo yêu cầu: chỉ khởi tạo khi test cần, có thể tái sử dụng giữa các file, gói cả setup và teardown ở một chỗ. beforeEach áp dụng cho mọi test trong file/nhóm.

📝 Ôn tập

1. Flaky test là gì?

Flaky = không ổn định. Cần tìm nguyên nhân gốc thay vì chạy lại cho qua.

2. Trên GitHub, xem kết quả CI ở tab nào?

Tab Actions hiển thị các lần chạy workflow.

Bảng tra cứu nhanh & Từ điển thuật ngữ

In trang này ra hoặc ghim lại – bạn sẽ quay lại đây rất nhiều lần.

⌨️ Lệnh Terminal

npm init playwright@latest              # tạo dự án mới
npx playwright test                     # chạy tất cả test
npx playwright test login.spec.ts       # chạy 1 file
npx playwright test -g "đăng nhập"      # chạy theo tên
npx playwright test --headed            # hiện trình duyệt
npx playwright test --ui                # UI Mode
npx playwright test --debug             # chạy từng bước
npx playwright test --project=firefox   # chọn trình duyệt
npx playwright test --grep @smoke       # chạy theo tag
npx playwright test --last-failed       # chạy lại test vừa fail
npx playwright show-report              # xem báo cáo
npx playwright codegen URL              # ghi thao tác thành code
npx playwright show-trace trace.zip     # xem trace
npx playwright install                  # cài/cập nhật trình duyệt

🔍 Locator

page.getByRole('button', { name: 'Lưu' })
page.getByRole('link', { name: 'Trang chủ' })
page.getByRole('heading', { name: 'Giỏ hàng' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('checkbox', { name: 'Đồng ý' })
page.getByLabel('Mật khẩu')
page.getByPlaceholder('Tìm kiếm...')
page.getByText('Thành công')
page.getByText('Thành công', { exact: true })
page.getByTestId('submit')
page.locator('#id'), page.locator('.class'), page.locator('[name="q"]')
locator.filter({ hasText: 'Áo' }), locator.first(), locator.last(), locator.nth(1)

👆 Action

await page.goto('/duong-dan');
await loc.click();          await loc.dblclick();
await loc.fill('chữ');      await loc.clear();
await loc.press('Enter');   await loc.pressSequentially('chậm');
await loc.check();          await loc.uncheck();
await loc.selectOption('giá trị');
await loc.hover();          await loc.focus();
await loc.setInputFiles('file.pdf');
await loc.dragTo(dich);
await page.reload();        await page.goBack();

✅ Assertion

await expect(page).toHaveURL(/inventory/);
await expect(page).toHaveTitle('Tiêu đề');
await expect(loc).toBeVisible();        await expect(loc).toBeHidden();
await expect(loc).toHaveText('chính xác');
await expect(loc).toContainText('một phần');
await expect(loc).toHaveValue('giá trị ô nhập');
await expect(loc).toHaveCount(3);
await expect(loc).toBeEnabled();        await expect(loc).toBeDisabled();
await expect(loc).toBeChecked();
await expect(loc).toHaveAttribute('href', '/cart');
await expect(loc).toHaveClass(/active/);
await expect(loc).not.toBeVisible();
await expect.soft(loc).toHaveText('...');
expect(giaTri).toBe(5);  expect(obj).toEqual({ a: 1 });

📖 Từ điển thuật ngữ

Thuật ngữGiải thích ngắn gọn
QAQuality Assurance – đảm bảo chất lượng phần mềm
BugLỗi: phần mềm hoạt động khác mong đợi
Test caseKịch bản kiểm thử: các bước + kết quả mong đợi
E2EEnd-to-End – kiểm thử từ đầu đến cuối như người dùng thật
Regression testKiểm thử lại để chắc chắn tính năng cũ không bị hỏng khi có code mới
Smoke testBộ test nhanh kiểm tra các chức năng quan trọng nhất
HeadlessChạy trình duyệt ẩn, không hiện cửa sổ
Locator“Địa chỉ” để tìm phần tử trên trang
AssertionCâu lệnh kiểm tra kết quả (expect)
DOMCây cấu trúc của trang web mà trình duyệt dựng từ HTML
Promise / async / awaitCơ chế xử lý việc cần chờ đợi
FixtureĐồ nghề chuẩn bị sẵn cho test (page, request, loginPage…)
HookCode chạy trước/sau test: beforeEach, afterAll…
POMPage Object Model – đóng gói mỗi trang thành một class
Browser ContextPhiên trình duyệt riêng biệt như cửa sổ ẩn danh
TraceBản ghi chi tiết toàn bộ quá trình chạy test
FlakyTest không ổn định, lúc pass lúc fail
CI/CDTự động build, test, triển khai mỗi khi có code mới
APIGiao diện để các phần mềm nói chuyện với nhau
JSONĐịnh dạng dữ liệu dạng { "khoa": "giá trị" }
SDETKỹ sư phát triển kiểm thử – QA biết lập trình
🎉

Chúc mừng bạn đã hoàn thành khóa học!

Bạn đã đi từ con số 0 đến việc tự xây dựng một bộ test tự động hoàn chỉnh. Hãy tiếp tục luyện tập mỗi ngày, đọc tài liệu chính thức tại playwright.dev, và đưa dự án SauceDemo lên GitHub để làm portfolio.