NỘI DUNG BÀI HỌC
✅ Làm chủ XPath: Hiểu và vận dụng tất cả các phương pháp xác định locator bằng XPath, từ cơ bản đến các kỹ thuật phức tạp sử dụng "trục" (axes).
✅ Thành thạo Codegen: Sử dụng công cụ Playwright Codegen để tự động tạo mã kịch bản, giúp tăng tốc độ viết test và tìm hiểu locators.
✅ Tư duy "Refactor": Hiểu rằng code do máy tạo ra là điểm khởi đầu, và kỹ năng cải tiến (refactor) nó thành code sạch, bền vững mới là mục tiêu cuối cùng.
✅ Giải quyết bài toán khó: Áp dụng XPath axes để giải quyết các bài toán định vị element phức tạp mà các locator thông thường không thể xử lý.
🧠 I. LÀM CHỦ XPATH (PHẦN NÂNG CAO)
Trong buổi trước, chúng ta đã biết đến XPath như một công cụ dự phòng. Hôm nay, chúng ta sẽ đi sâu vào lý do tại sao nó là công cụ mạnh mẽ và không thể thiếu trong các tình huống phức tạp.
1️⃣ Khái niệm XPath
XPath (XML Path Language) là một ngôn ngữ truy vấn, cho phép bạn viết một "đường dẫn" để điều hướng qua các phần tử (elements) trong một tài liệu HTML/XML.
-
Tưởng tượng trang web là một cây gia phả:
-
Thẻ
<html>
là cụ tổ. -
Thẻ
<body>
là ông. -
Thẻ
<div>
là cha. -
Thẻ
<p>
là con.
-
-
XPath chính là cách bạn chỉ đường trong cây gia phả đó, ví dụ: "Hãy tìm đến ông
body
, vào người condiv
thứ hai, rồi tìm đứa cháup
có chứa chữ 'Xin chào'".
2️⃣ Cú pháp XPath từ Cơ bản đến Nâng cao
a. Đường dẫn Tuyệt đối (Absolute Path) - ⛔ KHÔNG NÊN DÙNG
-
Khái niệm: Bắt đầu từ gốc của tài liệu (
/html
), chỉ đường từng bước một. -
Cú pháp: Bắt đầu bằng một dấu
/
. -
Ví dụ HTML:
<html><body><div><input id="user"></div></body></html>
-
Ví dụ Automation:
page.locator("/html/body/div/input")
-
Tại sao không nên dùng? Cực kỳ mong manh! Chỉ cần lập trình viên thêm một thẻ
<div>
ở giữa, đường dẫn này sẽ sai ngay lập tức.
b. Đường dẫn Tương đối (Relative Path) - ✅ LUÔN LUÔN DÙNG
-
Khái niệm: Tìm kiếm element ở bất kỳ đâu trong tài liệu mà không cần quan tâm đến đường dẫn từ gốc.
-
Cú pháp: Bắt đầu bằng hai dấu
//
. -
Ví dụ Automation:
page.locator("//input[@id='user']")
-
Lợi ích: Bền vững hơn rất nhiều vì nó không phụ thuộc vào cấu trúc cố định của trang.
c. Các cách xác định Locator với XPath
Loại | Khái niệm | Ví dụ HTML | Ví dụ Automation |
Theo Thẻ & Thuộc tính | Cách phổ biến nhất: tìm element theo tên thẻ và một thuộc tính bất kỳ (id , class , name ...). |
<input class="form-control" name="username"> |
page.locator("//input[@name='username']") |
Dùng hàm text() |
Tìm element dựa trên nội dung văn bản chính xác mà nó hiển thị. | <button>Đăng ký ngay</button> |
page.locator("//button[text()='Đăng ký ngay']") |
Dùng hàm contains() |
Tìm element một cách linh hoạt khi thuộc tính hoặc văn bản có chứa các giá trị động. | <div id="user-123-abc">...</div> |
page.locator("//div[contains(@id, 'user-')]") |
Kết hợp and / or |
Tạo ra locator siêu cụ thể bằng cách kết hợp nhiều điều kiện. | <button class="btn" name="submit">Login</button> |
page.locator("//button[@class='btn' and @name='submit']") |
3️⃣ XPath Axes (Trục trong XPath)
Đây là sức mạnh thực sự của XPath, cho phép bạn di chuyển trong cây DOM theo các mối quan hệ gia đình.
1. parent::
(Tìm Cha trực tiếp)
-
Tình huống: Bạn đã tìm thấy một ô
input
nhưng muốn kiểm tra hoặc tương tác với thẻ<div>
chứa nó, có thể để kiểm tra xem form group có class lỗi hay không. -
Ví dụ HTML:
<div class="form-group has-error"> <label>Username</label> <input type="text" id="username-field"> </div>
-
Giải thích & Phân tích XPath:
//input[@id='username-field']/parent::div
-
//input[@id='username-field']
: Bắt đầu bằng cách xác định vị trí của "đứa con" là ô input. -
/parent::div
: Từ vị trí đó, di chuyển lên một cấp (parent::
) để tìm cha trực tiếp của nó, với điều kiện người cha đó phải là một thẻdiv
.
-
-
Ví dụ Automation:
# Tìm thẻ div cha của ô input form_group = page.locator("//input[@id='username-field']/parent::div") # Kiểm tra xem thẻ div cha có class 'has-error' hay không expect(form_group).to_have_class("form-group has-error")
2. ancestor::
(Tìm Tổ tiên)
-
Tình huống: Bạn đã tìm thấy nút "Submit", và bạn muốn xác định xem nút này thuộc về form nào trên trang (ví dụ: form đăng nhập hay form đăng ký).
-
Ví dụ HTML:
<form id="login-form" action="/login"> <h2>Login</h2> <div class="form-group"> <input type="text" id="user"> </div> <div class="form-actions"> <button type="submit">Submit</button> </div> </form>
-
Giải thích & Phân tích XPath:
//button[text()='Submit']/ancestor::form
-
//button[text()='Submit']
: Bắt đầu bằng cách tìm đứa "chắt" là nút Submit. -
/ancestor::form
: Từ vị trí đó, di chuyển lên các cấp (ancestor::
) cho đến khi tìm thấy một tổ tiên là thẻ<form>
. Nó sẽ bỏ qua các thẻdiv
ở giữa.
-
-
Ví dụ Automation:
# Tìm thẻ form là tổ tiên của nút Submit login_form = page.locator("//button[text()='Submit']/ancestor::form") # Kiểm tra xem form đó có đúng là form đăng nhập không expect(login_form).to_have_attribute("id", "login-form")
3. child::
(Tìm Con trực tiếp)
-
Tình huống: Bạn đã tìm thấy một danh sách (
<ul>
) và bạn muốn lấy tất cả các mục (<li>
) là con trực tiếp của nó. -
Ví dụ HTML:
<ul id="main-menu"> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/contact">Contact</a></li> </ul>
-
Giải thích & Phân tích XPath:
//ul[@id='main-menu']/child::li
-
//ul[@id='main-menu']
: Tìm đến "người cha" là thẻ<ul>
. -
/child::li
: Từ vị trí đó, tìm tất cả các đứa con trực tiếp (child::
) là thẻ<li>
. -
Lưu ý:
child::
thường được viết tắt. XPath//ul[@id='main-menu']/li
có ý nghĩa tương đương và phổ biến hơn.
-
-
Ví dụ Automation:
# Lấy tất cả các mục con <li> của menu menu_items = page.locator("//ul[@id='main-menu']/child::li") # Kiểm tra xem có đúng 3 mục menu hay không expect(menu_items).to_have_count(3)
4. descendant::
(Tìm Con cháu)
-
Tình huống: Bạn đã tìm thấy một
div
chứa thông tin sản phẩm và muốn tìm một thẻspan
hiển thị chữ "Sale" có thể nằm rất sâu bên trong cấu trúc HTML. -
Ví dụ HTML:
<div class="product-card" data-product-id="123"> <img src="image.jpg"> <div class="product-info"> <h3>Laptop Pro</h3> <p class="price"> <span class="sale-badge">Sale</span> $999 </p> </div> </div>
-
Giải thích & Phân tích XPath:
//div[@data-product-id='123']/descendant::span
-
//div[@data-product-id='123']
: Tìm đến "ông tổ" là thẻdiv
của sản phẩm. -
/descendant::span
: Từ vị trí đó, tìm kiếm ở tất cả các cấp con cháu (descendant::
) để tìm một thẻ<span>
. -
Lưu ý:
descendant::
thường được viết tắt bằng//
. XPath//div[@data-product-id='123']//span
có ý nghĩa tương đương và phổ biến hơn.
-
-
Ví dụ Automation:
# Tìm thẻ span sale bên trong thẻ div của sản phẩm sale_badge = page.locator("//div[@data-product-id='123']/descendant::span[@class='sale-badge']") # Kiểm tra xem huy hiệu Sale có tồn tại không expect(sale_badge).to_be_visible()
5. following-sibling::
(Tìm Anh/Em đứng sau)
-
Tình huống: Trong một bảng, bạn đã tìm thấy ô chứa tên một người, và bạn muốn lấy thông tin ở ô "Email" nằm cùng hàng, ở cột bên phải.
-
Ví dụ HTML:
<tr> <td>John Doe</td> <td>johndoe@example.com</td> <td>Admin</td> </tr>
-
Giải thích & Phân tích XPath:
//td[text()='John Doe']/following-sibling::td
-
//td[text()='John Doe']
: Tìm đến ô<td>
chứa tên "John Doe". -
/following-sibling::td
: Từ vị trí đó, tìm tất cả các anh em (sibling
) đứng sau (following
) nó mà cũng là thẻ<td>
. XPath này sẽ trả về 2 element (ô email và ô vai trò).
-
-
Ví dụ Automation:
# Lấy ô email nằm sau ô tên "John Doe" # Dùng [1] để lấy người anh em đầu tiên tìm thấy email_cell = page.locator("//td[text()='John Doe']/following-sibling::td[1]") # Kiểm tra nội dung email expect(email_cell).to_have_text("johndoe@example.com")
6. preceding-sibling::
(Tìm Anh/Em đứng trước)
-
Tình huống: Ngược lại với ở trên. Bạn tìm thấy ô chứa email và muốn lấy tên của người đó ở cột bên trái.
-
Ví dụ HTML: (Giống như trên)
<tr> <td>John Doe</td> <td>johndoe@example.com</td> <td>Admin</td> </tr>
-
Giải thích & Phân tích XPath:
//td[text()='johndoe@example.com']/preceding-sibling::td
-
//td[text()='johndoe@example.com']
: Tìm đến ô<td>
chứa email. -
/preceding-sibling::td
: Từ vị trí đó, tìm tất cả các anh em (sibling
) đứng trước (preceding
) nó mà cũng là thẻ<td>
.
-
-
Ví dụ Automation:
# Lấy ô tên nằm trước ô email name_cell = page.locator("//td[text()='johndoe@example.com']/preceding-sibling::td") # Kiểm tra tên expect(name_cell).to_have_text("John Doe")
🌟 Các trường hợp đặc biệt: Khi nào KHÔNG cần dùng XPath Axes?
Sử dụng XPath Axes rất mạnh mẽ, nhưng đôi khi có những cách đơn giản và dễ đọc hơn. Luôn ưu tiên locator đơn giản nhất!
Trường hợp 1: Tìm input dựa vào label
-
Bài toán: Tìm ô input có label là "Password".
-
Cách nghĩ dùng Axes:
//label[text()='Password']/following-sibling::input
-
✅ Cách tốt hơn (dùng Playwright locator):
# Dễ đọc, bền vững và là best practice page.get_by_label("Password").fill("secret_sauce")
Trường hợp 2: Tìm một nút bên trong một form cụ thể
-
Bài toán: Tìm nút "Login" chỉ nằm trong form có id là
login-form
. -
Cách nghĩ dùng Axes:
//form[@id='login-form']/descendant::button
-
✅ Cách tốt hơn (dùng CSS Selector):
# Ngắn gọn và hiệu quả hơn page.locator("#login-form button").click()
Trường hợp 3: Tìm một ô cụ thể trong một hàng của bảng
-
Bài toán: Tìm email của "John Doe" trong bảng.
-
Cách nghĩ dùng Axes: Dùng
following-sibling
như ví dụ trên. -
✅ Cách khác (kết hợp locator): Đôi khi việc tìm cả hàng rồi tìm con sẽ dễ đọc hơn.
# Tìm hàng chứa "John Doe" trước row = page.locator("tr", has_text="John Doe") # Sau đó tìm ô email bên trong hàng đó email_cell = row.locator("td:nth-child(2)") # Tìm <td> thứ 2 expect(email_cell).to_have_text("johndoe@example.com")
⚡️ II. TĂNG TỐC VỚI PLAYWRIGHT CODEGEN
1️⃣ Codegen là gì?
Codegen (Code Generator) là một công cụ của Playwright, nó mở một trình duyệt, theo dõi mọi hành động của bạn (click, gõ phím, cuộn chuột) và tự động dịch các hành động đó thành mã nguồn Playwright Python.
Lưu ý quan trọng: Codegen là một trợ lý, không phải là một lập trình viên chuyên nghiệp. Nó giúp bạn:
Tạo nhanh script nháp.
Học và khám phá locator cho các element phức tạp.
Gỡ lỗi bằng cách xem Playwright đề xuất locator nào.
2️⃣ Khởi chạy và Sử dụng
-
Mở terminal (Command Prompt, PowerShell, hoặc terminal trong VS Code).
-
Gõ lệnh:
playwright codegen https://www.saucedemo.com/
-
Hai cửa sổ sẽ xuất hiện:
-
Cửa sổ trình duyệt: Nơi bạn thực hiện các thao tác như một người dùng bình thường.
-
Cửa sổ Playwright Inspector: Nơi mã nguồn được tự động sinh ra.
-
3️⃣ Thực hành với Codegen
-
Ghi kịch bản (Record):
-
Trên trang
saucedemo.com
, hãy nhập "standard_user" vào ô username, "secret_sauce" vào ô password, và click nút Login. -
Quan sát cửa sổ Inspector, bạn sẽ thấy các dòng code Python tương ứng được tạo ra.
-
-
Sao chép và "Refactor":
-
Copy đoạn code được tạo ra vào một file
test_login.py
. -
Phân tích code: Bạn sẽ thấy Codegen có thể tạo ra một locator khá phức tạp, ví dụ:
page.locator("[data-test='login-button']")
. -
Cải tiến (Refactor): Hãy tự mình viết lại locator đó theo cách tốt hơn đã học:
page.get_by_role("button", name="Login")
. -
Mục tiêu: Luôn luôn cải tiến code do máy tạo ra để nó dễ đọc và bền vững hơn.
-
🧩 BÀI TẬP THỰC HÀNH
🧠 Bài 1: Thử thách với XPath Axes
-
Truy cập trang
https://the-internet.herokuapp.com/tables
. -
Yêu cầu: Viết một locator XPath duy nhất để tìm số tiền nợ (
Due
) của người có họ là "Bach" trong bảng Table 1. -
Gợi ý:
-
Bắt đầu bằng cách tìm ô chứa chữ "Bach":
//table[@id='table1']//td[text()='Bach']
-
Từ đó, dùng
following-sibling::td
để di chuyển sang ô chứa số tiền nợ.
-
🧠 Bài 2: Codegen và Refactor
-
Truy cập trang
https://the-internet.herokuapp.com/challenging_dom
. -
Dùng Codegen để ghi lại hành động: click vào nút màu đỏ (có chữ "qux").
-
Sao chép code đã tạo.
-
Phân tích: Nút này có ID và text thay đổi mỗi lần tải lại trang. Locator do Codegen tạo ra có thể sẽ không chạy được ở lần tiếp theo.
-
Refactor: Viết một locator ổn định hơn bằng cách dựa vào class của nó (ví dụ:
page.locator("a.button.alert")
). Thêm mộtexpect
để xác nhận bạn đã click đúng.playwright codegen https://www.saucedemo.com/