瀏覽代碼

feat (1.40.0 -> 1.41.0): 添加Monkey Testing功能集成

在main.js、electron-builder.json和README.md中实现完整的Monkey Testing测试系统集成,支持开发者菜单启动和参数配置

- 在main.js中集成启动参数解析和开发者菜单支持
- 在electron-builder.json中更新文件包含配置,确保测试模块被打包
- 在README.md中添加详细的使用说明文档
- 新增electron/test.js文件,实现Monkey Testing核心功能类和菜单创建

改动文件:
- README.md
- main.js
- electron-builder.json
- electron/test.js
dengdx 1 周之前
父節點
當前提交
a34de0b1ea
共有 7 個文件被更改,包括 642 次插入14 次删除
  1. 17 0
      CHANGELOG.md
  2. 74 0
      README.md
  3. 4 4
      config/dev.ts
  4. 1 1
      electron-builder.json
  5. 491 0
      electron/test.js
  6. 54 8
      main.js
  7. 1 1
      package.json

+ 17 - 0
CHANGELOG.md

@@ -2,6 +2,23 @@
 
 本项目的所有重要变更都将记录在此文件中。
 
+## [1.41.0] - 2025-12-31 22:42
+
+### 新增 (Added)
+
+- **Monkey Testing 功能集成** - 添加了完整的 Monkey Testing 测试系统
+  - 在 main.js 中集成 Monkey Testing 启动参数解析和开发者菜单
+  - 在 electron-builder.json 中更新文件包含配置,确保测试模块被打包
+  - 在 README.md 中添加详细的使用说明文档
+  - 新增 electron/test.js 文件,实现 Monkey Testing 核心功能
+
+**改动文件:**
+
+- README.md
+- main.js
+- electron-builder.json
+- electron/test.js
+
 ## [1.40.0] - 2025-12-31 18:48
 
 ### 新增 (Added)

+ 74 - 0
README.md

@@ -136,3 +136,77 @@ node scripts/extract-i18n-json.js
 - npx cypress open --e2e
 
 最后人工选择要执行的用例
+
+# Monkey Testing 使用说明
+
+## 启动方式
+
+### 1. 启动时开启开发者菜单
+
+通过启动参数启用开发者菜单,菜单中包含Monkey Testing相关功能:
+
+```bash
+# 开发环境
+npm run start:electron -- --enable-dev-menu
+
+# 生产环境(已打包的应用)
+zsis.exe --enable-dev-menu
+dros.exe --enable-dev-menu
+```
+
+**参数说明**:
+- `--enable-dev-menu` : 启用开发者菜单,包含Monkey Testing启动选项
+- `--enable-dev-tools` : 启动时自动打开开发者工具(可选)
+
+### 2. 从菜单启动Monkey Test
+
+启用开发者菜单后,在应用菜单栏的 **View → Start Monkey Test** 中启动:
+
+1. 启动应用时添加 `--enable-dev-menu` 参数
+2. 在菜单中选择 **View → Start Monkey Test**
+3. 在弹出的参数输入窗口中设置:
+   - **Interval (ms)**: 操作间隔时间,默认1000ms
+   - **Duration (ms)**: 测试持续时间,默认30000ms (30秒)
+   - **Max Operations**: 最大操作次数,默认30次
+4. 点击 **Start Test** 开始测试
+
+## 测试期间功能
+
+### 菜单自动隐藏
+- 测试启动后,应用菜单栏会自动隐藏,避免干扰测试
+- 使用快捷键 **Ctrl+Alt+M** 可以临时显示菜单
+- 菜单会在5秒后自动隐藏
+
+### 停止测试
+测试会根据设置的持续时间或操作次数自动停止,也可以:
+- 再次按 **Ctrl+Alt+M** 显示菜单
+- 手动停止测试(需要添加停止功能)
+
+## 参数说明
+
+| 参数 | 说明 | 默认值 | 单位 |
+|------|------|--------|------|
+| Interval | 每次操作的时间间隔 | 100 | 毫秒 |
+| Duration | 测试总持续时间 | 2147483646 | 毫秒 |
+| Max Operations | 最大操作次数限制 | 600000 | 次 |
+
+## 操作类型
+
+Monkey Test 会随机执行以下操作:
+- 鼠标移动到随机位置
+- 鼠标左键点击
+- 鼠标双击
+- 鼠标右键点击
+- 键盘按键输入(a、b、c、Enter、Space、Tab)
+
+## 安全说明
+
+- 所有操作仅限于应用窗口内部
+- 自动避开窗口边缘50像素的安全区域
+- 支持运行时动态启动和停止
+- 测试期间菜单隐藏,但可通过快捷键访问
+
+## 快捷键汇总
+
+- **Ctrl+Alt+M**: 测试期间临时显示/隐藏菜单
+- **F12**: 切换开发者工具(如果启用)

