playwright录制脚本原理

server/2024/11/24 18:32:04/

 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.ppmy.cn/server/51593.html

相关文章

【科研基础】PRML

文章目录 IntroductionSupervised / unsupervised learningOverfittingBayesianIntroduction Supervised / unsupervised learning P3 Overfitting p6 p9: For a given model complexity, the over-fitting problem become less severe as the size of the data set increa…

微信小程序之横向列表展示

效果图 参考微信小程序可看 代码&#xff1a; <view class"lbtClass"><view class"swiper-container"><scroll-view class"swiper" scroll-x"true" :scroll-left"scrollLeft"><block v-for"(six…

主流电商平台API接口【京东商品详情按关键字搜索商品按图搜索京东商品(拍立淘)获得店铺的所有商品】

主流电商平台商品接口在电商企业中的应用可以帮助企业实现信息同步、库存管理、订单处理、数据分析和营销推广等多重功能&#xff0c;提升运营效率、优化用户体验&#xff0c;进而推动业务增长。 jd API 接入说明 API地址:申请调用KEY地址 调用示例&#xff1a; 参数说明 通用…

2024年特种设备(门式起重机司机)考试真题题库。

181."ZZ"表示钢丝绳为&#xff08; &#xff09;。 A.右同向捻 B.左同向捻 C.右交互捻 D.左交互捻 答案:A 182.桥式起重机的金属结构主要由起重机桥架&#xff08;又称大车桥架&#xff09;、&#xff08; &#xff09;和操纵室&#xff08;司机室&#xff09;…

深度學習筆記13-mnist手寫數字識別(Pytorch)

&#x1f368; 本文為&#x1f517;365天深度學習訓練營 中的學習紀錄博客&#x1f356; 原作者&#xff1a;K同学啊 | 接輔導、項目定制 一、我的環境 電腦系統&#xff1a;Windows 10 顯卡&#xff1a;NVIDIA Quadro P620 語言環境&#xff1a;Python 3.7.0 開發工具&…

代码随想录训练营Day48

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、买卖股票的最佳时机4二、买卖股票的最佳时机含冷冻期三、买卖股票含手续费 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 今天是…

layui的 input正则校验必填和必须为正整数

表单部分&#xff1a; <div class"layui-col-md6 layui-col-xs6"><div class"layui-form-item"><label class"layui-form-label">氮化次数</label><div class"layui-input-block"><input type"…

ElasticSearch7.17.19集群搭建+Kibana

下载 1、下载elasticSearch 官网&#xff1a;https://www.elastic.co/cn/downloads/elasticsearch解压 tar -zxvf elasticsearch-7.17.19-linux-x86_64.tar### 修改用户组&#xff0c;es不允许root用户启动 chown es:es elaticsearch### 修改elasticsearch.yml # 所有节点名…