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ùng loc.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ừ headerMap rồi mới dùng cells.nth(index), tuyệt đối không hard-code nth(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-fns thay vì dò UI từng tháng.
  • differenceInMonths cho 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 hasText giú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.

Teacher

Teacher

Nguyên Hoàng

Automation Engineer

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

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

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

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

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

Danh sách bài học