@becrafter/prompt-manager 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/desktop/assets/app.1.png +0 -0
- package/app/desktop/assets/app.png +0 -0
- package/app/desktop/assets/icons/icon.icns +0 -0
- package/app/desktop/assets/icons/icon.ico +0 -0
- package/app/desktop/assets/icons/icon.png +0 -0
- package/app/desktop/assets/icons/tray.png +0 -0
- package/app/desktop/assets/templates/about.html +147 -0
- package/app/desktop/assets/tray.1.png +0 -0
- package/app/desktop/assets/tray.png +0 -0
- package/app/desktop/docs/ASSETS_PLANNING.md +351 -0
- package/app/desktop/docs/REFACTORING_SUMMARY.md +205 -0
- package/app/desktop/main.js +340 -0
- package/app/desktop/package-lock.json +6912 -0
- package/app/desktop/package.json +119 -0
- package/app/desktop/preload.js +7 -0
- package/app/desktop/src/core/error-handler.js +108 -0
- package/app/desktop/src/core/event-emitter.js +84 -0
- package/app/desktop/src/core/logger.js +130 -0
- package/app/desktop/src/core/state-manager.js +125 -0
- package/app/desktop/src/services/module-loader.js +330 -0
- package/app/desktop/src/services/runtime-manager.js +398 -0
- package/app/desktop/src/services/service-manager.js +210 -0
- package/app/desktop/src/services/update-manager.js +267 -0
- package/app/desktop/src/ui/about-dialog-manager.js +208 -0
- package/app/desktop/src/ui/admin-window-manager.js +757 -0
- package/app/desktop/src/ui/splash-manager.js +253 -0
- package/app/desktop/src/ui/tray-manager.js +186 -0
- package/app/desktop/src/utils/icon-manager.js +133 -0
- package/app/desktop/src/utils/path-utils.js +58 -0
- package/app/desktop/src/utils/resource-paths.js +49 -0
- package/app/desktop/src/utils/resource-sync.js +260 -0
- package/app/desktop/src/utils/runtime-sync.js +241 -0
- package/app/desktop/src/utils/self-check.js +288 -0
- package/app/desktop/src/utils/template-renderer.js +284 -0
- package/app/desktop/src/utils/version-utils.js +59 -0
- package/env.example +1 -1
- package/package.json +12 -1
- package/packages/server/.eslintrc.js +70 -0
- package/packages/server/.husky/pre-commit +8 -0
- package/packages/server/.husky/pre-push +8 -0
- package/packages/server/.prettierrc +14 -0
- package/packages/server/dev-server.js +90 -0
- package/packages/server/jsdoc.conf.json +39 -0
- package/packages/server/package.json +2 -0
- package/packages/server/playwright.config.js +62 -0
- package/packages/server/scripts/generate-docs.js +300 -0
- package/packages/server/server.js +1 -0
- package/packages/server/services/TerminalService.js +218 -21
- package/packages/server/tests/e2e/terminal-e2e.test.js +315 -0
- package/packages/server/tests/integration/terminal-websocket.test.js +372 -0
- package/packages/server/tests/integration/tools.test.js +264 -0
- package/packages/server/tests/setup.js +45 -0
- package/packages/server/tests/unit/TerminalService.test.js +410 -0
- package/packages/server/tests/unit/WebSocketService.test.js +403 -0
- package/packages/server/tests/unit/core.test.js +94 -0
- package/packages/server/typedoc.json +52 -0
- package/packages/server/utils/config.js +1 -1
- package/packages/server/utils/util.js +59 -5
- package/packages/server/vitest.config.js +74 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { randomUUID } from 'crypto';
|
|
10
|
+
import fs from 'fs';
|
|
10
11
|
import { logger } from '../utils/logger.js';
|
|
11
12
|
import path from 'path';
|
|
12
13
|
import os from 'os';
|
|
@@ -68,7 +69,7 @@ class TerminalSession {
|
|
|
68
69
|
case 'win32':
|
|
69
70
|
return process.env.COMSPEC || 'cmd.exe';
|
|
70
71
|
case 'darwin':
|
|
71
|
-
return process.env.SHELL || '/bin/
|
|
72
|
+
return process.env.SHELL || '/bin/zsh';
|
|
72
73
|
case 'linux':
|
|
73
74
|
return process.env.SHELL || '/bin/bash';
|
|
74
75
|
default:
|
|
@@ -82,15 +83,34 @@ class TerminalSession {
|
|
|
82
83
|
setupPtyEvents() {
|
|
83
84
|
if (!this.pty) return;
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
86
|
+
// 对于正常的PTY进程
|
|
87
|
+
if (!this.pty.isFallback) {
|
|
88
|
+
this.pty.on('data', (data) => {
|
|
89
|
+
this.lastActivity = new Date();
|
|
90
|
+
this.emit('data', data);
|
|
91
|
+
});
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
this.pty.on('exit', (exitCode, signal) => {
|
|
94
|
+
this.isActive = false;
|
|
95
|
+
this.emit('exit', { exitCode, signal });
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
// 对于fallback PTY进程(child_process.spawn)
|
|
99
|
+
this.pty.process.stdout?.on('data', (data) => {
|
|
100
|
+
this.lastActivity = new Date();
|
|
101
|
+
this.emit('data', data);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.pty.process.stderr?.on('data', (data) => {
|
|
105
|
+
this.lastActivity = new Date();
|
|
106
|
+
this.emit('data', data);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.pty.process.on('exit', (exitCode, signal) => {
|
|
110
|
+
this.isActive = false;
|
|
111
|
+
this.emit('exit', { exitCode, signal });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
/**
|
|
@@ -100,7 +120,14 @@ class TerminalSession {
|
|
|
100
120
|
if (!this.isActive || !this.pty) {
|
|
101
121
|
throw new Error('Terminal session is not active');
|
|
102
122
|
}
|
|
103
|
-
|
|
123
|
+
|
|
124
|
+
if (this.pty.isFallback) {
|
|
125
|
+
// fallback PTY使用child_process
|
|
126
|
+
this.pty.process.stdin?.write(data);
|
|
127
|
+
} else {
|
|
128
|
+
// 正常PTY
|
|
129
|
+
this.pty.write(data);
|
|
130
|
+
}
|
|
104
131
|
this.lastActivity = new Date();
|
|
105
132
|
}
|
|
106
133
|
|
|
@@ -111,7 +138,14 @@ class TerminalSession {
|
|
|
111
138
|
if (!this.isActive || !this.pty) {
|
|
112
139
|
throw new Error('Terminal session is not active');
|
|
113
140
|
}
|
|
114
|
-
|
|
141
|
+
|
|
142
|
+
if (this.pty.isFallback) {
|
|
143
|
+
// fallback PTY不支持resize,记录日志
|
|
144
|
+
logger.debug(`Fallback PTY resize requested: ${cols}x${rows} (not supported)`);
|
|
145
|
+
} else {
|
|
146
|
+
// 正常PTY支持resize
|
|
147
|
+
this.pty.resize(cols, rows);
|
|
148
|
+
}
|
|
115
149
|
this.size = { cols, rows };
|
|
116
150
|
}
|
|
117
151
|
|
|
@@ -120,7 +154,13 @@ class TerminalSession {
|
|
|
120
154
|
*/
|
|
121
155
|
terminate() {
|
|
122
156
|
if (this.pty) {
|
|
123
|
-
this.pty.
|
|
157
|
+
if (this.pty.isFallback) {
|
|
158
|
+
// fallback PTY使用child_process
|
|
159
|
+
this.pty.process.kill();
|
|
160
|
+
} else {
|
|
161
|
+
// 正常PTY
|
|
162
|
+
this.pty.kill();
|
|
163
|
+
}
|
|
124
164
|
}
|
|
125
165
|
this.isActive = false;
|
|
126
166
|
}
|
|
@@ -249,20 +289,122 @@ export class TerminalService {
|
|
|
249
289
|
* 创建PTY进程
|
|
250
290
|
*/
|
|
251
291
|
async createPtyProcess(options) {
|
|
252
|
-
const shell = options.shell || this.getDefaultShellForPlatform();
|
|
253
|
-
const args = this.getShellArgs(shell);
|
|
254
292
|
const cwd = options.workingDirectory || os.homedir();
|
|
255
293
|
const env = { ...process.env, ...options.environment };
|
|
294
|
+
const shells = this.getShellCandidates(options.shell);
|
|
295
|
+
let lastError = null;
|
|
296
|
+
|
|
297
|
+
// 在macOS上确保PATH包含常用目录
|
|
298
|
+
if (process.platform === 'darwin') {
|
|
299
|
+
const defaultPath = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin';
|
|
300
|
+
if (!env.PATH || env.PATH === '') {
|
|
301
|
+
env.PATH = defaultPath;
|
|
302
|
+
} else if (!env.PATH.includes('/usr/local/bin')) {
|
|
303
|
+
env.PATH = `${env.PATH}:${defaultPath}`;
|
|
304
|
+
}
|
|
305
|
+
logger.debug(`macOS PATH configured: ${env.PATH}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const candidate of shells) {
|
|
309
|
+
if (!candidate) continue;
|
|
310
|
+
const resolvedShell = this.resolveShellPath(candidate);
|
|
311
|
+
if (!resolvedShell) {
|
|
312
|
+
logger.debug(`Shell not found on system: ${candidate}`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const args = this.getShellArgs(resolvedShell);
|
|
317
|
+
logger.debug(`Creating PTY with shell: ${resolvedShell}, args: ${args.join(' ')}, cwd: ${cwd}`);
|
|
256
318
|
|
|
257
|
-
|
|
319
|
+
try {
|
|
320
|
+
const ptyProcess = pty.default.spawn(resolvedShell, args, {
|
|
321
|
+
name: 'xterm-color',
|
|
322
|
+
cols: options.size.cols,
|
|
323
|
+
rows: options.size.rows,
|
|
324
|
+
cwd: cwd,
|
|
325
|
+
env: env
|
|
326
|
+
});
|
|
258
327
|
|
|
259
|
-
|
|
260
|
-
|
|
328
|
+
// 添加PTY进程的事件监听,用于调试
|
|
329
|
+
ptyProcess.on('data', (data) => {
|
|
330
|
+
logger.debug(`PTY data received: ${data.length} bytes`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
ptyProcess.on('exit', (code, signal) => {
|
|
334
|
+
logger.debug(`PTY process exited: code=${code}, signal=${signal}`);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return ptyProcess;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
lastError = error;
|
|
340
|
+
logger.warn(`Failed to spawn shell ${resolvedShell}: ${error.message}`);
|
|
341
|
+
|
|
342
|
+
// 如果是posix_spawnp失败,尝试备用方案
|
|
343
|
+
if (error.message.includes('posix_spawnp')) {
|
|
344
|
+
logger.warn('posix_spawnp failed, trying alternative approach...');
|
|
345
|
+
try {
|
|
346
|
+
// 尝试使用child_process.spawn作为备用方案
|
|
347
|
+
const { spawn } = await import('child_process');
|
|
348
|
+
const child = spawn(resolvedShell, args, {
|
|
349
|
+
cwd: cwd,
|
|
350
|
+
env: env,
|
|
351
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
352
|
+
shell: false
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// 包装child_process.spawn的结果以兼容PTY接口
|
|
356
|
+
return this.createFallbackPtyProcess(child, options);
|
|
357
|
+
} catch (fallbackError) {
|
|
358
|
+
logger.warn(`Fallback spawn also failed: ${fallbackError.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw lastError || new Error('Unable to create PTY session: no suitable shell found');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 创建备用PTY进程(使用child_process.spawn)
|
|
369
|
+
*/
|
|
370
|
+
createFallbackPtyProcess(childProcess, options) {
|
|
371
|
+
// 创建一个兼容PTY接口的对象
|
|
372
|
+
const fallbackPty = {
|
|
373
|
+
pid: childProcess.pid,
|
|
261
374
|
cols: options.size.cols,
|
|
262
375
|
rows: options.size.rows,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
376
|
+
process: childProcess,
|
|
377
|
+
isFallback: true,
|
|
378
|
+
|
|
379
|
+
write(data) {
|
|
380
|
+
if (childProcess.stdin) {
|
|
381
|
+
childProcess.stdin.write(data);
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
resize(cols, rows) {
|
|
386
|
+
this.cols = cols;
|
|
387
|
+
this.rows = rows;
|
|
388
|
+
// child_process不支持resize,记录日志
|
|
389
|
+
logger.debug(`Fallback PTY resize requested: ${cols}x${rows} (not supported)`);
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
kill(signal = 'SIGTERM') {
|
|
393
|
+
childProcess.kill(signal);
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
on(event, callback) {
|
|
397
|
+
if (event === 'data') {
|
|
398
|
+
childProcess.stdout?.on('data', callback);
|
|
399
|
+
childProcess.stderr?.on('data', callback);
|
|
400
|
+
} else if (event === 'exit') {
|
|
401
|
+
childProcess.on('exit', callback);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
logger.info('Created fallback PTY process (limited functionality)');
|
|
407
|
+
return fallbackPty;
|
|
266
408
|
}
|
|
267
409
|
|
|
268
410
|
/**
|
|
@@ -273,7 +415,8 @@ export class TerminalService {
|
|
|
273
415
|
case 'win32':
|
|
274
416
|
return process.env.COMSPEC || 'cmd.exe';
|
|
275
417
|
case 'darwin':
|
|
276
|
-
|
|
418
|
+
// 在 macOS 上优先使用用户的 SHELL 环境变量
|
|
419
|
+
return process.env.SHELL || '/bin/zsh';
|
|
277
420
|
case 'linux':
|
|
278
421
|
return process.env.SHELL || '/bin/bash';
|
|
279
422
|
default:
|
|
@@ -291,9 +434,63 @@ export class TerminalService {
|
|
|
291
434
|
}
|
|
292
435
|
return ['/c'];
|
|
293
436
|
}
|
|
437
|
+
|
|
438
|
+
// 某些精简 shell(如 /bin/sh)不支持 -l
|
|
439
|
+
if (shell.endsWith('/sh')) {
|
|
440
|
+
return ['-i'];
|
|
441
|
+
}
|
|
442
|
+
|
|
294
443
|
return ['-l'];
|
|
295
444
|
}
|
|
296
445
|
|
|
446
|
+
/**
|
|
447
|
+
* 获取 shell 候选列表(按优先级)
|
|
448
|
+
*/
|
|
449
|
+
getShellCandidates(preferredShell) {
|
|
450
|
+
const candidates = [];
|
|
451
|
+
|
|
452
|
+
if (preferredShell) candidates.push(preferredShell);
|
|
453
|
+
if (process.env.SHELL) candidates.push(process.env.SHELL);
|
|
454
|
+
|
|
455
|
+
if (process.platform === 'darwin') {
|
|
456
|
+
candidates.push('/bin/zsh', '/bin/bash', '/bin/sh');
|
|
457
|
+
} else if (process.platform === 'linux') {
|
|
458
|
+
candidates.push('/bin/bash', '/bin/sh');
|
|
459
|
+
} else if (process.platform === 'win32') {
|
|
460
|
+
candidates.push(process.env.COMSPEC || 'cmd.exe');
|
|
461
|
+
} else {
|
|
462
|
+
candidates.push('/bin/sh');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return [...new Set(candidates)];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 确保 shell 路径在当前系统存在
|
|
470
|
+
*/
|
|
471
|
+
resolveShellPath(shellPath) {
|
|
472
|
+
// 绝对路径直接检查
|
|
473
|
+
if (shellPath.startsWith('/')) {
|
|
474
|
+
return fs.existsSync(shellPath) ? shellPath : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Windows 可执行文件
|
|
478
|
+
if (process.platform === 'win32') {
|
|
479
|
+
return shellPath;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 如果是相对路径,尝试在常见目录查找
|
|
483
|
+
const searchPaths = ['/bin', '/usr/bin', '/usr/local/bin'];
|
|
484
|
+
for (const base of searchPaths) {
|
|
485
|
+
const fullPath = path.join(base, shellPath);
|
|
486
|
+
if (fs.existsSync(fullPath)) {
|
|
487
|
+
return fullPath;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
297
494
|
/**
|
|
298
495
|
* 获取会话
|
|
299
496
|
*/
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 终端E2E测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, expect } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
test.describe('终端功能E2E测试', () => {
|
|
8
|
+
test.beforeEach(async ({ page }) => {
|
|
9
|
+
// 导航到管理员界面
|
|
10
|
+
await page.goto('/admin/');
|
|
11
|
+
|
|
12
|
+
// 等待页面加载完成
|
|
13
|
+
await page.waitForLoadState('networkidle');
|
|
14
|
+
|
|
15
|
+
// 点击终端导航
|
|
16
|
+
await page.click('[data-nav="terminal"]');
|
|
17
|
+
|
|
18
|
+
// 等待终端区域显示
|
|
19
|
+
await page.waitForSelector('#terminalArea', { state: 'visible' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('应该显示终端界面', async ({ page }) => {
|
|
23
|
+
// 验证终端区域存在
|
|
24
|
+
await expect(page.locator('#terminalArea')).toBeVisible();
|
|
25
|
+
|
|
26
|
+
// 验证终端标题
|
|
27
|
+
await expect(page.locator('.terminal-title')).toContainText('终端');
|
|
28
|
+
|
|
29
|
+
// 验证终端内容区域
|
|
30
|
+
await expect(page.locator('.terminal-content')).toBeVisible();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('应该建立WebSocket连接', async ({ page }) => {
|
|
34
|
+
// 等待WebSocket连接建立
|
|
35
|
+
await page.waitForSelector('.status-indicator.connected', { timeout: 10000 });
|
|
36
|
+
|
|
37
|
+
// 验证连接状态显示
|
|
38
|
+
await expect(page.locator('.status-text')).toContainText('已连接');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('应该支持基本命令执行', async ({ page }) => {
|
|
42
|
+
// 等待终端初始化
|
|
43
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
44
|
+
|
|
45
|
+
// 模拟输入命令
|
|
46
|
+
await page.keyboard.type('echo "Hello, E2E Test!"');
|
|
47
|
+
await page.keyboard.press('Enter');
|
|
48
|
+
|
|
49
|
+
// 等待命令执行结果
|
|
50
|
+
await page.waitForTimeout(2000);
|
|
51
|
+
|
|
52
|
+
// 验证命令输出(这里需要根据实际终端实现调整)
|
|
53
|
+
// 由于使用xterm.js,我们需要检查终端内容
|
|
54
|
+
const terminalContent = await page.locator('.xterm-screen').textContent();
|
|
55
|
+
expect(terminalContent).toContain('Hello, E2E Test!');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('应该支持多个命令执行', async ({ page }) => {
|
|
59
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
60
|
+
|
|
61
|
+
// 执行多个命令
|
|
62
|
+
const commands = [
|
|
63
|
+
'pwd',
|
|
64
|
+
'ls -la',
|
|
65
|
+
'whoami'
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const command of commands) {
|
|
69
|
+
await page.keyboard.type(command);
|
|
70
|
+
await page.keyboard.press('Enter');
|
|
71
|
+
await page.waitForTimeout(1000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 验证所有命令都已执行
|
|
75
|
+
const terminalContent = await page.locator('.xterm-screen').textContent();
|
|
76
|
+
|
|
77
|
+
// 验证命令历史
|
|
78
|
+
for (const command of commands) {
|
|
79
|
+
expect(terminalContent).toContain(command);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('应该支持终端大小调整', async ({ page }) => {
|
|
84
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
85
|
+
|
|
86
|
+
// 获取初始终端大小
|
|
87
|
+
const initialTerminal = page.locator('.xterm-screen');
|
|
88
|
+
const initialBounds = await initialTerminal.boundingBox();
|
|
89
|
+
|
|
90
|
+
// 调整浏览器窗口大小
|
|
91
|
+
await page.setViewportSize({ width: 1200, height: 800 });
|
|
92
|
+
|
|
93
|
+
// 等待终端适应新大小
|
|
94
|
+
await page.waitForTimeout(1000);
|
|
95
|
+
|
|
96
|
+
// 验证终端已调整大小
|
|
97
|
+
const adjustedTerminal = page.locator('.xterm-screen');
|
|
98
|
+
const adjustedBounds = await adjustedTerminal.boundingBox();
|
|
99
|
+
|
|
100
|
+
// 终端应该适应新的窗口大小
|
|
101
|
+
expect(adjustedBounds.width).toBeGreaterThan(initialBounds.width);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('应该支持主题切换', async ({ page }) => {
|
|
105
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
106
|
+
|
|
107
|
+
// 获取初始主题
|
|
108
|
+
const terminal = page.locator('.xterm');
|
|
109
|
+
const initialBackground = await terminal.evaluate(el =>
|
|
110
|
+
getComputedStyle(el).getPropertyValue('background-color')
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// 点击主题切换按钮
|
|
114
|
+
await page.click('#themeBtn');
|
|
115
|
+
|
|
116
|
+
// 等待主题切换
|
|
117
|
+
await page.waitForTimeout(500);
|
|
118
|
+
|
|
119
|
+
// 验证主题已改变
|
|
120
|
+
const newBackground = await terminal.evaluate(el =>
|
|
121
|
+
getComputedStyle(el).getPropertyValue('background-color')
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(newBackground).not.toBe(initialBackground);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('应该支持搜索功能', async ({ page }) => {
|
|
128
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
129
|
+
|
|
130
|
+
// 输入一些文本
|
|
131
|
+
await page.keyboard.type('echo "search test"');
|
|
132
|
+
await page.keyboard.press('Enter');
|
|
133
|
+
await page.waitForTimeout(1000);
|
|
134
|
+
|
|
135
|
+
// 点击搜索按钮
|
|
136
|
+
await page.click('#searchBtn');
|
|
137
|
+
|
|
138
|
+
// 输入搜索词
|
|
139
|
+
const searchInput = await page.locator('.xterm-search input');
|
|
140
|
+
await searchInput.fill('search test');
|
|
141
|
+
|
|
142
|
+
// 验证搜索功能(这里需要根据实际搜索实现调整)
|
|
143
|
+
expect(searchInput).toHaveValue('search test');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('应该支持清除功能', async ({ page }) => {
|
|
147
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
148
|
+
|
|
149
|
+
// 输入一些内容
|
|
150
|
+
await page.keyboard.type('echo "clear test"');
|
|
151
|
+
await page.keyboard.press('Enter');
|
|
152
|
+
await page.waitForTimeout(1000);
|
|
153
|
+
|
|
154
|
+
// 获取清除前的内容
|
|
155
|
+
const contentBefore = await page.locator('.xterm-screen').textContent();
|
|
156
|
+
expect(contentBefore).toContain('clear test');
|
|
157
|
+
|
|
158
|
+
// 点击清除按钮
|
|
159
|
+
await page.click('#clearBtn');
|
|
160
|
+
|
|
161
|
+
// 等待清除完成
|
|
162
|
+
await page.waitForTimeout(500);
|
|
163
|
+
|
|
164
|
+
// 验证内容已清除(xterm.js清除后可能只保留提示符)
|
|
165
|
+
const contentAfter = await page.locator('.xterm-screen').textContent();
|
|
166
|
+
expect(contentAfter).not.toContain('clear test');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('应该支持重连功能', async ({ page }) => {
|
|
170
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
171
|
+
|
|
172
|
+
// 等待连接建立
|
|
173
|
+
await page.waitForSelector('.status-indicator.connected');
|
|
174
|
+
|
|
175
|
+
// 点击重连按钮
|
|
176
|
+
await page.click('#reconnectBtn');
|
|
177
|
+
|
|
178
|
+
// 等待重连完成
|
|
179
|
+
await page.waitForSelector('.status-indicator.connected', { timeout: 10000 });
|
|
180
|
+
|
|
181
|
+
// 验证连接状态
|
|
182
|
+
await expect(page.locator('.status-text')).toContainText('已连接');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('应该处理连接断开', async ({ page }) => {
|
|
186
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
187
|
+
|
|
188
|
+
// 模拟WebSocket断开(通过页面上下文)
|
|
189
|
+
await page.evaluate(() => {
|
|
190
|
+
// 查找WebSocket连接并关闭
|
|
191
|
+
const ws = window.terminalComponent?.websocket;
|
|
192
|
+
if (ws) {
|
|
193
|
+
ws.close();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 等待断开状态
|
|
198
|
+
await page.waitForSelector('.status-indicator.disconnected', { timeout: 5000 });
|
|
199
|
+
|
|
200
|
+
// 验证断开状态显示
|
|
201
|
+
await expect(page.locator('.status-text')).toContainText('未连接');
|
|
202
|
+
|
|
203
|
+
// 等待自动重连
|
|
204
|
+
await page.waitForSelector('.status-indicator.connected', { timeout: 15000 });
|
|
205
|
+
|
|
206
|
+
// 验证重连成功
|
|
207
|
+
await expect(page.locator('.status-text')).toContainText('已连接');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('应该支持键盘快捷键', async ({ page }) => {
|
|
211
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
212
|
+
|
|
213
|
+
// 测试Ctrl+C中断
|
|
214
|
+
await page.keyboard.type('sleep 10');
|
|
215
|
+
await page.keyboard.press('Enter');
|
|
216
|
+
await page.waitForTimeout(500);
|
|
217
|
+
|
|
218
|
+
// 发送中断信号
|
|
219
|
+
await page.keyboard.press('Control+c');
|
|
220
|
+
await page.waitForTimeout(1000);
|
|
221
|
+
|
|
222
|
+
// 验证命令被中断(终端应该回到提示符状态)
|
|
223
|
+
// 这里需要根据实际终端实现来验证
|
|
224
|
+
|
|
225
|
+
// 测试Ctrl+V粘贴
|
|
226
|
+
await page.evaluate(() => {
|
|
227
|
+
navigator.clipboard.writeText('echo "paste test"');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await page.keyboard.press('Control+v');
|
|
231
|
+
await page.keyboard.press('Enter');
|
|
232
|
+
await page.waitForTimeout(1000);
|
|
233
|
+
|
|
234
|
+
// 验证粘贴内容被执行
|
|
235
|
+
const terminalContent = await page.locator('.xterm-screen').textContent();
|
|
236
|
+
expect(terminalContent).toContain('paste test');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test.describe('终端性能测试', () => {
|
|
241
|
+
test('应该在合理时间内建立连接', async ({ page }) => {
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
|
|
244
|
+
await page.goto('/admin/');
|
|
245
|
+
await page.click('[data-nav="terminal"]');
|
|
246
|
+
await page.waitForSelector('.status-indicator.connected', { timeout: 10000 });
|
|
247
|
+
|
|
248
|
+
const connectionTime = Date.now() - startTime;
|
|
249
|
+
expect(connectionTime).toBeLessThan(5000); // 应该在5秒内连接
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('应该处理大量输出', async ({ page }) => {
|
|
253
|
+
await page.goto('/admin/');
|
|
254
|
+
await page.click('[data-nav="terminal"]');
|
|
255
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 10000 });
|
|
256
|
+
|
|
257
|
+
// 生成大量输出的命令
|
|
258
|
+
await page.keyboard.type('for i in {1..100}; do echo "Line $i"; done');
|
|
259
|
+
await page.keyboard.press('Enter');
|
|
260
|
+
|
|
261
|
+
// 等待命令执行完成
|
|
262
|
+
await page.waitForTimeout(5000);
|
|
263
|
+
|
|
264
|
+
// 验证终端仍然响应
|
|
265
|
+
await page.keyboard.type('echo "Still responsive"');
|
|
266
|
+
await page.keyboard.press('Enter');
|
|
267
|
+
await page.waitForTimeout(1000);
|
|
268
|
+
|
|
269
|
+
const terminalContent = await page.locator('.xterm-screen').textContent();
|
|
270
|
+
expect(terminalContent).toContain('Still responsive');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test.describe('跨浏览器兼容性', () => {
|
|
275
|
+
['chromium', 'firefox', 'webkit'].forEach(browserName => {
|
|
276
|
+
test(`${browserName}: 应该正常工作`, async ({ page, browserName: currentBrowser }) => {
|
|
277
|
+
test.skip(currentBrowser !== browserName, '跳过其他浏览器');
|
|
278
|
+
|
|
279
|
+
await page.goto('/admin/');
|
|
280
|
+
await page.click('[data-nav="terminal"]');
|
|
281
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 15000 });
|
|
282
|
+
|
|
283
|
+
// 基本功能测试
|
|
284
|
+
await page.keyboard.type('echo "Browser: $browserName"'.replace('$browserName', browserName));
|
|
285
|
+
await page.keyboard.press('Enter');
|
|
286
|
+
await page.waitForTimeout(2000);
|
|
287
|
+
|
|
288
|
+
const terminalContent = await page.locator('.xterm-screen').textContent();
|
|
289
|
+
expect(terminalContent).toContain(browserName);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test.describe('移动端适配', () => {
|
|
295
|
+
test('应该在移动设备上正常显示', async ({ page }) => {
|
|
296
|
+
// 设置移动设备视口
|
|
297
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
298
|
+
|
|
299
|
+
await page.goto('/admin/');
|
|
300
|
+
await page.click('[data-nav="terminal"]');
|
|
301
|
+
await page.waitForSelector('.xterm-container', { state: 'visible', timeout: 15000 });
|
|
302
|
+
|
|
303
|
+
// 验证移动端样式
|
|
304
|
+
const toolbar = page.locator('.terminal-toolbar');
|
|
305
|
+
await expect(toolbar).toBeVisible();
|
|
306
|
+
|
|
307
|
+
// 验证终端仍然可用
|
|
308
|
+
await page.keyboard.type('echo "Mobile test"');
|
|
309
|
+
await page.keyboard.press('Enter');
|
|
310
|
+
await page.waitForTimeout(2000);
|
|
311
|
+
|
|
312
|
+
const terminalContent = await page.locator('.xterm-screen').textContent();
|
|
313
|
+
expect(terminalContent).toContain('Mobile test');
|
|
314
|
+
});
|
|
315
|
+
});
|