playwright vscode 插件源码解析

Playwright vscode插件主要功能

  Playwright是微软开发的一款主要用于UI自动化测试的工具,在vscode中上安装playwright vscode插件,可以运行,录制UI自动化测试。

  playwright vscode插件主要包括两块功能,功能一是在Test Explorer中显示项目中所有的测试文件,选择某个测试文件,可以执行这些由playwright编写的测试。功能二是Playwright webview视图,里面可以选择projects,即选择执行测试的浏览器,settings设置,选择显示浏览器,还是trace viewer,Tools目录下有Pick locator,Record new,Record at cursor菜单,点击Pick locator或者录制菜单,会启动一个浏览器,在浏览器中输入被测web应用的url,就可以开始录制了,录制的代码会写到vscode管理下的测试文件中。

Test Explorer实现原理

  那么,如何实现在Test Explorer中显示测试文件并执行的呢?查看playwright vscode插件源码,testTree and testModel文件主要负责以treeview的方式显示测试文件,testModel里面封装了运行或者调试测试的代码。settingView里面是一个webview,palaywright下面的projects,settings,tools等UI的显示,都是由settingView里面编写。playwrightTestServer.ts 和playwrightTestCLI.ts主要是实现运行,录制测试的具体逻辑。backend.ts里面是启动一个websocket服务,通过发送和监听消息来执行测试。

  关于如何通过vscode提供的treeview和testcontroller来实现Test explorer下显示测试文件的部分,可以先阅读这两篇博客,treeview使用,testcontroller使用,这两篇博客中给出了构建treeview和testcontroller的简单易懂的例子。Test explorer中显示的测试文件实现思路和上面例子大致相同,解析source code中测试文件信息,并以treeview的方式显示出来。当选择某个文件执行后,会在测试文件名称下面显示测试执行时间,测试名称等。在测试执行是,获取测试时间,执行状态等信息,组装成testItem,再添加到testcontroller中,通过testcontroller来管理整个测试的生命周期。

  测试执行的底层逻辑是什么

  上面主要介绍的playwright vscode插件的UI部分,那么底层是如何实现测试执行的呢?下面是playwrightTestCLI.ts文件中runTests方法的代码。下面code中,首先设置了一些测试执行参数,例如--headed,--workers,--trace等,然后通过__innerSpawn方法执行测试,另外还创建ReportServer,监听测试执行结果信息。__innerSpawn通过child_process.spawn 方法启动一个新的 Node.js 进程来运行 Playwright 测试。具体步骤和效果如下:

