import type { BlockView, DocumentDiff, RemovedView } from './types' import { describe, expect, it } from 'vitest' import { changeStops, diffTree, foldUnchanged, nextStop, openedByDiff, removedTitle, statsParts } from './diffView' function block(id: string, anchor: string, children: BlockView[] = [], extra: Partial = {}): BlockView { return { id, anchor, kind: 'function', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'function' }, children, translation: [], ...extra } as BlockView } function removed(parent: string, after: string | undefined, kind = 'function'): RemovedView { return { parent, ...(after !== undefined && { after }), block: block('9', `${kind} gone`, [], { kind }), line: 3, kindName: '函数' } } function diff(parts: Partial): DocumentDiff { return { base: '上一次提交', version: 2, blocks: {}, removed: [], stats: { add: 0, mod: 0, del: 0 }, lineCount: 10, ...parts } } const BLOCKS = [ block('0', 'function a', [block('0.0', 'function a > return @1')]), block('1', 'function b'), ] describe('diffTree', () => { it('块按记号认成页面上的 id;页面上找不到的记号(文档刚改)不标', () => { const tree = diffTree(BLOCKS, diff({ blocks: { 'function a': { change: 'mod' }, 'function a > return @1': { change: 'mod', own: true }, 'function gone': { change: 'add' } } }), 2, () => true) expect([...tree!.changes]).toEqual([['0', { change: 'mod' }], ['0.0', { change: 'mod', own: true }]]) expect(tree!.current).toBe(true) }) it('页面上不是差异算的那一版时,卡片里的标记不画', () => { expect(diffTree(BLOCKS, diff({}), 3, () => true)?.current).toBe(false) }) it('删掉的块按层挂:父块、排在后面的那块换成页面上的 id,找不到的不画,被筛掉的不画', () => { const tree = diffTree(BLOCKS, diff({ removed: [ removed('', 'function b'), removed('function a', undefined), removed('function missing', undefined), removed('', 'function missing'), removed('', undefined, 'import'), ] }), 2, item => item.block.kind !== 'import') expect(tree!.removed.get('')).toEqual([{ id: 'removed-0', after: '1', removed: removed('', 'function b') }]) expect(tree!.removed.get('0')).toEqual([{ id: 'removed-1', removed: removed('function a', undefined) }]) expect(tree!.removed.size).toBe(2) }) it('不比、基准里没有这个文件时不逐块标', () => { expect(diffTree(BLOCKS, null, 2, () => true)).toBeUndefined() expect(diffTree(BLOCKS, diff({ newFile: '上一次提交里没有这个文件' }), 2, () => true)).toBeUndefined() }) it('标了改动的块默认展开', () => { const tree = diffTree(BLOCKS, diff({ blocks: { 'function b': { change: 'add', outermost: true } } }), 2, () => true) expect([...openedByDiff(tree)]).toEqual(['1']) expect(openedByDiff(undefined).size).toBe(0) }) }) describe('statsParts', () => { it('没有的那种不写', () => { expect(statsParts({ add: 2, mod: 0, del: 1 })).toEqual([{ change: 'add', text: '新增 2' }, { change: 'del', text: '删除 1' }]) expect(statsParts({ add: 0, mod: 0, del: 0 })).toEqual([]) }) }) describe('removedTitle', () => { it('属性表取第一行的值,一行译文取译文,只取第一行', () => { expect(removedTitle(block('0', 'x', [], { properties: [{ label: '名称', value: [{ text: '_is_owner', role: 'function' }] }] } as Partial))).toBe('_is_owner') expect(removedTitle(block('0', 'x', [], { translation: [{ text: '调用 f\n接着', role: 'plain' }] }))).toBe('调用 f') expect(removedTitle(block('0', 'x', [], { kind: 'comment', translation: { markdown: '说明第一行\n第二行' } }))).toBe('说明第一行') }) }) /** * 0 function a(修改) * 0.0 import(没变) * 0.1 return(修改,自己改了) * 0.2 return(没变) * 1 function b(没变) * 2 function c(没变) * 3 function d(新增,带子块 3.0) * 4 function e(没变) */ const TREE_BLOCKS = [ block('0', 'a', [block('0.0', 'a0', [], { startLine: 1 }), block('0.1', 'a1', [], { startLine: 2, endLine: 2 }), block('0.2', 'a2', [], { startLine: 3 })], { startLine: 0, endLine: 3 }), block('1', 'b', [], { startLine: 5 }), block('2', 'c', [], { startLine: 7 }), block('3', 'd', [block('3.0', 'd0')], { startLine: 9, endLine: 11 }), block('4', 'e', [], { startLine: 13 }), ] function tree(removedList: RemovedView[] = []): NonNullable> { return diffTree(TREE_BLOCKS, diff({ blocks: { a: { change: 'mod' }, a1: { change: 'mod', own: true }, d: { change: 'add', outermost: true }, d0: { change: 'add' } }, removed: removedList, }), 2, () => true)! } describe('foldUnchanged', () => { it('每层连着没变的块折成一段,改过的块下面同样按层折', () => { const { runs, folded } = foldUnchanged(TREE_BLOCKS, tree(), () => true, new Set(), new Set()) expect([...runs]).toEqual([['0.0', 1], ['0.2', 1], ['1', 2], ['4', 1]]) expect([...folded].sort()).toEqual(['0.0', '0.2', '1', '2', '4']) }) it('删掉的块排在哪块后面,那一段就在哪儿断开', () => { const { runs } = foldUnchanged(TREE_BLOCKS, tree([removed('', 'b')]), () => true, new Set(), new Set()) expect([...runs]).toContainEqual(['1', 1]) expect([...runs]).toContainEqual(['2', 1]) }) it('被筛掉的块不算也不打断;点开过的段、要露出来的块不折', () => { expect([...foldUnchanged(TREE_BLOCKS, tree(), block => block.id !== '2', new Set(), new Set()).runs]).toContainEqual(['1', 1]) expect(foldUnchanged(TREE_BLOCKS, tree(), () => true, new Set(['1']), new Set()).runs.has('1')).toBe(false) expect(foldUnchanged(TREE_BLOCKS, tree(), () => true, new Set(), new Set(['2'])).folded.has('2')).toBe(false) }) }) /** 一张卡片:两个副作用放在第 7 行下面,两个处理函数放在第 8 行的子行上,后面一个留下的子块;只有第二个处理函数改了 */ const CARD_BLOCKS = [ block('0', 'card', [ block('0.0', 'e1', [], { slot: { row: 7 } }), block('0.1', 'e2', [], { slot: { row: 7 } }), block('0.2', 'h1', [], { slot: { row: 8, item: 0 } }), block('0.3', 'h2', [block('0.3.0', 'h2s', [], { slot: { row: 2 } })], { slot: { row: 8, item: 1 }, startLine: 4 }), block('0.4', 'rest'), ]), ] function cardTree(): NonNullable> { return diffTree(CARD_BLOCKS, diff({ blocks: { card: { change: 'mod' }, h2: { change: 'mod' }, h2s: { change: 'mod', own: true } } }), 2, () => true)! } describe('放进卡片属性表里的子块', () => { it('只看改动:画在卡片里的按所在的行分开折,点子行才展开的不折、改了的照样往里看', () => { const { runs, folded } = foldUnchanged(CARD_BLOCKS, cardTree(), () => true, new Set(), new Set()) expect([...runs]).toEqual([['0.0', 2], ['0.4', 1]]) expect([...folded].sort()).toEqual(['0.0', '0.1', '0.4']) }) it('上一处 / 下一处:要展开的是子块所在的那一行(点子行才展开的连它自己),不是卡片', () => { expect(changeStops(CARD_BLOCKS, cardTree(), new Set()).map(stop => [stop.id, stop.ancestors])).toEqual([ ['0.3.0', ['0/p8', '0.3', '0.3/p2']], ]) }) }) describe('changeStops', () => { it('新增的最外面那块、自己改了的修改块、删掉的块,照页面上的先后;修改块只算自己那几行', () => { const stops = changeStops(TREE_BLOCKS, tree([removed('', 'c'), removed('a', undefined)]), new Set()) expect(stops.map(stop => [stop.id, stop.change, stop.from, stop.to, stop.ancestors])).toEqual([ ['removed-1', 'del', 3, 3, ['0']], ['0.1', 'mod', 2, 2, ['0']], ['removed-0', 'del', 3, 3, []], ['3', 'add', 9, 11, []], ]) }) it('被筛掉的块和它下面的不算', () => { expect(changeStops(TREE_BLOCKS, tree(), new Set(['3', '3.0'])).map(stop => stop.id)).toEqual(['0.1']) }) }) describe('nextStop', () => { const stops = [{ from: 2 }, { from: 9 }, { from: 20 }].map((stop, index) => ({ id: String(index), change: 'mod' as const, to: stop.from, ancestors: [], ...stop })) it('接着刚跳到的那处走,到头绕回另一头', () => { expect(nextStop(stops, { index: 0 }, 1)).toBe(1) expect(nextStop(stops, { index: 2 }, 1)).toBe(0) expect(nextStop(stops, { index: 0 }, -1)).toBe(2) }) it('按视口顶上那块的行找:下一处在它后面,上一处在它前面', () => { expect(nextStop(stops, { line: 9 }, 1)).toBe(2) expect(nextStop(stops, { line: 9 }, -1)).toBe(0) expect(nextStop(stops, { line: 30 }, 1)).toBe(0) expect(nextStop(stops, { line: 0 }, -1)).toBe(2) expect(nextStop(stops, {}, 1)).toBe(0) }) })