playwright录制脚本原理

news2024/11/18 8:13:45

 Paywright录制工具UI

  在上一篇博客中介绍了如何从0构建一款具备录制UI测试的小工具。此篇博客将从源码层面上梳理playwright录制原理。当打开playwright vscode插件时,点击录制按钮,会开启一个新浏览器,如下图所示,在新开浏览器页面上,有录制,查看等按钮。

  查看vscode的源码,会看到有个recorder的folder,该folder下由react构建了一个应用的UI,执行npm run dev,在5173端口启动这样一个web应用,web应用的UI如下图所示,可以看到里面的录制按钮等和上图vscode插件大家的相同。从这里可以推断,recorder里面用react构建的componen被嵌入到了开启的浏览器中。

 通过上一篇博客的介绍,我们知道,实现录制功能的核心原理是是在浏览器中注入了脚本,通过监听用户行为,并将用户行为转换为playwright的语法,从而实现录制脚本的能力。

Playwright的脚本注入

  查看playwright得source code,在playwright-core/src/server/injected目录下就是注入脚本相关的内容。查看injected/recorder/recorder.ts脚本,在该脚本中在interface RecordTool中定义了大量操作页面元素的方法,例如onClick,onInput等。

  在recorder.ts中,在document对象上添加了很多listener,如下图所示:

  具体每个listener完成了哪些逻辑呢?以onInput为例子,下面的onInput方法的部分代码,可以看到,首先是获取Input的目标对象target,再依次判断Input的具体属性,例如时textarea,或者select,或者checkbox等。根据判断的结果返回不同的内容。

 生成locator

 下面是playwright中生产locator的一个function,可以看到通过注入脚本injectedScript._evaluator.begin()开始,这段代码的主要作用是为指定的HTML元素生成一个或多个唯一的CSS选择器,并返回相关的选择器和匹配的元素列表,以便用于自动化测试或其他需要唯一定位元素的场景。首先是初始化,开始评估选择器生成过程,启用ARIA缓存。如果选项中包含 forTextExpect,则会尝试为目标元素生成一个带有文本的选择器。否则,首先尝试在目标元素的父元素或影子宿主中找到符合特定角色(如按钮、链接等)的元素。然后根据是否允许多个选择器,生成一个或多个选择器,可能包含或不包含文本和CSS ID。生成locator结束后,使用Set去重生成的选择器列表,确保唯一性。最后返回结果。可以看到,为了生成合理的locator,playwright进行很多逻辑处理来保证生成locator的唯一性和合理性。

