NỘI DUNG BÀI HỌC
✅ Kỹ thuật Lấy Dữ liệu: Hiểu rõ và sử dụng hiệu quả .textContent(), .innerText(), .getAttribute(), .inputValue(), .allTextContents(), và xử lý Strict Mode.
✅ Khẳng định Giá trị Tức thời: Nắm vững các expect thông thường (toBe, toEqual, toContain, toHaveLength, objectContaining, arrayContaining) để kiểm tra dữ liệu.
🔍 Phần 1: Lấy dữ liệu từ phần tử
Trước khi khẳng định kết quả, tester thường cần lấy dữ liệu thô ra khỏi phần tử để xử lý logic. Playwright cung cấp nhiều phương thức lấy dữ liệu, mỗi cái phục vụ một mục đích riêng.
Nguyên tắc quan trọng: các hàm lấy dữ liệu bên dưới không có auto-waiting. Nếu chỉ cần khẳng định text hay thuộc tính, hãy ưu tiên Web-First Assertions (toHaveText, toHaveAttribute, toHaveValue). Chỉ dùng nhóm lấy dữ liệu khi cần giá trị cho bước logic tiếp theo.
🔹 1. textContent() và innerText()
Đây là cặp dễ nhầm nhất. textContent() lấy text thô của phần tử và mọi con cháu, kể cả phần bị CSS ẩn. innerText() chỉ lấy text như người dùng nhìn thấy sau khi render, nên bỏ qua phần tử ẩn.
<div id="data">Mã: <span>ABC-123</span> <span style="display:none;">Internal</span></div>
const dataDiv = page.locator('#data');
const rawText = await dataDiv.textContent();
// "Mã: ABC-123 Internal" - lấy cả text bị ẩn
const visibleText = await dataDiv.innerText();
// "Mã: ABC-123" - bỏ qua "Internal" vì bị display:none
innerText() chậm hơn vì phải tính layout, nhưng phản ánh đúng những gì người dùng thấy.
🔹 2. getAttribute() và inputValue()
getAttribute() lấy giá trị một thuộc tính HTML (trả về null nếu không có). inputValue() dùng riêng cho phần tử form và phản ánh giá trị hiện tại sau khi người dùng đã tương tác.
// getAttribute: lấy href, data-id, src...
const link = page.locator('#myLink');
const url = await link.getAttribute('href'); // "/profile"
const userId = await link.getAttribute('data-user-id'); // "123"
const cls = await link.getAttribute('class'); // null nếu không có
// inputValue: giá trị thực tế trong ô input
const username = page.locator('#username');
console.log(await username.inputValue()); // "initial"
await username.fill('new_value');
console.log(await username.inputValue()); // "new_value"
🔹 3. allTextContents() cho nhiều phần tử
Khi locator khớp nhiều phần tử, dùng allTextContents() hoặc allInnerTexts() để lấy về một mảng chuỗi. Nhóm này không bị lỗi Strict Mode vì được thiết kế cho nhiều phần tử.
// <ul><li>Táo</li><li>Cam</li></ul>
const listItems = page.locator('ul > li');
const allTexts = await listItems.allInnerTexts();
// ['Táo', 'Cam']
⚖️ Phần 2: Khẳng định giá trị với expect thông thường
Nhóm expect này chạy tức thời, không chờ đợi phần tử trên giao diện. Nó dùng để xác minh dữ liệu và logic đã lấy được ở Phần 1.
🔹 1. toBe() và toEqual()
Đây là phân biệt nền tảng: toBe() so sánh nghiêm ngặt như === (dùng cho kiểu nguyên thủy), còn toEqual() so sánh sâu nội dung của object/array.
// toBe: so sánh tuyệt đối, đúng cho số, chuỗi, boolean, null
expect(name).toBe('Playwright'); // ✅
expect(version).toBe(1.4); // ✅
// expect(version).toBe('1.4'); // ❌ '1.4' (string) khác 1.4 (number)
// toEqual: so sánh nội dung của object/array
const user1 = { id: 1, name: 'A', roles: ['admin'] };
const user2 = { id: 1, name: 'A', roles: ['admin'] };
expect(user1).toEqual(user2); // ✅ nội dung giống hệt
// expect(user1).toBe(user2); // ❌ là 2 object khác nhau trong bộ nhớ
🔹 2. toContain() và toContainEqual()
Kiểm tra mảng hoặc chuỗi có chứa một phần tử. toContain() dùng === cho phần tử nguyên thủy; toContainEqual() so sánh sâu để tìm object trong mảng.
const permissions = ['read', 'write', 'delete'];
const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
expect(permissions).toContain('write'); // ✅
expect('Hello Playwright').toContain('Play'); // ✅ chuỗi con
expect(users).toContainEqual({ id: 2, name: 'B' }); // ✅ so sánh sâu
// expect(users).toContain({ id: 2, name: 'B' }); // ❌ toContain không so sánh sâu object
🔹 3. Kiểm tra truthy/falsy và so sánh số học
// Falsy gồm: false, 0, "", null, undefined, NaN
expect('hello').toBeTruthy(); // chuỗi không rỗng
expect([]).toBeTruthy(); // array luôn truthy
expect('').toBeFalsy(); // chuỗi rỗng
expect(0).toBeFalsy();
// So sánh số học
expect(itemCount).toBeGreaterThan(0);
expect(itemCount).toBeLessThanOrEqual(5);
expect(totalPrice).toBeGreaterThanOrEqual(100);
🧩 Phần 3: Kiểm tra Object và Array
Khi làm việc với dữ liệu trả về từ API, bạn thường phải khẳng định cả cấu trúc object hoặc số lượng phần tử. Đây là ba matcher hay dùng nhất.
🔹 1. toEqual() và toHaveProperty()
toEqual() so sánh toàn bộ object (kể cả lồng nhau). toHaveProperty() kiểm tra sự tồn tại của một key, có thể kèm giá trị, và dùng dấu chấm để truy cập thuộc tính lồng nhau.
const movie = {
title: "The Dark Knight",
details: { year: 2008, rating: 9.0 },
cast: ["Christian Bale", "Heath Ledger"]
};
expect(movie).toHaveProperty('title'); // chỉ kiểm tra tồn tại
expect(movie).toHaveProperty('title', 'The Dark Knight'); // tồn tại + giá trị
expect(movie).toHaveProperty('details.year'); // thuộc tính lồng nhau
expect(movie).toHaveProperty('details.rating', 9.0); // lồng nhau + giá trị
🔹 2. toHaveLength() kiểm tra độ dài mảng
const genres = ["Sci-Fi", "Thriller", "Action"];
expect(genres).toHaveLength(3); // ✅
expect([]).toHaveLength(0); // ✅ mảng rỗng
🎯 Phần 4: So sánh "một phần" cho dữ liệu hay thay đổi
Response API thật thường có những trường thay đổi liên tục như timestamp hay token. Nếu so sánh toàn bộ bằng toEqual(), test sẽ gãy. Hai công cụ dưới đây giúp chỉ kiểm tra phần cốt lõi.
🔹 1. expect.objectContaining()
Kiểm tra một object chứa ít nhất các cặp key-value chỉ định, bỏ qua mọi thuộc tính khác.
const apiResponse = {
userId: 'u-123',
username: 'gemini',
email: 'gemini@example.com',
timestamp: '2025-10-25T...', // giá trị thay đổi
token: 'xyz...' // giá trị thay đổi
};
// Chỉ quan tâm userId và username, bỏ qua timestamp/token
expect(apiResponse).toEqual(expect.objectContaining({
userId: 'u-123',
username: 'gemini'
}));
// Có thể lồng nhau để kiểm tra sâu
expect(complexResponse).toEqual(expect.objectContaining({
status: 'ok',
data: expect.objectContaining({
user: expect.objectContaining({ username: 'gemini' })
})
}));
🔹 2. expect.arrayContaining()
Kiểm tra một mảng chứa ít nhất các phần tử chỉ định, không quan tâm thứ tự và mảng thực tế có thể nhiều hơn. Rất hợp để kiểm tra quyền hạn hoặc tag.
const userPermissions = ['read', 'write', 'comment', 'delete'];
// Chỉ cần chứa 'read' và 'write', thứ tự không quan trọng
expect(userPermissions).toEqual(expect.arrayContaining(['read', 'write']));
expect(userPermissions).toEqual(expect.arrayContaining(['write', 'read'])); // vẫn ✅
// Cũng so sánh sâu được với object
const products = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 3, name: 'C' }];
expect(products).toEqual(expect.arrayContaining([
{ id: 2, name: 'B' },
{ id: 1, name: 'A' } // thứ tự không quan trọng
]));
📌 Phần 5: Ghi nhớ nhanh cho Tester
- Ưu tiên Web-First Assertions (
toHaveText,toHaveValue) vì có auto-waiting; nhóm lấy dữ liệu vàexpectthông thường thì không chờ. textContent()lấy cả text ẩn,innerText()chỉ lấy text nhìn thấy được.inputValue()phản ánh giá trị hiện tại của ô input, khác vớigetAttribute('value').toBe()so sánh nghiêm ngặt cho kiểu nguyên thủy;toEqual()so sánh sâu cho object/array.toContain()không so sánh sâu object, phải dùngtoContainEqual().objectContaining()vàarrayContaining()giúp test bền vững với dữ liệu hay thay đổi.
✅ Kết luận: Bài 3 trang bị cho bạn hai kỹ năng cốt lõi: lấy đúng dữ liệu ra khỏi phần tử và khẳng định nó một cách chính xác. Khi nắm vững khi nào dùng so sánh tuyệt đối, so sánh sâu, hay so sánh một phần, bạn sẽ viết được những assertion vừa chặt chẽ vừa không dễ gãy.
