Playwright/Puppeteer测试Tab通信:无法传递Class至evaluate求助
问题分析
你遇到的核心问题是:Playwright(或Puppeteer)的evaluate方法仅支持传递可JSON序列化的数据,类/类实例无法直接跨上下文传递,且你尝试的两种方式均存在缺陷:
- 传递类字符串:仅序列化了
Channel类代码,但它依赖的Utils类未同步注入页面环境,导致运行时找不到依赖变量。 - 传递类实例:实例的原型方法会在序列化过程中丢失,页面中的
window.channel仅保留实例自有属性,没有sendHelloWorld等方法。
可行解决方案
方案1:打包依赖为独立脚本,通过addScriptTag注入
将Utils和Channel打包成浏览器可执行的独立脚本(如IIFE格式),再通过Playwright的addScriptTag注入页面,让页面环境直接拥有完整的类定义。
步骤:
用打包工具(如esbuild、Rollup)编译TypeScript代码为单个JS文件:
以esbuild为例,执行命令:esbuild src/index.ts --bundle --format=iife --global-name=MyChannelLib --outfile=dist/channel.js编译后
window.MyChannelLib会包含Utils和Channel类。测试代码中注入脚本并使用:
import { test, expect, Page } from '@playwright/test'; let page1: Page, page2: Page; test.beforeEach(async ({ context }) => { [page1, page2] = await Promise.all([context.newPage(), context.newPage()]); await Promise.all([ page1.goto("https://www.google.com"), page2.goto("https://www.google.com"), // 注入打包后的脚本 page1.addScriptTag({ path: './dist/channel.js' }), page2.addScriptTag({ path: './dist/channel.js' }) ]); }); test('sendHelloWorld', async () => { // 在页面中初始化Channel await Promise.all([ page1.evaluate(() => { window.channel = new window.MyChannelLib.Channel('test-channel'); }), page2.evaluate(() => { window.channel = new window.MyChannelLib.Channel('test-channel'); }) ]); const msgPromise = page2.waitForEvent("console"); await page1.evaluate(() => { window.channel.sendHelloWorld(); }); const msg = await msgPromise; expect((await msg.args()[0].jsonValue()).msg).toBe("Hello World"); });
方案2:在evaluate中注入完整的依赖代码
如果不想打包,可以将Utils和Channel的源代码转换为字符串,一起传递给evaluate,先定义Utils再定义Channel,确保页面环境拥有完整的依赖链。
示例代码:
import { test, expect, Page } from '@playwright/test'; import fs from 'fs'; import path from 'path'; // 读取本地源代码文件 const utilsCode = fs.readFileSync(path.resolve(__dirname, '../src/utils.ts'), 'utf-8'); const channelCode = fs.readFileSync(path.resolve(__dirname, '../src/index.ts'), 'utf-8'); let page1: Page, page2: Page; test.beforeEach(async ({ context }) => { [page1, page2] = await Promise.all([context.newPage(), context.newPage()]); await Promise.all([page1.goto("https://www.google.com"), page2.goto("https://www.google.com")]); }); test('sendHelloWorld', async () => { await Promise.all([ page1.evaluate(({ utilsCode, channelCode }) => { // 先执行Utils代码,再执行Channel代码 eval(utilsCode); eval(channelCode); // TypeScript编译后export的类会挂在module.exports,手动挂载到window window.Channel = module.exports.Channel; window.channel = new window.Channel('test-channel'); }, { utilsCode, channelCode }), page2.evaluate(({ utilsCode, channelCode }) => { eval(utilsCode); eval(channelCode); window.Channel = module.exports.Channel; window.channel = new window.Channel('test-channel'); }, { utilsCode, channelCode }) ]); const msgPromise = page2.waitForEvent("console"); await page1.evaluate(() => { window.channel.sendHelloWorld(); }); const msg = await msgPromise; expect((await msg.args()[0].jsonValue()).msg).toBe("Hello World"); });
注意:如果TypeScript代码包含特殊语法(如装饰器)或ES模块配置,直接
eval可能报错,此时方案1的打包方式更可靠。
方案3:重构测试逻辑,直接在页面上下文中编写代码
将测试逻辑直接放在evaluate中,避免跨上下文传递类,适合小型模块快速验证:
import { test, expect, Page } from '@playwright/test'; let page1: Page, page2: Page; test.beforeEach(async ({ context }) => { [page1, page2] = await Promise.all([context.newPage(), context.newPage()]); await Promise.all([page1.goto("https://www.google.com"), page2.goto("https://www.google.com")]); }); test('sendHelloWorld', async () => { await page1.evaluate(() => { // 在页面中直接定义完整的Utils和Channel class Utils { static formatTime() { return new Date().toLocaleString(); } static generateRandomStr() { return Math.random().toString(36).slice(2); } } class Channel { uniqueID = Utils.generateRandomStr(); channel; constructor(name) { this.channel = new BroadcastChannel(name); this.channel.addEventListener("message", e => console.log(e.data)); } sendMsg(msg) { this.channel.postMessage({ id: this.uniqueID, time: Utils.formatTime(), msg }); } sendHelloWorld() { this.sendMsg("Hello World"); } } window.channel = new Channel('test-channel'); }); await page2.evaluate(() => { // 重复定义相同的类(保证页面上下文独立) class Utils { static formatTime() { return new Date().toLocaleString(); } static generateRandomStr() { return Math.random().toString(36).slice(2); } } class Channel { uniqueID = Utils.generateRandomStr(); channel; constructor(name) { this.channel = new BroadcastChannel(name); this.channel.addEventListener("message", e => console.log(e.data)); } sendMsg(msg) { this.channel.postMessage({ id: this.uniqueID, time: Utils.formatTime(), msg }); } sendHelloWorld() { this.sendMsg("Hello World"); } } window.channel = new Channel('test-channel'); }); const msgPromise = page2.waitForEvent("console"); await page1.evaluate(() => window.channel.sendHelloWorld()); const msg = await msgPromise; expect((await msg.args()[0].jsonValue()).msg).toBe("Hello World"); });
内容的提问来源于stack exchange,提问作者crazyones110
相关产品推荐
相关产品推荐

