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.
Bạn sẽ đi qua 5 chặng
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ần | Nội dung | Kết quả |
|---|---|---|
| Tuần 1 | Bài 0 → 5: QA, cài đặt, TypeScript cơ bản, async/await | Đọc hiểu được code đơn giản |
| Tuần 2 | Bài 6 → 11: Web, test đầu tiên, Locator, Action, Assertion, Debug | Tự viết test cho 1 trang web |
| Tuần 3 | Bài 12 → 15: Tổ chức test, POM, Config, Fixture, API | Viết test gọn gàng, dễ bảo trì |
| Tuần 4 | Bài 16 → 18: Dự án SauceDemo, CI/CD, ôn tập | Có dự án đưa vào CV |
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).
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.
- 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.
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ện | Kết quả mong đợi |
|---|---|---|
| TC01 | 1. 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” |
| TC02 | 1. 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
| Thủ công | Tự động | |
|---|---|---|
| Phù hợp khi | Khá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 đầu | Thấp | Cao hơn (phải viết code) |
| Chi phí về lâu dài | Cao (tốn người, tốn giờ) | Thấp (máy chạy miễn phí) |
| Độ chính xác khi lặp lại | Giảm dần vì mệt | Luôn giống nhau |
Kim tự tháp kiểm thử
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.
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:
Bạn gõ nhầm page.clik(). Không ai nhắc. Chạy test mới biết lỗi.
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?
2. Loại kiểm thử nào phù hợp nhất để tự động hóa?
3. Playwright có thể chạy trên trình duyệt nào?
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.
- 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ì?
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
- 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 🎉 - 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.
- 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 đó. - Cài Playwright
Mở Terminal trong VS Code và gõ:
Máy sẽ hỏi vài câu. Cứ chọn như sau (dùng phím mũi tên và Enter):
npm init playwright@latestCâu hỏi Chọ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 - Chạy thử test mẫu
Nếu thấy dòng chữ màu xanh kiể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ệt6 passedlà bạn đã thành công!
Cấu trúc thư mục sau khi cài
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)
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.
PowerShell đang chặn chạy script. Chạy lệnh sau một lần là xong:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned- Chạy
npx playwright test --ui. Một cửa sổ đẹp sẽ hiện ra – đây là UI Mode. - Bấm nút ▶ cạnh một test để chạy. Xem các bước hiện ra bên phải.
- 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?
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.
- 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ì?
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.tsHoặ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!
5Biế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.
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: 26const hay let?
Hộp niêm phong. Bỏ đồ vào rồi thì không đổi được nữa. Dùng mặc định.
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 const3 kiểu dữ liệu cơ bản
| Kiểu | Ý nghĩa | Ví dụ | Dùng trong test |
|---|---|---|---|
string | Chữ, đặt trong dấu nháy ' ' | 'admin@shop.vn' | Email, mật khẩu, tiêu đề trang |
number | Số, không có dấu nháy | 3, 99.5 | Số lượng sản phẩm, giá tiền |
boolean | Chỉ có true hoặc false | true | Đã đăng nhập chưa? Có hiển thị không? |
'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àngSo sánh & logic
| Ký hiệu | Ý nghĩa | Ví dụ | Kết quả |
|---|---|---|---|
=== | bằng nhau | 3 === 3 | true |
!== | khác nhau | 'a' !== 'b' | true |
> < >= <= | lớn hơn, nhỏ hơn… | 18 >= 18 | true |
&& | VÀ (cả hai đều đúng) | true && false | false |
|| | HOẶC (một trong hai đúng) | true || false | true |
! | KHÔNG (đảo ngược) | !true | false |
= là gán (bỏ đồ vào hộp). === là so sánh. Đừng nhầm hai cái này.
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?
&&) 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.
- 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
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ự
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
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); // falsepage.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
// 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 AnNhì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.vnViế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ì?
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ì?
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.
- Hiểu tại sao thao tác với trình duyệt cần “chờ”
- Biết Promise,
async,awaitlà gì - Tránh lỗi số 1 của người mới: quên
await
Câu chuyện quán cà phê ☕
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.”
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();- Muốn dùng
awaitbên trong một hàm → hàm đó phải có chữasync. - Trong Playwright, gần như mọi lệnh bắt đầu bằng
page.hoặcexpect(...)đều cầnawaitphí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();
});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.
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ê?
2. Dòng nào viết đúng?
await phía trước.3. Để dùng await trong một hàm, hàm đó cần gì?
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ì.
- 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 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ần | Ví dụ | Ý nghĩa |
|---|---|---|
| Tên thẻ | button, input, h1 | Loại phần tử: nút, ô nhập, tiêu đề |
id | id="email" | Mã định danh, thường là duy nhất trên trang |
class | class="btn-primary" | Nhóm để trang trí CSS, có thể trùng nhau |
placeholder | placeholder="ban@vidu.vn" | Chữ mờ gợi ý trong ô nhập |
data-testid | data-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”.
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ấy | Thẻ HTML | Role |
|---|---|---|
| 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 🔍
- Mở DevToolsTrên Chrome/Edge, nhấn F12 hoặc chuột phải vào phần tử → Inspect (Kiểm tra).
- 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.
- 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. - Xem roleTrong DevTools, tab Accessibility (bên phải tab Styles) hiển thị role và name của phần tử.
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?
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ì?
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.
- 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 code | Giả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
Các cách chạy test
| Lệnh | Tác dụng |
|---|---|
npx playwright test | Chạy tất cả test, ẩn trình duyệt (headless – nhanh) |
npx playwright test dau-tien.spec.ts | Chỉ chạy 1 file |
npx playwright test -g "tiêu đề" | Chỉ chạy test có tên chứa “tiêu đề” |
npx playwright test --headed | Hiện trình duyệt để xem nó tự bấm |
npx playwright test --project=chromium | Chỉ chạy trên Chromium |
npx playwright test --ui | Mở UI Mode – khuyên dùng khi học ⭐ |
npx playwright show-report | Mở báo cáo HTML |
UI Mode – Bảng điều khiển trực quan
Đọ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).
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?
3. Lệnh nào giúp xem test chạy từng bước trực quan nhất?
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”.
- 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
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
| Locator | Dùng khi | Ví dụ |
|---|---|---|
getByRole | Nú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áo | page.getByText('Đăng nhập thành công') |
getByAltText | Hình ảnh | page.getByAltText('Logo công ty') |
getByTestId | Có data-testid | page.getByTestId('login-btn') |
locator | Phương án cuối: CSS / XPath | page.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:
// 👆 Bấm vào một phần tử phía trênLocator 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)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ết | Nghĩa |
|---|---|
#email | Phần tử có id="email" |
.btn-primary | Phần tử có class btn-primary |
[data-test="username"] | Phần tử có thuộc tính data-test bằng “username” |
form button | Nút nằm bên trong form |
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?
2. “Strict mode violation” nghĩa là gì?
3. Ô nhập có <label>Họ tên</label> – locator tốt nhất?
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.
- 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 động | Code | |
|---|---|---|
| 🌐 | Mở trang | await page.goto('https://...') |
| 👆 | Bấm | await locator.click() |
| 👆👆 | Bấm đúp | await locator.dblclick() |
| ⌨️ | Điền chữ (xóa cũ, ghi mới) | await locator.fill('xin chào') |
| 🧹 | Xóa nội dung ô nhập | await locator.clear() |
| ⏎ | Nhấn phím | await locator.press('Enter') |
| ☑️ | Tích / bỏ tích | await locator.check() / uncheck() |
| 🔽 | Chọn trong dropdown | await locator.selectOption('Hà Nội') |
| 🖱️ | Rê chuột lên | await locator.hover() |
| 📎 | Tải file lên | await 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()?
Dán cả đoạn chữ vào ngay lập tức. Nhanh. Dùng trong 95% trường hợp.
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 popupHộ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/);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ờ.
- 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
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
| Assertion | Kiể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:
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.
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!await expect(loc).toHaveText('Xin chào');
// tự chờ, tự thử lại
// pass ngay khi chữ xuất hiệnSoft 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)?
3. Mặc định expect trên locator thử lại trong bao lâu?
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.
- 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/todomvcCode 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 --debugTrì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();
});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.zipTrong 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:5 | Test 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 / Received | Mong đợi “hiển thị” nhưng thực tế “không tìm thấy” |
> 15 | | Dòng code gây lỗi |
- Đọc lỗi: dòng nào, locator nào, mong đợi gì, nhận được gì.
- Xem ảnh chụp/trace: lúc đó màn hình hiển thị gì?
- Phân loại: lỗi do test viết sai (locator sai, quên await) hay ứng dụng có bug?
- 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ì?
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ơ.
- 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
| Hook | Chạy khi nào | Ví dụ dùng |
|---|---|---|
test.beforeEach | Trước mỗi test | Mở trang, đăng nhập |
test.afterEach | Sau mỗi test | Dọn dữ liệu, đăng xuất |
test.beforeAll | Một lần trước tất cả | Tạo dữ liệu dùng chung qua API |
test.afterAll | Mộ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
Đừ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ửanpx playwright test --grep @smoke # chỉ chạy test có tag @smoke
npx playwright test --grep-invert @slow # bỏ qua test có tag @slowtest.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?
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.
- 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ỗ 😱.
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(...).
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');
});- 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ảiclickButton1. - 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?
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.
- Đọ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 📱
],
});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 testFixture – đồ nghề chuẩn bị sẵn
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
},
],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?
2. Trong fixture, await use(x) làm gì?
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.
- 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ì? 🍜
Các phương thức HTTP
| Phương thức | Ý nghĩa | Ví dụ đời thường |
|---|---|---|
| GET | Lấy dữ liệu | Xem thực đơn |
| POST | Tạo mới | Gọi món mới |
| PUT / PATCH | Sửa | Đổi món: “cho thêm hành” |
| DELETE | Xóa | Hủy món |
Status code – mã trả lời
| Mã | Ý nghĩa |
|---|---|
| 200 OK | Thành công |
| 201 Created | Tạo mới thành công |
| 400 Bad Request | Bạn gửi sai dữ liệu |
| 401 / 403 | Chưa đăng nhập / không có quyền |
| 404 Not Found | Không tìm thấy |
| 500 Server Error | Lỗ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 🚀
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?
2. Mã 500 thường có nghĩa là gì?
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.
- 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ản | Mong đợi | Ưu tiên |
|---|---|---|---|
| LG-01 | Đăng nhập đúng | Vào trang Products | Cao |
| LG-02 | Sai mật khẩu | Báo lỗi “do not match” | Cao |
| LG-03 | Tài khoản bị khóa | Báo lỗi “locked out” | Trung bình |
| CT-01 | Thêm 2 sản phẩm vào giỏ | Biểu tượng giỏ hiện số 2 | Cao |
| CO-01 | Thanh toán đầy đủ | Hiện “Thank you for your order!” | Cao |
| CO-02 | Thanh toán thiếu họ tên | Báo lỗi “First Name is required” | Trung bình |
Bước 2: Cấu trúc thư mục
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áoTrang 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êm test: sắp xếp sản phẩm theo giá thấp → cao (dropdown có
data-test="product-sort-container", dùngselectOption('lohi')), kiểm tra sản phẩm đầu tiên có giá$7.99. - 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.
- Chạy trên cả 3 trình duyệt bằng cách thêm project firefox, webkit.
- Đư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.
- 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.
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
- 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
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.onlylê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ân | Cách sửa |
|---|---|
Quên await | Kiể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 đè nhau | Mỗ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ậm | Giả lập phản hồi bằng page.route() |
Lộ trình tiếp theo
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ì?
2. Trên GitHub, xem kết quả CI ở tab nào?
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 |
|---|---|
| QA | Quality Assurance – đảm bảo chất lượng phần mềm |
| Bug | Lỗi: phần mềm hoạt động khác mong đợi |
| Test case | Kịch bản kiểm thử: các bước + kết quả mong đợi |
| E2E | End-to-End – kiểm thử từ đầu đến cuối như người dùng thật |
| Regression test | Kiểm thử lại để chắc chắn tính năng cũ không bị hỏng khi có code mới |
| Smoke test | Bộ test nhanh kiểm tra các chức năng quan trọng nhất |
| Headless | Chạy trình duyệt ẩn, không hiện cửa sổ |
| Locator | “Địa chỉ” để tìm phần tử trên trang |
| Assertion | Câu lệnh kiểm tra kết quả (expect) |
| DOM | Cây cấu trúc của trang web mà trình duyệt dựng từ HTML |
| Promise / async / await | Cơ chế xử lý việc cần chờ đợi |
| Fixture | Đồ nghề chuẩn bị sẵn cho test (page, request, loginPage…) |
| Hook | Code chạy trước/sau test: beforeEach, afterAll… |
| POM | Page Object Model – đóng gói mỗi trang thành một class |
| Browser Context | Phiên trình duyệt riêng biệt như cửa sổ ẩn danh |
| Trace | Bản ghi chi tiết toàn bộ quá trình chạy test |
| Flaky | Test không ổn định, lúc pass lúc fail |
| CI/CD | Tự động build, test, triển khai mỗi khi có code mới |
| API | Giao 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ị" } |
| SDET | Kỹ 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.