Files
Genarrative/src/components/image-editor/ImageCanvasEditorModel.test.ts
kdletters 1f5605331f 拆分图片画布编辑器前端模型
抽出编辑器共享类型、画布模型、生成模型和导出模型

补充模型层单测覆盖素材、吸附、生成快照和导出规则

新增前端拆分计划并更新 TRACKING 浏览器回归记录
2026-06-17 01:53:59 +08:00

165 lines
4.3 KiB
TypeScript

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