如何在page.evaluate中定义命名函数避免ReferenceError?
编辑补充:问题根源与修复方案
经排查,该问题由使用tsx运行代码导致,与Playwright无关。可通过npx tsx <file-with-code>.ts复现问题。
可自行定义__name来解决该问题:
declare global { interface Window { __name: (func: Function)=> Function } } //... 获取page或context后: await context.addInitScript(() => { window.__name = (func) => func; })
相关问题涉及:
- ESBuild
- tsx
- Playwright
原始问题
我拥有一个Playwright的Page对象,想要在其上执行代码。由于需要在evaluate中多次执行某段逻辑,我尝试定义一个函数,代码如下:
await page.evaluate(() => { const foo = (): string => "foo"; return foo(); });
但每当我将函数赋值给变量时,就会出现如下错误:
await page.evaluate: ReferenceError: __name is not defined at eval (eval at evaluate (:226:30), <anonymous>:1:16) at UtilityScript.evaluate (<anonymous>:228:17) at UtilityScript.<anonymous> (<anonymous>:1:44)
我可以通过将函数赋值给window.foo = () => "foo"并调用它来解决问题,但我不明白为何会出现这个错误。使用箭头函数或普通函数没有区别。如果只是定义非可调用变量,在evaluate上下文中可以正常使用。
那么,如何在不将函数赋值给全局对象的情况下,在evaluate中定义函数?为何可调用变量会有这种特殊情况?这一切都在单次evaluate调用中完成,不需要持久化。
如果不赋值,使用匿名函数可以正常工作;定义非可调用变量也正常;赋值给某个对象也正常:
// 匿名函数直接调用 await page.evaluate(() => { return ((): string => "foo")(); }); // 定义非可调用变量 await page.evaluate(() => { const x = "foo"; return x; }); // 赋值给window对象 await page.evaluate(() => { window.foo = () => "foo"; return window.foo(); });
直接在浏览器控制台定义函数显然也可以正常工作:
该问题并非类似问题的重复,因为我想要在evaluate调用内部定义函数,而非外部。我理解代码需要序列化后发送到浏览器,但此处我仅在浏览器内定义一次,无需序列化。该问题的相关回答甚至就是我尝试的做法。
最小复现示例
以下是在我的机器上能稳定触发错误的最小示例:
const { chromium } = await import("@playwright/test"); const server = await chromium.launchServer(); const client = await chromium.connect(server.wsEndpoint()); const browser = await client.newContext.bind(client)(); const page = await browser.newPage(); const shouldBeFoo = await page.evaluate(() => { const foo = () => "foo"; return foo(); }); // ReferenceError: __name is not defined
这段代码即使在这种不必要使用函数的简化示例中,也能可靠触发错误。除此之外我没有执行其他操作。
如果我调试进入Playwright的evaluate调用,会来到这里:
async evaluate(pageFunction, arg) { (0, _jsHandle.assertMaxArguments)(arguments.length, 2); return await this._mainFrame.evaluate(pageFunction, arg); }
此时已经出现异常:调试器中的“pageFunction”参数是这样的函数:()=>{const foo=__name(()=>"foo","foo");return foo()}。此时调用它仍然可以正常工作。
在_mainFrame.evaluate内部,该函数会被序列化,我猜测这是问题所在:
const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: (0, _jsHandle.serializeArgument)(arg) });
这也可能只是Playwright的一个bug。
内容的提问来源于stack exchange,提问作者RunOrVeith

