NỘI DUNG BÀI HỌC

  • Nền tảng & Triết lý:

  • Làm chủ Locator Hướng tới Người dùng

  • Nắm vững Chiến lược Dự phòng:

  • Sử dụng Locator Kỹ thuật (CSS & XPath)

  • Khai phá Sức mạnh Quan hệ của Playwright



🎓 Phần 1: Locator là gì và Tại sao nó là Trái tim của Kiểm thử Tự động?

Hãy tưởng tượng bạn đang chỉ dẫn một robot 🤖 cách pha một tách cà phê. Bạn không thể chỉ nói "pha cà phê đi". Bạn phải đưa ra những chỉ dẫn cực kỳ cụ thể:

"Tìm cái hộp màu đỏ có nhãn 'Cà phê'."

"Lấy một muỗng từ hộp đó."

"Tìm cái máy có nút bấm màu xanh lá."

"Nhấn vào nút bấm đó."

Những câu chỉ dẫn "hộp màu đỏ", "nút bấm màu xanh lá" đó, trong thế giới tự động hóa, chính là Locators. Chúng là ngôn ngữ mà chúng ta dùng để giao tiếp và ra lệnh cho robot của mình trên một trang web.

📍 Locator là gì? - Tọa độ trên Bản đồ Website

Nếu coi trang web là một tấm bản đồ 🗺️, thì mỗi phần tử (nút bấm, ô nhập liệu, hình ảnh) là một địa điểm trên đó.

Định nghĩa: Locator là một bộ chỉ dẫn mà kịch bản test tự động sử dụng để tìm và tương tác với một hoặc nhiều phần tử (elements) trên một trang web.

Hãy luôn ghi nhớ công thức vàng này, nó đúng với MỌI bước test tự động:

MỌI BƯỚC TEST = TÌM KIẾM (Find) + HÀNH ĐỘNG (Act)

(Tìm) nút "Đăng nhập" ➡️ (Làm) click vào nó.

(Tìm) ô "Tên đăng nhập" ➡️ (Làm) điền chữ vào đó.

(Tìm) thông báo lỗi ➡️ (Làm) kiểm tra nội dung của nó.

Nếu bước TÌM KIẾM thất bại, toàn bộ chuỗi hành động sẽ sụp đổ. Vì vậy, khả năng tìm kiếm phần tử một cách chính xác và đáng tin cậy chính là trái tim của kiểm thử tự động.

🧗 Tại sao việc Tìm kiếm lại QUAN TRỌNG và KHÓ KHĂN?

Đây là những thách thức lớn nhất mà mọi kỹ sư tự động hóa đều phải đối mặt:

  • 🏗️ Website Luôn Thay Đổi: Giao diện web không bao giờ đứng yên. Lập trình viên có thể thay đổi màu sắc, cấu trúc, tên class bất cứ lúc nào. Một bộ chỉ dẫn hoạt động hôm nay có thể hoàn toàn vô dụng vào ngày mai. Điều này tạo ra các bài test "dễ gãy" (brittle).
  • 👯 Các Phần tử Giống hệt nhau: Một trang sản phẩm có thể có 10 nút "Mua ngay" trông y hệt nhau. Làm thế nào để bạn chỉ cho robot click đúng vào nút của "Sản phẩm A"? Bạn cần một bộ chỉ dẫn thật sự chính xác.
  • ⏳ Các Phần tử Động & Bất đồng bộ: Nhiều phần tử chỉ xuất hiện sau khi bạn thực hiện một hành động, hoặc sau khi dữ liệu được tải về từ server. Kịch bản của bạn phải đủ "kiên nhẫn" để chờ đợi những phần tử này xuất hiện.
  • ❓ Không có Định danh Rõ ràng: Đôi khi, một nút bấm quan trọng chỉ là một thẻ <div> không có id hay class đặc biệt. Tìm ra nó giống như tìm một người vô danh trong đám đông.


📜 Các "Ngôn ngữ" Chỉ dẫn Phổ biến (Các loại Selector)

Để tạo ra một locator, chúng ta sử dụng các "ngôn ngữ" truy vấn gọi là Selectors.

🆔 ID: id="username-input". Giống như số Căn cước Công dân, nó là duy nhất. Rất tốt, nhưng không phải lúc nào cũng có sẵn.

🎨 CSS Selectors: Ngôn ngữ rất linh hoạt, cho phép bạn tìm phần tử dựa trên "đặc điểm nhận dạng" của chúng (tên thẻ, class, thuộc tính...). Giống như bạn mô tả: "Tìm người mặc áo xanh, đội mũ đỏ".

Ví dụ: input.form-control[name='username']

🗺️ XPath: Ngôn ngữ mạnh mẽ nhất nhưng cũng phức tạp và "dễ gãy" nhất. Nó cho phép bạn "chỉ đường từng bước" trong cấu trúc HTML.

Ví dụ: //div[@class='form-group'][1]/input

Giống như bạn chỉ đường: "Từ cây cổ thụ, đi về phía bắc 50 bước, rẽ trái ở ngôi nhà thứ hai...". Chỉ cần một chi tiết thay đổi, toàn bộ chỉ dẫn sẽ sai.

💬 Text Content (Nội dung văn bản): Một cách tiếp cận tự nhiên hơn. "Tìm cho tôi nút bấm có chữ 'Đăng nhập'".

✨ Playwright Thay đổi Cuộc chơi như thế nào?

Hiểu được những thách thức trên, Playwright đã mang đến hai cải tiến đột phá:

Đối tượng Locator Thông minh: Locator trong Playwright không còn là một chuỗi văn bản tĩnh. Nó là một đối tượng chứa quy tắc tìm kiếm.

Auto-Waiting Tích hợp: Chính vì là một đối tượng, Locator của Playwright được tích hợp sẵn "phép thuật" tự động chờ. Nó tự giải quyết Thách thức số 3 (Các phần tử động) mà bạn không cần phải bận tâm.

 

♿ Phần 2: Web Accessibility (Khả năng Tiếp cận) - Nền tảng của getByRole

🏢 Web Accessibility là gì? - Một ví dụ đời thực

Hãy tạm quên code đi. Tưởng tượng bạn xây một tòa nhà văn phòng.

  • 🚫 Thiết kế không có Accessibility: Tòa nhà chỉ có cầu thang bộ 🚶‍♂️. Nó hoạt động tốt cho hầu hết mọi người. Nhưng một người phải ngồi xe lăn, một người chống nạng, hay một nhân viên giao hàng đẩy xe hàng nặng sẽ không thể đi vào.

  • ✅ Thiết kế có Accessibility: Tòa nhà có thêm đường dốc cho xe lăn ♿, cửa tự động 🚪, thang máy rộng rãi, và các nút bấm có chữ nổi Braille. Giờ đây, tất cả mọi người 🧑‍🤝‍🧑 đều có thể ra vào và sử dụng tòa nhà một cách dễ dàng và bình đẳng.

Web Accessibility (thường được viết tắt là a11y) cũng chính là nguyên tắc đó, nhưng được áp dụng cho thế giới kỹ thuật số 💻.

📖 Định nghĩa: Web Accessibility là việc thiết kế và xây dựng các trang web, ứng dụng, và công cụ sao cho mọi người, bao gồm cả những người khuyết tật, đều có thể sử dụng được chúng.

 

❤️ Accessibility Giúp đỡ những Ai?

Nó không chỉ dành cho một nhóm nhỏ. Accessibility hỗ trợ một phổ rộng người dùng, bao gồm những người:

👀 Khiếm thị (Visually Impaired):

  • Họ sử dụng web như thế nào? Họ dùng các phần mềm chuyên dụng gọi là trình đọc màn hình (screen readers) 🔊. Phần mềm này sẽ đọc to nội dung trên trang web.

  • Accessibility giúp ích ra sao? Trình đọc màn hình không "nhìn" thấy trang web. Nó đọc mã nguồn HTML 👨‍💻. Nếu code của bạn được viết đúng chuẩn ngữ nghĩa, trình đọc màn hình có thể thông báo: "Tiêu đề cấp 1: Chào mừng bạn. Liên kết: Xem sản phẩm. Nút bấm: Đăng nhập."

♿ Gặp khó khăn về Vận động (Mobility Impairments):

  • Họ sử dụng web như thế nào? Họ có thể không dùng chuột được 🖱️. Thay vào đó, họ chỉ dùng bàn phím ⌨️ (phím Tab, Enter, Space), các thiết bị điều khiển đặc biệt, hoặc ra lệnh bằng giọng nói.

  • Accessibility giúp ích ra sao? Một thẻ <button> chuẩn sẽ tự động nhận được focus khi nhấn phím Tab và có thể được kích hoạt bằng Enter hoặc Space. Một div được tạo kiểu để trông giống nút bấm sẽ không có hành vi này, trừ khi lập trình viên bổ sung nó một cách thủ công.

👂 Khiếm thính (Hearing Impairments):

  • Họ sử dụng web như thế nào? Họ không thể nghe được nội dung âm thanh 🔇.

  • Accessibility giúp ích ra sao? Cung cấp phụ đề chi tiết cho video 📹, hoặc một bản ghi nội dung (transcript) cho các file podcast.

🧠 Có vấn đề về Nhận thức hoặc Học tập:

  • Họ sử dụng web như thế nào? Họ có thể bị rối trí bởi các giao diện phức tạp 😵‍💫, các hiệu ứng nhấp nháy, hoặc luồng điều hướng không nhất quán.

  • Accessibility giúp ích ra sao? Thiết kế giao diện rõ ràng, đơn giản, nhất quán, và sử dụng ngôn ngữ dễ hiểu.

 

💬 "Role" - Ngôn ngữ Giao tiếp của Accessibility

Bây giờ đến phần quan trọng nhất: "Role" dựa trên Accessibility như thế nào?

