Files
superwork/vscode/webview-ui/src/codeReader/BlockTree.test.tsx
T

141 lines
6.3 KiB
TypeScript

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> = {}): 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<string>): 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, boolean>): string {
return renderToStaticMarkup(<BlockLevel blocks={[CARD]} tree={state(collapsedIds([CARD], folds))} />)
}
describe('挂在卡片子行上的子块', () => {
it('默认不画,子行的名称是展开它的按钮;卡片收着时留下的子块不画', () => {
const html = render({})
expect(html).not.toContain('启动服务')
expect(html).not.toContain('停止服务')
expect(html).not.toContain('留下的语句')
expect(html).toContain('<button type="button" class="cr-item-toggle" aria-expanded="false" aria-label="展开start">')
})
it('展开一张:画在表下面,那一行标成展开、垫上它的种类色,卡片左上角的箭头收起它', () => {
const html = render({ '0.0': false, '0': false })
expect(html).toContain('启动服务')
expect(html).not.toContain('停止服务')
expect(html).toContain('data-open="" data-badge="" style="--kind-dark:#61dafb;--kind-light:#0a7ea4"')
expect(html).toContain('aria-label="收起"')
expect(html).toContain('留下的语句')
// 子卡片画在「方法」那一行后面
expect(html.indexOf('启动服务')).toBeGreaterThan(html.indexOf('方法</button>'))
})
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(<BlockLevel blocks={[block]} tree={state(new Set())} />)
/** 按钮只有一个,排在右上角那排按钮(块操作)的最前面 */
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="块操作"><button[^>]*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('按注释规范')
})
})