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() là một trong những phương thức mạnh mẽ nhất, nhưng cũng dễ bị lạm dụng nhất trong Playwright. Nó là "cửa hậu" (backdoor) cho phép bạn chạy JavaScript thô (raw) trực tiếp bên trong trang web.

evaluate() là gì?

Định nghĩa: evaluate() là phương thức để thực thi JavaScript code trong browser context (tức là chạy bên trong trang web, giống như bạn gõ lệnh trong Console của DevTools) và nhận kết quả trả về.

Mục đích: Nó được dùng để lấy dữ liệu (fetch data) hoặc thực thi logic mà bạn không thể làm được bằng các hàm Playwright thông thường.

Phép loại suy (Analogy): Robot 🤖 vs. Tâm linh 🧠

locator.click(): Giống như một cánh tay robot (Playwright) tương tác với trang web từ bên ngoài. Nó phải tuân theo các quy tắc vật lý (chờ "Actionability", tự động cuộn...).

page.evaluate(): Giống như bạn ra lệnh bằng "tâm linh" (telepathy), bắt trình duyệt tự nó chạy một đoạn code. Nó bỏ qua mọi quy tắc của robot (không auto-wait, không scroll...).


Cú pháp cơ bản

Bạn có thể gọi evaluate từ page (cho các hành động toàn cục) hoặc từ locator (cho một element cụ thể).

// --- 1. Cú pháp cơ bản (với Page) ---
const title = await page.evaluate(() => {
  // Code này chạy trong browser context
  return document.title;
});

// --- 2. Với Argument (Đối số) ---
const text = await page.evaluate((selector) => {
  // 'selector' là '.my-element'
  return document.querySelector(selector).textContent;
}, '.my-element'); // '.my-element' là argument

// --- 3. Với Locator (Phổ biến nhất) ---
const locator = page.locator('#my-button');
const textContent = await locator.evaluate((el) => {
  // 'el' ở đây chính là <button id="my-button">
  return el.textContent;
});

 

Đặc điểm 

Những đặc điểm này rất quan trọng khi bạn làm việc với evaluate:

  • Chạy trong Browser Context: Code của bạn có thể truy cập document, window, localStorage, v.v.

  • Giá trị Serializable:

    • Bạn có thể truyền đối số (argument) vào và nhận giá trị trả về.

    • Giá trị trả về phải là "serializable" (có thể chuyển đổi thành chuỗi JSON), ví dụ: string, number, boolean, Array, Object.

  • Return Promise: Nếu hàm của bạn trả về Promise, Playwright sẽ tự động chờ cho đến khi Promise đó hoàn thành.

  • Giá trị Đặc biệt: Hỗ trợ trả về NaN, Infinity, -Infinity.

  • Non-Serializable: Nếu bạn cố gắng trả về một giá trị phức tạp không thể serialize (như window hoặc một Element), Playwright sẽ trả về undefined.


Quy tắc VÀNG: Khi nào KHÔNG DÙNG evaluate()

"Nếu Playwright đã có hàm gốc (native method), hãy dùng nó."

Lý do: Các hàm gốc của Playwright (như textContent(), inputValue()) có cơ chế "auto-waiting" (tự động chờ). Chúng sẽ chờ cho đến khi element sẵn sàng (visible, stable, enabled...).

evaluate() thì không chờ. Nó chạy ngay lập tức. Nếu bạn gọi evaluate trước khi element xuất hiện, test của bạn sẽ bị flaky (mong manh).

Bạn muốn lấy... ❌ Dùng evaluate() (KHÔNG TỐT / Flaky) ✅ Dùng Native (TỐT NHẤT / Ổn định)
Text Content await loc.evaluate(el => el.textContent) await loc.textContent()
Input Value await loc.evaluate(el => el.value) await loc.inputValue()
Attribute await loc.evaluate(el => el.getAttribute('href')) await loc.getAttribute('href')
Checked? await loc.evaluate(el => el.checked) await loc.isChecked()

Khi nào BẮT BUỘC dùng evaluate()?

Chỉ dùng evaluate() khi Playwright không có hàm gốc cho thuộc tính bạn cần, hoặc khi bạn cần tối ưu hóa.

