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 đọcwindow.__STATE__, gọi hàm trang expose, đo scroll, thao tác DOM đặc thù.page.exposeFunction(name, fn): gắnwindow.name= async function — khi trang gọi, thực thifntrong 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êmsource(đố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.addInitScriptliên quan chặt: chạy script TRƯỚC mọi script của trang — chỗ đặt expose/binding để app script nhìn thấywindow.myFnngay 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.exposeFunctionapply 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.contextcho 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.*, patchMath.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,thisNode đề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.
