import type { BlockView } from '../blockView' import type { Inline, PropertyItem, PropertyRow, Segment, TokenRole } from '../contract' import { describe, expect, it } from 'vitest' import { blockMarks, inlineDiff, itemsDiff, markdownDiff, pairByKey, removedItems, removedView, rowsDiff, textDiff, withoutSource } from './viewDiff' function seg(text: string, role: TokenRole = 'plain', offset?: number): Segment { return { text, role, ...(offset !== undefined && { offset }) } } function item(...cells: string[]): PropertyItem { return { cells: cells.map(cell => [seg(cell, 'variable')]) } } function row(label: string, value: Inline[] | { markdown: string }, items?: PropertyItem[]): PropertyRow { return { label, value, ...(items && { items }) } } function textView(translation: Inline[] | { markdown: string }, extra: Partial = {}): BlockView { return { id: '0', anchor: 'a', kind: 'statement', startLine: 0, endLine: 0, headerEndLine: 0, attributes: {}, children: [], translation, ...extra } as BlockView } function propertyView(properties: PropertyRow[]): BlockView { return { id: '0', anchor: 'a', kind: 'function', startLine: 0, endLine: 0, headerEndLine: 0, attributes: {}, concepts: [{ id: 'function', label: '函数', icon: 'square-function' }], children: [], properties } } describe('inlineDiff', () => { it('一样(只是源码下标不同)时没有改动', () => { expect(inlineDiff([seg('a', 'variable', 1)], [seg('a', 'variable', 9)])).toBeUndefined() }) it('大部分没变时逐截标:没变的、新加的照新版本(带下标),删掉的取旧版本、去掉下标', () => { const before = [seg('call', 'function', 0), seg('('), seg('key', 'variable', 5), seg(')')] const after = [seg('call', 'function', 0), seg('('), seg('key', 'variable', 5), seg(', '), seg('token', 'variable', 10), seg(')')] expect(inlineDiff(before, after)).toEqual([ { segments: [seg('call', 'function', 0), seg('('), seg('key', 'variable', 5)] }, { segments: [seg(', '), seg('token', 'variable', 10)], change: 'add' }, { segments: [seg(')')] }, ]) }) it('改得多时整段写成旧值 → 新值,旧值去掉下标', () => { expect(inlineDiff([seg('布尔', 'type', 3)], [seg('字符串', 'type'), seg(' | '), seg('空', 'storage')])).toEqual([ { segments: [seg('布尔', 'type')], change: 'del' }, { segments: [seg('字符串', 'type'), seg(' | '), seg('空', 'storage')], change: 'add' }, ]) }) it('框按样式、框头和框里的词比', () => { const group = (label: string): Inline => ({ style: 'iteration', label, segments: [seg('x', 'variable', 2)] }) expect(inlineDiff([group('列表')], [group('列表')])).toBeUndefined() expect(inlineDiff([group('列表')], [group('生成器')])).toHaveLength(2) }) }) describe('markdownDiff', () => { it('按词比,中文按字', () => { expect(markdownDiff('Lua 比对 token 再删.', 'Lua 比对 token 再续期 / 再删.')).toEqual([ { text: 'Lua 比对 token 再' }, { text: '续期 / 再', change: 'add' }, { text: '删.' }, ]) }) it('删掉的字取旧版本', () => { expect(markdownDiff('放锁前必须比对', '放锁都必须比对')).toEqual([ { text: '放锁' }, { text: '前', change: 'del' }, { text: '都', change: 'add' }, { text: '必须比对' }, ]) }) it('一样时没有改动', () => { expect(markdownDiff('同样', '同样')).toBeUndefined() }) }) describe('textDiff', () => { it('着色的词逐截比,Markdown 逐词比,一种换成另一种时按字逐词比', () => { expect(textDiff([seg('a')], [seg('b')])).toHaveProperty('inline') expect(textDiff({ markdown: 'a' }, { markdown: 'b' })).toHaveProperty('markdown') expect(textDiff([seg('无', 'hint')], { markdown: '说明' })).toEqual({ markdown: [{ text: '无', change: 'del' }, { text: '说明', change: 'add' }] }) }) }) describe('pairByKey / removedItems', () => { it('同 key 照先后配;没配上的旧项排在它前面最近配上的那项所配的新项后面', () => { const before = ['a', 'x', 'b', 'y'] const after = ['b', 'a', 'c'] const { toOld, toNew } = pairByKey(before, after, value => value) expect(toOld).toEqual([2, 0, undefined]) expect(toNew).toEqual([1, undefined, 0, undefined]) expect(removedItems(before, toNew, value => value)).toEqual([{ after: 1, value: 'x' }, { after: 0, value: 'y' }]) }) it('最前面没配上的排在最前面', () => { const { toNew } = pairByKey(['x', 'a'], ['a'], value => value) expect(removedItems(['x', 'a'], toNew, value => value)).toEqual([{ after: -1, value: 'x' }]) }) }) describe('itemsDiff', () => { it('按第一列配:新参数算加行,没了的参数算删行,配上的逐格比', () => { const before = [item('redis', 'Any'), item('ttl', '整数')] const after = [item('redis', 'Any'), item('ttl_sec', '整数'), item('prefix', '字符串')] const diff = itemsDiff(before, after) expect(diff?.changed).toEqual({ 1: { added: true }, 2: { added: true } }) expect(diff?.removed).toEqual([{ after: 0, value: item('ttl', '整数') }]) }) it('配上的行某一格变了只标那一格', () => { const diff = itemsDiff([item('key', '字符串')], [item('key', '字节串')]) expect(Object.keys(diff?.changed[0].cells ?? {})).toEqual(['1']) }) it('新加的行下面各层也是新加的', () => { const added: PropertyItem = { cells: [[seg('k')]], items: [item('inner')], table: { columns: ['名称'], items: [item('arg')] } } expect(itemsDiff([], [added])?.changed[0]).toEqual({ added: true, items: { changed: { 0: { added: true } }, removed: [] }, table: { changed: { 0: { added: true } }, removed: [] }, }) }) it('都没有、或一样时没有改动', () => { expect(itemsDiff(undefined, undefined)).toBeUndefined() expect(itemsDiff([item('a')], [item('a')])).toBeUndefined() }) }) describe('rowsDiff', () => { it('按属性名配:值变了逐截标,多出来的行算加行,没了的行算删行', () => { const before = [row('名称', [seg('f', 'function')]), row('输出参数', [seg('无', 'hint')]), row('装饰器', [seg('@x')])] const after = [row('名称', [seg('f', 'function')]), row('异步', [seg('是')]), row('输出参数', [seg('布尔', 'type')])] const diff = rowsDiff(before, after) expect(diff?.changed[1]).toEqual({ added: true }) expect(diff?.changed[2].value).toBeDefined() expect(diff?.removed).toEqual([{ after: 2, value: row('装饰器', [seg('@x')]) }]) }) it('子行改了时只标子行,值(子行的汇总)不另标', () => { const before = [row('输入参数', [seg('1 个', 'hint')], [item('a')])] const after = [row('输入参数', [seg('2 个', 'hint')], [item('a'), item('b')])] expect(rowsDiff(before, after)?.changed[0]).toEqual({ items: { changed: { 1: { added: true } }, removed: [] } }) }) it('新加的行带着子行时,子行也都是新加的', () => { const diff = rowsDiff([], [row('输入参数', [seg('1 个')], [item('a')])]) expect(diff?.changed[0]).toEqual({ added: true, items: { changed: { 0: { added: true } }, removed: [] } }) }) }) describe('blockMarks', () => { it('一行译文、摊开实参的表、说明各比各的', () => { const before = textView([seg('调用 f')], { doc: { markdown: '旧说明' } } as Partial) const after = textView([seg('调用 g')], { table: { columns: ['实参'], items: [item('x')] } } as Partial) const marks = blockMarks(before, after) expect(marks?.translation).toBeDefined() expect(marks?.table?.changed).toEqual({ 0: { added: true } }) expect(marks?.doc).toEqual({ markdown: [{ text: '旧说明', change: 'del' }] }) }) it('一行译文下面拆开的各项逐行比:按第一列配,多出来的算加行,没了的算删行', () => { const before = textView([seg('返回 列表 2 项')], { items: [item('第 1 项'), item('第 2 项')] } as Partial) const after = textView([seg('返回 列表 1 项')], { items: [item('第 1 项')] } as Partial) const marks = blockMarks(before, after) expect(marks?.items?.removed).toEqual([{ after: 0, value: item('第 2 项') }]) expect(blockMarks(after, before)?.items?.changed).toEqual({ 1: { added: true } }) }) it('一块是属性表、一块是一行译文时没法逐行比', () => { expect(blockMarks(textView([seg('a')]), propertyView([row('名称', [seg('a')])]))).toBeUndefined() }) it('内容一样时没有标记', () => { expect(blockMarks(propertyView([row('名称', [seg('a', 'variable', 1)])]), propertyView([row('名称', [seg('a', 'variable', 7)])]))).toBeUndefined() }) }) describe('removedView / withoutSource', () => { it('删掉的块只带自己的内容,词不带源码下标,没有名字的范围', () => { const view = propertyView([row('名称', [seg('f', 'function', 4)], [{ cells: [[{ ...seg('x', 'variable', 9), refers: { startIndex: 1, endIndex: 2 } }]] }])]) const removed = removedView({ ...view, defines: { startIndex: 4, endIndex: 5 }, children: [textView([seg('child', 'plain', 1)])] }) expect(removed.children).toEqual([]) expect(removed.defines).toBeUndefined() expect(JSON.stringify(removed)).not.toMatch(/"offset"|"refers"/) }) it('一行译文下面拆开的各项、摊开实参的表照样带着,词不带源码下标', () => { const named = { cells: [[seg('第 1 项')], [seg('x', 'variable', 9)]] } const removed = removedView(textView([seg('返回 列表 1 项')], { items: [named], table: { columns: ['参数', '值'], items: [named] } } as Partial)) expect('items' in removed && removed.items).toEqual([{ cells: [[seg('第 1 项')], [seg('x', 'variable')]] }]) expect('table' in removed && removed.table?.items).toHaveLength(1) expect(JSON.stringify(removed)).not.toMatch(/"offset"/) }) it('框里的词也去掉下标,颜色留着', () => { const color = { dark: '#fff', light: '#000' } expect(withoutSource([{ style: 'iteration', label: '列表', segments: [{ ...seg('x', 'variable', 3), color }] }])).toEqual([ { style: 'iteration', label: '列表', segments: [{ text: 'x', role: 'variable', color }] }, ]) }) })