将生成器对话框作为画布布局项序列化和恢复 生成成功后保留生成器快照并锚定到成品图层 图片类生成结果同步写入账号素材库 补充生成器持久化测试和浏览器回归相关文档
246 lines
7.4 KiB
TypeScript
246 lines
7.4 KiB
TypeScript
/* @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();
|
|
});
|
|
|
|
it('keeps saved generator placeholders hidden after a result layer exists', () => {
|
|
renderWorldView({
|
|
layers: [createLayer({ id: 'layer-generated', title: '生成图片' })],
|
|
canvasGenerationDialogs: [
|
|
createGenerationDialog({
|
|
generatedLayerId: 'layer-generated',
|
|
placeholder: {
|
|
x: 80,
|
|
y: 90,
|
|
width: 420,
|
|
height: 420,
|
|
originalWidth: 2048,
|
|
originalHeight: 2048,
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
expect(screen.getByRole('button', { name: '选择生成图片' })).toBeTruthy();
|
|
expect(
|
|
screen.queryByRole('button', { name: '图像生成占位图' }),
|
|
).toBeNull();
|
|
});
|
|
});
|