拆分编辑器前端画布视图

抽出素材栏、生成器、舞台工具栏和画布世界视图

补充各拆分视图的聚焦测试

更新 TRACKING.md 记录第三十四阶段验证
This commit is contained in:
2026-06-17 17:48:12 +08:00
parent 7a77ab4df7
commit d8b935317d
42 changed files with 6527 additions and 2992 deletions

View File

@@ -0,0 +1,221 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type {
CanvasGenerationDialogState,
CanvasLayer,
} from './ImageCanvasEditorTypes';
import { ImageCanvasWorldView } from './ImageCanvasWorldView';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
id: 'layer-1',
resourceId: 'resource-1',
title: '角色主图',
src: 'data:image/png;base64,layer',
x: 120,
y: 160,
width: 320,
height: 240,
originalWidth: 640,
originalHeight: 480,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
function createGenerationDialog(
overrides: Partial<CanvasGenerationDialogState> = {},
): CanvasGenerationDialogState {
return {
id: 'dialog-1',
mode: 'generate',
prompt: '生成一张图',
status: 'idle',
placeholder: {
x: 480,
y: 320,
width: 360,
height: 240,
originalWidth: 1024,
originalHeight: 768,
},
...overrides,
};
}
function renderWorldView(
overrides: Partial<Parameters<typeof ImageCanvasWorldView>[0]> = {},
) {
const props: Parameters<typeof ImageCanvasWorldView>[0] = {
viewport: { x: 10, y: 20, scale: 1.5 },
snapGuide: null,
layers: [createLayer()],
selectedLayerIds: [],
hoveredLayerId: null,
canvasMarquee: null,
canvasGenerationDialogs: [],
generateDialog: null,
quickEditPanel: null,
generationComposerStyle: null,
onLayerPointerDown: vi.fn(),
onLayerClick: vi.fn(),
onLayerContextMenu: vi.fn(),
onLayerMouseEnter: vi.fn(),
onLayerMouseLeave: vi.fn(),
onOpenLayerMetadata: vi.fn(),
onGenerationFramePointerDown: vi.fn(),
onActivateGenerationDialog: vi.fn(),
...overrides,
};
const result = render(<ImageCanvasWorldView {...props} />);
return { props, ...result };
}
describe('ImageCanvasWorldView', () => {
it('renders visible layers and filters hidden layers', () => {
const visibleLayer = createLayer({ title: '可见图层', zIndex: 2 });
const hiddenLayer = createLayer({
id: 'layer-hidden',
resourceId: 'resource-hidden',
title: '隐藏图层',
hidden: true,
});
renderWorldView({
layers: [hiddenLayer, visibleLayer],
selectedLayerIds: [visibleLayer.id],
hoveredLayerId: visibleLayer.id,
quickEditPanel: {
sourceLayerId: visibleLayer.id,
prompt: '快速编辑',
size: '1024x1024',
model: 'gpt-image-2',
status: 'generating',
},
});
const visibleButton = screen.getByRole('button', { name: '选择可见图层' });
expect(screen.queryByRole('button', { name: '选择隐藏图层' })).toBeNull();
expect(visibleButton.className).toContain(
'image-canvas-editor__layer--selected',
);
expect(visibleButton.className).toContain(
'image-canvas-editor__layer--hovered',
);
expect(visibleButton.className).toContain(
'image-canvas-editor__layer--generating',
);
expect(visibleButton.style.left).toBe('120px');
expect(visibleButton.style.top).toBe('160px');
expect(screen.getByText('640 x 480 px')).toBeTruthy();
expect(screen.getByRole('status', { name: '' }).textContent).toBe('生成中');
});
it('forwards layer pointer, hover, context menu and metadata actions', () => {
const layer = createLayer({ assetKind: 'character' });
const { props } = renderWorldView({ layers: [layer] });
const layerButton = screen.getByRole('button', { name: '选择角色主图' });
const metadataButton = within(layerButton).getByRole('button', {
name: '查看角色主图图片信息',
});
fireEvent.pointerDown(layerButton);
fireEvent.click(layerButton);
fireEvent.contextMenu(layerButton);
fireEvent.mouseEnter(layerButton);
fireEvent.mouseLeave(layerButton);
fireEvent.click(metadataButton);
expect(props.onLayerPointerDown).toHaveBeenCalledWith(
expect.any(Object),
layer,
);
expect(props.onLayerClick).toHaveBeenCalledWith(expect.any(Object), layer);
expect(props.onLayerContextMenu).toHaveBeenCalledWith(
expect.any(Object),
layer,
);
expect(props.onLayerMouseEnter).toHaveBeenCalledWith(layer.id);
expect(props.onLayerMouseLeave).toHaveBeenCalledWith(layer.id);
expect(props.onOpenLayerMetadata).toHaveBeenCalledWith(layer);
expect(screen.getByText('角色')).toBeTruthy();
});
it('renders snap guides, marquee and floating generation status', () => {
renderWorldView({
snapGuide: { vertical: 288, horizontal: 344 },
canvasMarquee: {
pointerId: 1,
startX: 40,
startY: 50,
currentX: 190,
currentY: 230,
},
generateDialog: {
id: 'dialog-active',
mode: 'generate',
prompt: '生成中',
status: 'generating',
},
generationComposerStyle: { left: 12, top: 24 },
});
expect(
screen.getByTestId('image-canvas-editor-snap-guide-vertical').style.left,
).toBe('288px');
expect(
screen.getByTestId('image-canvas-editor-snap-guide-horizontal').style.top,
).toBe('344px');
expect(screen.getByText('生成中')).toBeTruthy();
const world = document.querySelector('.image-canvas-editor__world');
const marquee = world?.querySelector('.image-canvas-editor__canvas-marquee');
expect(world?.getAttribute('style')).toContain(
'transform: translate(10px, 20px) scale(1.5)',
);
expect((marquee as HTMLElement | null)?.style.width).toBe('100px');
expect((marquee as HTMLElement | null)?.style.height).toBe('120px');
});
it('renders generation placeholders and forwards frame actions', () => {
const dialog = createGenerationDialog({
mode: 'icon',
status: 'generating',
});
const { props } = renderWorldView({
canvasGenerationDialogs: [
dialog,
createGenerationDialog({
id: 'dialog-without-placeholder',
placeholder: undefined,
}),
],
});
const frame = screen.getByRole('button', { name: '图标素材生成占位图' });
expect(within(frame).getByText('Icon Generator')).toBeTruthy();
expect(within(frame).getByText('图标')).toBeTruthy();
expect(within(frame).getByText('1024 x 768')).toBeTruthy();
expect(within(frame).getByRole('status').textContent).toBe('生成中');
fireEvent.pointerDown(frame);
fireEvent.doubleClick(frame);
expect(props.onGenerationFramePointerDown).toHaveBeenCalledWith(
expect.any(Object),
dialog,
);
expect(props.onActivateGenerationDialog).toHaveBeenCalledWith(dialog);
expect(screen.queryByText('dialog-without-placeholder')).toBeNull();
});
});