Files
Genarrative/src/components/image-editor/ImageCanvasOverlayModel.test.ts
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

241 lines
6.4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type {
CanvasGenerationDialogState,
CanvasLayer,
CharacterAnimationPanelState,
CropExpandPanelState,
QuickEditPanelState,
} from './ImageCanvasEditorTypes';
import {
isCanvasGenerationComposerVisible,
resolveCharacterAnimationPanelStyle,
resolveCropExpandPanelStyle,
resolveGenerationAnchor,
resolveGenerationComposerStyle,
resolveIconComposerStyle,
resolveQuickEditFocusViewport,
resolveQuickEditPanelStyle,
resolveSelectedToolbarStyle,
} from './ImageCanvasOverlayModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): 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> = {},
): 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('keeps icon composer aligned to the base generation composer width', () => {
expect(
resolveIconComposerStyle({
dialog: createDialog({ mode: 'icon' }),
composerStyle: { left: 100, top: 200 },
}),
).toEqual({
left: 100,
top: 200,
});
expect(
resolveIconComposerStyle({
dialog: createDialog({ mode: 'generate' }),
composerStyle: { left: 100, top: 200 },
}),
).toBeNull();
});
it('lets selected layer toolbar move past the horizontal canvas edges', () => {
expect(
resolveSelectedToolbarStyle({
selectedLayer: createLayer({ x: -500, y: -100 }),
viewport: { x: 0, y: 0, scale: 1 },
canvasSize: { width: 900, height: 640 },
}),
).toEqual({ left: -380, top: 10 });
expect(
resolveSelectedToolbarStyle({
selectedLayer: createLayer({ x: 1200, y: 100 }),
viewport: { x: 0, y: 0, scale: 1 },
canvasSize: { width: 900, height: 640 },
}),
).toEqual({ left: 1320, top: 88 });
});
it('fits tall quick edit sources above the reserved bottom composer area', () => {
const sourceLayer = createLayer({
x: 100,
y: 50,
width: 360,
height: 1600,
});
const viewport = resolveQuickEditFocusViewport({
sourceLayer,
canvasSize: { width: 900, height: 640 },
});
const sourceTop = viewport.y + sourceLayer.y * viewport.scale;
const sourceBottom = sourceTop + sourceLayer.height * viewport.scale;
expect(viewport.scale).toBeCloseTo(0.1925, 4);
expect(sourceTop).toBeCloseTo(56, 2);
expect(sourceBottom).toBeLessThanOrEqual(364);
expect(sourceBottom + 12 + 260).toBeLessThanOrEqual(640);
});
it('positions generation panels from their anchors without clamping to 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',
};
const cropExpandPanel: CropExpandPanelState = {
sourceLayerId: sourceLayer.id,
frame: {
x: sourceLayer.x,
y: sourceLayer.y,
width: sourceLayer.width,
height: sourceLayer.height,
},
ratio: 'free',
status: 'idle',
};
expect(
resolveQuickEditPanelStyle({
panel: quickEditPanel,
sourceLayer,
viewport: { x: 20, y: 10, scale: 1 },
canvasSize: { width: 900, height: 640 },
}),
).toEqual({ left: 880, top: 662 });
expect(
resolveCropExpandPanelStyle({
panel: cropExpandPanel,
sourceLayer,
viewport: { x: 20, y: 10, scale: 1 },
canvasSize: { width: 900, height: 640 },
}),
).toEqual({ left: 1012, top: 470 });
expect(
resolveCharacterAnimationPanelStyle({
panel: characterPanel,
sourceLayer,
viewport: { x: 20, y: 10, scale: 1 },
canvasSize: { width: 900, height: 640 },
}),
).toEqual({ left: 1012, top: 470 });
});
it('recognizes canvas generation composer dialog modes', () => {
expect(
isCanvasGenerationComposerVisible(createDialog({ mode: 'generate' })),
).toBe(true);
expect(
isCanvasGenerationComposerVisible(createDialog({ mode: 'spec' })),
).toBe(true);
expect(
isCanvasGenerationComposerVisible(createDialog({ mode: 'publication' })),
).toBe(true);
expect(
isCanvasGenerationComposerVisible({
mode: 'edit',
prompt: '',
status: 'idle',
}),
).toBe(false);
expect(isCanvasGenerationComposerVisible(null)).toBe(false);
});
});