启动一个 Node.js 进程:使用 node 可执行文件和 Playwright CLI 来运行测试。
传递命令行参数
this._model.config.cli: Playwright CLI 路径。
'test': 运行测试的命令。
'-c', configFile: 指定配置文件。
...extraArgs: 额外的命令行参数。
...escapedLocations: 要运行的测试文件或目录。
设置工作目录:使用 configFolder 作为当前工作目录。
设置标准输入输出:重定向子进程的所有标准输入输出流(包括标准输入、标准输出、标准错误输出和额外的自定义流)。
配置环境变量:通过扩展当前进程的环境变量,并添加或覆盖一些特定的环境变量,例如 CI, NODE_OPTIONS, PW_TEST_REUSE_CONTEXT, PW_TEST_CONNECT_WS_ENDPOINT 等。

 async runTests(items: vscodeTypes.TestItem[], options: PlaywrightTestRunOptions, reporter: reporterTypes.ReporterV2, token: vscodeTypes.CancellationToken): Promise<void> {
    const { locations, parametrizedTestTitle } = this._narrowDownLocations(items);
    if (!locations)
      return;
    const args = [];
    this._model.enabledProjectsFilter().forEach(p => args.push(`--project=${p}`));
    if (parametrizedTestTitle)
      args.push(`--grep=${escapeRegex(parametrizedTestTitle)}`);
    args.push('--repeat-each=1');
    args.push('--retries=0');
    if (options.headed)
      args.push('--headed');
    if (options.workers)
      args.push(`--workers=${options.workers}`);
    if (options.trace)
      args.push(`--trace=${options.trace}`);
    await this._innerSpawn(locations, args, options, reporter, token);
  }

  async _innerSpawn(locations: string[], extraArgs: string[], options: PlaywrightTestRunOptions, reporter: reporterTypes.ReporterV2, token: vscodeTypes.CancellationToken) {
    if (token?.isCancellationRequested)
      return;

    // Playwright will restart itself as child process in the ESM mode and won't inherit the 3/4 pipes.
    // Always use ws transport to mitigate it.
    const reporterServer = new ReporterServer(this._vscode);
    const node = await findNode(this._vscode, this._model.config.workspaceFolder);
    const configFolder = path.dirname(this._model.config.configFile);
    const configFile = path.basename(this._model.config.configFile);
    const escapedLocations = locations.map(escapeRegex).sort();

    {
      // For tests.
      const relativeLocations = locations.map(f => path.relative(configFolder, f)).map(escapeRegex).sort();
      const printArgs = extraArgs.filter(a => !a.includes('--repeat-each') && !a.includes('--retries') && !a.includes('--workers') && !a.includes('--trace'));
      this._log(`${escapeRegex(path.relative(this._model.config.workspaceFolder, configFolder))}> playwright test -c ${configFile}${printArgs.length ? ' ' + printArgs.join(' ') : ''}${relativeLocations.length ? ' ' + relativeLocations.join(' ') : ''}`);
    }

    const childProcess = spawn(node, [
      this._model.config.cli,
      'test',
      '-c', configFile,
      ...extraArgs,
      ...escapedLocations,
    ], {
      cwd: configFolder,
      stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
      env: {
        ...process.env,
        CI: this._options.isUnderTest ? undefined : process.env.CI,
        // Don't debug tests when running them.
        NODE_OPTIONS: undefined,
        ...this._options.envProvider(),
        PW_TEST_REUSE_CONTEXT: options.reuseContext ? '1' : undefined,
        PW_TEST_CONNECT_WS_ENDPOINT: options.connectWsEndpoint,
        ...(await reporterServer.env()),
        // Reset VSCode's options that affect nested Electron.
        ELECTRON_RUN_AS_NODE: undefined,
        FORCE_COLOR: '1',
        PW_TEST_HTML_REPORT_OPEN: 'never',
        PW_TEST_NO_REMOVE_OUTPUT_DIRS: '1',
      }
    });

    const stdio = childProcess.stdio;
    stdio[1].on('data', data => reporter.onStdOut?.(data));
    stdio[2].on('data', data => reporter.onStdErr?.(data));
    await reporterServer.wireTestListener(reporter, token);
  }

  在test-explorer中选择某个文件,选择dubug test,会在debug output窗口中显示如下信息,这段信息和上面的代码是完全匹配的,从这里可以看到当在vscode extension窗口中选择某个测试文件,点击执行按钮时,实际背后是通过node执行playwright的cli命令完成执行的。

  有了CLI为什么还要启动Backend呢

  查看testModel.ts文件中的code,会有这段代码,通过这行代码可以知道PlaywrightTestCLI方式是遗留的老方式,新方式是调用PlaywrightTestServer来执行测试。而PlaywrightTestServer.ts文件里面执行测试的时候需要启动Backend server。说明CLI方式是早期的方式,现在又构建了新的方式,即启动backend server的方式来执行测试。

    this._playwrightTest =  this._useLegacyCLIDriver ? new PlaywrightTestCLI(vscode, this, options) : new PlaywrightTestServer(vscode, this, options);

   BackendServer的实现逻辑是什么?

  查看PlaywrightTestServer.ts中runTest方法,实际调用的是testServerConnection.ts中的runTest方法,代码细节如下所示,这表明,启动backend server后,通过发送和监听消息的方式来执行或者录制测试。

  async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
    return await this._sendMessage('runTests', params);
  }

  查看backend.ts的代码,如下所示,这里是启动backend server的方法,可以看到启动backend server本质上也是使用spawn调用node进程而已。

