@hyzyn/dsh-tty 0.30.0 → 0.31.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyzyn/dsh-tty",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "DSH Web GUI 的终端面板插件:侧边栏「终端」提供 xterm.js + PTY 全交互终端与 ssh2 原生 SSH 远程连接(连接簿、主机指纹 TOFU、跳板机),支持 tmux 会话持久化、SFTP 文件传输、端口转发与服务器状态条,并向其他插件开放 ttyTerminal / ttyPanel / ttyConnbar 扩展点;Windows 宿主同样可用。",
5
5
  "keywords": [
6
6
  "dsh",
@@ -209,6 +209,63 @@
209
209
  await sleep(200)
210
210
  return item
211
211
  }
212
+ /**
213
+ * SSH 连接对话框「头尾固定」的公共断言(0.31.0):
214
+ * 标题钉在卡片上缘、底部动作行钉在下缘,**只有中段滚**——此前是整张卡片
215
+ * `overflow-y: auto`,滚一下就两头都不见了(底部按钮初始根本不在视口里)。
216
+ * 内容溢出时把中段滚到底再复查一次,否则「没滚动」等于这条断言没验到。
217
+ */
218
+ const sshFrameProblem = () => {
219
+ const card = q('.tt_sshCard')
220
+ const head = q('.tt_sshHead')
221
+ const body = q('.tt_sshBody')
222
+ const foot = q('.tt_sshFoot')
223
+ if (card === null || head === null || body === null || foot === null) return '连接对话框缺少头 / 身 / 尾三段'
224
+ if (getComputedStyle(card).overflowY !== 'hidden') return '卡片自己还在滚动(标题与按钮会跟着跑)'
225
+ if (getComputedStyle(body).overflowY !== 'auto') return '中段不是滚动容器'
226
+ const cardRect = card.getBoundingClientRect()
227
+ if (Math.abs(head.getBoundingClientRect().top - cardRect.top) > 1) return '标题没钉在卡片上缘'
228
+ if (Math.abs(foot.getBoundingClientRect().bottom - cardRect.bottom) > 1) return '底部动作行没钉在卡片下缘'
229
+ if (body.scrollHeight > body.clientHeight) {
230
+ body.scrollTop = body.scrollHeight
231
+ const after = card.getBoundingClientRect()
232
+ if (Math.abs(head.getBoundingClientRect().top - after.top) > 1) return '把中段滚到底后标题跟着跑了'
233
+ if (Math.abs(foot.getBoundingClientRect().bottom - after.bottom) > 1) return '把中段滚到底后底部动作行跟着跑了'
234
+ if (body.scrollTop === 0) return '中段内容溢出却滚不动'
235
+ }
236
+ return null
237
+ }
238
+ /**
239
+ * 「高级」折叠区的展开图标:收起 = 右尖括号、展开 = 下尖括号,**路径逐字取自宿主**
240
+ * `dsh-client-ui-primitives` 的 `IconChevronRightOutlineRegular` / `IconChevronDownOutlineRegular`
241
+ * (官方 provider 卡片「自定义设置」就是这一对;用户原话「展开图标和 dsh 官方的图标保持一致」)。
242
+ * 夹具里比一次实时的 DOM,源码那一半在 `test/ssh-dialog-shell.test.ts` 钉着。
243
+ */
244
+ const HOST_CHEVRON_RIGHT = 'M6 12L9.29289 8.70711C9.68342 8.31658 9.68342 7.68342 9.29289 7.29289L6 4'
245
+ const HOST_CHEVRON_DOWN = 'M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6'
246
+ /** 折叠区钉在图标上的判据:返回 null 表示与宿主一致,否则给出实测路径。 */
247
+ const advChevronProblem = (expected) => {
248
+ const path = q('.tt_sshAdvChevron path')
249
+ if (path === null) return '「高级」折叠区的展开图标缺失'
250
+ const d = path.getAttribute('d') ?? ''
251
+ if (d !== expected) return '展开图标与宿主的不是同一只(实测 d="' + d + '")'
252
+ return null
253
+ }
254
+ /** 从「+」菜单里按条目名点开编辑(对话框只认连接簿里已存在的名字)。 */
255
+ const openEditFor = async (name) => {
256
+ await clickAdd()
257
+ const rows = await waitFor(() => {
258
+ const r = qa('.tt_addMenuRow')
259
+ return r.length > 0 ? r : null
260
+ })
261
+ const row = rows.find((r) => (r.querySelector('.tt_addMenuMain')?.textContent ?? '') === name)
262
+ if (row === undefined) throw new Error('「+」菜单里找不到条目 ' + name)
263
+ const editBtn = [...row.querySelectorAll('.tt_addMenuEdit')].find((b) => attributeSays(b, 'title', '编辑连接', 'Edit connection'))
264
+ if (editBtn === undefined) throw new Error('条目 ' + name + ' 没有「编辑连接」按钮')
265
+ editBtn.click()
266
+ await waitFor(() => q('.tt_sshCard'))
267
+ await sleep(200)
268
+ }
212
269
  /** SFTP 落点断言:卡片在挂载位里(docked)还是退回了居中对话框(modal)。 */
