Filtering & Chaining Locators (Lọc & lồng locator)
automationplaywright
Filtering & chaining là kỹ thuật thu hẹp phạm vi locator: .filter() lọc danh sách theo điều kiện (hasText, has, hasNot, visible), and_()/or_() kết hợp locator, và locator.locator() / getBy* lồng nhau để tìm trong một vùng cụ thể. Đây là cách "chính thống" để xử lý UI lặp cấu trúc (bảng, card, list) — thay vì viết CSS/XPath siêu dài.
Khái niệm & Bản chất
- Chaining (lồng locator):
parent.locator(child)tìm child TRONG subtree của parent — định nghĩa scope, tránh match nhầm phần tử cùng kiểu ở chỗ khác trên trang. - Filtering (lọc):
.filter(options)biến "tất cả element match" thành "chỉ những element thoả điều kiện" — điều kiện chạy trên kết quả đã match, không đổi cách resolve ban đầu. - Bốn điều kiện filter chính:
hasText(chứa text),has(chứa descendant khớp locator kia),hasNot(KHÔNG chứa),visible(đang hiển thị). Kết hợp nhiều điều kiện trong 1 lần.filter()= AND. - and_() / or_() kết hợp locator:
locatorA.or_(locatorB)match union của hai;and_match giao — cần khi một selector đơn lẻ không diễn tả được điều kiện. - Bản chất vẫn lazy: filter/chain chỉ nối thêm mô tả vào "công thức" — mọi điều kiện đánh giá tại thời điểm action/assert, nên vẫn chịu được DOM thay đổi.
Cách hoạt động & API chính
- locator.filter({ hasText }): nhận string hoặc regex;
hasNotTextloại trừ. Đây là cách chọn 1 card/row cụ thể trong list lặp. - locator.filter({ has: locator }): giữ element nào chứa descendant match locator kia — cực mạnh: "row nào chứa nút Đã duyệt".
hasNotlà phủ định. - locator.filter({ visible: true }): chỉ giữ element đang hiển thị — dọn những bản sao
display:nonehay gặp ở responsive duplicate markup. - locator.and_(other): element phải match CẢ hai locator. Ví dụ
page.getByRole('button').and_(page.getByTitle('Đóng')). - locator.or_(other): match một trong hai — dùng cho UI có 2 trạng thái thay phiên (
toBeVisibletrênA.or_(B)). Chú ý: mọi lần dùng.or_trong TS/JS vìorlà từ khoá. - Chaining nhiều tầng:
page.getByRole('table').getByRole('row', { name: 'A123' }).getByRole('button', { name: 'Xoá' })— mỗi.getBy*/.locatorthu hẹp scope một bậc.
Ví dụ & Cú pháp thực tế
- Filter theo nội dung trong list:
``
ts import { test, expect } from '@playwright/test'; // Chọn đúng card sản phẩm rồi bấm nút trong card đó const card = page.locator('.product-card').filter({ hasText: 'iPhone 15' }); await card.getByRole('button', { name: 'Thêm vào giỏ' }).click(); // Filter theo descendant — dòng chứa badge "Mới" const newRows = page.locator('tr').filter({ has: page.getByText('Mới', { exact: true }), }); await expect(newRows).toHaveCount(2);`` - Loại trừ với hasNot / visible:
``
ts // Dòng chưa bị disable — loại row có badge 'Đã xoá' const activeRows = page.locator('tbody tr').filter({ hasNot: page.getByText('Đã xoá'), }); // Chỉ lấy bản đang hiển thị (tránh markup ẩn mobile/desktop) const visibleMenu = page.locator('.menu').filter({ visible: true });`` - or_ cho trạng thái thay phiên:
``
ts // Sau khi submit: hoặc toast thành công, hoặc form lỗi — chờ cái nào ra trước const outcome = page.getByText('Lưu thành công').or_(page.getByRole('alert')); await expect(outcome).toBeVisible();`` - Chaining nhiều tầng thay mega-selector:
``
ts // Thay cho 'xpath=//table//tr[td[2]="A123"]//button[3]' const row = page.getByRole('row').filter({ hasText: 'A123' }); await row.getByRole('button', { name: 'Chi tiết' }).click();``
Best practices & Khi nào dùng
- Bảng/list lặp cấu trúc → filter, đừng nth:
filter({ hasText: orderId })bền hơnnth(3)vì thứ tự dòng đổi khi data/sort đổi. Chỉ dùng nth khi thứ tự chính là thứ cần kiểm. - Chain để mô tả ngữ cảnh:
dialog.getByRole('button', {name:'OK'})rõ ràng hơnpage.getByRole('button', {name:'OK'})— tránh click nhầm nút OK của dialog khác trên trang. - hasNot/hasNotText cho negative case: kiểm "không có dòng nào chứa Lỗi" bằng
expect(rows.filter({hasText:'Lỗi'})).toHaveCount(0). - Đừng chain quá sâu: 2-3 tầng là đẹp; 5+ tầng nghĩa là cần testid ở tầng giữa. Mỗi tầng thêm một điểm giòn.
- Viết helper cho pattern lặp:
function rowById(page, id) { return page.getByRole('row').filter({ hasText: id }); }— POM gọn, test đọc như câu chữ.
Gotchas, So sánh & Lưu ý
- hasText vs text bên trong has:
filter({ hasText: 'X' })match substring ở bất kỳ đâu trong element (kể cả text của con);filter({ has: page.getByText('X', {exact:true}) })chặt hơn nhưng cần element con riêng. - filter không đổi kiểu match gốc:
.locator('div').filter(...)vẫn bắt đầu từ TẤT CẢ div rồi lọc — nếu gốc quá rộng, filter xong có thể vẫn nhiều kết quả → strict violation ở action. - or_ khác với XPath
|/ CSS,:or_giữ nguyên 2 locator riêng rồi hợp kết quả — nhưng một số API (vd.filterbên trong từng nhánh) hoạt động độc lập; đọc trace khi thấy kết quả lạ. - visible: true filter ≠ toBeVisible assertion: filter chọn element visible NGAY thời điểm resolve; assertion
toBeVisiblechờ tới khi visible — đừng nhầm, dùng cả hai khi cần. - So sánh Selenium: Selenium không có tương đương
.filter({has})— phải XPathancestor::hoặc lặpfindElements+getTexttừng cái (chậm + race). Playwright đẩy toàn bộ việc này xuống engine 1 lần query. - Debug chain bằng
page.pause()/ trace viewer: mỗi tầng locator hiện riêng trong trace — dễ soi tầng nào match sai.
