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(); }); });