+ 4 - 4
config/dev.ts

@@ -81,16 +81,16 @@ export default {
     devServer: {
       proxy: {
         '/dr': {
-          target: 'http://192.168.110.13', // 你的后端服务地址
-          // target: 'http://192.168.110.245',
+          // target: 'http://192.168.110.13', // 你的后端服务地址
+          target: 'http://192.168.110.245',
           changeOrigin: true, // 允许跨域
           // pathRewrite: {
           //   '^/dr/api': '' // 可选,用于重写路径
           // }
         },
         '/mqtt': {
-          target: 'ws://192.168.110.13:8083', // MQTT WebSocket 服务地址
-          // target: 'ws://192.168.110.245:8083',
+          // target: 'ws://192.168.110.13:8083', // MQTT WebSocket 服务地址
+          target: 'ws://192.168.110.245:8083',
           changeOrigin: true,
           ws: true, // 启用 WebSocket 代理
           // pathRewrite: {

+ 1 - 1
electron-builder.json

@@ -4,7 +4,7 @@
   "electronVersion": "36.2.1",
   "asar": true,
   "compression": "store",
-  "files": ["main.js", "src/log/**/*", "!node_modules/**/*", "preload.js"],
+  "files": ["main.js", "src/log/**/*", "!node_modules/**/*", "preload.js","electron/**/*"],
   "extraFiles": [
     {
       "from": "dist/h5",

+ 491 - 0
electron/test.js

@@ -0,0 +1,491 @@
+// electron/test.js - ESM 模块版本
+// Monkey Testing 和开发工具模块
+
+import { writeLog } from '../src/log/log-writer.js';
+import { BrowserWindow, ipcMain, Menu, globalShortcut } from 'electron';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+// 工具函数:安全的 JSON 序列化
+function safeStringify(obj) {
+  const cache = new Set();
+  return JSON.stringify(obj, (key, value) => {
+    if (typeof value === 'object' && value !== null) {
+      if (cache.has(value)) {
+        return '[Circular]';
+      }
+      cache.add(value);
+    }
+    return value;
+  });
+}
+
+// -------------- 命令行参数解析 --------------
+/**
+ * 解析命令行参数
+ * @returns {Object} 解析后的选项
+ */
+export function parseCommandLineArgs() {
+  const args = process.argv;
+
+  return {
+    enableMonkeyTesting: args.includes('--enable-monkey-testing'),
+    enableDevMenu: args.includes('--enable-dev-menu'),
+    enableDevTools: args.includes('--enable-dev-tools')
+  };
+}
+
+// -------------- Monkey Testing 功能类 --------------
+/**
+ * Monkey Testing 测试器
+ * 用于模拟随机的用户操作,进行压力测试
+ */
+export class MonkeyTester {
+  /**
+   * 创建 MonkeyTester 实例
+   * @param {BrowserWindow} win - Electron 窗口实例
+   * @param {boolean} isMac - 是否为macOS平台
+   * @param {BrowserWindow} mainWindow - 主窗口实例
+   */
+  constructor(win, isMac = false, mainWindow = null) {
+    this.win = win;
+    this.isMac = isMac;
+    this.mainWindow = mainWindow;
+    this.active = false;
+    this.interval = null;
+    this.originalMenu = null; // 保存原始菜单
+  }
+
+  /**
+   * 获取随机位置(在窗口内容区域内)
+   * @returns {Object} {x, y} 坐标
+   */
+  getRandomPosition() {
+    const bounds = this.win.getContentBounds();
+    const margin = 5; // 安全边距,避免点击到窗口边缘
+
+    return {
+      x: Math.floor(Math.random() * (bounds.width - 2 * margin)) + margin,
+      y: Math.floor(Math.random() * (bounds.height - 2 * margin)) + margin
+    };
+  }
+
+  /**
+   * 移动鼠标到指定位置
+   */
+  moveMouseTo(x, y) {
+    if (this.win && !this.win.isDestroyed()) {
+      this.win.webContents.sendInputEvent({
+        type: 'mouseMove',
+        x: x,
+        y: y
+      });
+    }
+  }
+
+  /**
+   * 模拟点击操作
+   */
+  performClick(x, y, button = 'left') {
+    if (this.win && !this.win.isDestroyed()) {
+      // 鼠标按下
+      this.win.webContents.sendInputEvent({
+        type: 'mouseDown',
+        x: x,
+        y: y,
+        button: button
+      });
+
+      // 短暂延迟后释放
+      setTimeout(() => {
+        if (this.win && !this.win.isDestroyed()) {
+          this.win.webContents.sendInputEvent({
+            type: 'mouseUp',
+            x: x,
+            y: y,
+            button: button
+          });
+        }
+      }, 50);
+    }
+  }
+
+  /**
+   * 模拟键盘输入
+   */
+  simulateKeyPress(key) {
+    if (this.win && !this.win.isDestroyed()) {
+      this.win.webContents.sendInputEvent({
+        type: 'keyDown',
+        keyCode: key
+      });
+
+      setTimeout(() => {
+        if (this.win && !this.win.isDestroyed()) {
+          this.win.webContents.sendInputEvent({
+            type: 'keyUp',
+            keyCode: key
+          });
+        }
+      }, 50);
+    }
+  }
+
+  /**
+   * 执行随机操作
+   */
+  performRandomOperation() {
+    if (!this.win || this.win.isDestroyed()) return;
+
+    const operations = ['move', 'click', 'doubleClick', 'rightClick', 'keyPress'];
+    const operation = operations[Math.floor(Math.random() * operations.length)];
+    const pos = this.getRandomPosition();
+
+    switch (operation) {
+      case 'move':
+        this.moveMouseTo(pos.x, pos.y);
+        break;
+      case 'click':
+        this.performClick(pos.x, pos.y);
+        break;
+      case 'doubleClick':
+        this.performClick(pos.x, pos.y);
+        this.performClick(pos.x, pos.y);
+        break;
+      case 'rightClick':
+        this.performClick(pos.x, pos.y, 'right');
+        break;
+      case 'keyPress':
+        const keys = ['a', 'b', 'c', 'Enter', 'Space', 'Tab'];
+        const key = keys[Math.floor(Math.random() * keys.length)];
+        this.simulateKeyPress(key);
+        break;
+    }
+  }
+
+  /**
+   * 启动 Monkey Testing
+   * @param {Object} options - 配置选项
+   * @param {number} options.interval - 操作间隔(ms)
+   * @param {number} options.duration - 持续时间(ms)
+   * @param {number} options.maxOperations - 最大操作次数
+   */
+  start(options = {}) {
+    if (this.active) return;
+
+    const { interval = 100, duration = 2147483646, maxOperations = 600000 } = options;
+
+    this.active = true;
+    let operationCount = 0;
+
+    writeLog('info', `启动monkey testing: 间隔${interval}ms, 持续${duration}ms, 最大操作${maxOperations}次`);
+
+    // 保存原始菜单并隐藏菜单
+    this.originalMenu = Menu.getApplicationMenu();
+    Menu.setApplicationMenu(null);
+    writeLog('info', '菜单已隐藏,测试期间不可见');
+
+    // 注册全局快捷键 Ctrl+Alt+M 来显示菜单
+    globalShortcut.register('CommandOrControl+Alt+M', () => {
+      if (this.originalMenu) {
+        Menu.setApplicationMenu(this.originalMenu);
+        writeLog('info', '菜单已通过快捷键显示');
+
+        // 5秒后自动隐藏菜单
+        setTimeout(() => {
+          if (this.active) {
+            Menu.setApplicationMenu(null);
+            writeLog('info', '菜单已自动隐藏');
+          }
+        }, 5000);
+      }
+    });
+    writeLog('info', '全局快捷键 Ctrl+Alt+M 已注册,可用于临时显示菜单');
+
+    this.interval = setInterval(() => {
+      if (!this.active || operationCount >= maxOperations) {
+        this.stop();
+        return;
+      }
+
+      this.performRandomOperation();
+      operationCount++;
+    }, interval);
+
+    // 超时自动停止
+    setTimeout(() => {
+      this.stop();
+    }, duration);
+  }
+
+  /**
+   * 停止 Monkey Testing
+   */
+  stop() {
+    if (!this.active) return;
+
+    this.active = false;
+    if (this.interval) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+
+    // 恢复菜单
+    if (this.originalMenu) {
+      Menu.setApplicationMenu(this.originalMenu);
+      this.originalMenu = null;
+      writeLog('info', '菜单已恢复显示');
+    }
+
+    // 注销全局快捷键
+    globalShortcut.unregister('CommandOrControl+Alt+M');
+    writeLog('info', '全局快捷键已注销');
+
+    writeLog('info', '停止monkey testing');
+  }
+
+  /**
+   * 检查是否正在运行
+   */
+  isActive() {
+    return this.active;
+  }
+}
+
+/**
+ * 输出调试信息
+ * @param {Object} options - 解析后的命令行选项
+ */
+export function logDebugInfo(options) {
+  writeLog('log', '=== Electron Debug Info ===');
+  writeLog('log', `Command line args: ${safeStringify(process.argv)}`);
+  writeLog('log', `Parsed options: ${safeStringify(options)}`);
+  writeLog('log', '===========================');
+}
+
+/**
+ * 显示Monkey Test参数输入对话框
+ * @param {BrowserWindow} mainWindow - 主窗口实例
+ */
+export function showMonkeyInputDialog(mainWindow) {
+  const __filename = fileURLToPath(import.meta.url);
+  const __dirname = dirname(__filename);
+
+  const inputWindow = new BrowserWindow({
+    width: 400,
+    height: 300,
+    parent: mainWindow,
+    modal: true,
+    show: false,
+    webPreferences: {
+      nodeIntegration: true, // 允许输入窗口使用 require
+      contextIsolation: false
+    },
+    resizable: false,
+    title: 'Monkey Test Parameters'
+  });
+
+  const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Monkey Test Parameters</title>
+  <style>
+    body {
+      font-family: Arial, sans-serif;
+      padding: 20px;
+      background-color: #f5f5f5;
+    }
+    .form-group {
+      margin-bottom: 15px;
+    }
+    label {
+      display: block;
+      margin-bottom: 5px;
+      font-weight: bold;
+    }
+    input {
+      width: 100%;
+      padding: 8px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      box-sizing: border-box;
+    }
+    .buttons {
+      text-align: right;
+      margin-top: 20px;
+    }
+    button {
+      padding: 8px 16px;
+      margin-left: 10px;
+      border: none;
+      border-radius: 4px;
+      cursor: pointer;
+    }
+    .start-btn {
+      background-color: #007bff;
+      color: white;
+    }
+    .cancel-btn {
+      background-color: #6c757d;
+      color: white;
+    }
+  </style>
+</head>
+<body>
+  <h3>Monkey Test Parameters</h3>
+  <form id="monkeyForm">
+    <div class="form-group">
+      <label for="interval">Interval (ms):</label>
+      <input type="number" id="interval" value="100" min="10" required>
+    </div>
+    <div class="form-group">
+      <label for="duration">Duration (ms):</label>
+      <input type="number" id="duration" value="2147483646" min="1000" required>
+    </div>
+    <div class="form-group">
+      <label for="maxOperations">Max Operations:</label>
+      <input type="number" id="maxOperations" value="600000" min="1" required>
+    </div>
+    <div class="buttons">
+      <button type="button" class="cancel-btn" onclick="window.close()">Cancel</button>
+      <button type="submit" class="start-btn">Start Test</button>
+    </div>
+  </form>
+
+  <script>
+    const { ipcRenderer } = require('electron');
+
+    document.getElementById('monkeyForm').addEventListener('submit', (e) => {
+      e.preventDefault();
+      
+      const interval = parseInt(document.getElementById('interval').value);
+      const duration = parseInt(document.getElementById('duration').value);
+      const maxOperations = parseInt(document.getElementById('maxOperations').value);
+      
+      ipcRenderer.send('start-monkey-test', { interval, duration, maxOperations });
+      window.close();
+    });
+  </script>
+</body>
+</html>
+  `;
+
+  inputWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
+
+  inputWindow.once('ready-to-show', () => {
+    inputWindow.show();
+  });
+
+  inputWindow.on('closed', () => {
+    // 清理引用
+  });
+}
+
+/**
+ * 创建开发者菜单模板
+ * @param {boolean} isMac - 是否为macOS平台
+ * @param {BrowserWindow} mainWindow - 主窗口实例
+ * @returns {Array} 菜单模板数组
+ */
+export function createDevMenuTemplate(isMac, mainWindow) {
+  return [
+    ...(isMac ? [{
+      label: 'Electron',
+      submenu: [
+        { role: 'about' },
+        { type: 'separator' },
+        { role: 'services' },
+        { type: 'separator' },
+        { role: 'hide' },
+        { role: 'hideOthers' },
+        { role: 'unhide' },
+        { type: 'separator' },
+        { role: 'quit' }
+      ]
+    }] : []),
+    {
+      label: 'File',
+      submenu: [
+        isMac ? { role: 'close' } : { role: 'quit' }
+      ]
+    },
+    {
+      label: 'Edit',
+      submenu: [
+        { role: 'undo' },
+        { role: 'redo' },
+        { type: 'separator' },
+        { role: 'cut' },
+        { role: 'copy' },
+        { role: 'paste' },
+        ...(isMac ? [
+          { role: 'pasteAndMatchStyle' },
+          { role: 'delete' },
+          { role: 'selectAll' },
+          { type: 'separator' },
+          {
+            label: 'Speech',
+            submenu: [
+              { role: 'startSpeaking' },
+              { role: 'stopSpeaking' }
+            ]
+          }
+        ] : [
+          { role: 'delete' },
+          { type: 'separator' },
+          { role: 'selectAll' }
+        ])
+      ]
+    },
+    {
+      label: 'View',
+      submenu: [
+        { role: 'reload' },
+        { role: 'forceReload' },
+        { role: 'toggleDevTools' },
+        { type: 'separator' },
+        { role: 'resetZoom' },
+        { role: 'zoomIn' },
+        { role: 'zoomOut' },
+        { type: 'separator' },
+        { role: 'togglefullscreen' },
+        { type: 'separator' },
+        { label: 'Start Monkey Test', click: () => showMonkeyInputDialog(mainWindow) }
+      ]
+    },
+    {
+      label: 'Window',
+      submenu: [
+        { role: 'minimize' },
+        { role: 'close' },
+        ...(isMac ? [
+          { type: 'separator' },
+          { role: 'front' },
+          { type: 'separator' },
+          { role: 'window' }
+        ] : [
+          { role: 'close' }
+        ])
+      ]
+    },
+    ...(isMac ? [{
+      role: 'help',
+      submenu: [
+        {
+          label: 'Learn More',
+          click: async () => {
+            const { shell } = require('electron');
+            await shell.openExternal('https://electronjs.org');
+          }
+        }
+      ]
+    }] : [])
+  ];
+}
+
+// 导出便捷函数:创建 MonkeyTester 实例
+export function createMonkeyTester(win) {
+  return new MonkeyTester(win);
+}

+ 54 - 8
main.js

@@ -51,8 +51,18 @@ const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
 const execAsync = promisify(exec);
 
+// -------------- 导入测试模块 --------------
+import { parseCommandLineArgs, MonkeyTester, createDevMenuTemplate } from './electron/test.js';
+
+// -------------- 全局Monkey Tester实例 --------------
+let monkeyTester = null;
+
 let win=null;
 
+// -------------- 解析启动参数 --------------
+writeLog('info', `启动参数-解析前: ${safeStringify(process.argv)}`);
+const launchOptions = parseCommandLineArgs();
+writeLog('info', `启动参数: ${safeStringify(launchOptions)}`);
 // -------------- 单实例检查 ------------
 // 1. 请求单实例锁
 const gotTheLock = app.requestSingleInstanceLock();
@@ -74,27 +84,49 @@ app.on('second-instance', (ev, cmd, cwd, additionalData) => {
 function createWindow() {
   const isMac = process.platform === 'darwin';
 
-  win = new BrowserWindow({
+  // 根据启动参数调整窗口配置
+  const windowConfig = {
     show: false,
-    frame: false,
-    titleBarStyle: 'hidden',
     webPreferences: {
       nodeIntegration: false,
       contextIsolation: true,
       preload: join(__dirname, 'preload.js'),
+      devTools: launchOptions.enableDevTools, // 根据启动参数启用开发者工具
     },
-  });
+  };
+
+  // 如果启用了开发者工具或开发者菜单,需要显示框架和默认标题栏样式
+  if (launchOptions.enableDevTools || launchOptions.enableDevMenu) {
+    windowConfig.frame = true;
+    windowConfig.titleBarStyle = 'default';
+  } else {
+    windowConfig.frame = false;
+    windowConfig.titleBarStyle = 'hidden';
+  }
 
-  // 去掉应用菜单栏
-  Menu.setApplicationMenu(null);
-  if (!isMac) win.removeMenu();
+  win = new BrowserWindow(windowConfig);
+
+  // 根据启动参数设置应用菜单
+  if (launchOptions.enableDevMenu) {
+    // 启用开发者菜单
+    const template = createDevMenuTemplate(isMac, win);
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+  } else {
+    // 去掉应用菜单栏
+    Menu.setApplicationMenu(null);
+    if (!isMac) win.removeMenu();
+  }
 
   win.maximize();
 
   // 加载外置 H5 页面
   win.loadFile(join(process.cwd(), 'h5/index.html'));
 
-  win.once('ready-to-show', () => win.show());
+  win.once('ready-to-show', () => {
+    win.show();
+  });
+
   //渲染进程死了,纪录日志
   win.webContents.on('render-process-gone', (event, details) => {
     writeLog('error',`渲染进程崩溃 ${details}`);
@@ -259,6 +291,20 @@ ipcMain.handle('write-log', async (_, level, msg) => {
   writeLog(level, msg);
 });
 
+// Monkey Test 启动
+ipcMain.on('start-monkey-test', (event, options) => {
+  if (!win) return;
+
+  // 如果已经有实例在运行,先停止
+  if (monkeyTester && monkeyTester.isActive()) {
+    monkeyTester.stop();
+  }
+
+  const isMac = process.platform === 'darwin';
+  monkeyTester = new MonkeyTester(win, isMac, win);
+  monkeyTester.start(options);
+});
+
 // 退出应用
 ipcMain.handle('exit-close', async () => {
   writeLog('info', '用户选择关闭应用程序');

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.40.0",
+  "version": "1.41.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",