export function generateSelector(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): { selector: string, selectors: string[], elements: Element[] } {
  injectedScript._evaluator.begin();
  beginAriaCaches();
  try {
    let selectors: string[] = [];
    if (options.forTextExpect) {
      let targetTokens = cssFallback(injectedScript, targetElement.ownerDocument.documentElement, options);
      for (let element: Element | undefined = targetElement; element; element = parentElementOrShadowHost(element)) {
        const tokens = generateSelectorFor(injectedScript, element, { ...options, noText: true });
        if (!tokens)
          continue;
        const score = combineScores(tokens);
        if (score <= kScoreThresholdForTextExpect) {
          targetTokens = tokens;
          break;
        }
      }
      selectors = [joinTokens(targetTokens)];
    } else {
      targetElement = closestCrossShadow(targetElement, 'button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]', options.root) || targetElement;
      if (options.multiple) {
        const withText = generateSelectorFor(injectedScript, targetElement, options);
        const withoutText = generateSelectorFor(injectedScript, targetElement, { ...options, noText: true });
        let tokens = [withText, withoutText];

        // Clear cache to re-generate without css id.
        cacheAllowText.clear();
        cacheDisallowText.clear();

        if (withText && hasCSSIdToken(withText))
          tokens.push(generateSelectorFor(injectedScript, targetElement, { ...options, noCSSId: true }));
        if (withoutText && hasCSSIdToken(withoutText))
          tokens.push(generateSelectorFor(injectedScript, targetElement, { ...options, noText: true, noCSSId: true }));

        tokens = tokens.filter(Boolean);
        if (!tokens.length) {
          const css = cssFallback(injectedScript, targetElement, options);
          tokens.push(css);
          if (hasCSSIdToken(css))
            tokens.push(cssFallback(injectedScript, targetElement, { ...options, noCSSId: true }));
        }
        selectors = [...new Set(tokens.map(t => joinTokens(t!)))];
      } else {
        const targetTokens = generateSelectorFor(injectedScript, targetElement, options) || cssFallback(injectedScript, targetElement, options);
        selectors = [joinTokens(targetTokens)];
      }
    }
    const selector = selectors[0];
    const parsedSelector = injectedScript.parseSelector(selector);
    return {
      selector,
      selectors,
      elements: injectedScript.querySelectorAll(parsedSelector, options.root ?? targetElement.ownerDocument)
    };
  } finally {
    cacheAllowText.clear();
    cacheDisallowText.clear();
    endAriaCaches();
    injectedScript._evaluator.end();
  }
}

   定义在injectedScript.js中的generateSelector方法,实际在recorder.ts中被调用。下图是recorder.ts中onMouseMove方法的完整代码。可以看到,通过注入脚本,playwright获取到了目标对象event,将event作为参数传入方法中,在生成selector部分就是通过injectedScript.generateSelector生成的。

onMouseMove(event: MouseEvent) {
    consumeEvent(event);
    let target: HTMLElement | null = this._recorder.deepEventTarget(event);
    if (!target.isConnected)
      target = null;
    if (this._hoveredElement === target)
      return;
    this._hoveredElement = target;

    let model: HighlightModel | null = null;
    let selectors: string[] = [];
    if (this._hoveredElement) {
      const generated = this._recorder.injectedScript.generateSelector(this._hoveredElement, { testIdAttributeName: this._recorder.state.testIdAttributeName, multiple: false });
      selectors = generated.selectors;
      model = {
        selector: generated.selector,
        elements: generated.elements,
        tooltipText: this._recorder.injectedScript.utils.asLocator(this._recorder.state.language, generated.selector),
        tooltipFooter: selectors.length > 1 ? `Click to select, right-click for more options` : undefined,
        color: this._assertVisibility ? '#8acae480' : undefined,
      };
    }

    if (this._hoveredModel?.selector === model?.selector)
      return;
    this._hoveredModel = model;
    this._hoveredSelectors = selectors;
    this._recorder.updateHighlight(model, true);
  }

    上面大致解释了playwright如何通过注入脚本的方式来录制脚本,接下来看看playwright是如何启动浏览器的。

启动浏览器

   在util/protocol-types-generator目录下的index.js文件中,可以看到调用了playwright.launch()方法可以加载启动不同的浏览器,例如chromium,firefox,webkit等。这里调用的还是playwright-core中的chromium包来launch浏览器的。

  继续查看源代码,可以看到,在playwright-core/server/chromium目录下,有很多代码,这里就是playwright自己实现的管理整个浏览器生命周期的代码。

  以上就是playwright实现录制的大致过程。当调用playwright的功能录制到页面内容后,再调用vscode插件的textedit对象,将生成的内容写入当前打开的测试文件中即可。playwright是一个非常强大的工具,源代码相对比较复杂,如果要快速理解如何通过注入脚本实现录制功能,可以参考上一篇博客,他们在实现思路上是一致的。上一遍博客直接调用puppeteer来启动浏览器,playwright是完全自己实现了浏览器整个生命周期管理,会更加复杂一些。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

理解人体手臂七个自由度对应的运动

