NỘI DUNG BÀI HỌC
🧠 Dùng evaluate() để tối ưu (1 round trip) hoặc đọc thuộc tính "ẩn" (như scrollTop, localStorage).
✅ Tránh evaluate(): Luôn ưu tiên hàm gốc (như textContent(), inputValue()) vì có auto-waiting.
📅 Xử lý Date Picker: Dùng date-fns (như differenceInMonths) để tính toán số lần click trước, thay vì lặp và đọc UI.
📊 Xử lý Table (Bảng): Dùng "Header Map" (Ánh xạ Cột) để chống lỗi (robust) khi thứ tự cột thay đổi.
⚡️ Tối ưu Table: Dùng locator:has-text (lọc hàng) + "Header Map" (lọc cột) để lấy 1 ô (cell) nhanh nhất.
🚪 Phần 1: evaluate() - "cửa hậu" vào trình duyệt
evaluate() cho phép bạn chạy JavaScript thô trực tiếp trong ngữ cảnh trình duyệt (giống như gõ lệnh trong Console của DevTools) và nhận về kết quả. Nếu locator.click() giống cánh tay robot phải tuân theo quy tắc vật lý, thì evaluate() giống như ra lệnh bằng "tâm linh": nó bỏ qua auto-wait và không tự cuộn tới phần tử.
// 1. Cú pháp cơ bản với page
const title = await page.evaluate(() => {
return document.title; // Code này chạy trong trình duyệt
});
// 2. Truyền argument vào
const text = await page.evaluate((selector) => {
return document.querySelector(selector).textContent;
}, ".my-element"); // ".my-element" là argument
// 3. Dùng với Locator (phổ biến nhất)
const locator = page.locator("#my-button");
const textContent = await locator.evaluate((el) => {
return el.textContent; // "el" chính là <button id="my-button">
});
Giá trị trả về phải "serializable" (string, number, boolean, array, object). Nếu hàm trả về Promise thì Playwright tự chờ; còn thứ không serialize được như window hay Element sẽ thành undefined.
🔹 1. Quy tắc vàng: khi nào KHÔNG dùng
Nguyên tắc rất đơn giản: nếu Playwright đã có hàm gốc thì dùng hàm gốc, vì chúng có auto-waiting, còn evaluate() chạy ngay lập tức nên dễ gây flaky.
- Lấy text: dùng
loc.textContent(), không dùngloc.evaluate(el => el.textContent). - Lấy giá trị input: dùng
loc.inputValue(). - Lấy thuộc tính: dùng
loc.getAttribute("href"). - Kiểm tra checked: dùng
loc.isChecked().
🔹 2. Khi nào BẮT BUỘC dùng
Bạn cần evaluate() khi phải đọc những thuộc tính DOM mà Playwright không có hàm gốc, ví dụ trạng thái cuộn hay computed style.
// Đọc thuộc tính scroll để kiểm tra hộp chat đã cuộn xuống cuối chưa
const chatBox = page.locator("#chat-box");
const scrollProps = await chatBox.evaluate((el) => {
return {
top: el.scrollTop,
totalHeight: el.scrollHeight,
visibleHeight: el.clientHeight,
};
});
const isScrolledToBottom =
scrollProps.top + scrollProps.visibleHeight >= scrollProps.totalHeight;
expect(isScrolledToBottom).toBe(true);
// Đọc computed style (màu nền thật sau khi render)
const bgColor = await page.locator("#my-btn").evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue("background-color");
}); // Ví dụ: "rgb(255, 0, 0)"
🔹 3. Tối ưu: gộp nhiều lệnh thành 1 round trip
Mỗi lệnh await là một lần "khứ hồi" từ Node.js sang trình duyệt. Nếu cần đọc nhiều thuộc tính cùng lúc, gói tất cả vào một evaluate sẽ nhanh hơn nhiều.
const locator = page.locator("#my-element");
// Chỉ 1 lệnh khứ hồi duy nhất thay vì 4
const data = await locator.evaluate((el) => {
return {
a: el.getAttribute("data-a"),
b: el.getAttribute("data-b"),
c: el.getAttribute("data-c"),
text: el.textContent,
};
});
📅 Phần 2: Xử lý Date Picker với date-fns
Có hai chiến lược khi thao tác lịch. Chiến lược "mù quáng" là cứ click Next/Prev rồi đọc UI mỗi vòng để dò tháng, cách này chậm và không tin cậy. Chiến lược "tính toán" dùng thư viện date-fns để tính chính xác số lần cần click trước khi tương tác, nhanh và ổn định hơn hẳn.
Các hàm date-fns hay dùng: parse() đổi chuỗi thành Date, startOfMonth() đưa về đầu tháng để so sánh công bằng, và differenceInMonths() đo chênh lệch số tháng - chính là "bộ não" của thuật toán.
import { parse, startOfMonth, differenceInMonths } from "date-fns";
async function chonNgay(panel, ymd: string) {
// Bước 1: Xác định ngày mục tiêu và ngày hiện tại
const target = startOfMonth(parse(ymd, "yyyy-MM-dd", new Date()));
const current = startOfMonth(new Date());
// Bước 2 + 3: Tính chênh lệch số tháng (số nguyên, có thể âm)
const monthsDiff = differenceInMonths(target, current);
// Bước 4: Điều hướng đúng số lần, không cần đọc UI
if (monthsDiff < 0) {
for (let i = 0; i < Math.abs(monthsDiff); i++) {
await panel.locator("#prev-button").click(); // Lùi về quá khứ
}
} else {
for (let i = 0; i < monthsDiff; i++) {
await panel.locator("#next-button").click(); // Tiến tới tương lai
}
}
// Bước 5: Chọn đúng ô ngày rồi kiểm tra
await panel.locator(`[data-date='${ymd}']`).click();
}
📊 Phần 3: Xử lý Table hiệu quả
Cách "ngây thơ" là lặp qua từng hàng rồi lấy ô theo index cố định như nth(0), nth(1). Cách này có hai nhược điểm lớn: rất mong manh (đổi thứ tự cột là test hỏng) và rất chậm (bảng 500 hàng x 7 cột tạo ra hàng nghìn lệnh await).
Chiến lược tốt hơn là "Lọc hàng, ánh xạ cột": tạo một Header Map để biết mỗi cột nằm ở index nào mà không cần hard-code.
// Đọc <th> và trả về Map { "Project" => 0, "Client" => 1, "Priority" => 5 }
async function createHeaderMap(tableLocator) {
const headerMap = new Map<string, number>();
const headers = tableLocator.locator("thead th");
const count = await headers.count();
// Chỉ lặp qua số cột (rất nhanh)
for (let i = 0; i < count; i++) {
const headerText = await headers.nth(i).textContent();
if (headerText) {
headerMap.set(headerText.trim(), i);
}
}
return headerMap;
}
🔹 Đối chiếu một hàng cụ thể (nhanh nhất)
Kết hợp Header Map với hasText để lọc đúng hàng, bạn chỉ cần vài lệnh await thay vì hàng nghìn.
const table = panel.locator("table").first();
const headerMap = await createHeaderMap(table); // Chỉ chạy 1 lần
// Lọc "hàng nào" bằng hasText (Playwright làm việc này cực nhanh)
const row = table.locator("tbody tr", { hasText: "Apollo UI" });
// Lấy "cột nào" từ bản đồ
const priorityIndex = headerMap.get("Priority"); // Ví dụ: 5
// Lấy đúng ô và kiểm tra
const priorityCell = row.locator("td").nth(priorityIndex);
await expect(priorityCell).toHaveText("High");
💡 Mẹo: Khi thu thập dữ liệu nhiều hàng, hãy lấy index cột từ
headerMaprồi mới dùngcells.nth(index), tuyệt đối không hard-codenth(0),nth(1)để tránh hỏng khi cột đổi vị trí.
🔹 Ghi nhớ nhanh cho Tester
evaluate()mạnh nhưng bỏ qua auto-wait, chỉ dùng khi hàm gốc của Playwright không đáp ứng được.- Dùng nó để đọc thuộc tính DOM đặc biệt (
scrollTop, computed style) hoặc gộp nhiều thao tác vào 1 round trip. - Với Date Picker, hãy tính số lần click bằng
date-fnsthay vì dò UI từng tháng. differenceInMonthscho biết cần click Next hay Prev và bao nhiêu lần.- Với Table, tạo Header Map để ánh xạ tên cột sang index, chống lỗi khi cột đổi chỗ.
- Lọc hàng bằng
hasTextgiúp thao tác nhanh và ổn định hơn nhiều so với lặp toàn bảng.
✅ Kết luận: Bài 7 nâng kỹ năng của bạn từ "click từng thứ" lên "tư duy như một engineer": biết khi nào nên nhờ tới trình duyệt qua evaluate(), biết tính toán trước thay vì dò dẫm với lịch, và biết tổ chức truy vấn bảng sao cho vừa nhanh vừa bền.
