/**
* 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>/);
});
});
});