写本篇的目的在于&#xff0c;我发现很多人理不清人体手臂运动时内收/外展、屈曲/伸展等动作描述的关系&#xff0c;包括我自己也是&#xff01; 我每次要用到的时候都要去查&#xff0c;记不下来&#xff0c;比较麻烦&#xff0c;于是归纳本篇&#xff0c;包含了我本人的理解 …

Android Media Framework(七)MediaCodecService

Android引入Treble架构后&#xff0c;OpenMAX框架以HIDL Service的形式为System分区提供服务&#xff0c;本文将探讨该服务是如何启动&#xff0c;服务提供了什么内容&#xff0c;以及服务是如何被应用层所使用的。 1 概述 在Android的Treble架构中&#xff0c;为了确保系统的…

LeetCode:经典题之1491、896 题解与延伸

系列目录 88.合并两个有序数组 52.螺旋数组 567.字符串的排列 643.子数组最大平均数 150.逆波兰表达式 61.旋转链表 160.相交链表 83.删除排序链表中的重复元素 389.找不同 1491.去掉最低工资和最高工资后的工资平均值 896.单调序列 206.反转链表 92.反转链表II 141.环形链表 …

HarmonyOS开发 弹窗组件

1.HarmonyOS开发 弹窗组件 弹窗是移动应用中常见的一种用户界面元素&#xff0c;常用于显示一些重要的信息、提示用户进行操作或收集用户输入。ArkTS提供了多种内置的弹窗供开发者使用&#xff0c;除此之外还支持自定义弹窗&#xff0c;来满足各种不同的需求。 1.1. 示例 1.…

基于Java蛋糕甜品商城系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f;感兴趣的可以先收藏起来&#xff0c;还…

封装了一个优雅的iOS转场动画

效果图 代码 // // LBTransition.m // LBWaterFallLayout_Example // // Created by mac on 2024/6/16. // Copyright © 2024 liuboliu. All rights reserved. //#import "LBTransition.h"interface LBPushAnimation:NSObject<UIViewControllerAnimated…

Halcon 如何对区域进行交集,补集,反选,合并操作

1 Intersection交集 dev_open_window(0,0,512,512,black,WindowHandle)gen_circle(Circle1,114.5,127.5,89.3588)gen_circle(Circle2,163.5,171.5,94.8472)intersection(Circle1,Circle2,RegionIntersection)dev_clear_window()dev_display(RegionIntersection)2 Differece 补集…

人机恋爱新趋势:与AI男友谈恋爱的甜蜜与挑战

"我曾经把ChatGPT当成工具&#xff0c;从未追过星&#xff0c;也没有嗑过CP。没想到&#xff0c;到了36岁&#xff0c;我竟然嗑上了AI男友。Open AI&#xff0c;你赢了。你不仅是最好的AI公司&#xff0c;还是乙女游戏公司。" 转行大龄互联网人&#xff0c;走遍20国…

相位和展开相位

相位 (Phase) 相位是一个周期信号在一个周期内的位置&#xff0c;通常以角度&#xff08;度或弧度&#xff09;表示。在许多应用中&#xff0c;相位被限制在一个周期内。例如&#xff0c;相位通常被限定在 −180∘到 180∘ 或 0∘ 到 360∘ 之间。 示例 −90∘ 表示信号在周…

【网络协议】精讲ARP协议工作原理!图解超赞超详细!!!

亲爱的用户&#xff0c;打开微信&#xff0c;搜索公众号&#xff1a;“风云说通信”&#xff0c;即可免费阅读该文章~~ 目录 前言 1. ARP协议介绍 1.1 ARP协议功能 1.2 ARP请求报文 1.3 ARP工作原理 2. ARP 缓存超时 2.1 RARP 3. ARP 攻击 3.1 ARP 攻击分类 前言 首先…

【Linux】—Apache Hive 安装部署

文章目录 前言认识Metadata认识Metastoremetastore三种配置方式 一、安装前准备二、下载hive-3.1.2安装包三、下载完成后&#xff0c;通过xftp6上传到Linux服务器上四、解压Hive安装包五、配置Hive六、内嵌模型安装—Hive元数据配置到Derby七、本地模式安装—Hive元数据配置到M…

