import { describe, expect, it } from 'vitest'; import { CANVAS_WORLD_ORIGIN, createLayerFromAsset, hydrateLayer, normalizeAssetLibrary, normalizeCanvasBackgroundHex, resolveSnappedLayerPosition, serializeLayer, } from './ImageCanvasEditorModel'; import type { CanvasLayer, EditorAsset } from './ImageCanvasEditorTypes'; describe('ImageCanvasEditorModel', () => { it('normalizes valid canvas background hex values and rejects invalid input', () => { expect(normalizeCanvasBackgroundHex(' #ABC ')).toBe('#aabbcc'); expect(normalizeCanvasBackgroundHex('#f8fafc')).toBe('#f8fafc'); expect(normalizeCanvasBackgroundHex('white')).toBeNull(); expect(normalizeCanvasBackgroundHex('#not-a-color')).toBeNull(); }); it('keeps only one default asset folder when normalizing the persisted library', () => { const library = normalizeAssetLibrary({ folders: [ { folderId: 'project', label: '项目素材', sortOrder: 0, collapsed: false, systemDefault: true, }, { folderId: 'project-duplicate', label: '旧项目素材', sortOrder: 1, collapsed: false, systemDefault: true, }, ], assets: [], }); expect(library.folders).toHaveLength(1); expect(library.folders[0]?.id).toBe('project'); }); it('creates a layer from an account asset at the requested screen point', () => { const asset: EditorAsset = { id: 'asset-1', label: '角色草图', src: 'data:image/png;base64,one', width: 640, height: 480, folderId: 'project', sourceKind: 'uploaded', sourceType: 'uploaded', persisted: true, objectKey: 'oss/asset-1.png', assetObjectId: 'object-1', }; const layer = createLayerFromAsset( asset, 3, { x: 20, y: 40, scale: 2 }, { x: 420, y: 340 }, ); expect(layer).toMatchObject({ id: 'layer-asset-1-3', title: '角色草图', width: 640, height: 480, originalWidth: 640, originalHeight: 480, objectKey: 'oss/asset-1.png', assetObjectId: 'object-1', sourceAssetId: 'asset-1', }); expect(layer.x).toBe(-18); expect(layer.y).toBe(12); }); it('serializes and hydrates canvas layer metadata without embedding image payloads', () => { const layer: CanvasLayer = { id: 'layer-generated', resourceId: 'resource-generated', title: '生成图', src: 'data:image/png;base64,heavy', x: 10, y: 20, width: 1024, height: 768, originalWidth: 1024, originalHeight: 768, zIndex: 9, sourceType: 'generated', objectKey: 'generated/object.png', assetKind: 'character', generationInputs: { fields: [{ title: '角色设定', value: '骑士' }], references: [], }, locked: true, }; const snapshot = serializeLayer(layer); expect(snapshot).not.toHaveProperty('src'); const hydrated = hydrateLayer( snapshot, new Map([['resource-generated', { imageSrc: '/read/generated.png' }]]), ); expect(hydrated).toMatchObject({ id: 'layer-generated', src: '/read/generated.png', sourceType: 'generated', assetKind: 'character', objectKey: 'generated/object.png', locked: true, }); }); it('snaps moving layers to nearby canvas and layer guides', () => { const movingLayer: CanvasLayer = { id: 'moving', resourceId: 'resource-moving', title: '移动图', src: 'data:image/png;base64,moving', x: 0, y: 0, width: 100, height: 100, originalWidth: 100, originalHeight: 100, zIndex: 1, sourceType: 'uploaded', }; const anchorLayer: CanvasLayer = { ...movingLayer, id: 'anchor', resourceId: 'resource-anchor', x: 300, y: 240, zIndex: 2, }; const snapped = resolveSnappedLayerPosition( movingLayer, CANVAS_WORLD_ORIGIN - 47, 238, [movingLayer, anchorLayer], 1, ); expect(snapped.x).toBe(CANVAS_WORLD_ORIGIN - movingLayer.width / 2); expect(snapped.y).toBe(anchorLayer.y); expect(snapped.guide).toEqual({ vertical: CANVAS_WORLD_ORIGIN, horizontal: anchorLayer.y, }); }); });