A. Đọc các thuộc tính DOM không được hỗ trợ

Đây là trường hợp kinh điển: scrollTop, scrollHeight, clientHeight.

Kịch bản: Kiểm tra một div (hộp chat) đã cuộn xuống cuối hay chưa.

const chatBox = page.locator('#chat-box');
// (Sau khi gửi tin nhắn...)

const scrollProps = await chatBox.evaluate(el => {
  // Code này chạy trong trình duyệt
  return {
    top: el.scrollTop,
    totalHeight: el.scrollHeight,
    visibleHeight: el.clientHeight
  };
});
// scrollProps = { top: 500, totalHeight: 800, visibleHeight: 300 }

const isScrolledToBottom = 
  (scrollProps.top + scrollProps.visibleHeight) >= scrollProps.totalHeight;

expect(isScrolledToBottom).toBe(true);

B. Đọc Computed Styles (CSS đã tính toán)

Kịch bản: Lấy màu nền (background-color) chính xác.

 
const myButton = page.locator('#my-btn');

const bgColor = await myButton.evaluate(el => {
  return window.getComputedStyle(el).getPropertyValue('background-color');
});
// 'bgColor' bây giờ sẽ là giá trị thật: "rgb(255, 0, 0)"

Tối ưu hóa: Đọc nhiều thuộc tính & Xử lý logic phức tạp

Đây là lý do "nâng cao" để dùng evaluate(): Giảm thiểu "Độ trễ Mạng" (Network Latency).

Mỗi lệnh await của Playwright là một lệnh khứ hồi (round trip) đi từ Node.js đến Trình duyệt. Việc này tốn thời gian.

❌ Cách không hiệu quả (4 lệnh = 4 Round Trips)

Cách này "nói nhiều" (chatty).

const locator = page.locator('#my-element');
const propA = await locator.getAttribute('data-a'); // Chờ 1...
const propB = await locator.getAttribute('data-b'); // Chờ 2...
const propC = await locator.getAttribute('data-c'); // Chờ 3...
const text = await locator.textContent();           // Chờ 4...

✅ Cách hiệu quả (dùng evaluate = 1 Round Trip)

Bạn "gói" 4 yêu cầu đó vào 1 lệnh evaluate duy nhất.

const locator = page.locator('#my-element');

const data = await locator.evaluate(el => {
  // Toàn bộ code này chạy bên trong trình duyệt (rất nhanh)
  return {
    a: el.getAttribute('data-a'),
    b: el.getAttribute('data-b'),
    c: el.getAttribute('data-c'),
    text: el.textContent
  };
}); 
// data = { a: '...', b: '...', c: '...', text: '...' }
// Chỉ mất 1 lệnh khứ hồi duy nhất.

 

Trường hợp đặc biệt: Ghi đè "Controlled Inputs" (Ví dụ: React)

Đây là kỹ thuật "lách luật" khi locator.fill() thất bại (thường là với các thư viện component phức tạp).

Vấn đề: Trong React, giá trị value của input bị "khóa" (controlled) bởi state. el.value = ... không thông báo cho React.

Giải pháp: Dùng evaluate để gọi "native setter" (hàm set gốc) và "dispatch event" (gửi sự kiện) để thông báo cho React.

const input = panel.locator('#eh-input');

await input.evaluate((el: HTMLInputElement) => {  
  // 1. Lấy hàm setter "gốc" của trình duyệt
  const setter = Object.getOwnPropertyDescriptor(    
    window.HTMLInputElement.prototype,    
    'value'  
  )?.set;  
  
  // 2. Gọi hàm setter đó để gán giá trị
  setter?.call(el, 'Hello evaluate');  
  
  // 3. Gửi sự kiện 'input' để thông báo cho React
  el.dispatchEvent(new Event('input', { bubbles: true }));
});


Phần 2: 📅 Xử lý Date Picker (Lịch) với date-fns

Khi xử lý Date Picker (công cụ chọn lịch), chúng ta có 2 chiến lược:

