Playwright Evaluate 深入指南跨环境执行 JavaScript、传递参数与 Init Scripts 原理剖析【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright本文围绕 Playwright 官方文档《Evaluating JavaScript》展开讲解如何在浏览器页面环境中安全执行 JavaScript 并将结果带回测试环境包括page.evaluate的四种语言用法、跨环境传参的正确姿势、序列化值与JSHandle混合参数的机制、页面加载前注入脚本的addInitScript方案并结合playwright-core客户端源码还原evaluate从序列化到结果解析的完整调用链帮助你在编写测试时彻底理解两个环境的边界。两个隔离的执行环境为什么需要 evaluatePlaywright 脚本运行在你的 Playwright 进程中而页面脚本运行在浏览器页面的环境中。这两个环境互不相交——它们运行在不同的虚拟机、不同的进程中甚至可能位于不同的计算机上。这正是page.evaluate存在的意义它在网页的上下文中执行一段 JavaScript 函数或表达式并把结果带回 Playwright 环境。在evaluate内部可以直接使用window、document等浏览器全局对象。const href await page.evaluate(() document.location.href);其他语言等价写法String href (String) page.evaluate(document.location.href);# async href await page.evaluate(() document.location.href) # sync href page.evaluate(() document.location.href)var href await page.EvaluateAsyncstring(document.location.href);异步函数自动等待如果传入的结果是 Promise或者函数本身是async的evaluate会自动等待其 resolve 之后才返回结果。这一点从客户端源码可以印证evaluate走的是协议调用evaluateExpression服务端执行完成后才把序列化结果传回客户端见 frame.ts因此页面上的异步逻辑天然被阻塞等待const status await page.evaluate(async () { const response await fetch(location.href); return response.status; });int status (int) page.evaluate(async () {\n const response await fetch(location.href);\n return response.status;\n });status await page.evaluate(async () { response await fetch(location.href) return response.status })int status await page.EvaluateAsyncint(async () { const response await fetch(location.href); return response.status; });从源码结构看Page.evaluate本身只是把调用委托给主 frame在 page.ts 中evaluate直接转发到this._mainFrame.evaluate(pageFunction, arg, options)因此Frame.evaluate、Frame.evaluateHandle与Page上的同名方法共享同一套参数序列化与结果解析逻辑。不同环境变量必须显式传参这是最容易踩的坑被测页面上不存在你测试脚本里的变量。下面的写法是错误的因为它直接引用了测试环境中的dataconst data some data; const result await page.evaluate(() { // WRONG: there is no data in the web page. window.myApp.use(data); });String data some data; Object result page.evaluate(() {\n // WRONG: there is no data in the web page.\n window.myApp.use(data);\n });data some data result await page.evaluate(() { // WRONG: there is no data in the web page. window.myApp.use(data) })var data some data; var result await page.EvaluateAsync(() { // WRONG: there is no data in the web page. window.myApp.use(data); });正确做法是把值作为参数显式传入。Playwright 会把第二个参数序列化后随表达式一起发送到页面中函数签名里接收同名参数const data some data; // Pass |data| as a parameter. const result await page.evaluate(data { window.myApp.use(data); }, data);String data some data; // Pass |data| as a parameter. Object result page.evaluate(data {\n window.myApp.use(data);\n }, data);data some data # Pass |data| as a parameter. result await page.evaluate(data { window.myApp.use(data) }, data)var data some data; // Pass |data| as a parameter. var result await page.EvaluateAsync(data { window.myApp.use(data); }, data);在源码层面这一步对应 frame.ts 中的关键两行arg: serializedArg把参数序列化为可传输的结构isFunction: typeof pageFunction function告知服务端这是函数 参数而非纯表达式。Evaluation Argument序列化值与 JSHandle 的混合page.evaluate等方法接受单个可选参数该参数可以是可序列化值Serializable与JSHandle实例的任意混合。Handle 会被自动转换为它代表的实际值即页面上那个真实对象。// A primitive value. await page.evaluate(num num, 42); // An array. await page.evaluate(array array.length, [1, 2, 3]); // An object. await page.evaluate(object object.foo, { foo: bar }); // A single handle. const button await page.evaluateHandle(window.button); await page.evaluate(button button.textContent, button); // Alternative notation using JSHandle.evaluate. await button.evaluate((button, from) button.textContent.substring(from), 5); // Object with multiple handles. const button1 await page.evaluateHandle(window.button1); const button2 await page.evaluateHandle(window.button2); await page.evaluate( o o.button1.textContent o.button2.textContent, { button1, button2 }); // Object destructuring works. Note that property names must match // between the destructured object and the argument. // Also note the required parenthesis. await page.evaluate( ({ button1, button2 }) button1.textContent button2.textContent, { button1, button2 }); // Array works as well. Arbitrary names can be used for destructuring. // Note the required parenthesis. await page.evaluate( ([b1, b2]) b1.textContent b2.textContent, [button1, button2]); // Any mix of serializables and handles works. await page.evaluate( x x.button1.textContent x.list[0].textContent String(x.foo), { button1, list: [button2], foo: null });其他语言写法Java 与 C#同样支持混合参数// A primitive value. page.evaluate(num num, 42); // An array. page.evaluate(array array.length, Arrays.asList(1, 2, 3)); // An object. MapString, Object obj new HashMap(); obj.put(foo, bar); page.evaluate(object object.foo, obj); // A single handle. ElementHandle button page.evaluateHandle(window.button); page.evaluate(button button.textContent, button); // Alternative notation using JSHandle.evaluate. button.evaluate((button, from) button.textContent.substring(from), 5); // Object with multiple handles. ElementHandle button1 page.evaluateHandle(window.button1); ElementHandle button2 page.evaluateHandle(window.button2); MapString, ElementHandle arg new HashMap(); arg.put(button1, button1); arg.put(button2, button2); page.evaluate(o o.button1.textContent o.button2.textContent, arg); // Object destructuring works. Note that property names must match // between the destructured object and the argument. // Also note the required parenthesis. MapString, ElementHandle arg2 new HashMap(); arg2.put(button1, button1); arg2.put(button2, button2); page.evaluate(({ button1, button2 }) button1.textContent button2.textContent, arg2); // Array works as well. Arbitrary names can be used for destructuring. // Note the required parenthesis. page.evaluate( ([b1, b2]) b1.textContent b2.textContent, Arrays.asList(button1, button2)); // Any mix of serializables and handles works. MapString, Object arg3 new HashMap(); arg3.put(button1, button1); arg3.put(list, Arrays.asList(button2)); arg3.put(foo, 0); page.evaluate( x x.button1.textContent x.list[0].textContent String(x.foo), arg3);// A primitive value. await page.EvaluateAsyncint(num num, 42); // An array. await page.EvaluateAsyncint[](array array.length, new[] { 1, 2, 3 }); // An object. await page.EvaluateAsyncobject(object object.foo, new { foo bar }); // A single handle. var button await page.EvaluateHandleAsync(window.button); await page.EvaluateAsyncIJSHandle(button button.textContent, button); // Alternative notation using JSHandle.EvaluateAsync. await button.EvaluateAsyncstring((button, from) button.textContent.substring(from), 5); // Object with multiple handles. var button1 await page.EvaluateHandleAsync(window.button1); var button2 await page.EvaluateHandleAsync(window.button2); await page.EvaluateAsync(o o.button1.textContent o.button2.textContent, new { button1, button2 }); // Object destructuring works. Note that property names must match // between the destructured object and the argument. // Also note the required parenthesis. await page.EvaluateAsync(({ button1, button2 }) button1.textContent button2.textContent, new { button1, button2 }); // Array works as well. Arbitrary names can be used for destructuring. // Note the required parenthesis. await page.EvaluateAsync(([b1, b2]) b1.textContent b2.textContent, new[] { button1, button2 }); // Any mix of serializables and handles works. await page.EvaluateAsync(x x.button1.textContent x.list[0].textContent String(x.foo), new { button1, list new[] { button2 }, foo null as object });Pythonasync 与 sync 用法一致仅是否await的差别# A primitive value. await page.evaluate(num num, 42) # An array. await page.evaluate(array array.length, [1, 2, 3]) # An object. await page.evaluate(object object.foo, { foo: bar }) # A single handle. button await page.evaluate_handle(window.button) await page.evaluate(button button.textContent, button) # Alternative notation using JSHandle.evaluate. await button.evaluate((button, from) button.textContent.substring(from), 5) # Object with multiple handles. button1 await page.evaluate_handle(window.button1) button2 await page.evaluate_handle(window.button2) await page.evaluate( o o.button1.textContent o.button2.textContent, { button1: button1, button2: button2 }) # Object destructuring works. Note that property names must match # between the destructured object and the argument. # Also note the required parenthesis. await page.evaluate( ({ button1, button2 }) button1.textContent button2.textContent, { button1: button1, button2: button2 }) # Array works as well. Arbitrary names can be used for destructuring. # Note the required parenthesis. await page.evaluate( ([b1, b2]) b1.textContent b2.textContent, [button1, button2]) # Any mix of serializables and handles works. await page.evaluate( x x.button1.textContent x.list[0].textContent String(x.foo), { button1: button1, list: [button2], foo: None })哪些值能安全往返传递官方测试 page-evaluate.spec.ts 对序列化边界给出了明确验证NaN、-0、Infinity、-Infinity能原样往返用Object.is校验普通数组在传输后仍是数组而非对象所有 TypedArrayInt8Array到BigUint64Array与BigInt均可完整回传Promise 包裹的值会被解包后返回。换句话说JSON 可表达的值加上一组特殊数值与 TypedArray 构成了 evaluate 参数的安全传输范围而函数、DOM 节点这类不可序列化值则必须通过JSHandle通道传递。源码级实现serializeArgument → evaluateExpression → parseResult结合 frame.ts 的Frame.evaluate实现一次page.evaluate调用的完整链路如下入参校验assertMaxArguments(arguments.length, 3)强制函数 单个参数的签名防止误传多个实参参数序列化默认路径调用serializeArgument(arg)若设置了options.exposeFunctions则走serializeArgumentWithCallbacks把测试环境中的函数注册为可回调的句柄页面端可以反向调用它——这两个函数都定义在 jsHandle.ts 中协议调用向服务端发送evaluateExpression请求携带{ expression: String(pageFunction), isFunction, arg }其中String(pageFunction)解释了为什么函数会被转成字符串传输也再次说明测试环境的闭包变量无法被页面直接访问结果解析返回的result.value是序列化结构由parseResult还原为测试环境中的 JS 值。evaluateHandleframe.ts与之平行它调用evaluateExpressionHandle不等待值序列化回传而是返回一个JSHandle由你决定后续在页面端继续操作——这就是前文单 handle / 多 handle示例中page.evaluateHandle(window.button)的产物。相关行为还可由 tests/page/ 目录下的page-evaluate.spec.ts、page-evaluate-handle.spec.ts、page-evaluate-callback.spec.ts函数回调场景与page-evaluate-no-stall.spec.ts作为回归依据。Init scripts在页面加载前注入脚本有时你需要在页面开始加载之前就执行代码例如设置 mock 或测试数据覆写Math.random、预置localStorage等。此时应使用page.addInitScript或browserContext.addInitScript。官方示例以把Math.random()替换为常量演示了完整流程。第一步创建preload.js文件存放 mock 代码。// preload.js Math.random () 42;第二步在测试中把 init script 注册到页面。import { test, expect } from playwright/test; import path from path; test.beforeEach(async ({ page }) { // Add script for every test in the beforeEach hook. // Make sure to correctly resolve the script path. await page.addInitScript({ path: path.resolve(__dirname, ../mocks/preload.js) }); });// In your test, assuming the preload.js file is in the mocks directory. page.addInitScript(Paths.get(mocks/preload.js));# In your test, assuming the preload.js file is in the mocks directory. # async await page.add_init_script(pathmocks/preload.js) # sync page.add_init_script(pathmocks/preload.js)// In your test, assuming the preload.js file is in the mocks directory. await Page.AddInitScriptAsync(scriptPath: mocks/preload.js);函数形式对于短小或一次性的脚本可以直接传函数而无需落盘为文件而且这种方式同样支持传参JS 语言import { test, expect } from playwright/test; // Add script for every test in the beforeEach hook. test.beforeEach(async ({ page }) { const value 42; await page.addInitScript(value { Math.random () value; }, value); });从实现看page.ts 中的Page.addInitScript会把Function | string | { path, content }三种形态统一提取为source字符串后下发addInitScript协议消息当参数包含测试侧函数exposeFunctions时则转由addInitScriptWithExposedFunctionspage.ts注册回调通道。返回值为 disposable意味着 init script 可以随对象销毁被清理。更完整的初始化脚本语义作用域、新文档生效时机等可参考 browsercontext-add-init-script.spec.ts 中的测试覆盖。小结编写 evaluate 代码的核对清单牢记两个环境测试变量必须通过evaluate的第二个参数显式传入绝不能依赖闭包捕获优先传函数而非字符串表达式函数形式自带类型提示与参数绑定且isFunction标志由客户端自动判定可序列化值走参数通道DOM 对象/页面全局走JSHandle通道二者可在同一个参数对象中自由混合支持对象解构与数组解构注意解构属性名必须与参数键名一致且解构需加括号需要在页面脚本运行前生效的 mock用addInitScript而非evaluate——evaluate执行时机晚于页面加载无法拦截初始化代码需要把测试侧函数暴露给页面时考虑exposeFunctions选项其底层走serializeArgumentWithCallbacks回调通道参见 tests/page/page-evaluate-callback.spec.ts。延伸阅读本文原始出处为 docs/src/evaluating.mdevaluate 的完整 API 签名见 packages/playwright-core/types 下的类型定义文件。【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考