export async function startBackend(vscode: vscodeTypes.VSCode, options: BackendServerOptions & { onError: (error: Error) => void, onClose: () => void }): Promise<string | null> {
  const node = await findNode(vscode, options.cwd);
  const serverProcess = spawn(node, options.args, {
    cwd: options.cwd,
    stdio: 'pipe',
    env: {
      ...process.env,
      ...options.envProvider(),
    },
  });
  serverProcess.stderr?.on('data', data => {
    if (options.dumpIO)
      console.log('[server err]', data.toString());
  });
  serverProcess.on('error', options.onError);
  serverProcess.on('close', options.onClose);
  return new Promise(fulfill => {
    serverProcess.stdout?.on('data', async data => {
      if (options.dumpIO)
        console.log('[server out]', data.toString());
      const match = data.toString().match(/Listening on (.*)/);
      if (!match)
        return;
      const wse = match[1];
      fulfill(wse);
    });
    serverProcess.on('exit', () => fulfill(null));
  });
}

  查看testServerConnection的构造方法,可以看到,这里通过new WebSocket启动了一个websocket服务,且这个服务队message进行监听处理。具体代码如下所示:

constructor(wsURL: string) {
    this.onClose = this._onCloseEmitter.event;
    this.onReport = this._onReportEmitter.event;
    this.onStdio = this._onStdioEmitter.event;
    this.onListChanged = this._onListChangedEmitter.event;
    this.onTestFilesChanged = this._onTestFilesChangedEmitter.event;
    this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event;

    this._ws = new WebSocket(wsURL);
    this._ws.addEventListener('message', event => {
      const message = JSON.parse(String(event.data));
      const { id, result, error, method, params } = message;
      if (id) {
        const callback = this._callbacks.get(id);
        if (!callback)
          return;
        this._callbacks.delete(id);
        if (error)
          callback.reject(new Error(error));
        else
          callback.resolve(result);
      } else {
        this._dispatchEvent(method, params);
      }
    });
    const pingInterval = setInterval(() => this._sendMessage('ping').catch(() => {}), 30000);
    this._connectedPromise = new Promise<void>((f, r) => {
      this._ws.addEventListener('open', () => f());
      this._ws.addEventListener('error', r);
    });
    this._ws.addEventListener('close', () => {
      this._isClosed = true;
      this._onCloseEmitter.fire();
      clearInterval(pingInterval);
    });
  }

  在playwrightTestServer.ts文件中有一个createTestServer的私有方法,该方法返回的是TestServerConnection对象,在这个方法中,首先调用startBackend 得到wsEnpoint,即websocket服务的endpoint信息,在将wsEnpoint传入TestServerConnection这个class中。最终实现启动一个websocket,通过向websocket服务发送消息的方式来执行、停止测试等操作。

 private async _createTestServer(): Promise<TestServerConnection | null> {
    const args = [this._model.config.cli, 'test-server', '-c', this._model.config.configFile];
    const wsEndpoint = await startBackend(this._vscode, {
      args,
      cwd: this._model.config.workspaceFolder,
      envProvider: () => {
        return {
          ...this._options.envProvider(),
          FORCE_COLOR: '1',
        };
      },
      dumpIO: false,
      onClose: () => {
        this._testServerPromise = undefined;
      },
      onError: error => {
        this._testServerPromise = undefined;
      },
    });
    if (!wsEndpoint)
      return null;
    const testServer = new TestServerConnection(wsEndpoint);
    testServer.onTestFilesChanged(params => this._testFilesChanged(params.testFiles));
    await testServer.initialize({
      serializer: require.resolve('./oopReporter'),
      interceptStdio: true,
      closeOnDisconnect: true,
    });
    return testServer;
  }

    查看testserverConnection,可以看到定义了很多message,除了运行和停止测试外,还包括listFile,listtest等。

  总结而言,不管是直接调用playwrighttestCLI文件里面的方法执行测试,还是启动backend的websocket服务,本质上都是启动node,调用playwright.cli.js文件,传入测试文件名称等参数来实现测试执行的。那么如何类似下面的node命令封装成websocket服务来执行测试呢?下一篇博客将介绍这个点的详细内容。

