import { describe, expect, it } from 'vitest'; import type { CanvasGenerationDialogState, CanvasLayer, CharacterAnimationPanelState, QuickEditPanelState, } from './ImageCanvasEditorTypes'; import { isCanvasGenerationComposerVisible, resolveCharacterAnimationPanelStyle, resolveGenerationAnchor, resolveGenerationComposerStyle, resolveIconComposerStyle, resolveQuickEditPanelStyle, resolveSelectedToolbarStyle, } from './ImageCanvasOverlayModel'; function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-a', resourceId: 'resource-a', title: '图层A', src: 'data:image/png;base64,layer', x: 100, y: 80, width: 240, height: 120, originalWidth: 240, originalHeight: 120, zIndex: 1, sourceType: 'uploaded', ...overrides, }; } function createDialog( overrides: Partial = {}, ): CanvasGenerationDialogState { return { id: 'dialog-a', mode: 'generate', prompt: '', status: 'idle', placeholder: { x: 300, y: 200, width: 360, height: 260, originalWidth: 1024, originalHeight: 1024, }, ...overrides, }; } describe('ImageCanvasOverlayModel', () => { it('anchors generation composer to generated layers before placeholders', () => { const generatedLayer = createLayer({ x: 40, y: 60, width: 320, height: 180 }); const dialog = createDialog({ generatedLayerId: generatedLayer.id }); const anchor = resolveGenerationAnchor({ dialog, generatedLayer }); expect(anchor).toBe(generatedLayer); expect( resolveGenerationComposerStyle({ dialog, anchor, viewport: { x: 10, y: 20, scale: 2 }, }), ).toEqual({ left: 410, top: 510, }); }); it('hides generation composer while generating or explicitly closed', () => { const placeholder = createDialog().placeholder ?? null; expect( resolveGenerationComposerStyle({ dialog: createDialog({ status: 'generating' }), anchor: placeholder, viewport: { x: 0, y: 0, scale: 1 }, }), ).toBeNull(); expect( resolveGenerationComposerStyle({ dialog: createDialog({ composerOpen: false }), anchor: placeholder, viewport: { x: 0, y: 0, scale: 1 }, }), ).toBeNull(); }); it('expands icon composer width by description count', () => { expect( resolveIconComposerStyle({ dialog: createDialog({ mode: 'icon' }), composerStyle: { left: 100, top: 200 }, iconDescriptionCount: 6, }), ).toEqual({ left: 100, top: 200, width: '52.8rem', }); expect( resolveIconComposerStyle({ dialog: createDialog({ mode: 'generate' }), composerStyle: { left: 100, top: 200 }, iconDescriptionCount: 6, }), ).toBeNull(); }); it('keeps selected layer toolbar inside the canvas width', () => { expect( resolveSelectedToolbarStyle({ selectedLayer: createLayer({ x: -500, y: -100 }), viewport: { x: 0, y: 0, scale: 1 }, canvasSize: { width: 900, height: 640 }, }), ).toEqual({ left: 132, top: 10 }); expect( resolveSelectedToolbarStyle({ selectedLayer: createLayer({ x: 1200, y: 100 }), viewport: { x: 0, y: 0, scale: 1 }, canvasSize: { width: 900, height: 640 }, }), ).toEqual({ left: 768, top: 88 }); }); it('positions quick edit and character animation panels with viewport bounds', () => { const sourceLayer = createLayer({ x: 740, y: 460, width: 240, height: 180 }); const quickEditPanel: QuickEditPanelState = { sourceLayerId: sourceLayer.id, prompt: '', size: '1024x1024', model: 'gpt-image-2', status: 'idle', }; const characterPanel: CharacterAnimationPanelState = { sourceLayerId: sourceLayer.id, promptText: '', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, status: 'idle', }; expect( resolveQuickEditPanelStyle({ panel: quickEditPanel, sourceLayer, viewport: { x: 20, y: 10, scale: 1 }, canvasSize: { width: 900, height: 640 }, }), ).toEqual({ left: 880, top: 280 }); expect( resolveCharacterAnimationPanelStyle({ panel: characterPanel, sourceLayer, viewport: { x: 20, y: 10, scale: 1 }, canvasSize: { width: 900, height: 640 }, }), ).toEqual({ left: 536, top: 120 }); }); it('recognizes canvas generation composer dialog modes', () => { expect(isCanvasGenerationComposerVisible(createDialog({ mode: 'generate' }))).toBe( true, ); expect(isCanvasGenerationComposerVisible(createDialog({ mode: 'spec' }))).toBe( true, ); expect( isCanvasGenerationComposerVisible({ mode: 'edit', prompt: '', status: 'idle', }), ).toBe(false); expect(isCanvasGenerationComposerVisible(null)).toBe(false); }); });