Trình duyệt làm hai việc cùng lúc:

  1. 🎨 Nó đọc HTML/CSS và vẽ nên một trang web đẹp đẽ cho người dùng (gọi là DOM Tree 🌳).

  2. 🗣️ Nó cũng đọc các thẻ HTML và thuộc tính role để xây dựng một cấu trúc "vô hình" thứ hai gọi là Accessibility Tree (Cây Hỗ trợ tiếp cận ♿🌳).

Accessibility Tree này loại bỏ tất cả các chi tiết về giao diện (màu sắc, vị trí) và chỉ giữ lại ý nghĩa ngữ nghĩa của trang. Chính cái cây này là thứ mà các công nghệ hỗ trợ (như trình đọc màn hình) sẽ sử dụng để giao tiếp với người dùng.

Ví dụ:

  • HTML của bạn: 👨‍💻 <button disabled>Gửi</button>

  • Accessibility Tree ♿🌳 sẽ có một nút (node) tương ứng, chứa thông tin:

    • Role: button (vai trò của nó là một nút bấm)

    • Name: "Gửi" (tên của nó là "Gửi")

    • State: disabled (trạng thái của nó là đang bị vô hiệu hóa)

  • Trình đọc màn hình sẽ đọc 🔊: "Gửi, nút bấm, bị làm mờ" (Submit, button, dimmed).

🔗 Đây chính là mối liên kết!

"Role" chính là thuộc tính cơ bản nhất định nghĩa nên một phần tử trong Accessibility Tree. Nó là ngôn ngữ chung mà trang web của bạn dùng để "nói" cho các công nghệ hỗ trợ biết về chức năng của từng thành phần.

Khi bạn sử dụng page.getByRole('button') trong Playwright, bạn không chỉ đang tìm một thẻ HTML. Bạn đang yêu cầu Playwright: "Hãy sử dụng Accessibility Tree, tìm cho tôi tất cả các phần tử có vai trò là 'button'."

🛡️ Tại sao Tester cần quan tâm đến Accessibility?

  • ✅ Viết Test Bền Vững Hơn (Lợi ích kỹ thuật): Đây là lý do chúng ta đã thảo luận. Bằng cách dựa vào role (chức năng), bài test của bạn không còn phụ thuộc vào các chi tiết dễ thay đổi như class, id, hay cấu trúc div. Test của bạn sẽ mô phỏng đúng ý định của người dùng và ít bị lỗi hơn.

  • 📈 Mở rộng Độ bao phủ Test: Khi bạn viết test với getByRole, bạn đang gián tiếp kiểm tra một phần của tiêu chuẩn accessibility. Nếu page.getByRole('button', { name: 'Đăng nhập' }) hoạt động, điều đó có nghĩa là người dùng trình đọc màn hình cũng có thể tìm thấy nút đó.

  • ⚖️ Yêu cầu Pháp lý & Kinh doanh: Ở nhiều quốc gia, các trang web (đặc biệt là của chính phủ và các công ty lớn) có yêu cầu pháp lý về việc phải tuân thủ các tiêu chuẩn accessibility. Xây dựng một trang web dễ tiếp cận cũng giúp bạn tiếp cận được một lượng lớn khách hàng tiềm năng 🤝.

  • 🎖️ Trở thành một Kỹ sư có Trách nhiệm: Với tư cách là người kiểm thử - người gác cổng cho chất lượng sản phẩm - việc đảm bảo sản phẩm có thể được sử dụng bởi càng nhiều người càng tốt là một phần trách nhiệm nghề nghiệp của chúng ta.

 

💡 Kết luận:

Accessibility không phải là một tính năng "thêm cho có", mà là một phần không thể thiếu của việc xây dựng một trang web hiện đại và nhân văn. Role là ngôn ngữ kỹ thuật nền tảng để hiện thực hóa điều đó.

Khi bạn sử dụng page.getByRole(), bạn không chỉ đang viết một câu lệnh tự động hóa. Bạn đang viết một bài test mô phỏng trải nghiệm của một người dùng dựa vào cấu trúc ngữ nghĩa, và đó là lý do tại sao nó là phương pháp mạnh mẽ và được khuyến khích nhất.


Phần 3:  "Vai Trò Ngầm Định" (Implicit Role) và HTML Ngữ nghĩa

Để tiện cho việc tra cứu, chúng ta sẽ bắt đầu ngay với bảng tổng hợp đầy đủ nhất về các vai trò ngầm định của những thẻ HTML phổ biến, sau đó sẽ đi vào phân tích chi tiết "tại sao" và "như thế nào".

📊 Bảng Tra Cứu Toàn Diện về Vai Trò Ngầm Định (Implicit Roles)

Đây là "cuốn từ điển" của bạn, giúp dịch từ Thẻ HTML sang "Vai trò" mà Playwright có thể hiểu.

Nhóm 1: Các Phần tử Tương tác (Interactive Elements)

Thẻ HTML (Tag)

Vai trò Ngầm định (Implicit Role)

Ghi chú

<a> (có href)

link

Thẻ <a> không có href sẽ không có vai trò này.

<button>

button

 

<input type="button">

button

 

<input type="submit">

button

 

<input type="reset">

button

 

<input type="image">

button

Nút bấm dạng hình ảnh.

<input type="text">

textbox

Gồm cả email, password, url, tel, number.

<input type="search">

searchbox

Một loại textbox chuyên dụng cho tìm kiếm.

<textarea>

textbox

 

<input type="checkbox">

checkbox

 

<input type="radio">

radio

 

<select>

combobox

 

<option>

option

 

<details> & <summary>

group & button

Thẻ <summary> hoạt động như một nút bấm.


Nhóm 2: Cấu trúc & Vùng (Structural & Landmark Regions)

Thẻ HTML (Tag)

Vai trò Ngầm định (Implicit Role)

Ghi chú

<main>

main

Khu vực chứa nội dung chính của trang.

<nav>

navigation

Khu vực chứa các liên kết điều hướng.

<header> (ở cấp body)

banner

Header của trang.

<footer> (ở cấp body)

contentinfo

Footer của trang (thông tin bản quyền, liên hệ...).

<aside>

complementary

Nội dung phụ, bên lề (sidebar).

<article>

article

Một khối nội dung độc lập (bài báo, bài post).

<section>

region

Một khu vực chung trên trang (nếu có tên).

<form>

form

 

<dialog>

dialog

Hộp thoại.


Nhóm 3: Nhóm & Danh sách (Grouping & Lists)

Thẻ HTML (Tag)

Vai trò Ngầm định (Implicit Role)

Ghi chú

<ul>, <ol>

list

Một danh sách.

<li>

listitem

Một mục trong danh sách.

<table>

table

 

<thead>, <tbody>, <tfoot>

rowgroup

Nhóm các hàng.

<tr>

row

Một hàng.

<td>

cell

Một ô dữ liệu.

<th>

columnheader/rowheader

Tiêu đề của cột/hàng.

<hr>

separator

Đường kẻ phân cách.


Nhóm 4: Nội dung & Văn bản (Content & Text)

Thẻ HTML (Tag)

Vai trò Ngầm định (Implicit Role)

Ghi chú

<h1> - <h6>

heading

Dùng level để chỉ định cấp độ, ví dụ { level: 1 } cho <h1>.

<img> (có alt text)

img

Nếu alt="", vai trò là presentation (chỉ để trang trí).

<figure>

figure

 

<blockquote>

blockquote

 

<p>

paragraph

 

"Vai trò Ngầm định" là gì? - Phân tích chi tiết

Bây giờ chúng ta sẽ phân tích ý nghĩa của bảng trên.

Vai trò ngầm định là chức năng mặc định, có sẵn mà trình duyệt tự động gán cho một thẻ HTML. Nó là "chức năng bẩm sinh" của thẻ đó, được quy định trong tiêu chuẩn HTML5.

Việc bạn sử dụng đúng thẻ cho đúng mục đích (<nav> cho thanh điều hướng, <main> cho nội dung chính, <button> cho nút bấm) được gọi là viết HTML Ngữ nghĩa (Semantic HTML).

Tại sao điều này quan trọng?

Khi bạn viết code ngữ nghĩa, bạn đang cung cấp một cấu trúc rõ ràng cho trang web. Cấu trúc này không chỉ giúp trình duyệt hiển thị trang, mà còn được sử dụng bởi:

♿ Công nghệ Hỗ trợ: Trình đọc màn hình có thể "đọc" cấu trúc này để mô tả trang web cho người khiếm thị một cách có ý nghĩa (ví dụ: "Khối điều hướng chính", "Tiêu đề cấp 1", "Nút bấm Gửi").

🤖 Công cụ Tìm kiếm (SEO): Google sử dụng cấu trúc này để hiểu nội dung và xếp hạng trang web của bạn.

🧪 Kiểm thử Tự động (Playwright): Đây chính là nền tảng cho getByRole. Playwright tận dụng chính sự hiểu biết có sẵn này của trình duyệt để tìm kiếm phần tử.


Kết nối với getByRole trong Playwright

Bảng tra cứu ở trên chính là chìa khóa để bạn làm chủ getByRole.

Quy tắc rất đơn giản:

Khi bạn viết page.getByRole('button'), Playwright sẽ tìm tất cả các phần tử có vai trò ngầm định là button (như <button>, <input type="submit">...) VÀ cả những phần tử có vai trò tường minh được gán là role="button".

Dựa vào bảng trên, bạn có thể viết các kịch bản test cực kỳ mạnh mẽ và dễ đọc

Bây giờ, hãy kết nối tất cả lý thuyết lại với nhau qua một ví dụ thực tế duy nhất. Chúng ta sẽ phân tích một trang web nhỏ và xem getByRole sử dụng vai trò ngầm định của từng thẻ HTML như thế nào.

Đây là một trang sản phẩm đơn giản được viết bằng HTML ngữ nghĩa.

<!DOCTYPE html>

<html lang="vi">

<head>

    <title>Trang Sản Phẩm</title>

</head>