/opt/homebrew/bin/node ./node_modules/@playwright/test/cli.js test -c playwright.config.js --headed --project=chromium --repeat-each=1 --retries=0 --timeout=0 --workers=1 /Users/taoli/study/playwrightDemo/tests/test-1.spec.ts:3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/739968.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

excel字符串列的文本分列合并

excel表有两列&#xff0c;第一列是“姓名”&#xff0c;第二列是“诊断”&#xff0c;有高血压、糖尿病等。我想出一个统计表&#xff0c;将每个人的诊断分为1-N列&#xff0c;比如张三&#xff0c;第一诊断高血压&#xff0c;第二诊断糖尿病&#xff0c;分列显示。我们可以用…

七人团购新体验:解锁数字时代购物新篇章

在数字化浪潮的推动下&#xff0c;购物体验正迈向新的里程碑。其中&#xff0c;七人团购模式以其独特的魅力和创新性&#xff0c;为消费者带来了前所未有的实惠与便利。现在&#xff0c;让我们一同探索这一新兴购物模式的运作机制与潜在价值&#xff0c;特别是针对一款标价599元…

Excel如果将一个表格拆分为多个表格,文末另赠彩蛋!

前期分享如何用数据透视表将一个表格拆分成多个工作薄Excel一个表格拆分多个表格&#xff0c;你学会了吗&#xff1f; 今天刘小生分享另外一种&#xff0c;如果拆分成多个工作表格文件&#xff01; 如何将一个表格根据部门进行拆分成多个表格&#xff0c;再点对点发送给各部门…

MacBook Pro 忘记root用户密码,重置密码步骤

一、以普通用户名登录系统&#xff0c;并打开terminal终端&#xff0c; 输入&#xff1a;sudo bash sudo bash Password:*****&#xff08;输入当前用户的密码&#xff09; 成功后进入bash-3.2#的命令模式 二、在bash-3.2#命令模式下 输入&#xff1a;sudo passwd root sud…

代码随想录算法训练营第三十三天|452. 用最少数量的箭引爆气球、 435. 无重叠区间、 763.划分字母区间

452. 用最少数量的箭引爆气球 题目链接&#xff1a;452. 用最少数量的箭引爆气球 文档讲解&#xff1a;代码随想录 状态&#xff1a;没想出来 思路&#xff1a;对气球终点位置排序&#xff0c;从第一个气球终点位置射出箭&#xff0c;看这支箭可以尽可能穿过几个气球&#xff0…

一文搞懂Linux多线程【上】

目录 &#x1f6a9;引言 &#x1f6a9;再次理解页表 &#x1f6a9;初识线程 &#x1f6a9;线程和pthread库 &#x1f680;线程创建 &#x1f6a9;线程的资源共享问题 &#x1f6a9;线程的优缺点 &#x1f6a9;引言 今天&#xff0c;我们开始学习Linux中的线程部分。Lin…

中国车牌检测数据集VOC+YOLO格式2001张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2001 标注数量(xml文件个数)&#xff1a;2001 标注数量(txt文件个数)&#xff1a;2001 标注…

了解SD-WAN与传统WAN的区别

近年来&#xff0c;许多企业选择了SD-WAN作为他们的网络解决方案。云基础架构的SD-WAN不仅具备成本效益&#xff0c;而且提供更安全、更可靠的WAN连接&#xff0c;有助于实现持续盈利。客户能够更好地控制他们的网络&#xff0c;个性化定制且无需额外成本。 那么&#xff0c;为…

Golang逃逸分析

在Go语言中&#xff0c;逃逸分析(Escape Analysis)是一种编译器优化技术&#xff0c;用于确定变量是应该分配在堆上还是在栈上。这对程序的性能有显著的影响&#xff0c;因为栈上资源的分配速度和释放速度要比堆上快得多&#xff0c;同时堆上的内存管理也更加简单。 基本概念 …

快速鲁棒的 ICP (Fast and Robust Iterative Closest Point)

迭代最近点&#xff08;Iterative Closet Point&#xff0c;ICP&#xff09;算法及其变体是两个点集之间刚性配准的基本技术&#xff0c;在机器人技术和三维重建等领域有着广泛的应用。ICP的主要缺点是&#xff1a;收敛速度慢&#xff0c;以及对异常值、缺失数据和部分重叠的敏…

