video-shuoshan/web/test/unit/removeReferenceRelabeling.test.ts
seaislee1209 dafdc8983f fix: v0.18.3 版权报错友好提示 + 图片删除即梦式连续重命名
Bug 1: 版权限制错误友好提示
- ERROR_MESSAGES 加 OutputVideoSensitiveContentDetected.PolicyViolation 映射
- 漫威等知名 IP 触发的版权拦截不再显示英文 raw error

Bug 2: 图片删除后同类型引用连续重命名(即梦逻辑)
- inputBar.ts::removeReference 重写:删除后同类型剩余引用按顺序 1/2/3 连续编号
- 用 DOMParser 同步更新 editorHtml 里对应 data-ref-id 的 @mention span textContent
- 缩略图区和提示词栏同步刷新,避免"两个图片2"命名冲突

验证
- 11 个 Vitest 单元测试覆盖图片/视频/音频删除、空 editorHtml、无 @mention、
  连续快速删除等边界场景
- 3 个 Playwright E2E 真实浏览器验证:上传 3 张图 → 删中间 → 再上传 → 编号不冲突

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:03:36 +08:00

235 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 `<span data-ref-id="${refId}" data-ref-type="${refType}" class="mention" contenteditable="false"><span style="font-size:0;width:0;overflow:hidden;display:inline">@</span>${label}</span>`;
}
describe('removeReference — 即梦式连续重命名', () => {
beforeEach(() => {
useInputBarStore.getState().reset();
});
describe('图片重命名', () => {
it('删除图片2 后图片3 重命名为图片2references + 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 = '<span>纯文本,没有 mention span</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>/);
});
});
});