UI Recorder扩展开发指南:从录制插件到自定义模板实战

📅 2026/6/30 18:19:30
UI Recorder扩展开发指南:从录制插件到自定义模板实战
1. 项目概述从录制到扩展UI Recorder的进阶之路如果你已经用UI Recorder做过一些自动化测试录制回放玩得挺溜那你可能已经遇到了它的“天花板”。比如想录一个带复杂拖拽的组件发现它不支持或者想把录制的脚本直接生成符合公司内部框架的测试用例而不是默认的WebDriverIO代码又或者每次录制前都要手动设置一堆浏览器参数烦不胜烦。这个时候你就需要把手伸向UI Recorder的扩展开发了。这不仅仅是“高级玩法”而是真正把工具变成自己趁手兵器的必经之路。简单来说UI Recorder本身是一个优秀的录制回放工具但它不可能预知所有业务场景和技术栈。它的强大之处在于提供了一个可扩展的架构允许我们通过开发自定义的“录制插件”来捕获更多类型的用户操作以及通过定制“模板”来改变最终生成的测试代码结构。这就像给你一套乐高基础颗粒你可以按照官方图纸搭出一个房子但如果你想造一座带可动炮塔的城堡就得自己设计并制作一些特殊形状的零件。本教程要做的就是教你如何设计和制作这些“特殊零件”。这个教程适合谁首先你得对UI Recorder的基本使用比较熟悉知道怎么录、怎么放。其次你需要有一定的JavaScript/Node.js基础因为扩展开发本质上是在写Node.js模块。最后也是最重要的你需要有明确的“痛点”或“定制化需求”。如果你只是想用用现成的功能那可能暂时用不上但如果你想打造一个深度贴合自己团队技术栈和测试规范的自动化工具链那么接下来的内容就是为你准备的。2. 核心概念拆解插件与模板到底是什么在动手之前我们必须把两个核心概念——录制插件和模板——彻底掰扯清楚。很多人容易混淆导致开发时方向错误。2.1 录制插件捕获行为的“传感器”你可以把录制过程想象成在用户界面上布满了各种传感器。默认的UI Recorder自带了一套基础传感器能感知点击、输入、跳转等常见操作。录制插件就是你自己新增的、更 specialized 的传感器。它的核心职责是监听特定的用户操作或浏览器事件并将其转化为一条结构化的“命令”。这条命令最终会被添加到录制脚本中。例如默认插件监听一个button的click事件生成{command: ‘click’, target: ‘css#submit-btn’, value: ‘’}。自定义拖拽插件监听dragstart,dragover,drop等一系列事件最终合成一条命令{command: ‘dragAndDrop’, source: ‘css.item’, target: ‘css.drop-zone’}。自定义文件上传插件监听input type“file”的change事件捕获文件路径生成{command: ‘uploadFile’, target: ‘css#file-input’, value: ‘/path/to/file.pdf’}。插件开发的关键在于事件监听与命令合成。你需要在页面的上下文中通常通过注入JavaScript代码实现精准地捕获事件并处理好事件间的时序和依赖关系最终输出一条干净、可回放的命令。2.2 模板脚本的“模具”与“翻译官”如果说插件决定了“录什么”那么模板就决定了“录出来的代码长什么样”。模板是一个代码生成器它接收录制插件产生的原始命令序列然后按照你定义的格式和规则输出最终的测试脚本。它的核心职责是将通用的命令对象翻译成特定测试框架/风格的源代码。例如同一条click命令WebDriverIO模板可能生成await $(‘#submit-btn’).click();Puppeteer模板可能生成await page.click(‘#submit-btn’);你司内部测试框架模板可能生成TestAction.click(ElementLocator.byId(‘submit-btn’));模板开发的关键在于字符串处理和逻辑抽象。你需要设计模板文件的语法通常是基于类似EJS、Handlebars的模板引擎并编写生成器逻辑处理条件判断如if/else、循环如遍历步骤、变量插入等使得输出的代码不仅语法正确而且结构清晰、符合团队规范。注意插件和模板是松耦合的。一个自定义插件录制的命令可以被任何模板使用反之一个自定义模板也可以处理所有插件包括默认插件产生的命令。这给了我们极大的灵活性。3. 开发环境搭建与项目结构剖析工欲善其事必先利其器。在开始编码前我们需要一个正确的开发环境并透彻理解UI Recorder扩展项目的组织结构。3.1 环境准备与初始化首先确保你的系统已经安装了Node.js建议LTS版本如16.x或18.x和npm。接下来我们不是直接修改UI Recorder主程序而是创建一个独立的扩展项目。# 1. 创建一个新的目录作为你的扩展项目 mkdir my-ui-recorder-extension cd my-ui-recorder-extension # 2. 初始化npm项目 npm init -y # 3. 安装UI Recorder作为开发依赖或peerDependency以便引用其类型定义和工具函数 # 注意这里需要根据你使用的UI Recorder具体版本和发布方式来安装。 # 假设它已发布到npm你可以 npm install ui-recorder --save-dev # 或者如果你是基于某个特定分支开发可能需要链接本地版本。一个典型的扩展项目结构如下所示my-ui-recorder-extension/ ├── package.json ├── src/ │ ├── plugins/ # 存放自定义录制插件 │ │ ├── my-drag-plugin.js │ │ └── my-upload-plugin.js │ └── templates/ # 存放自定义模板 │ ├── my-custom-template/ │ │ ├── index.js # 模板入口文件 │ │ ├── template.ejs # 模板文件以EJS为例 │ │ └── assets/ # 静态资源可选 │ └── another-template/ ├── dist/ # 构建输出目录可选 └── README.md3.2 理解插件与模板的契约接口UI Recorder通过预定义的接口与插件和模板进行交互。了解这些接口是开发的基础。对于录制插件它通常需要导出一个类或对象包含以下关键方法init(context): 初始化方法接收一个上下文对象其中包含浏览器页面的实例如Puppeteer的page对象、事件总线等。你在这里注入监听脚本。handleEvent(eventData): 可选处理从页面传递过来的自定义事件。getSupportedCommands(): 返回一个数组声明本插件能生成哪些命令类型如[‘dragAndDrop’, ‘customSwipe’]。cleanup(): 清理方法移除事件监听释放资源。对于模板它通常需要导出一个对象包含以下关键属性name: 模板名称用于在UI Recorder界面中选择。description: 模板描述。generate(commands, options): 核心方法接收命令数组和选项返回生成的代码字符串。fileExtension: 生成文件的后缀名如.js,.spec.js,.py。实操心得在开始编码前最好的方法是先研究UI Recorder自带的默认插件和模板源码。它们位于UI Recorder安装目录的lib/plugins和lib/templates下。这是最准确、最直接的学习资料能让你快速理解其内部工作机制和接口细节避免自己凭空想象。4. 实战一开发一个自定义拖拽录制插件让我们通过一个实际案例——为可排序列表开发拖拽录制插件来深入插件开发的全过程。我们的目标是当用户拖拽一个列表项改变其顺序时能录制下这个操作。4.1 需求分析与设计假设我们有一个使用Sortable.js库实现的列表。拖拽操作涉及多个事件dragstart开始拖、dragover经过目标、drop放下。我们不可能录制每一个细碎的鼠标事件那样回放会非常不可靠。我们需要的是语义化的录制录制“将A元素拖放到B位置”这个意图。因此插件设计思路是向页面注入脚本监听相关元素的拖拽事件。在dragstart时记录被拖拽的元素源元素。在drop时记录拖放的目标位置可能是某个容器或另一个元素。将“源”和“目标”信息合成一条dragAndDrop命令。4.2 插件代码实现以下是插件src/plugins/drag-and-drop-plugin.js的核心代码框架// 引入可能需要的工具具体取决于UI Recorder提供的运行时环境 // const { BasePlugin } require(ui-recorder/plugin-base); class DragAndDropPlugin { constructor() { this.name dragAndDropPlugin; this.supportedCommands [dragAndDrop]; this.draggedElement null; } // 初始化注入监听脚本 async init(context) { this.page context.page; // 假设context提供了puppeteer page对象 this.eventBus context.eventBus; // 用于发送录制命令 // 向页面注入JavaScript代码来监听拖拽事件 await this.page.addScriptTag({ content: (function() { // 监听全局的dragstart事件记录源元素 document.addEventListener(dragstart, function(event) { // 这里需要一种方式将元素信息传递回Node.js环境 // 通常可以通过window.postMessage或暴露全局函数 window.__uiRecorderDragStart { selector: getUniqueSelector(event.target), // 需要一个生成选择器的函数 tagName: event.target.tagName, // ... 其他有用信息 }; }, true); // 监听drop事件合成命令并通知录制器 document.addEventListener(drop, function(event) { event.preventDefault(); if (!window.__uiRecorderDragStart) return; const source window.__uiRecorderDragStart; const targetSelector getUniqueSelector(event.target); // 发送消息给插件 const message { type: DRAG_AND_DROP, data: { command: dragAndDrop, source: source.selector, target: targetSelector, timestamp: Date.now() } }; // 通过某种机制将message发送到Node.js端例如通过console.log一个特殊格式或者通过page.exposeFunction console.log([UI-RECORDER-DRAG] JSON.stringify(message)); }, true); // 一个简单的获取元素唯一选择器的函数实际应用需要更健壮的版本 function getUniqueSelector(el) { if (el.id) return # el.id; let path []; while (el el.nodeType Node.ELEMENT_NODE) { let selector el.nodeName.toLowerCase(); if (el.className) { selector . el.className.trim().replace(/\\s/g, .); } path.unshift(selector); el el.parentNode; } return path.join( ) || ; } })(); }); // 监听页面console消息捕获我们注入脚本打印的命令 this.page.on(console, async (msg) { const text msg.text(); if (text.startsWith([UI-RECORDER-DRAG])) { try { const message JSON.parse(text.replace([UI-RECORDER-DRAG], )); if (message.type DRAG_AND_DROP) { // 通过事件总线或直接调用方式添加命令到录制序列 this.eventBus.emit(command, message.data); } } catch (e) { console.error(解析拖拽命令失败:, e); } } }); } getSupportedCommands() { return this.supportedCommands; } async cleanup() { // 移除事件监听清理全局变量 await this.page.removeScriptTag(/* 需要保存之前注入的脚本标识 */); this.page.off(console, this._consoleHandler); } } module.exports DragAndDropPlugin;4.3 插件注册与集成开发完成后需要让UI Recorder知道这个插件的存在。通常有两种方式配置文件在UI Recorder的配置文件中如ui-recorder.config.js添加插件路径。动态加载UI Recorder提供API在启动时加载插件。假设采用配置文件方式// ui-recorder.config.js module.exports { plugins: [ require.resolve(./src/plugins/drag-and-drop-plugin), // ... 其他插件 ], // ... 其他配置 };注意事项与避坑指南选择器稳定性上面例子中的getUniqueSelector函数非常简陋。在生产环境中你必须使用更稳健的算法来生成唯一且回放时能准确定位的选择器可以考虑使用css-selector-generator这类库。事件干扰你注入的脚本可能会影响页面原有逻辑比如阻止了事件的默认行为。要确保你的监听器在完成工作后不影响页面的正常功能。有时需要使用passive: true等选项。通信机制本例通过console.log进行通信简单但不一定是最优解。更可靠的方式是利用page.exposeFunction在浏览器端和Node.js端建立直接的函数调用通道。异步处理页面事件是异步的要处理好命令生成的时序避免并发拖拽导致命令错乱。5. 实战二开发一个自定义模板以生成Jest风格测试用例为例现在假设你们的前端测试框架是Jest并且希望录制的脚本能直接生成*.test.js文件集成到现有的Jest测试流水线中。我们来开发一个自定义模板。5.1 模板结构与设计我们的模板将放在src/templates/jest-template/目录下。它需要生成类似下面的代码结构describe(‘用户登录流程’, () { beforeEach(async () { await page.goto(‘https://example.com’); }); it(‘应该能成功登录’, async () { await page.click(‘#username’); await page.type(‘#username’, ‘testuser’); await page.click(‘#password’); await page.type(‘#password’, ‘password123’); await page.click(‘#submit’); await expect(page).toMatchElement(‘.welcome-message’); }); });我们需要处理生成describe/it块、将通用命令转换为Puppeteer API调用、插入断言等。5.2 模板文件与生成器实现第一步创建模板文件 (template.ejs)。我们使用EJS语法因为它直观易懂。describe(‘% testSuiteName %’, () { % if (beforeEachHook) { % beforeEach(async () { await page.goto(‘% initialUrl %’); }); % } % % commands.forEach((cmd, index) { % % if (cmd.command ‘click’) { % await page.click(‘% cmd.target %’); % } else if (cmd.command ‘type’) { % await page.type(‘% cmd.target %’, ‘% cmd.value %’); % } else if (cmd.command ‘dragAndDrop’) { % // 这里需要调用自定义的拖拽辅助函数 await dragAndDrop(‘% cmd.source %’, ‘% cmd.target %’); % } % % // 可以在特定命令后插入断言例如在登录点击后检查URL % % if (cmd.command ‘click’ cmd.target ‘#submit’) { % await expect(page.url()).toMatch(/dashboard/); % } % % }); % });第二步创建模板入口文件 (index.js)。这是模板的核心逻辑。const ejs require(‘ejs’); const fs require(‘fs’); const path require(‘path’); class JestTemplate { constructor() { this.name ‘jest-puppeteer’; this.description ‘生成适用于Jest Puppeteer的测试用例’; this.fileExtension ‘.test.js’; } generate(commands, options {}) { // 读取模板文件 const templatePath path.join(__dirname, ‘template.ejs’); const templateStr fs.readFileSync(templatePath, ‘utf-8’); // 准备模板数据 const data { testSuiteName: options.testSuiteName || ‘录制的测试用例’, initialUrl: options.initialUrl || commands[0]?.url || ‘about:blank’, beforeEachHook: options.beforeEachHook ! false, // 默认生成beforeEach commands: this._preprocessCommands(commands), // 预处理命令 // 可以传入更多自定义选项如是否生成截图代码、超时时间等 }; // 渲染模板 let generatedCode ejs.render(templateStr, data); // 后处理添加必要的导入语句和辅助函数 generatedCode this._addImportsAndHelpers(generatedCode, options); return generatedCode; } _preprocessCommands(commands) { // 对命令进行清洗和转换 return commands.map(cmd { // 例如确保选择器格式符合Puppeteer要求 let target cmd.target; if (target target.startsWith(‘css’)) { target target.substring(4); } return { …cmd, target }; }).filter(cmd [‘click’, ‘type’, ‘dragAndDrop’].includes(cmd.command)); // 过滤支持的命令 } _addImportsAndHelpers(code, options) { const imports const { toMatchImageSnapshot } require(‘jest-image-snapshot’); expect.extend({ toMatchImageSnapshot }); // 拖拽辅助函数需要在实际项目中实现或引入 async function dragAndDrop(sourceSelector, targetSelector) { const source await page.$(sourceSelector); const target await page.$(targetSelector); const sourceBox await source.boundingBox(); const targetBox await target.boundingBox(); await page.mouse.move(sourceBox.x sourceBox.width / 2, sourceBox.y sourceBox.height / 2); await page.mouse.down(); await page.mouse.move(targetBox.x targetBox.width / 2, targetBox.y targetBox.height / 2); await page.mouse.up(); } ; return imports ‘\n\n’ code; } } module.exports new JestTemplate(); // 导出一个实例5.3 模板配置与使用同样需要在UI Recorder的配置中注册这个模板// ui-recorder.config.js module.exports { templates: { ‘jest’: require.resolve(‘./src/templates/jest-template’), }, defaultTemplate: ‘jest’, // 设置默认模板 // ... 其他配置 };实操心得模板设计的艺术可配置性好的模板应该提供丰富的选项options比如是否生成beforeEach/afterEach钩子、是否添加页面截图断言、自定义超时时间等。这能让模板适应更多场景。代码质量生成的代码应该格式优美、符合团队的lint规范。可以考虑集成prettier在生成后格式化代码。可维护性模板逻辑不宜过于复杂。将不同的命令转换逻辑拆分成独立的函数或子模板便于维护和扩展。例如可以为dragAndDrop命令单独写一个转换器。处理边界情况思考如果命令序列为空怎么办如果第一个命令不是导航命令怎么办模板应该足够健壮能处理这些情况或者给出友好的提示。6. 高级技巧与集成策略掌握了插件和模板的基础开发后我们可以探讨一些进阶话题让你的扩展更强大、更专业。6.1 插件间的协同与事件总线复杂的操作可能需要多个插件协同工作。例如一个“上传文件然后提交表单”的流程可能涉及“文件上传插件”和“表单提交插件”。它们之间如何通信这时事件总线Event Bus就派上用场了。UI Recorder的核心通常会提供一个全局的事件总线。插件可以在初始化时订阅和发布事件。发布事件当一个插件完成某个阶段如文件已选择它可以发布一个file:selected事件并携带文件信息。订阅事件另一个插件如表单提交插件可以订阅file:selected事件当收到事件后再执行后续的“点击提交按钮”操作并在生成的命令中关联前一个操作。这允许你将一个完整的用户流程拆解成多个原子插件提高复用性。在你的插件init方法中可以这样使用async init(context) { this.eventBus context.eventBus; // 订阅事件 this.eventBus.on(‘file:selected’, (fileData) { this.currentFile fileData; }); // 发布事件 this.eventBus.emit(‘plugin:ready’, { name: this.name }); }6.2 模板的动态片段与代码生成优化对于大型项目生成的测试代码可能非常长。我们可以通过“动态片段”和“代码分割”来优化。动态片段Partial类似于EJS的%- include(‘partials/header’) %你可以将常用的代码块如登录函数、页面对象模型定义抽离成独立的片段文件在模板中引入。按场景生成不是所有命令都需要转换成同样的代码。可以在模板的generate方法中先对命令序列进行分析和分组。例如将所有在同一个页面内的操作分组并为每个页面生成一个独立的describe块使得测试结构更清晰。生成Page Object更高级的模板可以直接生成Page Object模型。分析录制过程中频繁操作的元素自动生成一个对应的Page Class将page.click(‘#submit’)升级为LoginPage.submit()大幅提升生成代码的可维护性。6.3 扩展的调试、打包与分发调试调试插件最有效的方法是利用Node.js的调试工具和浏览器的DevTools。使用--inspect-brk参数启动UI Recorder。在Chrome DevTools中连接Node.js调试器可以给你的插件代码打断点。同时利用Puppeteer的page.evaluateOnNewDocument和page.exposeFunction在浏览器端输出详细的日志帮助理解事件流。打包与分发当你开发了一个好用的扩展想分享给团队时需要打包。使用Webpack或Rollup将你的插件/模板代码可能包含多个文件打包成一个单独的UMD或CommonJS文件。在package.json中定义好入口文件main字段和必要的元数据。可以发布到私有的npm仓库或者直接通过git仓库依赖。编写清晰的README.md说明安装、配置和使用方法并提供简单的示例。7. 常见问题排查与实战经验录在实际开发中你会遇到各种各样的问题。这里记录一些典型问题的排查思路和我踩过的坑。7.1 插件问题排查清单问题现象可能原因排查步骤插件已加载但无法录制特定操作1. 事件监听未正确注入。2. 选择器无法在回放时定位元素。3. 事件触发时机不对如动态加载内容。1. 检查注入脚本的page.addScriptTag是否成功在浏览器控制台查看是否有错误。2. 在回放环境中手动执行生成的选择器看能否找到元素。3. 尝试使用MutationObserver监听DOM变化或在操作前增加等待逻辑。录制的命令顺序错乱异步事件处理不当多个命令同时触发。1. 在插件内部引入命令队列确保命令按顺序发出。2. 为命令添加更精确的时间戳在模板生成时进行排序。插件导致原页面功能异常注入的脚本阻止了事件冒泡/默认行为或污染了全局变量。1. 检查事件监听器是否使用了passive: true或确保在必要时调用event.preventDefault()。2. 将插件使用的全局变量封装在唯一的命名空间下如window.__UI_RECORDER_MY_PLUGIN。7.2 模板问题排查清单问题现象可能原因排查步骤生成的代码无法运行语法错误1. 模板语法错误。2. 命令数据包含非法字符如未转义的单引号。3. 生成的代码格式混乱。1. 先用一组简单的命令数据测试模板确保基础生成逻辑正确。2. 对插入到代码字符串中的变量如cmd.value进行严格的转义处理。3. 集成代码格式化工具如prettier对输出进行后处理。生成的代码运行结果与录制不符1. 命令转换逻辑有误如选择器转换错误。2. 缺少必要的等待或断言。3. 页面状态在录制和回放时不一致。1. 对比录制时的原始命令和模板生成的代码逐行检查转换逻辑。2. 在容易出问题的操作如跳转、弹窗后模板应自动生成等待语句如await page.waitForNavigation()。3. 考虑在模板中引入“上下文感知”能力根据上一个命令推断是否需要等待。模板性能差生成慢1. 模板渲染逻辑复杂尤其是循环嵌套过多。2. 在generate方法中进行了同步的IO操作如频繁读文件。1. 优化模板逻辑减少不必要的循环和条件判断。2. 将静态资源如辅助函数代码缓存起来避免每次生成都读取。7.3 来自实战的几点核心经验选择器可靠性是第一生命线无论插件做得多花哨如果生成的选择器回放时找不到元素一切归零。投入精力打造一个健壮的选择器生成算法比开发十个新插件都重要。优先考虑id、>