import { describe, expect, it } from 'vitest'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import { buildProjectCoverSnapshotSignature, normalizeProjectCoverSnapshotViewport, resolveProjectCoverDrawableLayers, resolveProjectCoverSnapshotFrame, } from './ImageCanvasProjectCoverSnapshotModel'; function layer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-a', resourceId: 'resource-a', title: '图层', src: '/generated/layer-a.png', x: 10, y: 20, width: 320, height: 240, originalWidth: 320, originalHeight: 240, zIndex: 2, sourceType: 'generated', ...overrides, }; } describe('ImageCanvasProjectCoverSnapshotModel', () => { it('uses at least a centered 1280 by 960 viewport while preserving larger viewports', () => { expect( normalizeProjectCoverSnapshotViewport({ viewport: { x: 12, y: -8, scale: 0.5 }, viewportSize: { width: 900, height: 640 }, }), ).toEqual({ viewport: { x: 202, y: 152, scale: 0.5 }, viewportSize: { width: 1280, height: 960 }, }); expect( normalizeProjectCoverSnapshotViewport({ viewport: { x: 12, y: -8, scale: 0.5 }, viewportSize: { width: 1920, height: 1080 }, }), ).toEqual({ viewport: { x: 12, y: -8, scale: 0.5 }, viewportSize: { width: 1920, height: 1080 }, }); }); it('builds a stable cover signature from visible drawable layers', () => { const signature = buildProjectCoverSnapshotSignature({ viewport: { x: 12, y: -8, scale: 0.75 }, viewportSize: { width: 900, height: 640 }, backgroundColor: '#f8fafc', layers: [ layer({ id: 'hidden', hidden: true }), layer({ id: 'audio', mediaType: 'audio', src: '/generated/audio.mp3', }), layer({ id: 'front', objectKey: 'generated/front.png', zIndex: 5, }), layer({ id: 'back', src: '/generated/back.png', zIndex: 1, }), ], }); expect(signature).toContain('"scale":0.75'); expect(signature).toContain('"width":900'); expect(signature).toContain('"backgroundColor":"#f8fafc"'); expect(signature).toContain('"id":"back"'); expect(signature).toContain('"id":"front"'); expect(signature).not.toContain('"id":"hidden"'); expect(signature).not.toContain('"id":"audio"'); }); it('center-crops the live canvas viewport to fill the 4:3 cover', () => { const landscapeFrame = resolveProjectCoverSnapshotFrame({ viewport: { x: -260, y: 70, scale: 0.5 }, viewportSize: { width: 900, height: 640 }, }); expect(landscapeFrame.renderScale).toBe(0.5); expect(landscapeFrame.frameScale).toBeCloseTo(0.375); expect(landscapeFrame.offsetX).toBeCloseTo(-8.75); expect(landscapeFrame.offsetY).toBeCloseTo(0); const portraitFrame = resolveProjectCoverSnapshotFrame({ viewport: { x: 30, y: -40, scale: 1.25 }, viewportSize: { width: 640, height: 900 }, }); expect(portraitFrame.renderScale).toBe(1.25); expect(portraitFrame.frameScale).toBeCloseTo(0.5); expect(portraitFrame.offsetX).toBeCloseTo(0); expect(portraitFrame.offsetY).toBeCloseTo(-105); }); it('uses the first image-sequence frame as the drawable source', () => { expect( resolveProjectCoverDrawableLayers([ layer({ mediaType: 'image-sequence', imageSequenceFrames: [ { frameIndex: 1, imageSrc: '/generated/frame-1.png', objectKey: 'generated/frame-1.png', width: 320, height: 240, }, ], }), ]), ).toMatchObject([ { src: '/generated/frame-1.png', objectKey: 'generated/frame-1.png', }, ]); }); });