Chiến lược "Mù quáng" (Kém hiệu quả): Click nút "Next" hoặc "Prev" lặp đi lặp lại. Trong mỗi vòng lặp, đọc text của tháng/năm trên UI (await locator.textContent()) để xem đã đến đúng tháng/năm chưa.

Nhược điểm: Rất chậm (vì nhiều hành động click và read) và không đáng tin cậy.

Chiến lược "Tính toán" (Hiệu quả ): Sử dụng một thư viện (như date-fns) để tính toán chính xác số lần cần click trước khi tương tác với UI.

Ưu điểm: Cực kỳ nhanh (chỉ click đúng số lần) và ổn định.


date-fns là gì? (Bộ công cụ của bạn 🔧)

Date object (đối tượng Ngày) gốc của JavaScript rất khó dùng. Việc cộng/trừ tháng, tính toán chênh lệch, hay format (định dạng) ngày đều rất phức tạp và dễ gây lỗi.

date-fns là một thư viện JavaScript hiện đại, nhẹ, cung cấp các hàm "an toàn" để xử lý mọi thao tác về ngày/tháng.

Nó giống như một bộ "cờ lê" chuyên dụng. Thay vì bạn phải tự "gõ búa" để xử lý Date, date-fns đưa cho bạn đúng dụng cụ bạn cần.


Các hàm date-fns hay dùng

parse(ymd, 'yyyy-MM-dd', ...): "Dịch" 📝

Nghĩa là: Lấy một chuỗi (string) như "2025-10-29" và "dịch" (parse) nó thành một đối tượng Date thật mà máy tính hiểu được.


isValid(parsed): "Kiểm tra" ✅

Nghĩa là: Kiểm tra xem kết quả của parse có phải là một ngày hợp lệ không (ví dụ: "2025-02-30" là không hợp lệ).

startOfMonth(date): "Reset về đầu tháng" 🗓️

Nghĩa là: Lấy một ngày bất kỳ (ví dụ: 2025-10-29) và trả về đối tượng Date của ngày đầu tiên trong tháng đó (2025-10-01). Việc này rất quan trọng để so sánh 2 tháng với nhau.


differenceInMonths(targetDate, currentDate): "Đo chênh lệch" 📏

Nghĩa là: Tính toán xem có bao nhiêu tháng chênh lệch giữa hai ngày. Đây là "bộ não" của thuật toán.

differenceInMonths('2025-03-01', '2025-01-01') ➜ trả về 2.

differenceInMonths('2025-01-01', '2025-03-01') ➜ trả về -2.


getMonth(parsed) / getYear(parsed): "Lấy thông tin" 🏷️

Nghĩa là: Lấy số tháng (0-11) hoặc số năm từ một đối tượng Date.

Đây là các bước chi tiết cho một "thuật toán" xử lý Date Picker hiệu quả, không phụ thuộc vào UI (UI-independent) mà dựa trên tính toán logic.

Chiến lược này tránh việc "đọc" UI trong một vòng lặp (vốn rất chậm). Thay vào đó, chúng ta tính toán mọi thứ trong JavaScript trước, sau đó mới ra lệnh cho Playwright thực thi.

📦 Bước 1: Xác định Trạng thái (Define States)

Bạn cần xác định 2 điểm thời gian:

Ngày Mục tiêu (Target Date): Là ngày bạn muốn chọn (ví dụ: "2025-08-29"). Bạn cần "dịch" (parse) nó từ chuỗi (string) thành một đối tượng Date (dùng parse của date-fns).

Ngày Hiện tại (Current Date): Là tháng/năm mà Date Picker hiển thị khi vừa mở.

Lưu ý quan trọng: Bạn phải biết trạng thái bắt đầu này. Trong hầu hết các trường hợp, nó là new Date() (ngày giờ hệ thống hiện tại).


🗓️ Bước 2: Chuẩn hóa (Normalize)

Bạn không quan tâm đến ngày 29 hay 05; bạn chỉ quan tâm đến tháng/năm để điều hướng.

Hãy "reset" cả hai ngày về ngày đầu tiên của tháng (dùng startOfMonth) để so sánh chúng một cách công bằng.

Target: 2025-08-29 ➜ 2025-08-01

Current: 2025-11-05 (ví dụ) ➜ 2025-11-01

