/** * Bug 2 fix verification: 删除引用后,同类型剩余引用连续重命名(即梦逻辑) * 同时同步更新 editorHtml 中 @mention span 的文本。 */ import { describe, it, expect, beforeEach } from 'vitest'; import { useInputBarStore } from '../../src/store/inputBar'; function mockFile(name: string, type = 'image/jpeg'): File { return new File(['mock'], name, { type }); } function mockRef(id: string, type: 'image' | 'video' | 'audio', label: string) { return { id, file: mockFile(`${id}.${type === 'image' ? 'jpg' : type === 'video' ? 'mp4' : 'mp3'}`), type, previewUrl: `blob:${id}`, label, }; } function mentionSpan(refId: string, refType: string, label: string): string { return `@${label}`; } describe('removeReference — 即梦式连续重命名', () => { beforeEach(() => { useInputBarStore.getState().reset(); }); describe('图片重命名', () => { it('删除图片2 后,图片3 重命名为图片2(references + editorHtml 同步)', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), mockRef('ref_3', 'image', '图片3'), ]; const editorHtml = `开场 ${mentionSpan('ref_1', 'image', '图片1')} 和 ${mentionSpan('ref_2', 'image', '图片2')} 在和 ${mentionSpan('ref_3', 'image', '图片3')} 讲话`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_2'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(2); expect(state.references[0].id).toBe('ref_1'); expect(state.references[0].label).toBe('图片1'); expect(state.references[1].id).toBe('ref_3'); expect(state.references[1].label).toBe('图片2'); // 原图片3 → 图片2 // editorHtml 里 ref_3 的 textNode 应该变成 "图片2" expect(state.editorHtml).toContain('data-ref-id="ref_3"'); expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片2<\/span>/); // ref_1 保持 "图片1" expect(state.editorHtml).toMatch(/data-ref-id="ref_1"[^>]*>[\s\S]*?图片1<\/span>/); }); it('删除图片1 后,图片2、图片3 重命名为图片1、图片2', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), mockRef('ref_3', 'image', '图片3'), ]; const editorHtml = `${mentionSpan('ref_2', 'image', '图片2')} ${mentionSpan('ref_3', 'image', '图片3')}`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references[0].label).toBe('图片1'); // ref_2 expect(state.references[1].label).toBe('图片2'); // ref_3 expect(state.editorHtml).toMatch(/data-ref-id="ref_2"[^>]*>[\s\S]*?图片1<\/span>/); expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片2<\/span>/); }); it('删除最后一张图片(唯一图片)— references 清空,editorHtml 不变', () => { const refs = [mockRef('ref_1', 'image', '图片1')]; const editorHtml = `内容 ${mentionSpan('ref_1', 'image', '图片1')} 尾部`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(0); // remaining 为空 → labelUpdates 为空 → 跳过 DOM 操作 → editorHtml 原样保留 expect(state.editorHtml).toBe(editorHtml); }); }); describe('视频/音频独立编号', () => { it('图片和视频混合时,删图片只重命名图片,视频不动', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), mockRef('ref_3', 'video', '视频1'), ]; const editorHtml = `${mentionSpan('ref_1', 'image', '图片1')} ${mentionSpan('ref_2', 'image', '图片2')} ${mentionSpan('ref_3', 'video', '视频1')}`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(2); expect(state.references[0].label).toBe('图片1'); // 原图片2 expect(state.references[1].label).toBe('视频1'); // 视频不变 expect(state.editorHtml).toMatch(/data-ref-id="ref_2"[^>]*>[\s\S]*?图片1<\/span>/); expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?视频1<\/span>/); }); it('删除视频2,视频3 重命名为视频2', () => { const refs = [ mockRef('ref_1', 'video', '视频1'), mockRef('ref_2', 'video', '视频2'), mockRef('ref_3', 'video', '视频3'), ]; const editorHtml = `${mentionSpan('ref_3', 'video', '视频3')}`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_2'); const state = useInputBarStore.getState(); expect(state.references[0].label).toBe('视频1'); expect(state.references[1].label).toBe('视频2'); // 原视频3 expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?视频2<\/span>/); }); it('删除音频1,音频2 重命名为音频1', () => { const refs = [ mockRef('ref_1', 'audio', '音频1'), mockRef('ref_2', 'audio', '音频2'), ]; const editorHtml = `${mentionSpan('ref_1', 'audio', '音频1')} ${mentionSpan('ref_2', 'audio', '音频2')}`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(1); expect(state.references[0].id).toBe('ref_2'); expect(state.references[0].label).toBe('音频1'); // 原音频2 expect(state.editorHtml).toMatch(/data-ref-id="ref_2"[^>]*>[\s\S]*?音频1<\/span>/); }); }); describe('边界场景', () => { it('editorHtml 为空 — 不报错,只重命名 references', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), ]; useInputBarStore.setState({ references: refs, editorHtml: '' }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(1); expect(state.references[0].label).toBe('图片1'); expect(state.editorHtml).toBe(''); }); it('editorHtml 中没有对应的 @mention span — 只改 references', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), ]; const editorHtml = '纯文本,没有 mention span'; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(1); expect(state.references[0].label).toBe('图片1'); // ref_2 重命名 // editorHtml 不含对应 span,无法更新,但不报错 }); it('传入不存在的 id — 静默返回,状态不变', () => { const refs = [mockRef('ref_1', 'image', '图片1')]; const editorHtml = mentionSpan('ref_1', 'image', '图片1'); useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('nonexistent_id'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(1); expect(state.references[0].label).toBe('图片1'); expect(state.editorHtml).toBe(editorHtml); }); it('删除的图片没被 @ 到 editor,其他图片仍被重命名', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), mockRef('ref_3', 'image', '图片3'), ]; // editorHtml 只 @ 了图片3,没 @图片1/2 const editorHtml = `${mentionSpan('ref_3', 'image', '图片3')}`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references[0].label).toBe('图片1'); // 原图片2 expect(state.references[1].label).toBe('图片2'); // 原图片3 // editor 里只有 ref_3 的 span,应该更新成"图片2" expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片2<\/span>/); }); }); describe('连续删除(并发)', () => { it('连续删除两张图片,剩余图片正确重编号', () => { const refs = [ mockRef('ref_1', 'image', '图片1'), mockRef('ref_2', 'image', '图片2'), mockRef('ref_3', 'image', '图片3'), mockRef('ref_4', 'image', '图片4'), ]; const editorHtml = `${mentionSpan('ref_1', 'image', '图片1')} ${mentionSpan('ref_2', 'image', '图片2')} ${mentionSpan('ref_3', 'image', '图片3')} ${mentionSpan('ref_4', 'image', '图片4')}`; useInputBarStore.setState({ references: refs, editorHtml }); useInputBarStore.getState().removeReference('ref_2'); useInputBarStore.getState().removeReference('ref_1'); const state = useInputBarStore.getState(); expect(state.references).toHaveLength(2); expect(state.references[0].id).toBe('ref_3'); expect(state.references[0].label).toBe('图片1'); expect(state.references[1].id).toBe('ref_4'); expect(state.references[1].label).toBe('图片2'); expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片1<\/span>/); expect(state.editorHtml).toMatch(/data-ref-id="ref_4"[^>]*>[\s\S]*?图片2<\/span>/); }); }); });