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:
expectweb-first assertions KHÔNG nhận ElementHandle — không viết đượcexpect(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.evaluategiữ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ằnglocator(sel)+expect(...).toHaveText(...); thay$$+ loop bằnglocator.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ả quadragTohiế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
Locatorlà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ó
StaleElementReferenceExceptionhẳ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).
