Files
Genarrative/src/components/project/ProjectCanvasCover.test.ts
T
kdletters 28e11c4718 修复项目封面本地兜底
新增项目封面 IndexedDB 本地缓存。

封面 Blob 生成后先写入本地缓存,上传失败仍可展示。

项目列表优先服务端封面,其次本地缓存,再回退画布图层。

补充封面兜底测试和项目记忆文档。
2026-06-25 21:59:05 +08:00

313 lines
9.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { EditorProjectSnapshot } from '../../services/image-editor/editorProjectClient';
import {
LOCAL_PROJECT_COVER_CACHE_ASSET_KIND,
resolveProjectCoverResource,
resolveProjectCoverSnapshotResource,
} from './ProjectCanvasCover';
function project(
resources: EditorProjectSnapshot['resources'],
layers: EditorProjectSnapshot['layers'] = [],
): EditorProjectSnapshot {
return {
projectId: 'editor-project-cover',
title: '封面项目',
viewport: { x: 0, y: 0, scale: 1 },
layers,
resources,
updatedAt: '2026-06-12T00:00:00.000Z',
};
}
describe('resolveProjectCoverSnapshotResource', () => {
it('prefers the newest cover snapshot object timestamp', () => {
const cover = resolveProjectCoverSnapshotResource(
project([
{
resourceId: 'resource-newer-created-row',
projectId: 'editor-project-cover',
imageSrc:
'/generated-character-drafts/editor/project-covers/editor-project-cover/1771400000000/cover.png',
objectKey:
'generated-character-drafts/editor/project-covers/editor-project-cover/1771400000000/cover.png',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'project-cover-snapshot',
updatedAt: '2026-06-12T09:10:00.000Z',
},
{
resourceId: 'resource-newer-snapshot',
projectId: 'editor-project-cover',
imageSrc:
'/generated-character-drafts/editor/project-covers/editor-project-cover/1771400000100/cover.png',
objectKey:
'generated-character-drafts/editor/project-covers/editor-project-cover/1771400000100/cover.png',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'project-cover-snapshot',
updatedAt: '2026-06-12T09:00:00.000Z',
},
]),
);
expect(cover?.resourceId).toBe('resource-newer-snapshot');
});
it('ignores non-cover resources and empty cover snapshots', () => {
const cover = resolveProjectCoverSnapshotResource(
project([
{
resourceId: 'resource-image',
projectId: 'editor-project-cover',
imageSrc: '/generated/source.png',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'character',
},
{
resourceId: 'resource-empty-cover',
projectId: 'editor-project-cover',
imageSrc: '',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'project-cover-snapshot',
},
]),
);
expect(cover).toBeNull();
});
it('supports legacy editor object asset kind identifier', () => {
const cover = resolveProjectCoverSnapshotResource(
project([
{
resourceId: 'resource-legacy-cover',
projectId: 'editor-project-cover',
imageSrc: '',
objectKey:
'generated-character-drafts/editor/project-covers/editor-project-cover/1771400000200/cover.png',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'editor_project_cover_snapshot',
},
]),
);
expect(cover?.resourceId).toBe('resource-legacy-cover');
});
it('supports legacy snake-case project cover resource payload', () => {
const cover = resolveProjectCoverSnapshotResource(
project([
{
resourceId: 'resource-snake-cover',
projectId: 'editor-project-cover',
imageSrc: '',
width: 320,
height: 240,
sourceType: 'uploaded',
asset_kind: 'project-cover-snapshot',
src: '/generated-character-drafts/editor/project-covers/editor-project-cover/1771400000300/cover.png',
} as EditorProjectSnapshot['resources'][number],
]),
);
expect(cover?.resourceId).toBe('resource-snake-cover');
});
});
describe('resolveProjectCoverResource', () => {
it('prefers the saved cover snapshot over canvas layer resources', () => {
const cover = resolveProjectCoverResource(
project(
[
{
resourceId: 'resource-layer',
projectId: 'editor-project-cover',
imageSrc: '/generated/layer.png',
width: 320,
height: 240,
sourceType: 'uploaded',
},
{
resourceId: 'resource-cover',
projectId: 'editor-project-cover',
imageSrc: '/generated/cover.png',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'project-cover-snapshot',
},
],
[{ layerId: 'layer-top', resourceId: 'resource-layer', zIndex: 9 }],
),
);
expect(cover?.resourceId).toBe('resource-cover');
});
it('uses local cached covers only when no saved cover snapshot exists', () => {
const cover = resolveProjectCoverResource(
project([
{
resourceId: 'resource-local-cover',
projectId: 'editor-project-cover',
imageSrc: 'blob:local-cover',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: LOCAL_PROJECT_COVER_CACHE_ASSET_KIND,
updatedAt: '2026-06-12T09:10:00.000Z',
},
{
resourceId: 'resource-saved-cover',
projectId: 'editor-project-cover',
imageSrc: '/generated/cover.png',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: 'project-cover-snapshot',
updatedAt: '2026-06-12T09:00:00.000Z',
},
]),
);
expect(cover?.resourceId).toBe('resource-saved-cover');
const localCover = resolveProjectCoverResource(
project([
{
resourceId: 'resource-local-cover',
projectId: 'editor-project-cover',
imageSrc: 'blob:local-cover',
width: 320,
height: 240,
sourceType: 'uploaded',
assetKind: LOCAL_PROJECT_COVER_CACHE_ASSET_KIND,
},
]),
);
expect(localCover?.resourceId).toBe('resource-local-cover');
});
it('falls back to the top visible canvas layer resource', () => {
const cover = resolveProjectCoverResource(
project(
[
{
resourceId: 'resource-back',
projectId: 'editor-project-cover',
imageSrc: '/generated/back.png',
width: 320,
height: 240,
sourceType: 'uploaded',
},
{
resourceId: 'resource-front',
projectId: 'editor-project-cover',
imageSrc: '/generated/front.png',
width: 320,
height: 240,
sourceType: 'uploaded',
},
],
[
{ layerId: 'layer-back', resourceId: 'resource-back', zIndex: 1 },
{ layerId: 'layer-front', resourceId: 'resource-front', zIndex: 3 },
],
),
);
expect(cover?.resourceId).toBe('resource-front');
});
it('ignores hidden and audio layers when falling back', () => {
const cover = resolveProjectCoverResource(
project(
[
{
resourceId: 'resource-hidden',
projectId: 'editor-project-cover',
imageSrc: '/generated/hidden.png',
width: 320,
height: 240,
sourceType: 'uploaded',
},
{
resourceId: 'resource-audio',
projectId: 'editor-project-cover',
imageSrc: '/generated/audio.mp3',
width: 320,
height: 240,
sourceType: 'uploaded',
},
{
resourceId: 'resource-visible',
projectId: 'editor-project-cover',
imageSrc: '/generated/visible.png',
width: 320,
height: 240,
sourceType: 'uploaded',
},
],
[
{
layerId: 'layer-hidden',
resourceId: 'resource-hidden',
hidden: true,
zIndex: 9,
},
{
layerId: 'layer-audio',
resourceId: 'resource-audio',
mediaType: 'audio',
zIndex: 8,
},
{
layerId: 'layer-visible',
resourceId: 'resource-visible',
zIndex: 1,
},
],
),
);
expect(cover?.resourceId).toBe('resource-visible');
});
it('returns null when the project has no visible layer image resource', () => {
const cover = resolveProjectCoverResource(
project(
[
{
resourceId: 'resource-hidden',
projectId: 'editor-project-cover',
imageSrc: '/generated/hidden.png',
width: 320,
height: 240,
sourceType: 'uploaded',
},
],
[
{
layerId: 'layer-hidden',
resourceId: 'resource-hidden',
hidden: true,
},
],
),
);
expect(cover).toBeNull();
});
});