ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Nut.js桌面自动化测试:跨平台原生应用UI测试实战指南

Nut.js桌面自动化测试:跨平台原生应用UI测试实战指南 1. 项目概述当UI自动化测试遇上Nut.js最近在捣鼓UI自动化测试发现了一个挺有意思的玩意儿——Nut.js。这可不是什么坚果或者零食而是一个基于Node.js的跨平台桌面自动化库。你可能听说过Selenium、Playwright或者Puppeteer它们都是浏览器自动化领域的佼佼者。但Nut.js走的是另一条路它专注于原生桌面应用的自动化。这意味着你可以用它来控制Windows、macOS或者Linux系统上的任何桌面程序从记事本、计算器到Photoshop、Visual Studio Code甚至是游戏客户端。我花了些时间深度体验发现它确实为UI自动化测试特别是桌面端打开了一扇新的大门。如果你正在为如何自动化测试一个传统的Win32应用、一个Java Swing程序或者一个macOS上的原生App而头疼那Nut.js很可能就是你要找的“瑞士军刀”。2. Nut.js的核心设计理念与优势解析2.1 为什么是Nut.js定位与差异化在UI自动化测试的生态里工具的选择往往取决于你的测试对象。对于Web应用我们有丰富的选择但对于桌面应用尤其是那些没有Web界面或者混合架构的应用选择就少得多且往往伴随着高昂的学习成本和许可费用。Nut.js的出现精准地填补了这个空白。它的核心设计理念是“简单、灵活、跨平台”。它不试图成为一个大而全的框架而是提供了一套底层、高效的API让你能够直接与操作系统的图形界面和输入设备进行交互。这与基于浏览器引擎的工具如Playwright有本质区别。Playwright通过控制浏览器来模拟用户行为而Nut.js则是直接模拟键盘敲击、鼠标移动和屏幕图像识别。这种“直连”操作系统的方式带来了几个关键优势真正的跨平台支持一套脚本理论上可以在Windows、macOS和Linux上运行无需为不同平台重写核心逻辑。这对于需要保证多平台一致性的产品如Electron应用、跨平台工具软件的测试来说价值巨大。无侵入性你不需要在被测应用中注入任何代码、插件或者开启特殊的调试模式。Nut.js像一个真正的用户一样从外部操作应用这降低了对被测应用架构的依赖也使得测试更接近真实用户场景。强大的图像识别能力这是Nut.js的杀手锏之一。它内置了基于OpenCV的模板匹配功能。当你的应用控件无法通过传统的 accessibility tree如UIA、AX API定位时比如游戏界面、自定义绘制的控件、某些老旧应用你可以直接截取控件图片作为“模板”让Nut.js在屏幕上找到它并与之交互。这在测试一些“黑盒”应用时非常有用。2.2 核心架构与关键技术栈Nut.js的架构非常清晰。它主要建立在几个核心模块之上nut-tree/nut-js 这是核心库提供了屏幕操作截图、找图、鼠标键盘控制、剪贴板访问等基础能力。nut-tree/template-matcher 专门负责图像识别和模板匹配的模块精度和性能都不错。nut-tree/plugin-ocr 光学字符识别插件。当需要从图像中读取文字时比如验证一个对话框的提示信息这个插件就派上用场了。它通常与Tesseract.js集成。它的工作流程可以概括为定位 - 操作 - 断言。定位通过图像匹配find、颜色匹配或者未来可能支持的Accessibility API来找到屏幕上的目标元素。操作使用mouse和keyboard模块对定位到的目标执行点击、拖拽、输入文本等操作。断言通过再次截图、图像匹配或OCR读取结果与预期状态进行比对验证测试是否通过。这种基于图像和输入模拟的范式决定了它的使用场景和最佳实践。它不是万能的但在其擅长的领域——桌面原生应用自动化——表现得相当出色。3. 从零开始Nut.js环境搭建与第一个脚本3.1 环境准备与项目初始化开始之前你需要确保系统已经安装了Node.js建议版本16或以上和npm。然后创建一个新的项目目录并初始化。mkdir nutjs-demo cd nutjs-demo npm init -y接下来安装Nut.js的核心包。由于我们主要关注桌面自动化先安装基础包和模板匹配器。npm install nut-tree/nut-js nut-tree/template-matcher如果你需要OCR功能可以额外安装OCR插件及其依赖例如Tesseract。不过对于入门前两个包就足够了。注意Nut.js在Windows上依赖一些系统组件。首次运行时它可能会提示你安装Microsoft Visual C Redistributable按照提示操作即可。在macOS和Linux上通常需要确保系统已安装必要的图形库如OpenCV的开发包不过Nut.js的安装脚本通常会尝试自动处理。3.2 编写并运行你的第一个自动化脚本让我们写一个经典的“Hello World”脚本自动打开Windows自带的“记事本”输入一段文字然后保存。这个例子能直观地展示Nut.js的工作方式。首先创建一个文件比如first-test.js。const { mouse, keyboard, Key, screen, imageResource, straightTo, centerOf, sleep } require(nut-tree/nut-js); // 注意我们这里先不用find函数因为它来自template-matcher我们先用更基础的方式演示 (async () { console.log(测试开始自动化记事本); // 1. 模拟按下Win键打开开始菜单 await keyboard.pressKey(Key.LeftSuper); // Win键 await keyboard.releaseKey(Key.LeftSuper); await sleep(500); // 等待开始菜单弹出 // 2. 输入“notepad”并回车打开记事本 await keyboard.type(notepad); await sleep(300); await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); await sleep(1000); // 等待记事本完全打开 // 3. 在记事本中输入文本 await keyboard.type(Hello, this is typed by Nut.js!); await sleep(500); // 4. 模拟保存操作CtrlS await keyboard.pressKey(Key.LeftControl); await keyboard.pressKey(Key.S); await keyboard.releaseKey(Key.S); await keyboard.releaseKey(Key.LeftControl); await sleep(800); // 等待“另存为”对话框弹出 // 5. 输入文件名并保存这里假设直接保存到默认位置实际可能需要处理对话框 // 为了简化我们直接按回车使用默认文件名和位置 await keyboard.pressKey(Key.Enter); await keyboard.releaseKey(Key.Enter); console.log(测试完成记事本已打开、输入文本并尝试保存。); })();运行这个脚本node first-test.js你会看到屏幕上的光标自己动了起来开始菜单弹出记事本被打开文字被输入。虽然这个脚本很基础也没有做任何验证但它清晰地展示了Nut.js如何通过模拟键盘事件来控制整个系统。实操心得在编写这类脚本时sleep函数的使用非常关键。因为UI操作有延迟你必须给应用程序足够的时间来响应上一个操作。但盲目使用固定的sleep时间会导致脚本不稳定运行快慢不同的机器上可能失败。更好的实践是结合“等待条件”比如等待某个特定窗口出现、某个像素点颜色变化或者使用后面会讲到的图像识别来等待目标出现。这里为了示例清晰使用了固定延时。4. 核心能力深度解析图像识别与精准控制4.1 基于模板匹配的智能元素定位上面例子中的操作是“盲操”我们知道记事本大概会在哪里打开。但在真实的自动化测试中我们需要精确地定位到特定的按钮、图标或区域。这就是nut-tree/template-matcher模块大显身手的地方。假设我们要测试一个计算器应用并点击其中的“加号”按钮。首先你需要准备一张“加号”按钮的截图作为模板图片例如plus.png。这张图不需要很大只要能清晰显示按钮特征即可。const { mouse, screen, imageResource, straightTo, centerOf } require(nut-tree/nut-js); const { find } require(nut-tree/template-matcher); (async () { // 0. 确保计算器应用已经在前台打开 console.log(寻找计算器的加号按钮...); // 1. 加载模板图片 const plusButtonTemplate await imageResource(plus.png); // 图片放在项目根目录或指定路径 try { // 2. 在屏幕上查找匹配该模板的区域 // find函数会返回一个Region对象描述了匹配到的区域在屏幕上的位置和大小 const plusButtonRegion await find(plusButtonTemplate); // 3. 将鼠标移动到该区域的中心并点击 await mouse.move(straightTo(centerOf(plusButtonRegion))); await mouse.leftClick(); console.log(成功找到并点击了加号按钮); } catch (error) { console.error(未能在屏幕上找到加号按钮模板:, error.message); } })();find函数的核心是模板匹配算法。它会遍历屏幕或你指定的一个区域寻找与模板图片最相似的部分。你可以通过参数控制匹配的精度confidence阈值默认0.99、搜索区域和是否彩色匹配等。注意事项图像匹配对视觉变化非常敏感。如果按钮的样式、颜色、大小甚至光照条件发生了变化匹配就可能失败。因此模板图片最好取自与被测环境一致的界面状态相同的主题、分辨率、缩放比例。对于动态或样式多变的UI可能需要准备多套模板或者结合其他定位方式。4.2 鼠标与键盘的精细化操作Nut.js提供了对鼠标和键盘极其细致的控制远超简单的“点击”和“输入”。鼠标操作移动mouse.move()支持绝对坐标、相对移动以及像上面例子中straightTo这样的路径规划模拟人类直线移动。点击左键、右键、中键单击、双击都支持。拖拽mouse.drag()可以实现按下、移动、释放的完整拖拽操作。滚动mouse.scrollUp(),mouse.scrollDown()可以控制滚轮。键盘操作单键keyboard.pressKey(Key.A)和keyboard.releaseKey(Key.A)。组合键通过顺序按压和释放模拟如CtrlC。文本输入keyboard.type(“text”)会自动处理大小写和部分符号。特殊键Key枚举包含了几乎所有你能想到的键包括功能键、媒体键、方向键等。一个实用的技巧是模拟复杂的快捷键操作比如在IDE中格式化代码通常是CtrlAltL或CmdOptionL。await keyboard.pressKey(Key.LeftControl); await keyboard.pressKey(Key.LeftAlt); await keyboard.pressKey(Key.L); await sleep(50); // 极短的延迟确保系统识别为组合键 await keyboard.releaseKey(Key.L); await keyboard.releaseKey(Key.LeftAlt); await keyboard.releaseKey(Key.LeftControl);4.3 屏幕交互截图、取色与OCR除了控制获取屏幕状态同样重要。截图screen.grab()可以截取全屏或指定区域的图像用于保存为证据或后续的图像分析。取色screen.colorAt(point)可以获取屏幕上某个坐标点的RGB颜色值。这在验证某个状态指示灯是否变绿时非常有用。OCR结合nut-tree/plugin-ocr你可以从截图中提取文字。这对于验证对话框消息、读取软件版本号等文本信息至关重要。// 示例验证对话框标题 const { screen } require(nut-tree/nut-js); const { readText } require(nut-tree/plugin-ocr); (async () { // 假设一个错误对话框弹出了我们截取它的标题栏区域 const dialogRegion {x: 100, y: 100, width: 400, height: 30}; // 需要根据实际情况调整区域 const dialogScreenshot await screen.grabRegion(dialogRegion); const extractedText await readText(dialogScreenshot); if (extractedText.includes(错误)) { console.log(检测到错误对话框正在处理...); // 执行处理操作比如点击“确定” } })();5. 构建健壮的UI自动化测试套件5.1 测试结构与组织从脚本到套件单个脚本解决单个任务但真正的自动化测试需要一套组织良好、可维护的套件。你可以将Nut.js与任何你喜欢的Node.js测试框架结合使用比如Jest、Mocha或AVA。我个人推荐Jest因为它内置了断言、测试生命周期钩子和漂亮的报告。首先安装Jestnpm install --save-dev jest然后配置package.json{ scripts: { test: jest } }接下来我们可以将之前的计算器点击测试改造成一个Jest测试用例。创建一个文件calculator.test.js。const { mouse, screen, imageResource, straightTo, centerOf } require(nut-tree/nut-js); const { find } require(nut-tree/template-matcher); describe(计算器基础功能测试, () { // 在每个测试用例开始前可以做一些准备工作比如确保计算器窗口在最前 beforeEach(async () { // 这里可以加入激活计算器窗口的代码例如通过模拟AltTab console.log(准备测试环境...); await new Promise(resolve setTimeout(resolve, 1000)); // 简单等待 }); test(应该能识别并点击加号按钮, async () { // 这是一个图像识别测试 const plusButtonTemplate await imageResource(templates/plus.png); // 设置查找参数降低一点精度要求以增加容错 const matchOptions { confidence: 0.9 }; await expect(find(plusButtonTemplate, matchOptions)).resolves.toBeDefined(); // 如果find成功即找到了这个断言就会通过 // 实际上我们可能更想点击它那么可以这样 try { const region await find(plusButtonTemplate, matchOptions); await mouse.move(straightTo(centerOf(region))); await mouse.leftClick(); // 点击后可以再截图断言结果区域的变化 } catch (e) { // 如果没找到Jest会将此作为测试失败 throw new Error(加号按钮未找到: ${e.message}); } }, 30000); // 设置这个测试的超时时间为30秒 test(验证1加1等于2, async () { // 这是一个完整的流程测试 // 步骤1: 点击1 // 步骤2: 点击 // 步骤3: 点击1 // 步骤4: 点击 // 步骤5: 对结果显示区域进行OCR断言文字是“2” // 这里省略具体实现展示思路 console.log(执行计算流程...); // ... 一系列鼠标点击操作 ... // const resultRegion {x: ...}; // const resultText await readText(await screen.grabRegion(resultRegion)); // expect(resultText.trim()).toBe(2); }); });这样你就拥有了一个结构化的测试文件。运行npm testJest会自动发现并运行所有*.test.js文件并给出清晰的测试报告。5.2 页面对象模式Page Object在Nut.js中的应用对于复杂的应用直接在测试用例中堆砌find和click操作会导致代码难以维护。我们可以借鉴Web自动化测试中的“页面对象模式”Page Object Model, POM。为应用的每个主要窗口或界面创建一个类封装该界面的元素定位和操作。例如为计算器创建一个CalculatorPage类// pages/CalculatorPage.js const { mouse, straightTo, centerOf } require(nut-tree/nut-js); const { find } require(nut-tree/template-matcher); const { imageResource } require(nut-tree/nut-js); class CalculatorPage { constructor() { // 可以在这里预加载所有模板图片 this.templates {}; } async loadTemplate(name) { if (!this.templates[name]) { this.templates[name] await imageResource(templates/calc_${name}.png); } return this.templates[name]; } async clickButton(buttonName) { const template await this.loadTemplate(buttonName); const region await find(template, { confidence: 0.92 }); await mouse.move(straightTo(centerOf(region))); await mouse.leftClick(); await mouse.sleep(100); // 每次点击后稍作停顿 } async add(a, b) { // 假设我们有数字和运算符的模板 await this.clickButton(digit_${a}); await this.clickButton(plus); await this.clickButton(digit_${b}); await this.clickButton(equals); } async getResult() { // 定位结果显示屏区域进行OCR // 返回识别到的文本 // ... OCR实现 ... } } module.exports CalculatorPage;然后在测试中使用就非常清晰了// tests/calculator-pom.test.js const CalculatorPage require(../pages/CalculatorPage); describe(使用POM的计算器测试, () { let calculator; beforeAll(async () { calculator new CalculatorPage(); }); test(加法测试, async () { await calculator.add(5, 3); const result await calculator.getResult(); expect(result).toBe(8); }); });这种模式极大地提高了代码的可读性和可维护性。当计算器界面按钮图片更新时你只需要更新templates/目录下的图片文件并可能微调confidence值而不需要修改每一个测试用例。5.3 等待策略与稳定性提升UI自动化测试最大的敌人是不稳定Flaky Tests。Nut.js脚本的不稳定性主要来源于应用响应速度操作后应用需要时间渲染。系统性能波动CPU、内存占用导致操作延迟。图像匹配时机目标出现前就开始查找。除了使用固定的sleep我们必须实现更智能的等待。1. 轮询等待Polling Wait 这是最常用的策略。不断尝试某个操作如查找图像直到成功或超时。async function waitForImage(template, timeoutMs 10000, intervalMs 500) { const startTime Date.now(); while (Date.now() - startTime timeoutMs) { try { const region await find(template, { confidence: 0.9 }); console.log(找到图像耗时 ${Date.now() - startTime}ms); return region; } catch (error) { // 没找到等待一段时间再试 await new Promise(resolve setTimeout(resolve, intervalMs)); } } throw new Error(在 ${timeoutMs}ms 内未找到目标图像); } // 使用方式 const saveButton await imageResource(save.png); await waitForImage(saveButton, 15000); // 最多等15秒 await mouse.click(centerOf(saveButtonRegion));2. 结合视觉与逻辑的混合等待 有时仅仅出现某个元素还不够还需要它处于“可交互”状态。例如一个按钮出现了但可能是灰色的。你可以结合颜色检查。async function waitForButtonActive(buttonTemplate, activeColorRGB, timeoutMs 10000) { const region await waitForImage(buttonTemplate, timeoutMs); const startTime Date.now(); while (Date.now() - startTime timeoutMs) { const centerColor await screen.colorAt(centerOf(region)); // 简单比较颜色实际中可能需要更复杂的比较函数 if (colorsAreSimilar(centerColor, activeColorRGB)) { return region; } await sleep(200); } throw new Error(按钮未在超时时间内变为激活状态); }3. 重试机制 对于非关键步骤或已知的不稳定操作可以在测试框架层面如Jest配置重试逻辑或者在脚本中自己实现简单的重试。在Jest中可以这样配置重试// jest.config.js module.exports { // ... 其他配置 retryTimes: 2, // 失败后重试2次 };通过组合使用这些策略可以显著提升Nut.js自动化脚本的稳定性和可靠性。6. 常见问题排查与实战技巧实录6.1 图像匹配失败原因分析与调试手段图像匹配是Nut.js中最强大也最易出问题的环节。当你发现find函数总是抛出“未找到匹配”的错误时可以按以下步骤排查第一步检查模板图片本身截图质量模板是否清晰有没有多余的半透明边框、阴影最好使用无损PNG格式。尺寸比例模板图片的尺寸是否与屏幕上实际显示的元素尺寸一致特别注意系统显示缩放如Windows的125%、150%缩放。最佳实践是在100%缩放比例下截取模板并在相同的缩放比例下运行脚本。内容唯一性模板内容是否具有唯一性一个纯色的圆形按钮可能在整个屏幕上有很多相似区域匹配就会出错。尽量截取包含部分周围特征区域的图片增加唯一性。第二步调整匹配参数置信度confidence默认0.99非常高。如果界面有细微反锯齿变化或颜色微差可以适当调低比如0.92或0.85。但调得太低会增加误匹配风险。搜索区域searchRegion如果知道目标大致出现在屏幕的哪个区域比如某个窗口内可以指定searchRegion来缩小搜索范围这能极大提升查找速度和准确性。匹配方法Nut.js底层使用OpenCV的模板匹配方法。虽然API可能没有直接暴露所有方法但了解原理有帮助。TM_CCOEFF_NORMED默认对光照变化有一定鲁棒性。调试技巧保存调试截图在匹配失败时将当前屏幕截图和模板图片一起保存下来进行人工比对。const screenImage await screen.grab(); await screenImage.toFile(debug_screen.png); console.log(屏幕截图已保存为 debug_screen.png请与模板对比。);可视化匹配区域写一个辅助函数在找到目标后用鼠标移动或画框的方式高亮出匹配到的区域确认定位是否准确。6.2 跨平台兼容性陷阱“一次编写到处运行”是理想但现实是不同平台的UI细节、快捷键、甚至应用本身都存在差异。快捷键差异最典型的例子是CtrlvsCmd。你需要根据平台动态选择。const modifierKey process.platform darwin ? Key.LeftSuper : Key.LeftControl; await keyboard.pressKey(modifierKey); await keyboard.pressKey(Key.C); // ... 释放应用路径与启动方式不同系统上打开计算器的方式可能不同calc命令在Windows有效在macOS是open -a Calculator。你需要准备不同的启动脚本或使用条件判断。UI细微差别按钮样式、字体渲染、窗口装饰可能不同。这意味着你可能需要为Windows、macOS和Linux准备三套不同的模板图片。在项目里可以这样组织templates/ ├── win/ │ └── button_ok.png ├── mac/ │ └── button_ok.png └── linux/ └── button_ok.png然后在代码中根据process.platform动态加载对应平台的模板。6.3 性能优化与执行速度UI自动化测试通常不会太快但我们可以避免让它变得更慢。限制搜索区域这是提升find速度最有效的方法。尽量通过其他方式如窗口位置确定一个较小的searchRegion。合理使用截图全屏截图 (screen.grab()) 和高分辨率下的区域截图都是耗时的操作。避免在循环中频繁进行全屏截图。并行执行如果测试套件中有多个独立的测试用例例如测试不同的功能模块可以考虑使用Jest的--maxWorkers参数进行并行测试。但要注意真正的UI操作很难并行因为鼠标和键盘是全局资源并行操作会相互干扰。通常并行指的是非UI的单元测试部分或者将UI测试拆分成完全独立的作业在不同的机器或虚拟机上运行。模板图片优化在不影响识别率的前提下尽量缩小模板图片的尺寸。更小的图片意味着更快的像素比对。6.4 与CI/CD流水线集成将Nut.js测试集成到持续集成/持续部署CI/CD流水线中如Jenkins, GitLab CI, GitHub Actions可以实现自动化回归测试。关键考虑点无头环境HeadlessCI服务器通常没有图形界面。Nut.js本身需要图形环境来模拟输入和捕获屏幕。解决方案是使用虚拟显示服务器如Xvfb(X Virtual Framebuffer)。在Linux CI上在运行测试前启动Xvfb。# .gitlab-ci.yml 示例片段 test: stage: test script: - apt-get update apt-get install -y xvfb # 安装Xvfb - Xvfb :99 -screen 0 1920x1080x24 - export DISPLAY:99 - npm test在Windows CI上可能需要配置允许服务与桌面交互或者使用Windows自带的虚拟显示驱动对于较新的Windows Server版本。更稳定的做法是使用提供了UI的Windows CI代理如Azure DevOps的Windows代理。依赖安装确保CI环境中安装了Nut.js所需的所有原生依赖如OpenCV库。这通常可以通过包管理器apt,yum,brew解决或者使用已经预装好环境的Docker镜像。测试报告与制品配置Jest或其他测试框架生成JUnit格式的XML报告方便CI平台如GitLab, Jenkins解析和展示。同时将测试失败时的屏幕截图、日志文件作为构建制品Artifacts保存下来便于后续排查。稳定性与重试在CI环境中由于资源竞争测试可能更不稳定。务必启用前面提到的重试机制并对超时时间给予更宽松的设定。7. 超越测试Nut.js的其他应用场景虽然我们主要讨论测试但Nut.js的能力远不止于此。它的本质是一个桌面自动化机器人这打开了更多可能性软件安装与配置自动化为新机器批量安装和配置软件自动点击“下一步”、同意协议、设置路径。数据录入与迁移将数据从一个旧版桌面软件不支持API导出中通过模拟操作“录入”到新的Web系统中。日常任务自动化自动登录某个客户端、下载日报、整理到指定文件夹。任何你每天手动在电脑上重复的操作都可以尝试用Nut.js自动化。监控与告警定期对某个业务系统进行截图通过图像识别或OCR判断关键指标是否正常异常时发送告警。辅助游戏自动执行游戏中重复性的任务需注意游戏规则避免违规。在这些场景下你更像是一个“流程自动化工程师”而不仅仅是测试工程师。思路从“验证正确性”转变为“可靠地执行一系列预定操作”。8. 总结与个人体会折腾Nut.js有一段时间了它给我的感觉更像是一把精巧的“螺丝刀”而不是一把“瑞士军刀”。它不提供现成的、针对特定应用如Chrome的高级API而是给了你最基础的“拧螺丝”的能力——控制鼠标、键盘、看屏幕。这带来了极高的灵活性但也意味着你需要自己构建很多工具比如智能等待、页面对象。它的优势在跨平台桌面原生应用的自动化上非常明显。当你面对一个没有API、没有可访问性信息、甚至文档都稀少的“黑盒”桌面软件时基于图像的Nut.js往往是唯一可行的自动化方案。它的学习曲线比Playwright这类Web自动化工具要陡峭一些主要难点在于图像模板的管理和脚本的稳定性调优。我个人最大的体会是成功的Nut.js自动化项目30%靠编码70%靠对被测应用行为的理解和稳定的环境控制。你需要花大量时间去观察应用的反应速度、理解各种状态变化、准备健壮的模板图片、设计合理的等待和重试逻辑。这是一项更偏向于“工程”而不仅仅是“编程”的工作。最后一个小技巧在开发调试阶段不妨把鼠标移动速度调慢加上一些高亮显示比如找到元素后画个红框这样你能清晰地看到脚本每一步在做什么就像看一个慢速回放对于排查问题有奇效。当脚本稳定后再把这些调试代码去掉或禁用。Nut.js可能不会成为你唯一的自动化测试工具但当你的测试边界从浏览器扩展到整个桌面时它绝对是一个值得放入工具箱的利器。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进