<body>

    <header> <img src="/logo.png" alt="Logo cửa hàng"> </header>
 
    <nav> <ul> <li><a href="/">Trang chủ</a></li> <li><a href="/products">Sản phẩm</a></li>

        </ul>

    </nav>

    <main> <h1 id="product-title">Sản phẩm A</h1> <img src="/product-a.png" alt="Ảnh sản phẩm A">
      
        <form> <label for="quantity">Số lượng:</label>

            <input type="number" id="quantity" value="1"> <button type="submit">Thêm vào giỏ hàng</button> </form>

    </main>
    <footer> <p>© 2025 Cửa hàng ABC</p>

    </footer>

</body>

</html>

Viết Kịch Bản Test Playwright

Bây giờ, chúng ta sẽ viết một kịch bản test để tương tác với trang này, sử dụng getByRole để tìm các phần tử dựa trên vai trò ngầm định của chúng.

import { test, expect } from '@playwright/test';

test('should correctly find elements using their implicit roles', async ({ page }) => {

  const htmlContent = `...`; // Dán toàn bộ code HTML ở trên vào đây

  await page.setContent(htmlContent);

  // === KIỂM TRA CÁC VÙNG CẤU TRÚC (Landmark Roles) ===

  console.log('Tìm các vùng cấu trúc chính...');

  // 1. Tìm thanh điều hướng dựa trên thẻ <nav>

  // Vai trò ngầm định của <nav> là 'navigation'

  const mainNav = page.getByRole('navigation');

  await expect(mainNav).toBeVisible();

  // 2. Tìm khu vực nội dung chính dựa trên thẻ <main>

  // Vai trò ngầm định của <main> là 'main'

  const mainContent = page.getByRole('main');

  await expect(mainContent).toBeVisible();

  // === TƯƠNG TÁC VỚI CÁC PHẦN TỬ BÊN TRONG ===

  console.log('Tương tác với các phần tử cụ thể...');

  // 3. Tìm tiêu đề chính H1

  // Vai trò ngầm định của <h1> là 'heading' với level 1

  const pageTitle = mainContent.getByRole('heading', { name: 'Sản phẩm A', level: 1 });

  await expect(pageTitle).toBeVisible()

  // 4. Tìm hình ảnh sản phẩm dựa trên text 'alt'

  // Vai trò ngầm định của <img> là 'img'

  const productImage = mainContent.getByRole('img', { name: 'Ảnh sản phẩm A' });

  await expect(productImage).toBeVisible();

  // 5. Tìm nút "Thêm vào giỏ hàng" và click

  // Vai trò ngầm định của <button type="submit"> là 'button'

  const addToCartButton = page.getByRole('button', { name: 'Thêm vào giỏ hàng' });

  await addToCartButton.click(); // Giả sử click này sẽ gửi form
  console.log('Tất cả các phần tử đã được tìm thấy và tương tác thành công!');

});

Phân tích kịch bản:

Mỗi locator chúng ta viết (getByRole('navigation'), getByRole('main'), getByRole('heading', { level: 1 }), getByRole('button')) đều ánh xạ trực tiếp tới một vai trò ngầm định từ một thẻ HTML ngữ nghĩa (<nav>, <main>, <h1>, <button>).

Kịch bản test trở nên cực kỳ dễ đọc. Nó không chứa bất kỳ id hay class nào. Nó đọc lên giống như cách một người dùng mô tả trang web.

Nếu một lập trình viên quyết định bọc thẻ <main> trong một thẻ <div> mới, locator page.getByRole('main') vẫn sẽ hoạt động vì chức năng ngữ nghĩa của thẻ <main> không thay đổi

"Vai trò ngầm định" không phải là một khái niệm trừu tượng. Nó là chức năng được quy định sẵn của các thẻ HTML mà chúng ta sử dụng hàng ngày.

Bằng cách hiểu rõ và tra cứu bảng trên, bạn có thể dễ dàng xác định được vai trò của hầu hết các phần tử trên một trang web được code tốt. Điều này cho phép bạn viết các bài test với getByRole một cách tự tin, tạo ra các kịch bản tự động hóa không chỉ hoạt động hiệu quả mà còn bền vững và dễ bảo trì trong dài hạn.

 

🎓 Phần 4: "Vai Trò Tường Minh" (Explicit Role) và Sức mạnh của ARIA

Ở phần trước, chúng ta đã khám phá "Vai trò Ngầm định" đến từ các thẻ HTML ngữ nghĩa như <button>, <nav>, <h1>... Đây là nền tảng và là cách làm tốt nhất.

Nhưng, hãy tưởng tượng bạn cần xây dựng một component cực kỳ phức tạp như:

Một thanh trượt chọn khoảng giá (price range slider).

Một giao diện lịch để chọn ngày (date picker).

Một menu dạng cây (tree view).

HTML không có các thẻ <slider>, <datepicker>, hay <treeview>. Các lập trình viên thường phải xây dựng chúng từ những "viên gạch" cơ bản nhất: các thẻ <div> và <span>.

Vấn đề: Một div trơn không có ý nghĩa ngữ nghĩa. Làm sao để trình duyệt, trình đọc màn hình, và Playwright hiểu được rằng "cụm div này đang hoạt động như một thanh trượt"?

Đây chính là lúc ARIA xuất hiện.

ARIA là gì? - Bộ "Dán nhãn" Ngữ nghĩa cho Web

ARIA là viết tắt của Accessible Rich Internet Applications.

Mục đích: Là một bộ tiêu chuẩn của W3C, cung cấp các thuộc tính HTML để bổ sung "siêu dữ liệu" về ngữ nghĩa cho các phần tử, giúp chúng trở nên dễ tiếp cận hơn.

Hãy coi ARIA như một bộ "nhãn dán" chuyên nghiệp. Nếu HTML ngữ nghĩa giống như những món đồ có sẵn chức năng (cái ghế, cái bàn), thì ARIA cho phép bạn lấy một đống gỗ (<div>) và dán nhãn lên chúng: "Cái này là ghế", "cái này là bàn".

Công cụ chính để làm việc này là thuộc tính role.

Vai trò Tường minh (Explicit Role) chính là vai trò được gán một cách rõ ràng, trực tiếp cho một phần tử thông qua thuộc tính role="...".

Ví dụ:

<div class="custom-button" role="button" tabindex="0">

    Đăng ký ngay

</div>

role="button": Đây là một lời khai báo tường minh. Nó nói với trình duyệt và các công cụ khác: "Hãy bỏ qua việc tôi là một thẻ div. Về mặt chức năng, tôi là một nút bấm."

Playwright, khi nhận được lệnh page.getByRole('button'), sẽ tìm thấy cả thẻ <button> và cả div này.



So sánh Tường minh và Ngầm định - Cuộc đối đầu về Quyền Ưu tiên

Đây là phần quan trọng nhất. Điều gì sẽ xảy ra khi một phần tử vừa có vai trò ngầm định (từ thẻ HTML), vừa có vai trò tường minh (từ thuộc tính role)?

Quy tắc Vàng:

Vai trò Tường minh (Explicit) từ ARIA luôn "thắng" và sẽ GHI ĐÈ Vai trò Ngầm định (Implicit) của thẻ HTML.

Hãy xem một ví dụ thực tế để chứng minh điều này.

Chúng ta sẽ tạo ra hai "nút bấm".

Một là nút bấm chuẩn dùng thẻ <button>.

Một là một thẻ tiêu đề <h2> nhưng được "ép" phải đóng vai một nút bấm bằng ARIA.

<!DOCTYPE html>

<html lang="vi">

<head><title>Role Priority Demo</title></head>

<body>

    <button id="btn-normal">Nút Bấm Chuẩn</button>

   

    <h2 id="btn-heading" role="button" tabindex="0">Tiêu đề hoạt động như Nút bấm</h2>

</body>

</html>

Kịch Bản Test Playwright để Chứng minh:

Kịch bản này sẽ chứng minh cách Playwright "nhìn" thấy các phần tử này.

import { test, expect } from '@play-wright/test';
test.describe('Explicit vs. Implicit Role Priority', () => {
  const htmlContent = `...`; // Dán code HTML ở trên vào đây
  test('should demonstrate that explicit role overrides implicit role', async ({ page }) => {

    await page.setContent(htmlContent);
    // --- KIỂM TRA CÁC NÚT BẤM ---
    // 1. Tìm nút bấm chuẩn. Vai trò ngầm định của <button> là 'button'.

    const normalButton = page.getByRole('button', { name: 'Nút Bấm Chuẩn' });

    await expect(normalButton).toBeVisible(); // SẼ THÀNH CÔNG (PASS)

    console.log('✅ Đã tìm thấy <button> bằng getByRole("button").');
    // 2. Tìm "nút bấm" được tạo từ thẻ <h2>.

    // Vì có `role="button"`, vai trò tường minh này sẽ ghi đè vai trò ngầm định 'heading'.

    const headingAsButton = page.getByRole('button', { name: 'Tiêu đề hoạt động như Nút bấm' });

    await expect(headingAsButton).toBeVisible(); // SẼ THÀNH CÔNG (PASS)

    console.log('✅ Đã tìm thấy <h2> với role="button" bằng getByRole("button").');

    // --- KIỂM TRA SỰ GHI ĐÈ --

    // 3. Bây giờ, hãy thử tìm lại phần tử <h2> đó bằng vai trò ngầm định 'heading' của nó.

    const headingLocator = page.getByRole('heading', { name: 'Tiêu đề hoạt động như Nút bấm' });

    // Lệnh này SẼ THẤT BẠI, vì vai trò 'heading' đã bị 'button' ghi đè.

    // Trình duyệt không còn coi nó là một 'heading' nữa.

    await expect(headingLocator).toBeHidden(); // Khẳng định rằng nó bị ẩn/không tìm thấy

    console.log('✅ Đã xác nhận không thể tìm thấy <h2> đó bằng getByRole("heading").');

  });

});

Phân tích kết quả:

Playwright tìm thấy cả hai phần tử khi dùng getByRole('button'), chứng tỏ nó nhận diện được cả vai trò ngầm định và tường minh.

