import type { BlockTreeState } from './BlockTree' import type { BlockView, KindBadge } from './types' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { BlockLevel } from './BlockTree' import { collapsedIds } from './viewState' // 高亮的上下文所在的模块、编辑框都连着和 VS Code 收发消息的那一套,渲染成 HTML 用不着 vi.mock('./useOccurrences', async () => { const { createContext } = await import('react') return { HighlightContext: createContext(undefined) } }) vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } })) const BADGE: KindBadge = { label: '卡片', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'box' } function view(id: string, kind: string, text: string, extra: Partial = {}): BlockView { const badge = extra.badge return { id, anchor: id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: badge?.label ?? kind, icon: badge?.icon ?? 'box', ...(badge && { color: badge.color }) }], children: [], translation: [{ text, role: 'plain' }], ...extra, } as BlockView } /** 一张卡片:第 1 行的两个子行各挂一张子卡片,后面还有一块留下的语句。 */ const CARD: BlockView = { ...view('0', 'demo.card', ''), badge: BADGE, properties: [ { label: '名称', value: [{ text: 'Server', role: 'type' }] }, { label: '方法', value: [{ text: '2 个', role: 'hint' }], columns: ['名称', '参数', '返回'], items: [ { cells: [[{ text: 'start', role: 'function' }], [], []] }, { cells: [[{ text: 'stop', role: 'function' }], [], []] }, ] }, ], children: [ view('0.0', 'demo.method', '启动服务', { slot: { row: 1, item: 0 }, badge: { ...BADGE, label: '方法', icon: 'square-function' } }), view('0.1', 'demo.method', '停止服务', { slot: { row: 1, item: 1 }, badge: { ...BADGE, label: '方法', icon: 'square-function' } }), view('0.2', 'statement', '留下的语句'), ], } function state(collapsed: ReadonlySet): BlockTreeState { return { edit: { version: 1, editing: new Set(), focusId: null, onFocused: () => {}, onToggleEdit: () => {}, rewrites: new Map(), onRewriteComment: () => {}, onStopRewrite: () => {}, }, collapsed, hidden: new Set(), temporary: new Set(), covered: new Map(), kindName: kind => kind, onToggleFold: () => {}, onSetFolds: () => {}, onOpenRun: () => {}, } } function render(folds: Record): string { return renderToStaticMarkup() } describe('挂在卡片子行上的子块', () => { it('默认不画,子行的名称是展开它的按钮;卡片收着时留下的子块不画', () => { const html = render({}) expect(html).not.toContain('启动服务') expect(html).not.toContain('停止服务') expect(html).not.toContain('留下的语句') expect(html).toContain('')) }) it('折叠挂着子块的那一行:展开着的子块跟着收起', () => { expect(render({ '0.0': false })).toContain('启动服务') expect(render({ '0.0': false, '0/p1': true })).not.toContain('启动服务') }) it('画出来的子层记着它是哪张卡片的(钉住卡片时按它算卡片管到哪):子行下面那层、卡片后面那层都是;收着时没有', () => { expect(render({})).not.toContain('data-owner') expect(render({ '0.0': false, '0': false }).match(/data-owner="0"/g)).toHaveLength(2) }) }) describe('「注释」一行和「重写注释」按钮', () => { const comment = { label: '注释', value: { markdown: '按名字找' } } const html = (block: BlockView): string => renderToStaticMarkup() /** 按钮只有一个,排在右上角那排按钮(块操作)的最前面 */ const inToolbar = (markup: string, label: string): void => { expect(markup.match(new RegExp(`aria-label="${label}"`, 'g'))).toHaveLength(1) expect(markup).toMatch(new RegExp(`role="group" aria-label="块操作">]*aria-label="${label}"`)) } it('属性表卡片:「注释」一行照属性表画,按钮在右上角那排,不在这一行上', () => { const card = view('1', 'function', '', { properties: [{ label: '名称', value: [{ text: 'find', role: 'function' }] }, comment, { label: '异步', value: [{ text: '否', role: 'storage' }] }], commentTarget: { kind: 'replace', startLine: 0, endLine: 0 }, }) const markup = html(card) inToolbar(markup, '按注释规范重写') expect(markup.indexOf('按名字找')).toBeLessThan(markup.indexOf('异步')) }) it('一行译文的卡片:「注释」一行排在译文下面,按钮同样在右上角那排', () => { const markup = html(view('2', 'statement', 'save()', { rows: [comment], commentTarget: { kind: 'replace', startLine: 0, endLine: 0 } })) expect(markup.indexOf('save()')).toBeLessThan(markup.indexOf('按名字找')) inToolbar(markup, '按注释规范重写') }) it('还没有注释的块:按钮同样在右上角那排,叫「按注释规范写注释」;不能写注释的块没有按钮', () => { inToolbar(html(view('3', 'statement', 'save()', { commentTarget: { kind: 'insert', line: 0 } })), '按注释规范写注释') expect(html(view('4', 'statement', 'save()'))).not.toContain('按注释规范') }) })