@becrafter/prompt-manager 0.1.14 → 0.1.15
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/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 +84 -11
- 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';
|
|
@@ -249,20 +250,37 @@ export class TerminalService {
|
|
|
249
250
|
* 创建PTY进程
|
|
250
251
|
*/
|
|
251
252
|
async createPtyProcess(options) {
|
|
252
|
-
const shell = options.shell || this.getDefaultShellForPlatform();
|
|
253
|
-
const args = this.getShellArgs(shell);
|
|
254
253
|
const cwd = options.workingDirectory || os.homedir();
|
|
255
254
|
const env = { ...process.env, ...options.environment };
|
|
255
|
+
const shells = this.getShellCandidates(options.shell);
|
|
256
|
+
let lastError = null;
|
|
257
|
+
|
|
258
|
+
for (const candidate of shells) {
|
|
259
|
+
if (!candidate) continue;
|
|
260
|
+
const resolvedShell = this.resolveShellPath(candidate);
|
|
261
|
+
if (!resolvedShell) {
|
|
262
|
+
logger.debug(`Shell not found on system: ${candidate}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
256
265
|
|
|
257
|
-
|
|
266
|
+
const args = this.getShellArgs(resolvedShell);
|
|
267
|
+
logger.debug(`Creating PTY with shell: ${resolvedShell}, args: ${args.join(' ')}, cwd: ${cwd}`);
|
|
258
268
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
try {
|
|
270
|
+
return pty.default.spawn(resolvedShell, args, {
|
|
271
|
+
name: 'xterm-color',
|
|
272
|
+
cols: options.size.cols,
|
|
273
|
+
rows: options.size.rows,
|
|
274
|
+
cwd: cwd,
|
|
275
|
+
env: env
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
278
|
+
lastError = error;
|
|
279
|
+
logger.warn(`Failed to spawn shell ${resolvedShell}: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw lastError || new Error('Unable to create PTY session: no suitable shell found');
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
/**
|
|
@@ -273,7 +291,8 @@ export class TerminalService {
|
|
|
273
291
|
case 'win32':
|
|
274
292
|
return process.env.COMSPEC || 'cmd.exe';
|
|
275
293
|
case 'darwin':
|
|
276
|
-
|
|
294
|
+
// 在 macOS 上优先使用用户的 SHELL 环境变量
|
|
295
|
+
return process.env.SHELL || '/bin/zsh' || '/bin/bash';
|
|
277
296
|
case 'linux':
|
|
278
297
|
return process.env.SHELL || '/bin/bash';
|
|
279
298
|
default:
|
|
@@ -291,9 +310,63 @@ export class TerminalService {
|
|
|
291
310
|
}
|
|
292
311
|
return ['/c'];
|
|
293
312
|
}
|
|
313
|
+
|
|
314
|
+
// 某些精简 shell(如 /bin/sh)不支持 -l
|
|
315
|
+
if (shell.endsWith('/sh')) {
|
|
316
|
+
return ['-i'];
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
return ['-l'];
|
|
295
320
|
}
|
|
296
321
|
|
|
322
|
+
/**
|
|
323
|
+
* 获取 shell 候选列表(按优先级)
|
|
324
|
+
*/
|
|
325
|
+
getShellCandidates(preferredShell) {
|
|
326
|
+
const candidates = [];
|
|
327
|
+
|
|
328
|
+
if (preferredShell) candidates.push(preferredShell);
|
|
329
|
+
if (process.env.SHELL) candidates.push(process.env.SHELL);
|
|
330
|
+
|
|
331
|
+
if (process.platform === 'darwin') {
|
|
332
|
+
candidates.push('/bin/zsh', '/bin/bash', '/bin/sh');
|
|
333
|
+
} else if (process.platform === 'linux') {
|
|
334
|
+
candidates.push('/bin/bash', '/bin/sh');
|
|
335
|
+
} else if (process.platform === 'win32') {
|
|
336
|
+
candidates.push(process.env.COMSPEC || 'cmd.exe');
|
|
337
|
+
} else {
|
|
338
|
+
candidates.push('/bin/sh');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return [...new Set(candidates)];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 确保 shell 路径在当前系统存在
|
|
346
|
+
*/
|
|
347
|
+
resolveShellPath(shellPath) {
|
|
348
|
+
// 绝对路径直接检查
|
|
349
|
+
if (shellPath.startsWith('/')) {
|
|
350
|
+
return fs.existsSync(shellPath) ? shellPath : null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Windows 可执行文件
|
|
354
|
+
if (process.platform === 'win32') {
|
|
355
|
+
return shellPath;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 如果是相对路径,尝试在常见目录查找
|
|
359
|
+
const searchPaths = ['/bin', '/usr/bin', '/usr/local/bin'];
|
|
360
|
+
for (const base of searchPaths) {
|
|
361
|
+
const fullPath = path.join(base, shellPath);
|
|
362
|
+
if (fs.existsSync(fullPath)) {
|
|
363
|
+
return fullPath;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
297
370
|
/**
|
|
298
371
|
* 获取会话
|
|
299
372
|
*/
|
|
@@ -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
|
+
});
|