fb0be97609
- src/components/image-editor/ImageCanvasGenerationDialogModel.ts:单槽对话框只在素材库确实选到了图时才做「替换」,空确认退回原行为(保留画布点选/上传的那张)。此前确认按钮不校验空选,空确认会把已有参考图直接清掉 - src/components/image-editor/projectAssetReferencePickerModel.test.ts:补空确认保留画布参考图的用例
310 lines
9.2 KiB
TypeScript
310 lines
9.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type {
|
|
EditorAsset,
|
|
GenerateDialogState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { replaceProjectAssetPickerReferences } from './ImageCanvasGenerationDialogModel';
|
|
import {
|
|
appendLimitedImageReferences,
|
|
createGenerationInputReference,
|
|
} from './ImageCanvasGenerationModel';
|
|
import {
|
|
PROJECT_ASSET_PICKER_CATEGORY_OPTIONS,
|
|
projectAssetMatchesPickerFilter,
|
|
projectAssetPickerCategory,
|
|
projectAssetPickerReferences,
|
|
projectAssetPickerUsesThumbnail,
|
|
} from './projectAssetReferencePickerModel';
|
|
|
|
function createAsset(overrides: Partial<EditorAsset>): EditorAsset {
|
|
return {
|
|
id: 'asset-1',
|
|
label: '素材 1',
|
|
src: 'https://example.test/asset-1.png',
|
|
mediaType: 'image',
|
|
width: 64,
|
|
height: 64,
|
|
folderId: 'folder-1',
|
|
sourceKind: 'uploaded',
|
|
sourceType: 'uploaded',
|
|
persisted: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('参考图弹窗素材筛选与快照', () => {
|
|
it('derives the category from the asset media type', () => {
|
|
expect(projectAssetPickerCategory({ mediaType: 'image' })).toBe('image');
|
|
expect(projectAssetPickerCategory({ mediaType: 'video' })).toBe('video');
|
|
expect(projectAssetPickerCategory({ mediaType: 'audio' })).toBe('audio');
|
|
expect(projectAssetPickerCategory({})).toBe('image');
|
|
// 序列帧单列一类:它不是静态图片,参考图链路也不接受它(见下一条用例)。
|
|
expect(projectAssetPickerCategory({ mediaType: 'image-sequence' })).toBe(
|
|
'image-sequence',
|
|
);
|
|
expect(
|
|
PROJECT_ASSET_PICKER_CATEGORY_OPTIONS.map((item) => item.id),
|
|
).toEqual(['all', 'image', 'video', 'audio', 'image-sequence']);
|
|
});
|
|
|
|
it('keeps image-sequence assets out of the image category', () => {
|
|
const sequence = createAsset({ mediaType: 'image-sequence' });
|
|
|
|
expect(
|
|
projectAssetMatchesPickerFilter(sequence, {
|
|
category: 'image',
|
|
query: '',
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
projectAssetMatchesPickerFilter(sequence, {
|
|
category: 'image-sequence',
|
|
query: '',
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
projectAssetMatchesPickerFilter(sequence, { category: 'all', query: '' }),
|
|
).toBe(true);
|
|
// 缩略图口径跟分类走:序列帧的 src 也是图片字节,照旧画出来。
|
|
expect(projectAssetPickerUsesThumbnail('image-sequence')).toBe(true);
|
|
expect(projectAssetPickerUsesThumbnail('image')).toBe(true);
|
|
expect(projectAssetPickerUsesThumbnail('video')).toBe(false);
|
|
expect(projectAssetPickerUsesThumbnail('audio')).toBe(false);
|
|
});
|
|
|
|
it('参考图链路只接受图片:序列帧引用会被 appendLimitedImageReferences 过滤掉', () => {
|
|
// 这条用例把「分类展示」与「参考图接受口径」的边界显式钉住:序列帧可以看见、
|
|
// 可以选中(与音频 / 视频一致),但不会作为参考图进入提交数据。
|
|
const sequence = createAsset({
|
|
id: 'asset-sequence',
|
|
mediaType: 'image-sequence',
|
|
label: '勇者挥手',
|
|
});
|
|
const references = projectAssetPickerReferences(
|
|
[sequence],
|
|
['asset-sequence'],
|
|
);
|
|
expect(references[0]?.mediaType).toBe('image-sequence');
|
|
expect(appendLimitedImageReferences(undefined, references, 4)).toEqual([]);
|
|
});
|
|
|
|
it('stacks the category filter with the search query', () => {
|
|
const asset = createAsset({ label: '小镇背景', assetKind: 'scene' });
|
|
|
|
expect(
|
|
projectAssetMatchesPickerFilter(asset, { category: 'all', query: '' }),
|
|
).toBe(true);
|
|
expect(
|
|
projectAssetMatchesPickerFilter(asset, {
|
|
category: 'image',
|
|
query: '小镇',
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
projectAssetMatchesPickerFilter(asset, {
|
|
category: 'image',
|
|
query: 'scene',
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
projectAssetMatchesPickerFilter(asset, {
|
|
category: 'audio',
|
|
query: '小镇',
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
projectAssetMatchesPickerFilter(asset, {
|
|
category: 'all',
|
|
query: '主角',
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('builds references that carry the asset id snapshot', () => {
|
|
const assets = [
|
|
createAsset({
|
|
id: 'asset-hero',
|
|
label: '主角立绘',
|
|
objectKey: 'oss/hero.png',
|
|
}),
|
|
createAsset({
|
|
id: 'asset-bgm',
|
|
label: '背景音乐',
|
|
mediaType: 'audio',
|
|
sourceResourceId: 'resource-bgm',
|
|
}),
|
|
];
|
|
|
|
const references = projectAssetPickerReferences(assets, [
|
|
'asset-hero',
|
|
'asset-bgm',
|
|
'asset-missing',
|
|
]);
|
|
|
|
expect(references).toEqual([
|
|
{
|
|
id: 'project-asset-asset-hero',
|
|
label: '主角立绘',
|
|
src: 'oss/hero.png',
|
|
mediaType: 'image',
|
|
objectKey: 'oss/hero.png',
|
|
resourceId: null,
|
|
sourceAssetId: 'asset-hero',
|
|
},
|
|
{
|
|
id: 'project-asset-asset-bgm',
|
|
label: '背景音乐',
|
|
src: 'https://example.test/asset-1.png',
|
|
mediaType: 'audio',
|
|
resourceId: 'resource-bgm',
|
|
sourceAssetId: 'asset-bgm',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('records the selected asset ids as the submitted task reference snapshot', () => {
|
|
const assets = [
|
|
createAsset({ id: 'asset-hero', label: '主角立绘' }),
|
|
createAsset({ id: 'asset-town', label: '小镇背景' }),
|
|
];
|
|
const references = projectAssetPickerReferences(assets, [
|
|
'asset-hero',
|
|
'asset-town',
|
|
]);
|
|
|
|
const snapshot = references.flatMap((reference) =>
|
|
createGenerationInputReference('参考图', reference),
|
|
);
|
|
|
|
expect(snapshot).toEqual([
|
|
{
|
|
title: '参考图',
|
|
label: '主角立绘',
|
|
refType: 'asset',
|
|
refId: 'asset-hero',
|
|
},
|
|
{
|
|
title: '参考图',
|
|
label: '小镇背景',
|
|
refType: 'asset',
|
|
refId: 'asset-town',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns no references and dedupes repeated ids', () => {
|
|
const assets = [createAsset({ id: 'asset-hero', label: '主角立绘' })];
|
|
|
|
expect(projectAssetPickerReferences(assets, [])).toEqual([]);
|
|
expect(
|
|
projectAssetPickerReferences(assets, ['asset-hero', 'asset-hero']),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it('replaces the library-sourced references and keeps canvas-picked ones', () => {
|
|
const assets = [
|
|
createAsset({ id: 'asset-hero', label: '主角立绘' }),
|
|
createAsset({ id: 'asset-town', label: '小镇背景' }),
|
|
];
|
|
const dialog = {
|
|
mode: 'generate',
|
|
prompt: '画一张封面',
|
|
status: 'idle',
|
|
generationReferences: [
|
|
{
|
|
id: 'canvas-layer-1',
|
|
label: '画布点选',
|
|
src: 'https://example.test/canvas.png',
|
|
mediaType: 'image',
|
|
},
|
|
{
|
|
id: 'project-asset-asset-old',
|
|
label: '已移除素材',
|
|
src: 'https://example.test/old.png',
|
|
mediaType: 'image',
|
|
sourceAssetId: 'asset-old',
|
|
},
|
|
],
|
|
} satisfies GenerateDialogState;
|
|
|
|
const next = replaceProjectAssetPickerReferences(
|
|
dialog,
|
|
projectAssetPickerReferences(assets, ['asset-hero', 'asset-town']),
|
|
);
|
|
|
|
expect(
|
|
next?.generationReferences?.map((reference) => reference.id),
|
|
).toEqual([
|
|
'canvas-layer-1',
|
|
'project-asset-asset-hero',
|
|
'project-asset-asset-town',
|
|
]);
|
|
expect(next?.composerOpen).toBe(true);
|
|
});
|
|
|
|
it('replaces the 3D image-to-model source with the picked project asset', () => {
|
|
const assets = [createAsset({ id: 'asset-hero', label: '主角立绘' })];
|
|
const dialog = {
|
|
mode: 'model3d-image-to-model',
|
|
prompt: '',
|
|
status: 'failed',
|
|
errorMessage: '失败',
|
|
composerOpen: false,
|
|
generationReferences: [
|
|
{
|
|
id: 'canvas-layer-1',
|
|
label: '画布点选',
|
|
src: 'https://example.test/canvas.png',
|
|
mediaType: 'image',
|
|
},
|
|
],
|
|
} satisfies GenerateDialogState;
|
|
|
|
const next = replaceProjectAssetPickerReferences(
|
|
dialog,
|
|
projectAssetPickerReferences(assets, ['asset-hero']),
|
|
);
|
|
|
|
expect(
|
|
next?.generationReferences?.map((reference) => reference.id),
|
|
).toEqual(['project-asset-asset-hero']);
|
|
expect(next).toMatchObject({ status: 'idle', composerOpen: true });
|
|
});
|
|
|
|
it('keeps the 3D image-to-model source when the picker is confirmed empty', () => {
|
|
const dialog = {
|
|
mode: 'model3d-image-to-model',
|
|
prompt: '',
|
|
status: 'failed',
|
|
errorMessage: '失败',
|
|
composerOpen: false,
|
|
generationReferences: [
|
|
{
|
|
id: 'canvas-layer-1',
|
|
label: '画布点选',
|
|
src: 'https://example.test/canvas.png',
|
|
mediaType: 'image',
|
|
},
|
|
],
|
|
} satisfies GenerateDialogState;
|
|
|
|
const next = replaceProjectAssetPickerReferences(dialog, []);
|
|
|
|
expect(
|
|
next?.generationReferences?.map((reference) => reference.id),
|
|
).toEqual(['canvas-layer-1']);
|
|
});
|
|
|
|
it('leaves dialogs outside the reference-composer modes untouched', () => {
|
|
const dialog = {
|
|
mode: 'video',
|
|
prompt: '',
|
|
status: 'idle',
|
|
} satisfies GenerateDialogState;
|
|
|
|
expect(replaceProjectAssetPickerReferences(dialog, [])).toBe(dialog);
|
|
expect(replaceProjectAssetPickerReferences(null, [])).toBeNull();
|
|
});
|
|
});
|