- 新 src/client/EditorHost.tsx:'editor' tab 组件。matchFileViewer → 按 fetchStrategy 分发(纯逻辑在 editor-load.ts)→ 渲染 viewer.component; binary 无匹配时渲染共享 BinaryDownload。标题栏保留在 host - 新 src/client/TextEditor.tsx:code/markdown 的 viewer 组件(CodeMirror + 保存/脏点/截断横幅 + md 预览/编辑切换),内容由 host 经 fsRead 传入, 自身不再 fetch;按 viewerId === 'markdown' 分支(不再解析扩展名) - 新 src/client/editor-load.ts:planFirstMatch / planFsReadOutcome / decodeHead 纯函数,策略分发可单测 - 新 src/client/binary-download.tsx:共享下载兜底组件(viewer 注册与 host fallback 同一 UI) - host src/index.ts:fs.read 二进制响应增加 head 字段(base64,前 4KB, 唯一 host 触碰,设计文档 detect 机制所需);客户端 FsBinaryResult 同步 - builtins:注册 8 个 viewer(新增 markdown exts md/markdown、code catch-all -100);binary-download 增加 NUL detect + 复用 BinaryDownload - 删除 src/client/EditorView.tsx(职责一分为二);陈旧注释同步 - 新增 tests/editor-load.spec.ts(12 用例)
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
/**
|
|
* Tests for the editor-load planning: the pure strategy dispatch the
|
|
* editor host runs (planFirstMatch / planFsReadOutcome / decodeHead).
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { decodeHead, planFirstMatch, planFsReadOutcome } from '../src/client/editor-load.ts'
|
|
import type { FileViewerDescriptor } from '../src/client/service.ts'
|
|
|
|
const viewer = (over: Partial<FileViewerDescriptor>): FileViewerDescriptor => ({
|
|
id: 'v',
|
|
exts: [],
|
|
fetchStrategy: 'fsRead',
|
|
component: () => null,
|
|
...over,
|
|
})
|
|
|
|
describe('planFirstMatch', () => {
|
|
it('no viewer → binary (download UI)', () => {
|
|
expect(planFirstMatch(undefined, () => '/m')).toEqual({ kind: 'binary' })
|
|
})
|
|
|
|
it('binary-download strategy → binary', () => {
|
|
const v = viewer({ id: 'dl', fetchStrategy: 'binary-download' })
|
|
expect(planFirstMatch(v, () => '/m')).toEqual({ kind: 'binary' })
|
|
})
|
|
|
|
it('mediaUrl / none → render with the media URL', () => {
|
|
const v = viewer({ id: 'img', fetchStrategy: 'mediaUrl' })
|
|
expect(planFirstMatch(v, () => '/media')).toEqual({ kind: 'render', viewer: v, mediaUrl: '/media' })
|
|
expect(planFirstMatch(viewer({ id: 'none', fetchStrategy: 'none' }), () => '/m').kind).toBe('render')
|
|
})
|
|
|
|
it('custom → customLoad (the viewer loads its own bytes)', () => {
|
|
const v = viewer({ id: 'csv', fetchStrategy: 'custom', load: async () => [] })
|
|
expect(planFirstMatch(v, () => '/m')).toEqual({ kind: 'customLoad', viewer: v })
|
|
})
|
|
|
|
it('fsRead → fetchFsRead', () => {
|
|
const v = viewer({ id: 'code', fetchStrategy: 'fsRead' })
|
|
expect(planFirstMatch(v, () => '/m')).toEqual({ kind: 'fetchFsRead', viewer: v })
|
|
})
|
|
})
|
|
|
|
describe('planFsReadOutcome', () => {
|
|
const rematch = (head: Uint8Array): FileViewerDescriptor | undefined => {
|
|
// The builtin binary-download NUL probe shape.
|
|
if (head.includes(0)) return viewer({ id: 'binary-download', fetchStrategy: 'binary-download' })
|
|
return undefined
|
|
}
|
|
|
|
it('text result renders with content, same viewer', () => {
|
|
const v = viewer({ id: 'code' })
|
|
const action = planFsReadOutcome(v, { binary: false, content: 'hello', truncated: true }, rematch, () => '/m')
|
|
expect(action).toEqual({ kind: 'render', viewer: v, content: 'hello', truncated: true })
|
|
})
|
|
|
|
it('binary without head bytes → binary', () => {
|
|
expect(planFsReadOutcome(viewer({}), { binary: true, content: '', truncated: false }, rematch, () => '/m')).toEqual({ kind: 'binary' })
|
|
})
|
|
|
|
it('binary whose NUL head re-matches nothing renderable → binary', () => {
|
|
const action = planFsReadOutcome(
|
|
viewer({}),
|
|
{ binary: true, content: '', truncated: false, head: 'AAAAAA==' }, // 0x00 bytes
|
|
rematch,
|
|
() => '/m',
|
|
)
|
|
// binary-download claims it — still the download UI.
|
|
expect(action).toEqual({ kind: 'binary' })
|
|
})
|
|
|
|
it('binary re-matched to a custom viewer → customLoad (external sniffer)', () => {
|
|
const sniffer = viewer({ id: 'my:raw', fetchStrategy: 'custom', load: async () => ({}) })
|
|
const action = planFsReadOutcome(
|
|
viewer({}),
|
|
{ binary: true, content: '', truncated: false, head: 'AAAAAA==' },
|
|
() => sniffer,
|
|
() => '/m',
|
|
)
|
|
expect(action).toEqual({ kind: 'customLoad', viewer: sniffer })
|
|
})
|
|
|
|
it('binary re-matched to a mediaUrl viewer → render with the media URL', () => {
|
|
const media = viewer({ id: 'my:binimg', fetchStrategy: 'mediaUrl' })
|
|
const action = planFsReadOutcome(
|
|
viewer({}),
|
|
{ binary: true, content: '', truncated: false, head: 'AAAAAA==' },
|
|
() => media,
|
|
() => '/m?x=1',
|
|
)
|
|
expect(action).toEqual({ kind: 'render', viewer: media, mediaUrl: '/m?x=1' })
|
|
})
|
|
})
|
|
|
|
describe('decodeHead', () => {
|
|
it('decodes base64 to the exact bytes', () => {
|
|
const head = decodeHead('AP//AA==')
|
|
expect(Array.from(head)).toEqual([0x00, 0xff, 0xff, 0x00])
|
|
})
|
|
})
|