Playwright không tìm thấy thẻ <h2> khi dùng getByRole('heading'), bởi vì vai trò heading ngầm định của nó đã bị vai trò button tường minh ghi đè.


Khi nào nên sử dụng Vai trò Tường minh?

Mặc dù ARIA rất mạnh mẽ, hãy luôn nhớ quy tắc này:

Quy tắc số 1 của ARIA là: Nếu có thể sử dụng một phần tử HTML ngữ nghĩa có sẵn (<button>, <nav>, <table>...), hãy luôn dùng nó. Đừng cố gắng "phát minh lại bánh xe".

Bạn chỉ nên sử dụng vai trò tường minh của ARIA khi:

Xây dựng các Component Tùy chỉnh: Khi bạn cần tạo các widget phức tạp không có thẻ HTML tương đương (tabs, sliders, tree views, custom combobox...).

Làm việc với Code Cũ: Khi bạn phải làm việc với một hệ thống cũ được xây dựng chủ yếu bằng div và span, việc thêm các thuộc tính role là một cách để cải thiện accessibility mà không phải viết lại toàn bộ HTML.

Cập nhật Trạng thái Động: ARIA không chỉ có role, mà còn có các thuộc tính trạng thái như aria-expanded, aria-selected... rất hữu ích để mô tả các component động.

Lời kết:

HTML ngữ nghĩa cung cấp cho chúng ta Vai trò Ngầm định - đây là nền tảng vững chắc. ARIA cung cấp cho chúng ta Vai trò Tường minh - đây là bộ công cụ nâng cao để xử lý các trường hợp phức tạp.

Đối với một kỹ sư kiểm thử, việc quan trọng nhất là phải biết rằng vai trò tường minh luôn được ưu tiên. Công cụ tốt nhất của bạn để xác định vai trò cuối cùng của một phần tử chính là tab Accessibility trong DevTools. Locator getByRole của bạn phải khớp với vai trò cuối cùng đó.


Phần 5: Kế Hoạch B - Chiến Lược Tìm Kiếm Phần Tử Khi getByRole Thất Bại

 

Chúng ta đã học về getByRole - tiêu chuẩn vàng để viết locator bền vững. Nhưng trong thế giới thực, bạn sẽ thường xuyên gặp những trang web "khó tính", nơi các vai trò không được định nghĩa rõ ràng.

Vậy khi getByRole thất bại, chúng ta có nên nhảy ngay xuống sử dụng CSS Selector hay XPath không? Câu trả lời là KHÔNG.

Playwright cung cấp một bộ công cụ locator hướng tới người dùng khác cực kỳ mạnh mẽ. Hôm nay, chúng ta sẽ học cách sử dụng chúng theo một thứ tự ưu tiên rõ ràng, để đảm bảo bạn luôn chọn được locator tốt nhất có thể trong mọi tình huống.

Thứ tự Ưu tiên khi "Kế hoạch A" Thất bại

Khi không tìm được locator tốt bằng getByRole, hãy đi theo quy trình sau:

Kế hoạch B: page.getByLabel()

Triết lý: "Tìm một ô nhập liệu thông qua nhãn dán (label) của nó."

Khi nào sử dụng? Đây là lựa chọn ưu tiên hàng đầu để tìm các phần tử trong form như <input>, <textarea>, <select>. Nó vẫn dựa trên một tiêu chuẩn accessibility rất cơ bản và quan trọng.

Ví dụ:

<label for="user-email">Địa chỉ email:</label>

<input type="email" id="user-email">

Playwright Test:

// Cực kỳ dễ đọc và bền vững

await page.getByLabel('Địa chỉ email:').fill('test@example.com');

👍 Ưu điểm:

Rất bền vững: Miễn là văn bản của label không đổi, test của bạn sẽ không bị lỗi dù lập trình viên thay đổi id, class, hay cấu trúc của input.

Rất dễ đọc: Mô tả chính xác hành động của người dùng.

👎 Nhược điểm:

Chỉ áp dụng cho các phần tử form có thẻ <label> được liên kết đúng cách (qua thuộc tính for).

Kế hoạch C: page.getByPlaceholder()

Triết lý: "Tìm một ô nhập liệu qua dòng chữ gợi ý bên trong nó."

Khi nào sử dụng? Khi một ô input không có label rõ ràng nhưng có thuộc tính placeholder.

Ví dụ:

HTML:

<input type="text" placeholder="Tìm kiếm sản phẩm...">

Playwright Test:

await page.getByPlaceholder('Tìm kiếm sản phẩm...').fill('iPhone 15');

👍 Ưu điểm:

Vẫn là một locator hướng tới người dùng, dễ hiểu.

👎 Nhược điểm:

Kém bền vững hơn getByLabel vì placeholder có thể thay đổi.

Placeholder sẽ biến mất khi người dùng gõ chữ, làm cho việc debug thủ công đôi khi khó hơn.

Về mặt accessibility, placeholder không thể thay thế cho một label thực sự.

Kế hoạch D: page.getByText()

Triết lý: "Tìm bất cứ thứ gì dựa trên nội dung văn bản mà mắt người nhìn thấy."

Khi nào sử dụng? Cực kỳ linh hoạt. Dùng cho các phần tử không phải form như <div>, <span>, <p>, hoặc các nút bấm/liên kết mà getByRole không tìm thấy. Rất tốt để xác minh các thông báo, kết quả trên màn hình.

Ví dụ:

HTML:

<div class="alert-error">Tên đăng nhập hoặc mật khẩu không đúng.</div>

<a class="legacy-link">Xem chi tiết</a>

Playwright Test:

const errorMessage = page.getByText('Tên đăng nhập hoặc mật khẩu không đúng.');

await expect(errorMessage).toBeVisible();

await page.getByText('Xem chi tiết').click();

👍 Ưu điểm:

Cực kỳ trực quan và dễ viết. Code của bạn mô tả chính xác những gì bạn muốn thấy trên màn hình.

👎 Nhược điểm:

Dễ gãy nếu text thay đổi: Đây là nhược điểm lớn nhất. Nếu đội ngũ marketing đổi câu chữ, hoặc trang được dịch sang ngôn ngữ khác, test sẽ lỗi.

Có thể bị trùng lặp nếu cùng một đoạn text xuất hiện ở nhiều nơi. (Có thể dùng { exact: true } để tăng độ chính xác).

Kế hoạch E & F: page.getByAltText() và page.getByTitle()

Đây là các trường hợp sử dụng cụ thể hơn.

getByAltText('...'): Dùng để tìm thẻ <img> dựa vào thuộc tính alt (văn bản thay thế). Đây là cách đúng đắn nhất để tìm hình ảnh.

Ưu điểm: Bền vững, tuân thủ chuẩn accessibility.

Nhược điểm: Phụ thuộc vào việc dev có viết alt text hay không.

getByTitle('...'): Dùng để tìm một phần tử dựa vào thuộc tính title của nó (thường hiển thị dưới dạng tooltip khi di chuột qua).

Ưu điểm: Hữu ích cho các nút bấm chỉ có icon mà không có text.

Nhược điểm: Thuộc tính title không phải lúc nào cũng được sử dụng nhất quán.

Sơ đồ ra quyết định - Luồng suy nghĩ của Tester chuyên nghiệp

Khi cần tìm một phần tử, hãy đi theo luồng sau:

Thử getByRole() đầu tiên. Nó có hoạt động không?

Có: Tuyệt vời! Dừng lại ở đây.

Không? Nó có phải là một phần tử trong form không?

Có: Thử getByLabel(). Nếu không có, thử getByPlaceholder().

Không phải phần tử form? Nó có văn bản duy nhất để nhận diện không?

Có: Dùng getByText().

Nó là hình ảnh?

Có: Dùng getByAltText().

Nó có tooltip không?

Có: Dùng getByTitle().

Nếu TẤT CẢ các cách trên đều thất bại?

Lúc này, bạn mới nên cân nhắc đến getByTestId() (yêu cầu dev thêm) hoặc phương án cuối cùng là CSS SelectorXPath.

Kết luận:

Một kỹ sư kiểm thử tự động giỏi sẽ có một quy trình lựa chọn ưu tiên rõ ràng. Họ sẽ khai thác triệt để các locator hướng tới người dùng trước, bởi vì chúng giúp tạo ra các bài test dễ đọc hơn, bền vững hơn, và gần với trải nghiệm người dùng thực tế hơn. Chỉ khi nào những công cụ tốt nhất này không còn phù hợp, họ mới dùng đến những công cụ "nặng ký" và dễ gãy hơn như CSS và XPath.

 

Phần 6: CSS Selector

So sánh tốc độ giữa CSS và XPath.

Làm thế nào để xử lý các thuộc tính động (dynamic attributes) bằng các công cụ khác nhau.

Hãy cùng phân tích chi tiết.

Lời đồn cũ: "CSS Selector luôn nhanh hơn XPath."

Lời đồn này xuất phát từ thời kỳ đầu của các công cụ tự động hóa và các trình duyệt cũ. Khi đó, các trình duyệt có cơ chế xử lý CSS được tối ưu hóa ở cấp độ native (vì nó là cốt lõi của việc render trang), trong khi việc xử lý XPath thường cần một engine riêng, dẫn đến nó chậm hơn một chút.

Sự thật ngày nay: Với các trình duyệt hiện đại (Chrome, Firefox, Edge...) và các công cụ như Playwright, sự khác biệt về tốc độ giữa một selector CSS và một selector XPath tương đương là không đáng kể đến mức có thể bỏ qua.

Các engine trình duyệt đã được tối ưu hóa cực độ.

Thời gian thực thi một locator chỉ chiếm một phần siêu nhỏ trong toàn bộ thời gian chạy test.

"Nút thắt cổ chai" thực sự trong một bài test End-to-End đến từ:

Độ trễ mạng (Network latency).

Thời gian server phản hồi (Server response time).

Thời gian ứng dụng render giao diện (Client-side rendering).

Thời gian chờ của Playwright (Auto-waiting, timeouts).

