|
@@ -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);
|
|
|
|
|
+}
|