拆分编辑器前端画布视图
抽出素材栏、生成器、舞台工具栏和画布世界视图 补充各拆分视图的聚焦测试 更新 TRACKING.md 记录第三十四阶段验证
This commit is contained in:
221
src/components/image-editor/ImageCanvasWorldView.test.tsx
Normal file
221
src/components/image-editor/ImageCanvasWorldView.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user