Kết luận: Đừng bao giờ chọn locator dựa trên "tốc độ". Hãy luôn chọn locator dựa trên độ bền vững (resilience) và tính dễ đọc (readability).

🎓  Làm Chủ CSS Selector trong Playwright

Chúng ta đã học về một loạt các locator "Web-First" như getByRole, getByLabel, getByText... Đó là những lựa chọn ưu tiên hàng đầu. Nhưng trong thực tế, sẽ có lúc bạn đối mặt với những trang web cũ, phức tạp, hoặc đơn giản là không được code theo chuẩn, khiến tất cả các locator đó đều thất bại.

Đây là lúc chúng ta mở ra một hộp công cụ khác, cực kỳ mạnh mẽ: CSS Selectors.

Một phép so sánh:

Nếu getByRole giống như tìm một người qua chức danh của họ ("Tìm cho tôi 'Giám đốc Marketing'"), thì CSS Selector giống như bạn mô tả quần áo và vị trí của họ: "Tìm người mặc áo sơ mi xanh, đứng ở góc phòng, cạnh cửa sổ". Nó rất chính xác tại thời điểm đó, nhưng nếu ngày mai người đó mặc áo khác hoặc đứng ở chỗ khác, chỉ dẫn của bạn sẽ thất bại.


CSS Selector là gì?

CSS Selectors là một ngôn ngữ quy tắc được tạo ra ban đầu để các nhà phát triển web có thể "chọn" các phần tử HTML và áp dụng các style (màu sắc, kích thước, vị trí) cho chúng bằng CSS.

Các công cụ kiểm thử tự động như Playwright đã "mượn" ngôn ngữ mạnh mẽ này để phục vụ cho mục đích của riêng mình: tìm kiếm phần tử.

Trong Playwright, bạn sẽ sử dụng CSS Selector chủ yếu thông qua lệnh page.locator():

const myElement = page.locator('your-css-selector');

Cú pháp CSS Selector từ Cơ bản đến Nâng cao

Dưới đây là các cú pháp bạn sẽ sử dụng thường xuyên nhất, được sắp xếp theo mức độ ưu tiên và độ bền vững.

Loại

Cú pháp

Ví dụ

Mức độ Ưu tiên

ID

#id

#user-profile-button

Cao nhất (Rất tốt)

Thuộc tính (Attribute)

[attr="value"]

[data-testid="login-btn"] [name="email"]

Cao (Tốt)

Class

.class

.btn-primary .error-message

Trung bình

Tên Thẻ (Tag)

tagname

h1, button, table

Thấp

Descendant (Con cháu)

A B

nav a (tìm thẻ a bất kỳ bên trong nav)

Thấp (Dễ gãy)

Child (Con trực tiếp)

A > B

ul > li (tìm li là con trực tiếp của ul)

Rất thấp (Rất dễ gãy)

Pseudo-class

:nth-child(n) :first-child :not(.class)

li:nth-child(2) (mục thứ 2) button:not(.disabled)

Rất thấp (Rất dễ gãy)

Lưu ý quan trọng về Class: Hãy cực kỳ cẩn thận với các tên class được sinh ra tự động bởi các framework CSS-in-JS (ví dụ: button-a1B2c_root). Chúng có thể thay đổi sau mỗi lần build code. Hãy ưu tiên các class có ý nghĩa rõ ràng.

 ✍️ Chiến Lược Viết CSS Selector Bền Vững

Biết cú pháp là một chuyện, viết được một selector tốt là một chuyện khác. Hãy luôn tuân thủ các quy tắc sau:

Quy tắc 1: Ưu tiên Định danh Ổn định

Thứ tự ưu tiên khi chọn thuộc tính để viết selector:

ID > data-testid > các thuộc tính chức năng (name, type, placeholder) > class ngữ nghĩa > Tên thẻ.

Quy tắc 2: Giữ cho Selector Ngắn và Độc lập

Selector càng dài, càng phụ thuộc vào cấu trúc HTML, thì càng có nhiều "điểm có thể gãy".

❌ Xấu: div#app > div.main-content > section:nth-child(2) > div.user-card > button

Chỉ cần dev thêm một thẻ div bao ngoài, selector này sẽ thất bại.

✅ Tốt hơn: div.user-card > button

✅ Tốt nhất (nếu có): #user-card-delete-button hoặc [data-testid="user-card-delete-button"]

Quy tắc 3: Tìm "Mỏ neo" và Bám vào đó

Nếu phần tử bạn muốn tìm không có gì đặc biệt, hãy nhìn ra xung quanh nó. Có "hàng xóm" hay "cha mẹ" nào của nó có một định danh ổn định không? Hãy dùng nó làm "mỏ neo".

Tình huống: Bạn cần tìm nút "Sửa" trong một dòng của bảng, nhưng tất cả các nút "Sửa" đều giống hệt nhau. Tuy nhiên, mỗi dòng (<tr>) lại có một data-id duy nhất.

<tr data-product-id="123">

    <td>iPhone 15</td>

    <td><button class="edit-btn">Sửa</button></td>

</tr>

❌ Xấu: Cố gắng tìm nút "Sửa" thứ N. Rất dễ gãy.

✅ Tốt: Dùng data-product-id làm mỏ neo.

 

// Tìm nút 'Sửa' BÊN TRONG dòng có data-product-id='123'

const editButtonForIphone = page.locator("tr[data-product-id='123'] .edit-btn");

await editButtonForIphone.click();


Ví dụ Thực tế

Hãy xem một đoạn HTML "khó tính" không có role, label hay text duy nhất.

Mã HTML Mẫu:

<div class="user-list">

    <div class="user-row" data-username="alice">

        <span>Alice</span>

        <button class="btn delete-btn">Xóa</button>

    </div>

    <div class="user-row" data-username="bob">

        <span>Bob</span>

        <button class="btn delete-btn">Xóa</button>

    </div>

</div>


Nhiệm vụ: Viết kịch bản test để xóa người dùng "bob".

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

getByRole('button', { name: 'Xóa' }) sẽ tìm thấy 2 nút, không dùng được.

getByText('Bob') chỉ tìm thấy thẻ <span>, không phải nút bấm.

Chiến lược "Mỏ neo" là phù hợp nhất:

Tìm "mỏ neo": Dòng của "bob" có một thuộc tính rất ổn định là data-username="bob".

Từ mỏ neo đó, tìm nút bấm "Xóa" nằm bên trong nó.

Kịch bản Test:

import { test, expect } from '@playwright/test';

test('should delete the correct user using a resilient CSS selector', async ({ page }) => {

  const html = `...`; // Dán code HTML ở trên

  await page.setContent(html);

  // Xây dựng locator theo chiến lược "Mỏ neo"

  const deleteBobButton = page.locator("div[data-username='bob'] button.delete-btn");
  // Khẳng định rằng locator của chúng ta chỉ tìm thấy 1 phần tử duy nhất

  await expect(deleteBobButton).toHaveCount(1);

  // Thực hiện hành động

  await deleteBobButton.click();

  // (Trong ứng dụng thực, ta sẽ xác minh rằng dòng của 'bob' đã biến mất)

  const bobRow = page.locator("div[data-username='bob']");

  await expect(bobRow).toBeHidden();

});

Kết luận:

CSS Selector là một công cụ không thể thiếu trong kho vũ khí của bạn. Mặc dù nó không phải là lựa chọn ưu tiên hàng đầu, nhưng khi buộc phải sử dụng, hãy luôn tư duy một cách chiến lược: Ưu tiên các định danh ổn định, giữ cho selector ngắn gọn, và tận dụng các "mỏ neo" để tăng độ bền vững.

Làm chủ được nghệ thuật viết CSS Selector tốt sẽ giúp bạn chinh phục được cả những trang web khó tự động hóa nhất.

 

🎓 Hướng Dẫn Cú pháp: Cách Dùng page.locator() với CSS Selector trong Playwright

Nguyên tắc cơ bản:

Bất kỳ chuỗi CSS Selector hợp lệ nào bạn có thể dùng trong file .css hoặc trong DevTools, bạn đều có thể đặt nó vào bên trong page.locator('...').

Hãy cùng đi qua từng loại cú pháp một.