213
270
  const sftpSurfaceAssert = () => ({
214
271
  docked: q('.tt_dockPaneBody > .tt_sftpCard') !== null || q('.tt_dockPaneBody > .tt_sftpDualCard') !== null,
@@ -425,18 +482,94 @@
425
482
  await waitFor(() => resultEl() && resultEl().textContent !== '' && !says(resultEl(), '测试中', 'Testing'))
426
483
  await sleep(250)
427
484
  },
485
+ /* SSH 连接对话框:「保存到连接簿」名称行常显 + 填名字自动勾上(0.31.0) */
486
+ async 'ssh-save-book'() {
487
+ await openPanel()
488
+ await clickAdd()
489
+ await clickMenuItem('SSH 连接…', 'SSH connection…')
490
+ await waitFor(() => q('.tt_sshCard'))
491
+ /*
492
+ * 用户原话「这个默认展开吧」(贴的是「保存到连接簿 + 连接簿名称」那一块)。这里按**文案**
493
+ * 找那两个控件,再把四条联动规则逐条敲一遍——它们错掉时界面都看不出来(最典型:取消勾选后
494
+ * 名字还留在框里 ⇒ 按下去既不保存也没提示)。
495
+ */
496
+ window.__previewAssert = async () => {
497
+ const labelFor = (...names) => qa('.tt_sshCard .tt_cardLabel').find((el) => says(el, ...names))
498
+ const saveLabel = labelFor('保存到连接簿', 'Save to the host book')
499
+ const nameLabel = labelFor('连接簿名称', 'Book name')
500
+ if (saveLabel === undefined || nameLabel === undefined) return '找不到「保存到连接簿 / 连接簿名称」这两个控件'
501
+ const saveCheck = saveLabel.closest('.tt_cardRow')?.querySelector('input[type="checkbox"]')
502
+ const nameRow = nameLabel.closest('.tt_sshRow')
503
+ const nameInput = nameRow?.querySelector('input')
504
+ if (saveCheck === undefined || saveCheck === null || nameRow === null || nameInput === null) {
505
+ return '「保存到连接簿」那一组的结构不对'
506
+ }
507
+ const hostInput = qa('.tt_sshCard input')[0]
508
+ // ① 默认:名称行就在眼前(不必先勾勾选框),且勾选框是关的
509
+ if (nameRow.getBoundingClientRect().height <= 0) return '连接簿名称这一行默认不显示(用户要的是「默认展开」)'
510
+ if (saveCheck.checked !== false) return '默认就把「保存到连接簿」勾上了(那一项应当是关的)'
511
+ // ② 填了名字 → 自动勾上
512
+ nameInput.value = 'staging-db'
513
+ nameInput.dispatchEvent(new Event('input'))
514
+ if (saveCheck.checked !== true) return '名称框里填了名字,「保存到连接簿」却没自动勾上'
515
+ // ③ 清空名字 → 自动取消
516
+ nameInput.value = ''
517
+ nameInput.dispatchEvent(new Event('input'))
518
+ if (saveCheck.checked !== false) return '名字清空了,「保存到连接簿」还勾着(等于静默保存一条无名条目)'
519
+ // ④ 手动勾上且名称为空 → 用主机名兜底
520
+ hostInput.value = 'db.internal'
521
+ saveCheck.checked = true
522
+ saveCheck.dispatchEvent(new Event('change'))
523
+ if (nameInput.value !== 'db.internal') return '手动勾上时没有用主机名兜底(实测 "' + nameInput.value + '")'
524
+ // ⑤ 手动取消 → 名字一起清掉(不留「名字还在、但不会保存」的悬空态)
525
+ saveCheck.checked = false
526
+ saveCheck.dispatchEvent(new Event('change'))
527
+ if (nameInput.value !== '') return '取消勾选后名字还留在框里(实测 "' + nameInput.value + '")'
528
+ // 截图落在「填了名字、勾选框自动跟上」这个态上
529
+ nameInput.value = 'staging-db'
530
+ nameInput.dispatchEvent(new Event('input'))
531
+ return null
532
+ }
533
+ },
428
534
  /* SSH 连接对话框(编辑连接簿条目,含 env 选择器) */
429
535
  async 'ssh-edit'() {
536
+ await openPanel()
537
+ await openEditFor('staging-db')
538
+ window.__previewAssert = async () => {
539
+ const advBody = q('.tt_sshAdvBody')
540
+ const advToggle = q('.tt_sshAdvToggle')
541
+ if (advBody === null || advToggle === null) return '「高级」折叠区缺失'
542
+ // 这条条目一项「高级」都没配:必须默认收起,否则这轮优化等于没做
543
+ if (advBody.hidden !== true) return '条目里没配过高级项,折叠区却是展开的'
544
+ if (advToggle.getAttribute('aria-expanded') !== 'false') return 'aria-expanded 与收起态不一致'
545
+ return advChevronProblem(HOST_CHEVRON_RIGHT) ?? sshFrameProblem()
546
+ }
547
+ },
548
+ /* SSH 连接对话框:展开「高级」并检验头尾固定(中段溢出时滚到底再复查) */
549
+ async 'ssh-adv'() {
430
550
  await openPanel()
431
551
  await clickAdd()
432
- const rows = await waitFor(() => {
433
- const r = qa('.tt_addMenuRow')
434
- return r.length > 0 ? r : null
435
- })
436
- const editBtn = [...rows[1].querySelectorAll('.tt_addMenuEdit')].find((b) => attributeSays(b, 'title', '编辑连接', 'Edit connection'))
437
- editBtn.click()
552
+ await clickMenuItem('SSH 连接…', 'SSH connection…')
438
553
  await waitFor(() => q('.tt_sshCard'))
439
- await sleep(200)
554
+ const advBody = await waitFor(() => q('.tt_sshAdvBody'))
555
+ if (advBody.hidden !== true) throw new Error('新建连接时「高级」应当默认收起')
556
+ q('.tt_sshAdvToggle').click()
557
+ await sleep(150)
558
+ if (advBody.hidden === true) throw new Error('点「高级」没有展开')
559
+ window.__previewAssert = async () => advChevronProblem(HOST_CHEVRON_DOWN) ?? sshFrameProblem()
560
+ },
561
+ /* SSH 连接对话框:编辑一条**配过高级项**的条目 → 折叠区自动展开并点名已配置的项 */
562
+ async 'ssh-jump'() {
563
+ await openPanel()
564
+ await openEditFor('bastion-jump')
565
+ window.__previewAssert = async () => {
566
+ const advBody = q('.tt_sshAdvBody')
567
+ const meta = q('.tt_sshAdvMeta')
568
+ if (advBody === null) return '「高级」折叠区缺失'
569
+ if (advBody.hidden === true) return '条目里配了跳板机 / 代理命令,折叠区却默认收起(配置被藏起来了)'
570
+ if (meta === null || !says(meta, '跳板机', 'Jump host')) return '折叠区元信息没点名已配置的跳板机'
571
+ return advChevronProblem(HOST_CHEVRON_DOWN) ?? sshFrameProblem()
572
+ }
440
573
  },
441
574
  /* 设置卡片(展开) */
442
575
  async settings() {
@@ -53,6 +53,25 @@
53
53
  // 刻意**没有**任何隧道:验证「没配隧道时收进 ⋯ 更多」这条路径
54
54
  // (上面两条都有隧道,走的是常驻「隧道 N」按钮)
55
55
  { name: 'bare-host', host: 'bare.internal', port: 22, username: 'ops', auth: 'agent', keyPath: '', passphrase: '', password: '', agentForward: false, persist: false },
56
+ /*
57
+ * 唯一一条把「高级」三段**都配上**的条目(跳板机 + 代理命令 + 主机密钥别名):
58
+ * `ssh-jump` 场景用它验折叠区的默认展开判据——配过却收起来,等于把配置藏了。
59
+ */
60
+ {
61
+ name: 'bastion-jump',
62
+ host: 'internal.corp',
63
+ port: 22,
64
+ username: 'deploy',
65
+ auth: 'key',
66
+ keyPath: '~/.ssh/id_ed25519',
67
+ passphrase: '',
68
+ password: '',
69
+ agentForward: false,
70
+ persist: false,
71
+ hostKeyAlias: 'corp-internal',
72
+ proxyCommand: 'ssh -W %h:%p bastion.corp',
73
+ jump: { host: 'bastion.corp', port: 2222 },
74
+ },
56
75
  ],
57
76
  hostKeys: [
58
77
  { host: '198.51.100.20', port: 22, fingerprint: 'SHA256:9xQ2mVr7Kp3sTf1cZbLd8uYwAe4nHjR6gPoXkMvQ2Bc' },
@@ -13,6 +13,7 @@
13
13
  * node scripts/preview.mjs --build # 顺带重建 client.js(默认不重建,产物落后直接报错)
14
14
  * node scripts/preview.mjs --theme=light # 浅色主题
15
15
  * node scripts/preview.mjs --width=560 # 压窄视口走查窄容器排版(默认 1680)
16
+ * node scripts/preview.mjs --height=560 # 压矮视口走查「内容溢出时头尾还固不固定」(默认 1050)
16
17
  * PREVIEW_CHROME_ARGS=--no-sandbox node scripts/preview.mjs # 受限沙箱 / CI 里 Chrome 起不来时
17
18
  *
18
19
  * 需要本机有 Chrome/Chromium(默认找 playwright 缓存的 Chrome for Testing,
@@ -46,6 +47,12 @@ const shotsDir = join(previewDir, outName)
46
47
  * `--width=560` 之类显式压窄。
47
48
  */
48
49
  const viewportWidth = Number((argv.find((a) => a.startsWith('--width=')) || '--width=1680').split('=')[1]) || 1680
50
+ /**
51
+ * 视口高度(默认 1050)。**与连接对话框的「头尾固定」直接相关**:那张卡片是
52
+ * `max-height: 86vh`,矮视口下内容才会溢出中段——1050px 高时展开态刚好塞得下,
53
+ * 「滚到底头尾还在不在」这条断言就永远走不到。走查时用 `--height=560` 压矮。
54
+ */
55
+ const viewportHeight = Number((argv.find((a) => a.startsWith('--height=')) || '--height=1050').split('=')[1]) || 1050
49
56
  /**
50
57
  * 额外 Chrome 参数(空格分隔)。受限环境(文件沙箱 / CI 容器)里 Chrome 自己的
51
58
  * sandbox 起不来,CDP 会一直卡在 `Page.enable` 直到超时——此时用
@@ -65,6 +72,9 @@ const SCENARIOS = [
65
72
  ['ssh', 'SSH 连接对话框'],
66
73
  ['ssh-probe', 'SSH 连接对话框(试连结果)'],
67
74
  ['ssh-edit', 'SSH 连接对话框(编辑)'],
75
+ ['ssh-save-book', 'SSH 连接对话框:连接簿名称行常显 + 填名字自动勾上'],
76
+ ['ssh-adv', 'SSH 连接对话框:「高级」展开 + 中段滚动(头尾固定)'],
77
+ ['ssh-jump', 'SSH 连接对话框:配过高级项的条目默认展开折叠区'],
68
78
  ['settings', '设置卡片'],
69
79
  ['settings-assist', '设置卡片:AI 辅助小节(默认关 + 模型路由一个控件)'],
70
80
  ['settings-shell', '设置卡片:Shell 路径候选表(浮层,不顶动卡片布局)'],
@@ -547,7 +557,7 @@ async function main() {
547
557
  await ensureVendor()
548
558
  log('启动 headless Chrome(' + theme + ' 主题)')
549
559
  const { child, cdp, userDataDir } = await launchChrome()
550
- await cdp.send('Emulation.setDeviceMetricsOverride', { width: viewportWidth, height: 1050, deviceScaleFactor: 2, mobile: false })
560
+ await cdp.send('Emulation.setDeviceMetricsOverride', { width: viewportWidth, height: viewportHeight, deviceScaleFactor: 2, mobile: false })
551
561
  let failed = 0
552
562
  try {
553
563
  for (const name of wanted) {