Linux:基础IO(三.软硬链接、动态库和静态库、动精态库的制作和加载)

上次介绍了基础IO&#xff08;二&#xff09;&#xff1a;Linux&#xff1a;基础IO&#xff08;二.缓冲区、模拟一下缓冲区、详细讲解文件系统&#xff09; 文章目录 1.软硬链接1.1硬链接1.2软链接使用场景 2.动态库和静态库1.1回顾1.2静态库的制作和使用为什么要有库制作者角度…

基于单片机的智能浇花系统设计与实现

摘要: 设计了一种智能湿度感应浇花系统 。 系统以单片机 AT89S52 为控制芯片&#xff0c;利用 SLHT5-1 土壤湿度传感器来检测土壤的相对湿度&#xff0c;再通过单片机进行信息处理&#xff0c;采用模糊控制方法&#xff0c;输出控制信号&#xff0c;控制继电器的动作&…

[RPI4] 树莓派4b安装istoreos及使用 -- 1. 系统安装

最近在研究家庭智能化的一些东西,其中包括网络,智能家居等一系列内容,然后看过的资料有的想再回来看的时候就找不到了,然后就想着开这么一个系列,做一些记录,先从智能家居开始吧。 1 安装istoreos系统 iStoreOS 目标是提供一个人人会用的路由兼轻 NAS 系统,不管是作为路…

MES管理系统中的仓库管理功能有哪些用途

在当今制造业迅猛发展的背景下&#xff0c;企业对于车间生产调度的需求日益迫切。为此&#xff0c;MES管理系统应运而生&#xff0c;它作为一款专注于车间生产调度的管理信息系统&#xff0c;正逐步成为制造业提升生产效率、优化资源配置的利器。特别是其在仓储和物流管理方面的…

算法社区-从零开始构建(一)

好久没动笔了&#xff0c;一是要处理的东西很多&#xff0c;二则写出来未见得深刻&#xff0c;感觉沉淀得不够&#xff0c;太浅显的东西就没必要分享。 正好最近在研究算法层面的东西&#xff0c;感觉挺受用的&#xff0c;就想着把这些东西整理出来&#xff0c;有点像社区的雏形…

NAPI篇【4】——NAPI应用点亮一个LED

OpenHarmony的NAPI功能为开发者提供了JS与C/C不同语言模块之间的相互访问&#xff0c;交互的能力&#xff0c;使得开发者使用C或者C语言实现应用的关键功能。如操作开发板中某个GPIO节点的状态&#xff08;OpenHarmony并没有提供直接操作GPIO口状态的API&#xff09;&#xff0…

Vue-Cli 创建vue2.0 + TypeScript 项目

这里写目录标题 一、创建项目名称二、选择 Manually select features三、勾选配置项四、选择vue版本五、其它配置 一、创建项目名称 vue create 项目名称&#xff08;项目名字不能含义大写字母&#xff09;二、选择 Manually select features &#xff08;按箭头上下进行移动…

万物皆对象,你信吗?

**内存空间和数据都消失&#xff0c;数据怎么会消失的&#xff1f;**空间没了&#xff0c;数据自然也跟着消失。因为数据就是在空间里面的。就像宇宙大爆炸&#xff0c;我们还能存在嘛&#xff0c;是不是已经undefined了。「一块小内存上有2种数据类型」 内部存储的数据 地址值…

使用Fiddler如何创造大量数据

在调试和分析网络流量时&#xff0c;您是否曾为无法深入了解请求和响应的数据而感到困惑&#xff1f;如果有一种工具可以帮助您轻松抓取和分析网络流量&#xff0c;您的工作效率将大大提升。Fiddler正是这样一款功能强大的抓包工具&#xff0c;广受开发者和测试人员的青睐。 Fi…