Wiki

← Tất cả bài wiki

Locator vs ElementHandle (Đối tượng mới vs legacy)

automationplaywright

ElementHandle là API thế hệ cũ của Playwright (và Puppeteer): nó giữ tham chiếu TRỰC TIẾP tới một DOM node cụ thể. Locator ra đời để thay thế — chỉ lưu cách tìm, resolve mới mỗi lần dùng, tích hợp auto-wait + strict mode. Tài liệu chính thức khuyên "dùng Locator, chỉ dùng ElementHandle khi không còn lựa chọn" — vì handle dễ stale và không được hưởng lớp bảo vệ nào.

Khái niệm & Bản chất

  • ElementHandle = con trỏ tới 1 node cụ thể: page.$('button') trả về handle trỏ cứng vào DOM node tại thời điểm đó. Node bị remove/re-render → handle trỏ vào "xác" node (detached).
  • Locator = mô tả cách tìm: không giữ node nào; mỗi action resolve lại DOM hiện tại → luôn bắt node mới nhất khớp selector.
  • Auto-wait có và không: action qua Locator chờ element actionable; action qua ElementHandle cũng có 1 ít chờ cơ bản, nhưng phần lớn API handle (JSHandle/evaluate) không đi qua pipeline actionability — rủi ro race condition.
  • Không strict mode: handle vốn chỉ trỏ 1 node nên không có khái niệm "match nhiều"; nghĩa là page.$('button') lấy phần tử đầu tiên lặng lẽ — đúng cái mà strict mode được tạo ra để ngăn.
  • Vòng đời thủ công: handle cần dispose() khi xài xong để tránh rò rỉ tài nguyên trong browser; locator không cần quản lý gì.

Cách hoạt động & API chính

  • API lấy handle (legacy): page.$(selector) (1 cái đầu tiên), page.$$(selector) (mảng), locator.elementHandle(), locator.elementHandles() — vẫn tồn tại nhưng đánh dấu "discouraged".
  • API trên handle: handle.click(), handle.fill(), handle.textContent(), handle.getAttribute(), handle.evaluate(fn), handle.$eval/$$eval, handle.dispose(), handle.isVisible()...
  • Điểm chết: detached node: expect web-first assertions KHÔNG nhận ElementHandle — không viết được expect(handle).toBeVisible() kiểu auto-retry (thực tế assertion với handle tồn tại nhưng non-retrying).
  • Chuyển đổi: mọi thứ handle làm được đều có đường qua locator: handle.evaluate(fn) → locator.evaluate(fn); handle.$eval → locator.evaluate + scope; $$ → locator.all() / elementHandles().
  • Khi nào handle không tránh được: tương tác với JSHandle phức tạp (truyền node làm argument cho page.evaluate giữa các context), một số API cũ như elementHandle.screenshot(), hoặc đọc code base cũ.

Ví dụ & Cú pháp thực tế

  • Cùng một việc — cách legacy vs cách đúng: ``ts import { test, expect } from '@playwright/test'; // LEGACY — ElementHandle: không auto-retry, dễ stale const handle = await page.$('.price'); const price1 = await handle?.textContent(); // lấy lúc này, DOM đổi là chết await handle?.dispose(); // phải tự dọn // ĐÚNG — Locator: lazy + auto-wait + retrying assertion const price = page.locator('.price'); await expect(price).toHaveText(/\d+/); // tự retry tới khi có giá ``
  • Kịch bản stale kinh điển: ``ts // LỖI NGẦM: giữ handle qua một lần re-render const btn = await page.$('#pay'); await page.getByRole('button', { name: 'Áp mã' }).click(); // DOM patch, #pay bị replace await btn?.click(); // bấm vào node đã detached — fail hoặc im lặng không tác dụng // SỬA: dùng locator — tự re-resolve await page.locator('#pay').click(); ``
  • evaluate với locator thay handle: ``ts // Đọc property không có sẵn API — vẫn qua locator được const isReadonly = await page.getByLabel('Email') .evaluate((el: HTMLInputElement) => el.readOnly); // Truyền element làm arg cho page.evaluate — đây là chỗ handle còn hữu ích const el = await page.getByRole('slider').elementHandle(); await page.evaluate((s) => s.scrollIntoView(), el); await el?.dispose(); ``

Best practices & Khi nào dùng

  • Rule ngón tay cái: 99% code mới viết locator. Gặp page.$/$$/.elementHandle() trong code review → hỏi "vì sao không dùng locator?" trước khi approve.
  • Nếu bắt buộc handle: lấy ra dùng NGAY trong cùng scope, đừng truyền qua hàm/đợi lâu; luôn dispose() sau khi xong.
  • Migrate code cũ: thay page.$(sel) + .textContent() bằng locator(sel) + expect(...).toHaveText(...); thay $$ + loop bằng locator.all() (trả locator con, vẫn giữ auto-wait).
  • Dùng handle cho nhiệm vụ "bản chất node": screenshot 1 element cụ thể đã resolve, scrollIntoViewIfNeeded, kéo thả qua dragTo hiếm gặp — nhưng cũng kiểm tra trước xem locator đã cover chưa (locator.screenshot() có sẵn).
  • Trong POM: expose Locator làm property, KHÔNG expose ElementHandle — handle rò ra ngoài là mất kiểm soát vòng đời.

Gotchas, So sánh & Lưu ý

  • Stale không báo lỗi rõ như Selenium: Selenium có StaleElementReferenceException hẳn hoi; handle của Playwright đôi khi fail lạ (Node is detached from document) hoặc thao tác "như không" — khó debug hơn.
  • page.$ KHÔNG chờ: khác locator.click() chờ element xuất hiện, page.$ query ngay — element render chậm → trả null, và null.click() = TypeError. Người mới từ Puppeteer sang hay vấp.
  • evaluateAll trên locator: locator.evaluateAll((els) => els.map(e => e.textContent)) là đường hiện đại thay cho $$ + evaluate — chạy 1 lần trong browser, nhanh và không tạo handle lẻ.
  • So sánh Puppeteer: Puppeteer chủ yếu ElementHandle → Playwright kế thừa API này nhưng đề xuất Locator; khi port script Puppeteer, đừng port nguyên si — convert sang locator.
  • elementHandle() vẫn từ locator: locator.elementHandle() phá lazy — nó resolve NGAY và đóng băng node. Gọi xong handle chết theo DOM chứ locator gốc vẫn sống.
  • Lưu ý typing: ElementHandle | null — quên check null là crash runtime; với locator thì API luôn trả object, lỗi chỉ nổ lúc action (dễ bắt hơn).