应用监控eBPF 版调研

参考&#xff1a; https://www.toutiao.com/article/7327353509735596559/?appnews_articletamp1717488680&use_new_style1&req_id20240604161119838096AAE4AD4F44788E&group_id7327353509735596559&wxshare_count1&tt_fromweixin&utm_sourceweixin&…

CentOS7.6安装RabbitMQ

前言&#xff1a;因为RabbitMQ是ERlang语言编写所以要先安装ERlang再安装RabbitMQ 安装ERlang 借鉴前辈原文地址&#xff1a;https://www.cnblogs.com/fengyumeng/p/11133924.html 第一步&#xff1a;安装依赖 yum -y install gcc glibc-devel make ncurses-devel open…

PDF秒变翻页式电子画册

​在当今数字化时代&#xff0c;将PDF文档转换成翻页式电子画册是一种提升作品展示效果和传播效率的有效方式。以下是将PDF秒变翻页式电子画册的攻略&#xff0c;帮助您轻松掌握数字创作技巧。 首先&#xff0c;选择一个合适的制作工具是关键。目前市场上有多种在线平台和软件可…

HTML5休闲小游戏《猫猫超市》源码,引流、刷广告利器

HTML5休闲小游戏《猫猫超市》源码&#xff0c;直接把源码上传到服务器就能使用了&#xff01; 下载链接&#xff1a;https://www.huzhan.com/code/goods467910.html

k8s如何使用 HPA 实现自动扩展

使用Horizontal Pod Autoscaler (HPA) 实验目标&#xff1a; 学习如何使用 HPA 实现自动扩展。 实验步骤&#xff1a; 创建一个 Deployment&#xff0c;并设置 CPU 或内存的资源请求。创建一个 HPA&#xff0c;设置扩展策略。生成负载&#xff0c;观察 HPA 如何自动扩展 Pod…

【Oracle安装】Linux安装Oracle内存不够怎么都装不上,卡在46%、60%、36%;内存不足解决办法,疑难杂症

一、问题描述 1.oracle 安装不报错&#xff0c;但就是无法安装成功&#xff0c;卡住 总是中途卡住&#xff0c;不一定卡在哪儿&#xff0c;也许是36%、46%、60%等等 它也不报错&#xff0c;什么都不说&#xff0c;或者过一会儿服务器自己把oracle的安装进程给杀了&#xff08…

echo 反引号 tail 重定向符

echo 作用&#xff1a;在命令行内输出指定内容&#xff0c;类似与 print()语句。 语法&#xff1a;echo 输出内容 当输出内容复杂时&#xff0c;可以使用""包围。 反引号 被 包围的内容&#xff0c;会被当做命令执行&#xff0c;而非普通字符。 tail 作用&…

2024中国·淮安高端人才精英赛北京分站赛首战告捷

“诚意满淮&#xff0c;创赢未来”&#xff01;6月20-21日&#xff0c;2024中国淮安高端人才精英赛首场分站赛在北京产业创新中心顺利举办。淮安市科技局党组书记、局长胡长青&#xff0c;淮安市委组织部人才处处长沈雪娇&#xff0c;淮安经开区党工委委员、管委会副主任、科技…

【2024德国工作】蓝卡攻略:人在中国,怎么去德国工作?

德国工作签证解析 外国人只要拥有符合德国劳动法的劳动合同&#xff0c;工资符合当地标准&#xff08;非紧缺专业&#xff0c;税前工资一般需达到49600欧元&#xff09;&#xff0c;并且具备一定的外语能力&#xff0c;就可以申请德国境内工作签证&#xff01;不申请者还需要有…

Perplexily首席执行官Aravind Srinivas斯里尼·瓦斯回应抄袭和侵权指控

Perplexity的“答案引擎”工作原理是收集网络上的海量信息&#xff0c;构建一个庞大的内容数据库&#xff08;索引&#xff09;。用户无需输入关键字&#xff0c;只需在Perplexity的平台或应用中提问&#xff0c;就能获得包含引文及网络内容链接的详细回答。 网站通过“机器人…