Wiki

← Tất cả bài wiki

page.evaluate, exposeFunction & exposeBinding (Cầu nối Node ↔ Browser)

automationplaywright

page.evaluate chạy JS bên trong trang để đọc state nội bộ hay thao tác DOM khi locator không đủ; exposeFunction/exposeBinding đi chiều ngược lại — gắn function Node vào window để code trang gọi ngược về test (crypto, DB, log). Hai hướng của cây cầu giữa test runner và browser context.

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

  • page.evaluate(fn, arg): serialize function → chạy trong page context → serialize kết quả trả về Node. Dùng đọc window.__STATE__, gọi hàm trang expose, đo scroll, thao tác DOM đặc thù.
  • page.exposeFunction(name, fn): gắn window.name = async function — khi trang gọi, thực thi fn trong NODE rồi trả kết quả (Promise) về trang. Trang gọi được Node, Node thấy được trang.
  • page.exposeBinding(name, fn, { handle? }): như exposeFunction nhưng nhận thêm source (đối tượng { browser, context, page, frame } biết ai gọi) và có thể truyền JSHandle sống thay vì chỉ JSON value.
  • page.addInitScript liên quan chặt: chạy script TRƯỚC mọi script của trang — chỗ đặt expose/binding để app script nhìn thấy window.myFn ngay từ đầu.

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

  • evaluate signature: page.evaluate((a, b) => a + b, [1, 2]) — arg truyền qua serialization, fn không thấy closure Node.
  • Trả về handle sống: page.evaluateHandle(() => document.body) → JSHandle để thao tác tiếp, truyền lại vào evaluate khác; nhớ handle.dispose() khi xong.
  • ElementHandle.evaluate: el.evaluate(node => node.scrollIntoView()) — chạy trên node cụ thể.
  • exposeFunction/exposeBinding trên cả context: context.exposeFunction apply cho mọi page mới trong context — tốt hơn gắn từng page.
  • exposeBinding signature: fn(source, arg) — source.page, source.frame, source.context cho biết caller là ai; { handle: true } cho phép nhận JSHandle.

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

  • Đọc state nội bộ không lộ ra DOM: ``ts const cart = await page.evaluate(() => { return JSON.parse(localStorage.getItem('cart') ?? '[]'); }); expect(cart).toHaveLength(2); // đọc biến global app expose const userId = await page.evaluate(() => (window as any).__APP_STATE__?.userId); ``
  • exposeFunction — trang gọi ngược Node (crypto/fs/DB): ``ts await page.exposeFunction('hashInNode', async (text: string) => { const { createHash } = await import('node:crypto'); return createHash('sha256').update(text).digest('hex'); }); await page.goto('/checksum-form'); // trong trang: await window.hashInNode('abc') → hash do Node tính ``
  • exposeBinding + addInitScript — đẩy lỗi trang về Node: ``ts await context.exposeBinding('reportError', (source, msg: string) => { console.log([page ${source.page.url()}], msg); }); await context.addInitScript(() => { window.addEventListener('error', e => (window as any).reportError(e.message)); }); ``
  • Truyền element handle vào evaluate: ``ts const table = await page.getByRole('table').elementHandle(); const rowCount = await table?.evaluate(t => t.rows.length); ``

Best practices & Khi nào dùng

  • Ưu tiên locator + expect trước: evaluate là lối tắt qua 'cửa hậu' — dùng khi cần state không render ra DOM (Redux state, map instance, scroll position) hoặc trigger API trang expose.
  • exposeFunction cho capability chỉ Node có: đọc file fixture, hash/crypto, query DB assert ngầm, gọi API nội bộ từ browser context.
  • addInitScript cho setup trước app boot: polyfill, flag AB test, stub navigator.*, patch Math.random — mọi thứ app script cần thấy ngay frame đầu.
  • Giữ fn evaluate ngắn & pure: không import được module Node, không dùng được biến ngoài — logic nặng để Node xử lý, trang chỉ trả data.
  • Trả plain data từ evaluate: object/array/number/string/boolean — DOM node hay function phải bọc qua JSHandle/ElementHandle.

Gotchas, So sánh & Lưu ý

  • Mất closure: fn bị toString() rồi eval trong trang — biến ngoài scope, import, this Node đều không tồn tại; truyền qua arg duy nhất.
  • Serialization 2 chiều: undefined→OK, NaN/Infinity/Date/Map/Set/RegExp được support riêng; function, Symbol, DOM node, circular ref có cycle phức tạp sẽ fail/convert lạ — kiểm tra docs 'serializable'.
  • Kết quả exposeFunction là Promise trong trang: app phải await window.myFn() — dev quên await thì nhận pending Promise chứ không phải value.
  • Debug khó: lỗi trong evaluate stack trace trỏ vào trang, không map ngược file TS — bọc try/catch trả lỗi dạng string khi cần điều tra.
  • CSP (Content-Security-Policy): evaluate/exposeFunction chạy ở isolated world nên vượt được script-src — đây là feature, nhưng nhớ trang có thể không nhìn thấy biến bạn đặt nếu check từ main world.
  • So sánh: evaluate = Node gọi trang (pull/push data); exposeFunction = trang gọi Node (callback); addInitScript = chèn code trước boot; page.route = chặn network — đừng lẫn 4 công cụ cho 4 việc khác nhau.