You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Playwright/Puppeteer测试Tab通信:无法传递Class至evaluate求助

问题分析

你遇到的核心问题是:Playwright(或Puppeteer)的evaluate方法仅支持传递可JSON序列化的数据,类/类实例无法直接跨上下文传递,且你尝试的两种方式均存在缺陷:

  • 传递类字符串:仅序列化了Channel类代码,但它依赖的Utils类未同步注入页面环境,导致运行时找不到依赖变量。
  • 传递类实例:实例的原型方法会在序列化过程中丢失,页面中的window.channel仅保留实例自有属性,没有sendHelloWorld等方法。
可行解决方案

方案1:打包依赖为独立脚本,通过addScriptTag注入

将Utils和Channel打包成浏览器可执行的独立脚本(如IIFE格式),再通过Playwright的addScriptTag注入页面,让页面环境直接拥有完整的类定义。

步骤:

  1. 用打包工具(如esbuild、Rollup)编译TypeScript代码为单个JS文件:
    以esbuild为例,执行命令:

    esbuild src/index.ts --bundle --format=iife --global-name=MyChannelLib --outfile=dist/channel.js
    

    编译后window.MyChannelLib会包含Utils和Channel类。

  2. 测试代码中注入脚本并使用:

    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 15:12:53