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: 'React 组件', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'atom' } 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 行下面放着一个副作用,第 2 行的两个子行各讲一个处理函数,后面还有一个留下的注释块。 */ const CARD: BlockView = { ...view('0', 'react.component', ''), badge: BADGE, properties: [ { label: '名称', value: [{ text: 'Page', role: 'function' }] }, { label: '副作用', value: [{ text: '1 个', role: 'hint' }] }, { label: '事件处理', value: [{ text: '2 个', role: 'hint' }], columns: ['名称', '绑定在', '做什么'], items: [ { cells: [[{ text: 'handleA', role: 'function' }], [], []] }, { cells: [[{ text: 'handleB', role: 'function' }], [], []] }, ] }, ], children: [ view('0.0', 'react.effect', '滚回顶部', { slot: { row: 1 }, badge: { ...BADGE, label: '副作用', icon: 'zap' } }), view('0.1', 'react.handler', '处理 A', { slot: { row: 2, item: 0 }, badge: { ...BADGE, label: '事件处理', icon: 'mouse-pointer-click' } }), view('0.2', 'react.handler', '处理 B', { slot: { row: 2, item: 1 }, badge: { ...BADGE, label: '事件处理', icon: 'mouse-pointer-click' } }), view('0.3', 'comment', '留下的注释'), ], } function render(folds: Record): string { const collapsed = collapsedIds([CARD], folds) const tree: BlockTreeState = { version: 1, editing: new Set(), collapsed, hidden: new Set(), temporary: new Set(), covered: new Map(), kindName: kind => kind, focusId: null, onFocused: () => {}, onToggleEdit: () => {}, onToggleFold: () => {}, onSetFolds: () => {}, onOpenRun: () => {}, } return renderToStaticMarkup() } describe('放进卡片属性表里的子块', () => { it('副作用画在它那一行下面;点子行才展开的处理函数默认不画,名称是展开它的按钮;卡片收着时留下的子块不画', () => { const html = render({}) expect(html).toContain('滚回顶部') expect(html).not.toContain('处理 A') expect(html).not.toContain('留下的注释') expect(html).toContain('')) expect(html.indexOf('滚回顶部')).toBeLessThan(html.indexOf('事件处理')) }) it('展开一个处理函数:画在表下面,那一行标成展开、垫上它的种类色,卡片左上角的箭头收起它', () => { const html = render({ 0.1: false, 0: false }) expect(html).toContain('处理 A') expect(html).not.toContain('处理 B') expect(html).toContain('data-open="" data-badge="" style="--kind-dark:#61dafb;--kind-light:#0a7ea4"') expect(html).toContain('aria-label="收起"') expect(html).toContain('留下的注释') }) it('折叠放着子块的那一行:它下面的子块跟着收起', () => { expect(render({ '0/p1': true })).not.toContain('滚回顶部') }) })