Theo ID (#)

Đây là cách chọn một phần tử duy nhất, bền vững nhất nếu có.

Cú pháp CSS: #my-id

Cú pháp Playwright:

const loginButton = page.locator('#login-button');

Ví dụ HTML:

<button id="login-button">Đăng nhập</button>

Theo Tên Class (.)

Dùng để tìm các phần tử có chung một lớp CSS.

Cú pháp CSS: .my-class

Cú pháp Playwright:

TypeScript

const errorMessages = page.locator('.error-message');

Ví dụ HTML:

<div class="error-message">Tên đăng nhập không đúng.</div>

<div class="error-message">Mật khẩu không được để trống.</div>

Lưu ý: locator('.error-message') sẽ tìm thấy cả hai div trên.

Theo Tên Thẻ (Tag Name)

Tìm tất cả các phần tử có cùng một loại thẻ.

Cú pháp CSS: h1

Cú pháp Playwright:

const mainHeading = page.locator('h1');

Ví dụ HTML:

HTML

<h1>Chào mừng đến với trang web</h1>

Theo Thuộc tính ([...])

Cực kỳ mạnh mẽ và hữu ích, đặc biệt là với các thuộc tính chức năng.

Cú pháp CSS: [attribute="value"]

Cú pháp Playwright:

// Tìm input có thuộc tính name là 'email'

const emailInput = page.locator("[name='email']");

// Tìm phần tử có thuộc tính data-testid

const saveButton = page.locator("[data-testid='save-button']");

Ví dụ HTML:

<input type="email" name="email" placeholder="Nhập email">

<button data-testid="save-button">Lưu</button>

Kết hợp các Selector

Đây là lúc sức mạnh của CSS được thể hiện.

Kết hợp trên cùng một phần tử (Không có khoảng trắng):

Cú pháp CSS: tag[attribute].class

Cú pháp Playwright:

// Tìm thẻ 'button' CÓ class 'btn-primary' VÀ thuộc tính 'type' là 'submit'

const submitButton = page.locator("button.btn-primary[type='submit']");

Ví dụ HTML:

HTML

<button class="btn-primary" type="submit">Gửi</button>

Kết hợp Cha - Con (Có khoảng trắng - Descendant):

Cú pháp CSS: parent child

Cú pháp Playwright:

// Tìm tất cả các thẻ 'a' NẰM BÊN TRONG thẻ 'nav'

const navLinks = page.locator('nav a');

Ví dụ HTML:

HTML

<nav>

    <ul>

        <li><a href="/home">Trang chủ</a></li>

    </ul>

</nav>

 

Xử lý Thuộc tính Động (Substring Matching)

Khi thuộc tính của phần tử thay đổi, bạn có thể khớp một phần của nó.

Cú pháp CSS & Playwright:

// Bắt đầu với (^): Tìm phần tử có id bắt đầu bằng 'user-'

const userRow = page.locator("[id^='user-']");

// Ví dụ: khớp với <div id="user-123-abc">


// Chứa (*): Tìm phần tử có id chứa '-profile-'

const profileSection = page.locator("[id*='-profile-']");

// Ví dụ: khớp với <div id="main-profile-widget">


// Kết thúc với ($): Tìm phần tử có id kết thúc bằng '-btn'

const actionButton = page.locator("[id$='-btn']");

// Ví dụ: khớp với <button id="user-delete-btn">

 

Nối chuỗi Locators (Chaining) - Cách làm của Playwright

Thay vì viết một chuỗi CSS dài và phức tạp, cách làm tốt hơn trong Playwright là nối chuỗi các locators.

Bối cảnh: Tìm nút "Xóa" cho người dùng "bob".

<div class="user-row" data-username="bob">

    <span>Bob</span>

    <button class="delete-btn">Xóa</button>

</div>

Cách dùng 1 chuỗi CSS dài (Tốt, nhưng có thể tốt hơn):

const deleteBobButton_CSS = page.locator("div[data-username='bob'] button.delete-btn");

 

Cách Nối chuỗi (Tốt hơn và dễ đọc hơn):

// Bước 1: Tìm "mỏ neo" là dòng của 'bob'

const bobRow = page.locator("div[data-username='bob']");

// Bước 2: Từ dòng đó, tìm nút bấm bên trong

const deleteBobButton_Chained = bobRow.locator('button.delete-btn');

Cách Nối chuỗi Tốt nhất (Kết hợp với getByRole):

const bobRow = page.locator("div[data-username='bob']");

 

// Tận dụng locator hướng tới người dùng bất cứ khi nào có thể

const deleteBobButton_Best = bobRow.getByRole('button', { name: 'Xóa' });

Tại sao Nối chuỗi lại tốt hơn?

Dễ đọc: Mỗi bước là một hành động logic riêng biệt.

Tái sử dụng: Bạn có thể dùng lại biến bobRow để tìm các phần tử khác trong cùng hàng đó (ví dụ: bobRow.locator('span') để lấy tên).

Dễ gỡ lỗi: Nếu test thất bại, bạn sẽ biết chính xác bước nào trong chuỗi bị lỗi.

Kết luận:

page.locator() là cổng vào linh hoạt của bạn để sử dụng CSS selector. Hãy bắt đầu với các cú pháp cơ bản (ID, thuộc tính, class), học cách kết hợp chúng, và đừng quên tận dụng các tính năng nâng cao như nối chuỗi và các pseudo-class tùy chỉnh của Playwright để viết ra những locator vừa mạnh mẽ, vừa dễ bảo trì.

 

Phần 7: "Vũ Khí Tối Thượng" - Làm Chủ XPath trong Playwright

Sau khi đã có CSS Selector làm "kế hoạch dự phòng", chúng ta sẽ đến với "vũ khí tối thượng" trong bộ công cụ tìm kiếm truyền thống. Đây là công cụ mạnh nhất, có thể tìm thấy bất cứ thứ gì, nhưng cũng là công cụ phức tạp và nguy hiểm nhất nếu không dùng cẩn thận: XPath.

Chúng ta đã đi qua một hành trình dài trong việc lựa chọn locator, từ những công cụ lý tưởng như getByRole cho đến "kế hoạch cuối cùng" là CSS Selector. Hôm nay, chúng ta sẽ khám phá lựa chọn cuối cùng trong kim tự tháp: XPath.

Một phép so sánh:

getByRole: Tìm người theo chức danh ("Giám đốc").

getByText: Tìm người theo tên ("Tìm anh Bob").

CSS Selector: Tìm người theo đặc điểm quần áo, vị trí ("Người mặc áo xanh đứng ở góc phòng").

XPath: Giống như bạn có một bản đồ chi tiết của cả tòa nhà và đưa ra chỉ dẫn GPS từng bước từ một vị trí bất kỳ: "Bắt đầu từ quầy lễ tân, đi lên tầng 3 bằng thang máy bên trái, rẽ phải, tìm căn phòng đối diện phòng họp, và người đó đang ngồi ở bàn làm việc thứ hai từ cửa sổ."

Nó cực kỳ chính xác và có thể điều hướng các mối quan hệ phức tạp, nhưng chỉ cần một thay đổi nhỏ (đổi vị trí phòng họp), toàn bộ chỉ dẫn sẽ sai.

XPath là gì?

XPath là viết tắt của XML Path Language (Ngôn ngữ Đường dẫn XML).

Nó được tạo ra để điều hướng trong các tài liệu XML. Vì HTML có cấu trúc rất giống XML, XPath hoạt động hoàn hảo để truy vấn các phần tử trên trang web.

Sức mạnh lớn nhất: Khác với CSS chỉ có thể đi "xuống" (tìm con cháu), XPath có thể di chuyển theo mọi hướng trên cây DOM: lên (cha), xuống (con), và ngang (anh em).

Trong Playwright, bạn sẽ sử dụng XPath với page.locator(). Playwright đủ thông minh để tự động nhận diện một chuỗi bắt đầu bằng // hoặc .. là XPath.

const myElement = page.locator('//div[@id="main-content"]/h1');

// Hoặc viết tường minh hơn:

// const myElement = page.locator('xpath=//div[@id="main-content"]/h1');

 

Cú pháp XPath - "Ngôn ngữ chỉ đường"

Cú pháp XPath rất phong phú, nhưng đây là những phần quan trọng nhất bạn cần nắm.

Các Ký tự Cơ bản:

//: (Dùng nhiều nhất) "Tìm ở bất kỳ đâu trong tài liệu". Ví dụ: //button (tìm tất cả các nút bấm).

/: Tìm từ gốc tài liệu, hoặc tìm con trực tiếp. HÃY TRÁNH viết locator bắt đầu bằng /html/body/... (sẽ giải thích ở dưới).

. : Đại diện cho "nút hiện tại" (current node).

.. : Di chuyển lên phần tử cha.

*: Đại diện cho bất kỳ thẻ nào. Ví dụ: //*[@id='main'] (tìm phần tử bất kỳ có id='main').

Chọn Phần tử theo Thuộc tính:

//tag[@attribute='value']

Ví dụ: //input[@type='submit']

Kết hợp nhiều thuộc tính: //input[@type='text' and @name='username']

Các Hàm Hữu ích (Rất quan trọng):

text(): Tìm phần tử có nội dung text khớp chính xác.

//button[text()='Đăng nhập']

contains(): Tìm phần tử có nội dung text hoặc giá trị thuộc tính chứa một chuỗi con.

//h2[contains(text(), 'Chào mừng')] (Tìm h2 chứa chữ "Chào mừng").

//div[contains(@class, 'user-profile')] (Tìm div có class chứa "user-profile").

starts-with(): Tương tự contains(), nhưng tìm phần tử có thuộc tính bắt đầu bằng một chuỗi. Rất tốt để xử lý ID động.

//button[starts-with(@id, 'submit-btn-')]


"Trục" di chuyển (Traversal Axes) - Sức mạnh Độc nhất của XPath:

Đây là thứ mà CSS không thể làm được.

parent:: (hoặc ..): Đi lên cha.

//span[text()='Alice']/parent::div -> Tìm div cha của span có text là 'Alice'.

following-sibling::: Tìm tất cả các phần tử anh em đứng sau phần tử hiện tại.

//h2[text()='Thông tin']/following-sibling::div -> Tìm div đứng ngay sau h2.

preceding-sibling::: Tìm tất cả các phần tử anh em đứng trước phần tử hiện tại.

 ✍️ Chiến Lược Sử dụng XPath Bền Vững

Sức mạnh đi kèm với rủi ro. Hãy dùng XPath một cách khôn ngoan.

Quy tắc 1: KHÔNG BAO GIỜ dùng Absolute XPath.

❌ Rất Xấu: /html/body/div[1]/div[2]/section/div/button[3]

Đây là một "quả bom hẹn giờ". Chỉ cần dev thêm một thẻ <div> vào trang, toàn bộ đường dẫn này sẽ sai và test của bạn sẽ thất bại.

Quy tắc 2: Càng ngắn gọn và ngữ nghĩa càng tốt.

❌ Xấu: //div/div/div/button[text()='Gửi']

✅ Tốt: //button[contains(@class, 'submit') and text()='Gửi']

Quy tắc 3: Tận dụng các "Trục" khi thực sự cần thiết.

Đừng dùng parent:: một cách tùy tiện. Hãy dùng nó khi bạn thực sự cần đi "ngược" từ một phần tử đáng tin cậy.

Ví dụ Thực tế - Khi nào XPath là "Vị cứu tinh"?

Kịch bản:

Bạn cần tick vào một ô checkbox, nhưng checkbox này không có định danh gì đặc biệt. Tuy nhiên, dòng text mô tả của nó (nằm trong thẻ <label>) lại ở phía sau nó.

Mã HTML:

<div class="form-row">

    <input type="checkbox" name="terms"> <label>Tôi đồng ý với các điều khoản dịch vụ</label>

</div>

Thách thức:

CSS không thể đi "ngược" từ label ra input đứng trước nó.

Giải pháp XPath:

Tìm label có text đáng tin cậy.

Từ label đó, sử dụng trục preceding-sibling để tìm thẻ input đứng ngay trước nó.

Kịch bản Test Playwright:

import { test, expect } from '@playwright/test';

test('should find checkbox using its following label text', async ({ page }) => {

  const html = `...`; // Dán code HTML ở trên

  await page.setContent(html);
  // Xây dựng locator XPath

  const termsCheckbox = page.locator(

    "//label[text()='Tôi đồng ý với các điều khoản dịch vụ']/preceding-sibling::input[@type='checkbox']"

  );

  // Thực hiện hành động

  await termsCheckbox.check();


  // Xác minh kết quả

  await expect(termsCheckbox).toBeChecked();

  console.log('XPath đã giải cứu chúng ta!');

});

Đây là một trong số ít các kịch bản mà XPath thể hiện sức mạnh độc nhất của nó.

Kết luận:

XPath là ngôn ngữ locator mạnh mẽ nhất về mặt kỹ thuật, đặc biệt là khả năng di chuyển tự do trên cây DOM.

Tuy nhiên, sức mạnh này phải trả giá bằng sự phức tạp, khó đọc và độ bền vững thấp.

Trong hệ thống ưu tiên của Playwright, XPath nằm ở dưới cùng.

Hãy coi XPath như một "dụng cụ phẫu thuật" chuyên dụng: cực kỳ mạnh mẽ, chính xác, nhưng chỉ nên được sử dụng bởi người có kinh nghiệm và chỉ khi các công cụ thông thường, hiện đại và an toàn hơn (getByRole, .filter, CSS...) không đủ hiệu quả.

🎓  Cú pháp: Cách Dùng page.locator() với XPath trong Playwright


Mặc dù là phương án cuối cùng, nhưng việc nắm vững XPath là cực kỳ cần thiết để xử lý những tình huống phức tạp nhất mà không có locator nào khác giải quyết được.


Cách Gọi XPath trong Playwright

Playwright cung cấp hàm page.locator() để sử dụng XPath. Có hai cách gọi:

Tường minh (Explicit): Sử dụng tiền tố xpath=.

page.locator("xpath=//button[@id='login-btn']");

Ngầm định (Implicit): Playwright đủ thông minh để tự động nhận diện một chuỗi bắt đầu bằng // hoặc .. là một biểu thức XPath. Đây là cách phổ biến và ngắn gọn hơn.

page.locator("//button[@id='login-btn']");

Lưu ý: Khi viết locator, bạn nên dùng dấu ngoặc kép " cho chuỗi trong locator() để có thể sử dụng dấu ngoặc đơn ' bên trong biểu thức XPath một cách tự do.

"Cheat Sheet" Cú pháp XPath trong Playwright

Dưới đây là các cú pháp XPath thông dụng nhất, được trình bày dưới dạng một "cheat sheet" để bạn tiện tra cứu.

Theo Tên Thẻ (Tag Name)

Cú pháp XPath: //tagname

Cú pháp Playwright: page.locator('//button')

Ví dụ HTML: <button>Click Me</button>

Ghi chú: Tìm tất cả các thẻ có tên tương ứng. Thường quá chung chung, nên kết hợp với các điều kiện khác.


Theo Thuộc tính (Attribute)

Cực kỳ hữu ích và được sử dụng thường xuyên.

Khớp chính xác:

Cú pháp XPath: //tag[@attribute='value']

Cú pháp Playwright: page.locator("//input[@type='submit']")

Ví dụ HTML: <input type="submit" value="Gửi">

Ghi chú: Rất tốt cho các thuộc tính tĩnh và duy nhất.


Thuộc tính chứa chuỗi con (contains):

Cú pháp XPath: //tag[contains(@attribute, 'value')]

Cú pháp Playwright: page.locator("//div[contains(@class, 'error-message')]")

Ví dụ HTML: <div class="main error-message hidden">...</div>

Ghi chú: "Vũ khí" chính để xử lý các class động hoặc các thuộc tính có giá trị phức tạp.


Thuộc tính bắt đầu bằng (starts-with):

Cú pháp XPath: //tag[starts-with(@attribute, 'value')]

Cú pháp Playwright: page.locator("//button[starts-with(@id, 'btn-submit-')]")

Ví dụ HTML: <button id="btn-submit-a1b2c3">Gửi</button>

Ghi chú: Cực kỳ hữu ích để xử lý các ID được sinh tự động.

Theo Nội dung Text (Text Content)
Khớp chính xác text():

Cú pháp XPath: //tag[text()='value']

Cú pháp Playwright: page.locator("//button[text()='Đăng nhập']")

Ví dụ HTML: <button>Đăng nhập</button>

Ghi chú: Yêu cầu text phải khớp 100%, bao gồm cả khoảng trắng.

Text chứa chuỗi con contains(text(), ...):

Cú pháp XPath: //tag[contains(text(), 'value')]

Cú pháp Playwright: page.locator("//h2[contains(text(), 'Chào mừng')]")

Ví dụ HTML: <h2>Chào mừng Gemini!</h2>

Ghi chú: Linh hoạt hơn so với text() và được sử dụng rất phổ biến.


Kết hợp Điều kiện (and, or)

Cú pháp XPath: //tag[@attr1='val1' and contains(@attr2, 'val2')]

Cú pháp Playwright: page.locator("//input[@type='checkbox' and @name='terms']")

Ví dụ HTML: <input type="checkbox" name="terms" checked>

Ghi chú: Giúp tăng độ chính xác và duy nhất cho locator của bạn.


Sử dụng "Trục" Di chuyển (Traversal Axes) - Sức mạnh độc nhất

Đây là những cú pháp thể hiện rõ nhất sức mạnh của XPath so với CSS.

Lên Cha (parent:: hoặc ..):

Cú pháp XPath: //child/parent::parent_tag hoặc //child/..

Cú pháp Playwright: page.locator("//span[text()='Tên sản phẩm']/parent::div")

Ví dụ HTML: <div> <p>...</p> <span>Tên sản phẩm</span> </div>

Ghi chú: Tìm div cha của span có text là 'Tên sản phẩm'.

Anh em Đứng sau (following-sibling::):

Cú pháp XPath: //tag1/following-sibling::tag2

Cú pháp Playwright: page.locator("//label[text()='Email']/following-sibling::input")

Ví dụ HTML: <label>Email</label> <input type="email">

Ghi chú: Tìm input đứng ngay sau label có text là 'Email'.


Tổ tiên (ancestor::):

Cú pháp XPath: //child/ancestor::ancestor_tag

Cú pháp Playwright: page.locator("//button[text()='Gửi']/ancestor::form")

Ví dụ HTML: <form> ... <div> <button>Gửi</button> </div> ... </form>

Ghi chú: Tìm thẻ form là "tổ tiên" của nút "Gửi". Mạnh hơn parent:: vì nó có thể nhảy qua nhiều cấp.



Nối chuỗi (Chaining) - Kết hợp Sức mạnh

Bạn không nhất thiết phải viết toàn bộ locator bằng XPath. Hãy kết hợp nó với các locator khác của Playwright để code dễ đọc hơn.

Kịch bản: Tìm ô checkbox đứng trước label "Tôi đồng ý".

Cách viết thuần XPath (Khó đọc):

 

page.locator("//label[text()='Tôi đồng ý']/preceding-sibling::input");

Cách Nối chuỗi (Dễ đọc hơn):

// Bước 1: Dùng locator hướng tới người dùng để tìm "mỏ neo". Rất dễ đọc.

const termsLabel = page.getByText('Tôi đồng ý');

// Bước 2: Từ mỏ neo đó, dùng XPath để đi ngược lại tìm anh em đứng trước.

// Lưu ý cần có tiền tố 'xpath=' khi dùng XPath trong .locator() nối chuỗi.

const termsCheckbox = termsLabel.locator('xpath=preceding-sibling::input');

await termsCheckbox.check();

 

🎓 Phần 8 Nâng cao: Vượt Qua Giới Hạn của CSS & XPath

Ở bài học trước, chúng ta đã khám phá XPath – "vũ khí tối thượng" trong bộ công cụ tìm kiếm truyền thống. Chúng ta đã thấy sức mạnh tuyệt đối của nó trong việc di chuyển tự do trên cây DOM theo mọi hướng: lên (parent), xuống (child), và ngang (sibling). Về mặt kỹ thuật, không có bài toán tìm kiếm nào mà XPath không thể giải quyết.

Vậy, nếu đã có một công cụ toàn năng như vậy, tại sao chúng ta còn cần học thêm? Liệu có cách nào tốt hơn không?

Câu trả lời nằm ở triết lý cốt lõi của Playwright: Sức mạnh không nên phải đánh đổi bằng sự phức tạp và độ bền vững thấp. Một bài test tốt không chỉ chạy được, mà còn phải dễ đọc, dễ hiểu và dễ bảo trì.

Hãy dùng một phép so sánh:

XPath giống như một con dao đa năng Thụy Sĩ có 100 chức năng. Nó có thể làm mọi thứ, từ vặn ốc vít đến cưa một cành cây. Nhưng để vặn 100 con ốc, việc sử dụng cái tuốc nơ vít nhỏ xíu của nó sẽ rất cồng kềnh và tốn sức.

Các công cụ của Playwright mà chúng ta sắp học giống như một bộ dụng cụ điện chuyên dụng: một cái máy khoan, một cái máy cưa, một máy vặn vít... Mỗi công cụ được thiết kế hoàn hảo cho một nhiệm vụ cụ thể, giúp bạn hoàn thành công việc nhanh hơn, an toàn hơn và hiệu quả hơn rất nhiều.

Hôm nay, chúng ta sẽ mở ra "hộp đồ nghề" hiện đại mà Playwright đã đặc biệt chế tạo cho tester, giúp giải quyết các bài toán tìm kiếm quan hệ một cách thanh lịch hơn XPath. 


Các Pseudo-class Tùy chỉnh (:has, :has-text):


Phân tích :has-text("text") - "Người Tìm kiếm Văn bản"

Triết lý: "Tìm một phần tử nếu nó, hoặc bất kỳ con cháu nào của nó, CHỨA một đoạn văn bản cụ thể."

Cách Hoạt Động & Đặc điểm:

Tìm kiếm chuỗi con (Substring): page.locator('p:has-text("Playwright")') sẽ tìm thấy <p>Playwright is amazing.</p>.

Không phân biệt chữ hoa/thường (Case-insensitive): Sẽ tìm thấy cả "Playwright", "playwright", và "PLAYWRIGHT".

Bỏ qua khoảng trắng thừa (Trims whitespace): Tìm kiếm "Playwright" sẽ khớp với Playwright.

Tìm kiếm sâu (Deep search): Nó sẽ quét qua tất cả các phần tử con cháu bên trong để tìm text.

Khi nào sử dụng?

Đây là công cụ bạn sẽ dùng thường xuyên nhất.

Dùng khi bạn cần tìm một container (như một div bao ngoài, một dòng <tr>, một mục <li>) dựa trên một đoạn văn bản duy nhất nằm đâu đó bên trong nó.

Ví dụ Chi tiết:

HTML: Một danh sách sản phẩm.

<div class="product-list">

    <div class="product-card">

        <h3>iPhone 15</h3>

        <span>Giá: 25.000.000đ</span>

    </div>

    <div class="product-card">

        <h3>Samsung S23</h3>

        <p>Sản phẩm bán chạy</p>

    </div>

</div>

Playwright Test:

// Tìm card sản phẩm có chứa text "Samsung"

const samsungCard = page.locator('.product-card:has-text("Samsung")');

await expect(samsungCard).toBeVisible();

// Bạn có thể nối chuỗi để tìm một phần tử cụ thể bên trong card đó

const samsungTagline = samsungCard.locator('p');

await expect(samsungTagline).toHaveText('Sản phẩm bán chạy');

<hr size=2 width="100%" noshade style='color:gray' align=center>



Phân tích :has(selector) - "Người Tìm kiếm Cấu trúc"

Triết lý: "Tìm một phần tử nếu nó CHỨA một phần tử con/cháu khác khớp với một selector bên trong."

Cách Hoạt Động & Đặc điểm:

Nó không quan tâm đến nội dung văn bản. Nó quan tâm đến sự tồn tại và cấu trúc của các thẻ HTML con.

Selector bên trong :has() có thể là bất kỳ CSS selector hợp lệ nào khác (#id, .class, [attribute], thậm chí là một :has-text() khác!).

Khi nào sử dụng?

Khi dấu hiệu nhận biết không phải là text, mà là một phần tử cụ thể.

Ví dụ: "Tìm tất cả các bài viết có chứa hình ảnh", "Tìm các dòng sản phẩm đang được giảm giá (có một badge 'sale')", "Tìm form có chứa một input với name='username'".

Ví dụ Chi tiết:

HTML: Một danh sách người dùng, trong đó admin có một "huy hiệu" đặc biệt.

<ul class="user-list">

    <li class="user-item">

        <span>Alice</span>

    </li>

    <li class="user-item">

        <span>Bob</span>

        <span class="admin-badge">Admin</span>

    </li>

</ul>

Playwright Test:

// Tìm mục danh sách (li) NÀO CÓ CHỨA một span với class 'admin-badge'

const adminUserItem = page.locator('li.user-item:has(span.admin-badge)');

// Khẳng định rằng nó cũng chứa text "Bob"

await expect(adminUserItem).toContainText('Bob');

<hr size=2 width="100%" noshade style='color:gray' align=center>

 

So sánh Trực tiếp và Sự kết hợp

Đây là phần quan trọng nhất để phân biệt chúng.

Tiêu chí

:has-text("text")

:has(selector)

Mục tiêu tìm kiếm

Nội dung văn bản (Content)

Cấu trúc phần tử (Structure)

Tham số đầu vào

Một chuỗi văn bản (string)

Một chuỗi CSS selector khác (string)

Câu hỏi nó trả lời

"Phần tử này có chứa text 'ABC' không?"

"Phần tử này có chứa một thẻ <img class="icon"> không?"

Ví dụ

li:has-text("Alice")

li:has(span.admin-badge)

Sức mạnh thực sự: Kết hợp cả hai!

Bạn có thể nối chuỗi các pseudo-class này lại với nhau để tạo ra các locator cực kỳ chính xác.

Kịch bản: "Tìm mục người dùng (li) có tên là 'Bob' VÀ người đó phải là Admin."

const adminBobItem = page.locator('li.user-item:has-text("Bob"):has(span.admin-badge)');

await expect(adminBobItem).toBeVisible();

Phân tích: Playwright sẽ tìm một li.user-item thỏa mãn CẢ HAI điều kiện:

Bên trong nó có chứa text "Bob".

Bên trong nó có chứa một span.admin-badge.

Công cụ Mạnh mẽ nhất: Phương thức .filter()

Đây là cách làm được khuyến nghị và linh hoạt nhất của Playwright để xử lý các mối quan hệ phức tạp.

Triết lý: Thay vì viết một selector dài, hãy làm theo chuỗi: Chọn một nhóm lớn ➡️ Lọc ra cá th➡️ Hành động trên cá thđó.

.filter({ hasText: '...' }): Tương đương với :has-text().

.filter({ has: locator }): Tương đương với :has().

Sức mạnh thực sự của .filter() là khả năng kết hợp và sự rõ ràng.

Ví dụ thực tế: Tìm phần tử "bên cạnh" trong một danh sách

Mã HTML:

<ul class="user-list">

  <li class="user-item">

    <span>Tên: Alice</span>

    <button>Chỉnh sửa</button>

  </li>

  <li class="user-item">

    <span>Tên: Bob</span>

    <button>Chỉnh sửa</button>

  </li>

</ul>

Nhiệm vụ: Click vào nút "Chỉnh sửa" của Bob.

Kịch bản Test Playwright:

test('should find a sibling element using .filter()', async ({ page }) => {

  const html = `...`; // Dán code HTML ở trên

  await page.setContent(html);

  // Bước 1: Chọn nhóm lớn (tất cả các mục trong danh sách)

  const listItems = page.getByRole('listitem');

  // Bước 2: Lọc ra đúng mục bạn muốn

  // "Từ các mục trong danh sách, hãy tìm mục nào CÓ CHỨA text 'Bob'"

  const bobItem = listItems.filter({ hasText: 'Bob' });

  // Bước 3: Từ mục đã được lọc, thực hiện hành động trên phần tử con (là "hàng xóm" của text 'Bob')

  // "Bên trong mục của Bob, hãy tìm nút bấm có tên 'Chỉnh sửa' và click vào nó"

  await bobItem.getByRole('button', { name: 'Chỉnh sửa' }).click();

  // Giờ đây bạn có thể xác minh kết quả, ví dụ như form chỉnh sửa của Bob đã hiện ra.

});

Tại sao cách này lại vượt trội?

Dễ đọc: Luồng listItems.filter(...).getByRole(...) đọc lên rất tự nhiên.

Bền vững: Nó không phụ thuộc vào việc nút bấm đứng trước hay sau text, hay chúng được bọc trong bao nhiêu thẻ div phụ. Miễn là chúng cùng nằm trong li.user-item, locator vẫn hoạt động.

Tái sử dụng: Bạn có thể dùng lại biến bobItem để thực hiện các khẳng định khác (expect(bobItem).toContainText('...')).


 Công cụ "Trực quan": Locator Bố cục (Layout Locators)

Đây là một tính năng độc đáo của Playwright, là phương án dự phòng khi cấu trúc DOM quá phức tạp và không có mối quan hệ cha-con rõ ràng.

Các công cụ: .rightOf(), .leftOf(), .above(), .below(), .near()

Triết lý: "Tìm cho tôi ô input nằm bên phải của dòng chữ 'Tên đăng nhập'."

Khi nào dùng: Chỉ dùng khi các phần tử được căn chỉnh một cách trực quan nhưng không có mối liên kết trong code (ví dụ: không có thẻ cha chung, không có label for).

Ví dụ:

Mã HTML (Layout dạng bảng, grid):

<div><span>Username</span></div>

<div><input type="text" /></div>

Kịch bản Test Playwright:

// Tìm phần tử input NẰM BÊN PHẢI của phần tử có text 'Username'

await page.locator('input').rightOf(page.getByText('Username')).fill('my_user');

Cảnh báo quan trọng: Locator bố cục rất dễ gãy với thiết kế responsive. Trên màn hình điện thoại, ô input có thể nhảy xuống nằm bên dưới (below) label, khiến locator .rightOf() thất bại. Do đó, hãy hạn chế sử dụng chúng và ưu tiên .filter() bất cứ khi nào có thể.

Sơ đồ Ra quyết định - Chọn Vũ khí Phù hợp

Cần tìm một phần tử cha hoặc anh em?

➡️ Mối quan hệ có thể được xác định bằng một đoạn text hoặc một phần tử con độc nhất không?

CÓ: Sử dụng .filter(). Đây là lựa chọn ưu tiên, mạnh mẽ và bền vững nhất.

KHÔNG: Đi tiếp bước 2.

Cấu trúc DOM quá phức tạp nhưng các phần tử có vị trí trực quan cố định (trái/phải/trên/dưới)?

CÓ: Cân nhắc sử dụng Layout Locators (.rightOf()...), nhưng hãy nhận thức rõ rủi ro với responsive design.

KHÔNG: Đi tiếp bước 3.

Các cách trên đều không hiệu quả?

Đây là lúc bạn có thể cân nhắc đến XPath với các trục parent:: hoặc following-sibling::, nhưng hãy nhớ rằng nó khó đọc và khó bảo trì hơ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