Files
apes-sidebar/tests/open-external-client.spec.ts
T
Menghuan1918 b011be1e98 feat(sidebar): 打开方式双源并存 + 多选压缩下载 + 文件变动层级树
用户追加三条需求,全部落地:

1) 打开方式 = 宿主探测应用 + 插件自研 open-with(并存)
- 逐字恢复 src/client/open-with.ts / open-with-settings.tsx / plugin-settings.ts /
  src/open-external.ts / open.external 路由 / api.openExternal / editor descriptor
  的 settings.render,以及 20 个 openWith* 词条(19 份词典从 main 取回原文)
- 恢复 menu-flip.ts + layout.css 子菜单翻转规则(子菜单回来了)
- FileTree 菜单段(无标题行):宿主行 → 分隔线 → 插件 pinned + 「在应用中打开 >」
  子菜单(图钉 / SSH 后缀原样)→ 分隔线 → 宿主 reveal;宿主可用时过滤插件
  explorer 目标,宿主不可用时插件行照旧(SSH 远端场景保持可用)
- 死键清理:菜单去掉标题行后 openInApp 词条无引用,已从 21 份词典删除

2) 文件页多选右键「压缩并打包下载」
- 新增 src/zip.ts(自研 ZIP:方法 0/8 按收益二选一、UTF-8 bit 11、目录条目、
  自实现 CRC32、无第三方依赖)+ src/archive-route.ts(名净化 + 递归收集)+
  GET /sidebar/archive(同一道 workspace fence、目录递归跳过符号链接、
  10000 条目 / 256 MiB 上限)+ api.archiveUrl
- FileTree:多选 ≥2 项或单选目录时出现;fetch → blob → objectURL 下载,
  路由错误(超限 / fence / 非 GET)以 zipFailed 显示在错误条
- 修掉真实缺陷:central directory 只写 MS-DOS 位而 version-made-by 声称 Unix,
  Info-ZIP 会把每个文件解成 mode 000;改为 Unix 语义(0o100644 * 0x10000,
  << 16 会溢出 int32)。系统 unzip 复验:mode 644/755、unzip -t 无错

3) 文件变动页层级树 + 视觉打磨
- 新增 src/client/changes/change-tree.ts:单子目录链压缩成一行、目录先于文件、
  大小写不敏感排序、文件带 GitFileStatus、目录带下级变更数
- GitLens 两组各自渲染树:折叠状态按「组+路径」独立、默认展开;分组头吸顶 +
  计数胶囊;Stage all/Unstage all 改为带 aria-label 的 IconButton;选中 =
  interactive-bg-active + 2px 内嵌强调条;12px/级缩进 + l1 参考线;
  历史行 hash 徽标 / ref 胶囊 / 作者·时间右对齐

验证:typecheck / lint / test(134 文件 1422 通过)/ build 全绿;
DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount 7/7 通过;
真实宿主截图确认三条需求(多选+双源菜单+zip 项、变动页树形层级)
2026-09-28 18:08:21 +08:00

99 lines
3.2 KiB
TypeScript

/**
* Client/host routing for the file tree's external-open action. Remote
* VSCode-family SSH URLs must launch on the browser machine, while local
* editor URLs and reveal actions keep using the DSH host opener.
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { api } from '../src/client/api.ts'
afterEach(() => {
vi.unstubAllGlobals()
})
function hostOk(): ReturnType<typeof vi.fn> {
return vi.fn(async () => ({
ok: true,
status: 200,
json: async () => ({ ok: true, value: { started: true } }),
}))
}
describe('api.openExternal', () => {
it('launches an SSH remote-editor URL on the client without calling the host', async () => {
const assign = vi.fn()
const fetchMock = hostOk()
vi.stubGlobal('window', { location: { assign } })
vi.stubGlobal('fetch', fetchMock)
const url = 'vscode://vscode-remote/ssh-remote+dev/home/u/f.ts'
await expect(api.openExternal({ action: 'url', url })).resolves.toEqual({ started: true })
expect(assign).toHaveBeenCalledOnce()
expect(assign).toHaveBeenCalledWith(url)
expect(fetchMock).not.toHaveBeenCalled()
})
it('launches Cursor/custom VSCode-family SSH URLs on the client too', async () => {
const assign = vi.fn()
const fetchMock = hostOk()
vi.stubGlobal('window', { location: { assign } })
vi.stubGlobal('fetch', fetchMock)
await api.openExternal({
action: 'url',
url: 'cursor://vscode-remote/ssh-remote+dev/home/u/f.ts',
})
await api.openExternal({
action: 'url',
url: 'myfork://vscode-remote/ssh-remote+dev/home/u/f.ts',
})
expect(assign).toHaveBeenCalledTimes(2)
expect(fetchMock).not.toHaveBeenCalled()
})
it('keeps local editor URLs on the host opener', async () => {
const assign = vi.fn()
const fetchMock = hostOk()
vi.stubGlobal('window', { location: { assign } })
vi.stubGlobal('fetch', fetchMock)
await expect(api.openExternal({
action: 'url',
url: 'vscode://file//tmp/a.ts',
})).resolves.toEqual({ started: true })
expect(assign).not.toHaveBeenCalled()
expect(fetchMock).toHaveBeenCalledOnce()
expect(fetchMock.mock.calls[0]?.[0]).toBe('/sidebar/api/open.external')
})
it('keeps reveal actions and http(s) lookalikes on the host opener', async () => {
const assign = vi.fn()
const fetchMock = hostOk()
vi.stubGlobal('window', { location: { assign } })
vi.stubGlobal('fetch', fetchMock)
await api.openExternal({ action: 'reveal', path: '/tmp/a.ts' })
await api.openExternal({
action: 'url',
url: 'https://vscode-remote/ssh-remote+dev/home/u/f.ts',
})
expect(assign).not.toHaveBeenCalled()
expect(fetchMock).toHaveBeenCalledTimes(2)
})
it('returns a rejected promise when client navigation throws', async () => {
const error = new Error('navigation failed')
const fetchMock = hostOk()
vi.stubGlobal('window', { location: { assign: vi.fn(() => { throw error }) } })
vi.stubGlobal('fetch', fetchMock)
await expect(api.openExternal({
action: 'url',
url: 'vscode://vscode-remote/ssh-remote+dev/home/u/f.ts',
})).rejects.toBe(error)
expect(fetchMock).not.toHaveBeenCalled()
})
})