🧮 Bước 3: Tính toán Chênh lệch (Calculate the Difference)

Đây là "bộ não" 🧠 của thuật toán.

Sử dụng differenceInMonths(Target, Current).

Hàm này sẽ trả về một số nguyên (âm, dương, hoặc 0) cho biết chính xác số lần bạn cần click.

Ví dụ:

differenceInMonths('2025-08-01', '2025-11-01') ➜ trả về -3

(Nghĩa là: "Mục tiêu của bạn là -3 tháng so với hiện tại").

👆 Bước 4: Thực thi Điều hướng (Execute Navigation)

Bây giờ, bạn "thực thi" con số bạn vừa tính toán:

Đọc kết quả (monthsDiff).

Nếu monthsDiff < 0 (bằng -3):

Gán button = page.locator('#prev-button')

Gán clicks = Math.abs(-3) (là 3)

Nếu monthsDiff > 0 (ví dụ: là 2):

Gán button = page.locator('#next-button')

Gán clicks = 2

Dùng một vòng lặp for để click button đúng clicks lần.

Kết quả: Sau bước này, bạn đảm bảo 100% rằng UI của Date Picker đang hiển thị đúng tháng/năm bạn cần (ví dụ: Tháng 8, 2025).

✅ Bước 5: Chọn và Kiểm tra (Select and Verify)

Sau khi đã ở đúng "trang lịch", giờ bạn mới thực hiện hành động cuối cùng:

Chọn (Select): Tìm ngày chính xác. Đây là lúc bạn dùng một locator thật cụ thể, không thể nhầm lẫn (ví dụ này dùng data-date vì nó là duy nhất, nhưng getByRole('button', { name: '29' }) cũng tốt).

const dayCell = panel.locator(`[data-date='${ymd}']`);

await dayCell.click();

Kiểm tra (Verify): Kiểm tra xem input hoặc text hiển thị kết quả có đúng là giá trị bạn đã chọn hay không.


Phần 3: 📊 Xử lý Table (Bảng) hiệu quả

Mục tiêu của chúng ta khi làm việc với table (bảng) là làm sao tìm đúng "cột nào" và "hàng nào" một cách ổn định nhất.

 

1. Chiến lược 1: Lấy tất cả dữ liệu

Đây là chiến lược "thu thập" (collecting), lặp qua từng hàng và lấy dữ liệu bằng index (thứ tự).

const panel = page.getByRole('tabpanel', { name: '📊 Table' });

// 1. Đợi table render
const table = panel.locator('table').first();
await table.waitFor({ state: 'visible' });

// 2. Lấy tất cả rows (không bao gồm header)
const rows = table.locator('tbody tr');
const rowCount = await rows.count();
console.log('Total rows:', rowCount);

// 3. Lặp qua từng row và lấy dữ liệu
const tableData = [];
for (let i = 0; i < rowCount; i++) {
  const row = rows.nth(i);
  const cells = row.locator('td');
    
  // ⚠️ Vấn đề nằm ở đây ⚠️
  const data = {
    project: await cells.nth(0).textContent(),
    client: await cells.nth(1).textContent(),
    startDate: await cells.nth(2).textContent(),
    // ...
  };
    
  tableData.push(data);
}

Phân tích chiến lược này:

  • Ưu điểm:

    • Rất dễ hiểu và dễ viết.

    • Hoạt động tốt nếu mục tiêu của bạn là xác thực (validate) toàn bộ dữ liệu trong bảng (ví dụ: so sánh với một file JSON).

  • Nhược điểm:

    1. Rất Mong manh (Flaky) fragile:

      • Vấn đề lớn nhất là cells.nth(0), cells.nth(1),... Bạn đang "hard-code" (cố định) index của cột.

      • Nếu ngày mai, team UI/UX thay đổi thứ tự cột (ví dụ: dời cột "Client" lên đầu), thì cells.nth(0) sẽ trả về "Client" chứ không phải "Project".

      • Test của bạn hỏng, mặc dù chức năng của trang web vẫn hoàn toàn đúng.

    2. Rất Chậm (Slow) 🐢:

      • Nếu bảng có 500 hàng và 7 cột (như ví dụ), vòng lặp for của bạn sẽ thực hiện 500 lần (để lấy row) + (500 * 7) lần (để lấy textContent của 7 ô) = 3501 lệnh await.

      • Mỗi await là một "lệnh khứ hồi" (round trip) đi từ Playwright (Node.js) đến trình duyệt. Việc này cực kỳ tốn thời gian.

 

