Files
Genarrative/src/components/image-editor/ImageCanvasLayerCommandModel.test.ts
kdletters 7b5d74037a 抽出图片画布图层命令模型
新增 ImageCanvasLayerCommandModel 收口右键图层复制、粘贴、层级、分组、显隐、锁定、翻转和删除规则

主视图保留历史、选中态、菜单关闭、元数据清理和导出副作用

补充图片右键菜单真实浏览器冒泡回归测试

更新图片画布前端拆分计划和 TRACKING 验证记录
2026-06-17 03:37:52 +08:00

196 lines
6.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
createCanvasLayerClipboard,
duplicateCanvasLayers,
flipCanvasLayers,
getCanvasLayersByIds,
groupCanvasLayers,
moveCanvasLayers,
removeCanvasLayers,
resolveContextTargetLayerIds,
toggleCanvasLayersLock,
toggleCanvasLayersVisibility,
ungroupCanvasLayers,
} from './ImageCanvasLayerCommandModel';
import type {
CanvasContextMenuState,
CanvasLayer,
} from './ImageCanvasEditorTypes';
function createLayer(overrides: Partial<CanvasLayer>): CanvasLayer {
const id = overrides.id ?? 'layer-a';
return {
id,
resourceId: `resource-${id}`,
title: id,
src: `data:image/png;base64,${id}`,
x: 10,
y: 20,
width: 100,
height: 80,
originalWidth: 100,
originalHeight: 80,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
describe('ImageCanvasLayerCommandModel', () => {
it('resolves context menu targets from the current multi-selection', () => {
const menu: CanvasContextMenuState = {
kind: 'layer',
x: 0,
y: 0,
layerId: 'layer-b',
canvasPoint: { x: 10, y: 20 },
};
expect(resolveContextTargetLayerIds(menu, ['layer-a', 'layer-b'])).toEqual([
'layer-a',
'layer-b',
]);
expect(resolveContextTargetLayerIds(menu, ['layer-a'])).toEqual(['layer-b']);
expect(
resolveContextTargetLayerIds(
{ kind: 'blank', x: 0, y: 0, canvasPoint: { x: 0, y: 0 } },
['layer-a'],
),
).toEqual([]);
});
it('duplicates layers to a canvas point while preserving relative offsets', () => {
const first = createLayer({ id: 'first', title: '第一张', x: 20, y: 30 });
const second = createLayer({
id: 'second',
title: '第二张',
x: 70,
y: 90,
zIndex: 4,
groupId: 'group-old',
});
const duplicated = duplicateCanvasLayers({
sourceLayers: [first, second],
allLayers: [first, second],
canvasPoint: { x: 300, y: 200 },
stamp: 'test',
});
expect(duplicated).toMatchObject([
{
id: 'layer-copy-test-0',
resourceId: 'local-resource-copy-test-0',
title: '第一张 副本',
x: 300,
y: 200,
zIndex: 5,
groupId: null,
},
{
id: 'layer-copy-test-1',
resourceId: 'local-resource-copy-test-1',
title: '第二张 副本',
x: 350,
y: 260,
zIndex: 6,
groupId: null,
},
]);
const cutPaste = duplicateCanvasLayers({
sourceLayers: [first],
allLayers: [first, second],
renameCopies: false,
stamp: 'cut',
});
expect(cutPaste[0]?.title).toBe('第一张');
expect(cutPaste[0]?.x).toBe(first.x + 32);
});
it('creates a cloned clipboard and removes target layers', () => {
const layers = [
createLayer({ id: 'first' }),
createLayer({ id: 'second', generationInputs: { fields: [], references: [] } }),
];
const clipboard = createCanvasLayerClipboard(layers, ['second'], 'copy');
expect(clipboard?.mode).toBe('copy');
expect(clipboard?.layers).toEqual([layers[1]]);
expect(clipboard?.layers[0]).not.toBe(layers[1]);
expect(getCanvasLayersByIds(layers, ['first'])).toEqual([layers[0]]);
expect(removeCanvasLayers(layers, ['first'])).toEqual([layers[1]]);
});
it('moves layer z-indexes with the same commands as the context menu', () => {
const layers = [
createLayer({ id: 'bottom', zIndex: 1 }),
createLayer({ id: 'middle', zIndex: 3 }),
createLayer({ id: 'top', zIndex: 8 }),
];
expect(moveCanvasLayers(layers, ['middle'], 'up')[1]?.zIndex).toBe(4);
expect(moveCanvasLayers(layers, ['middle'], 'down')[1]?.zIndex).toBe(2);
expect(moveCanvasLayers(layers, ['bottom', 'middle'], 'top')).toMatchObject([
{ id: 'bottom', zIndex: 9 },
{ id: 'middle', zIndex: 10 },
{ id: 'top', zIndex: 8 },
]);
expect(
moveCanvasLayers(layers, ['bottom', 'middle'], 'bottom'),
).toMatchObject([
{ id: 'bottom', zIndex: -2 },
{ id: 'middle', zIndex: -1 },
{ id: 'top', zIndex: 8 },
]);
});
it('groups, ungroups, toggles visibility and lock, and flips layers', () => {
const layers = [
createLayer({ id: 'first', hidden: true, locked: true }),
createLayer({ id: 'second' }),
createLayer({ id: 'third' }),
];
const targetIds = ['first', 'second'];
const groupedLayers = groupCanvasLayers(layers, targetIds, 'group-next');
expect(groupedLayers[0]?.groupId).toBe('group-next');
expect(groupedLayers[1]?.groupId).toBe('group-next');
expect(groupedLayers[2]?.groupId).toBeUndefined();
const ungroupedLayers = ungroupCanvasLayers(groupedLayers, targetIds);
expect(ungroupedLayers[0]?.groupId).toBeNull();
expect(ungroupedLayers[1]?.groupId).toBeNull();
expect(ungroupedLayers[2]?.groupId).toBeUndefined();
const hiddenLayers = toggleCanvasLayersVisibility(layers, targetIds);
expect(hiddenLayers[0]?.hidden).toBe(true);
expect(hiddenLayers[1]?.hidden).toBe(true);
expect(hiddenLayers[2]?.hidden).toBeUndefined();
const shownLayers = toggleCanvasLayersVisibility([
createLayer({ id: 'first', hidden: true }),
createLayer({ id: 'second', hidden: true }),
], targetIds);
expect(shownLayers[0]?.hidden).toBe(false);
expect(shownLayers[1]?.hidden).toBe(false);
const lockedLayers = toggleCanvasLayersLock(layers, targetIds);
expect(lockedLayers[0]?.locked).toBe(true);
expect(lockedLayers[1]?.locked).toBe(true);
expect(lockedLayers[2]?.locked).toBeUndefined();
const flippedXLayers = flipCanvasLayers(layers, targetIds, 'x');
expect(flippedXLayers[0]?.flipX).toBe(true);
expect(flippedXLayers[1]?.flipX).toBe(true);
expect(flippedXLayers[2]?.flipX).toBeUndefined();
const flippedYLayers = flipCanvasLayers(layers, targetIds, 'y');
expect(flippedYLayers[0]?.flipY).toBe(true);
expect(flippedYLayers[1]?.flipY).toBe(true);
expect(flippedYLayers[2]?.flipY).toBeUndefined();
});
});