Wiki

← Tất cả bài wiki

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; hasNotText loạ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". hasNot là phủ định.
  • locator.filter({ visible: true }): chỉ giữ element đang hiển thị — dọn những bản sao display:none hay 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 (toBeVisible trên A.or_(B)). Chú ý: mọi lần dùng .or_ trong TS/JS vì or là từ khoá.
  • Chaining nhiều tầng: page.getByRole('table').getByRole('row', { name: 'A123' }).getByRole('button', { name: 'Xoá' }) — mỗi .getBy*/.locator thu 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ơn nth(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ơn page.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 .filter bê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 toBeVisible chờ 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 XPath ancestor:: hoặc lặp findElements + getText từ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.