2. Chiến lược 2: "Lọc Hàng, Ánh xạ Cột" (Cách Tốt Nhất) 🎯

Đây là thuật toán 2 bước để giải quyết cả hai vấn đề trên. Nó giúp bạn tìm đúng "cột nào" và "hàng nào" một cách ổn định và nhanh chóng.

Bước 1: Giải quyết "Cột nào?" 🗺️ (Thuật toán Ánh xạ Cột - Header Map)

  • Vấn đề: Làm sao biết cột "Priority" là nth(5) hay nth(6) mà không hard-code?

  • Giải pháp: Đừng hard-code. Hãy "đọc" các tiêu đề (header <th>) một lần duy nhất lúc bắt đầu, và tạo ra một "bản đồ" (Map) về vị trí của chúng.

/**
 * Thuật toán tạo "Bản đồ Cột"
 * Nó đọc <th> và trả về: Map { 'Project' => 0, 'Client' => 1, 'Priority' => 5 }
 */
async function createHeaderMap(tableLocator) {
  const headerMap = new Map<string, number>();
  
  // 1. Lấy tất cả tiêu đề
  const headers = tableLocator.locator('thead th');
  const count = await headers.count();

  // 2. Lặp qua tiêu đề (chỉ C cột, rất nhanh)
  for (let i = 0; i < count; i++) {
    const headerText = await headers.nth(i).textContent();
    if (headerText) {
      // 3. Tạo Map
      headerMap.set(headerText.trim(), i);
    }
  }
  return headerMap;
}

Bước 2: Giải quyết "Hàng nào?" (Áp dụng bản đồ)

Bây giờ bạn đã có headerMap, bạn có thể dùng nó trong 2 kịch bản:

Kịch bản A: Đối chiếu 1 Hàng (Nhanh nhất) 🏎️

Đây là kịch bản test hiệu quả nhất. Thay vì lặp 500 hàng, bạn hãy lọc (filter) để tìm 1 hàng duy nhất bạn quan tâm.

  • Mục tiêu test: "Xác nhận rằng hàng có 'Project' là 'Apollo UI' thì 'Priority' phải là 'High'."

// 1. Lấy bản đồ (chỉ chạy 1 lần)
const table = panel.locator('table').first();
const headerMap = await createHeaderMap(table);

// 2. Lọc "hàng nào": Dùng locator:has-text để tìm 1 hàng
// (Playwright làm việc này CỰC KỲ nhanh)
const row = table.locator('tbody tr', { hasText: 'Apollo UI' });

// 3. Lấy "cột nào": Dùng bản đồ
const priorityIndex = headerMap.get('Priority'); // Ví dụ: trả về 5

// 4. Lấy ô cụ thể và kiểm tra
// (Chúng ta chỉ chạy 2-3 lệnh await thay vì 3501)
const priorityCell = row.locator('td').nth(priorityIndex);
await expect(priorityCell).toHaveText('High');

Kịch bản B: Thu thập Dữ liệu (Ổn định) 🛡️

// (Giả sử đã có headerMap)
const headerMap = await createHeaderMap(table);

// Lấy index cột bạn cần
const projectIndex = headerMap.get('Project'); // 0
const clientIndex = headerMap.get('Client');   // 1
const priorityIndex = headerMap.get('Priority'); // 5

const tableData = [];
for (let i = 0; i < rowCount; i++) {
  const row = rows.nth(i);
  const cells = row.locator('td');
    
  // KHÔNG DÙNG nth(0), nth(1)
  // DÙNG index từ bản đồ:
  const data = {
    project: await cells.nth(projectIndex).textContent(),
    client: await cells.nth(clientIndex).textContent(),
    priority: await cells.nth(priorityIndex).textContent(),
    // ...
  };
  tableData.push(data);
}

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