Files
Genarrative/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx
T
JenkenB 17768119ea 恢复画布视频生成入口
恢复画布底部工具栏的视频生成和 UI 设计入口

恢复视频生成弹窗的参数、模型和提交状态处理

补齐规范参考、快捷键和生成流程相关测试
2026-06-17 23:05:17 +08:00

214 lines
7.9 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { useRef, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import type {
CanvasGenerationDialogState,
CanvasLayer,
CanvasTool,
GenerateDialogState,
ImageContextMenuState,
SidebarPanel,
} from './ImageCanvasEditorTypes';
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
import { useImageCanvasGenerationSurface } from './useImageCanvasGenerationSurface';
vi.mock('../../services/image-editor/editorImageReference', () => ({
resolveEditorImageReferenceDataUrl: vi.fn(async (src: string) => src),
}));
vi.mock('../../services/image-editor/editorProjectClient', async () => {
const actual = await vi.importActual<
typeof import('../../services/image-editor/editorProjectClient')
>('../../services/image-editor/editorProjectClient');
return {
...actual,
editEditorImage: vi.fn(),
generateEditorCharacterAnimation: vi.fn(),
generateEditorIconSpritesheet: vi.fn(),
generateEditorImage: vi.fn(),
};
});
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: 80,
y: 90,
width: 240,
height: 180,
originalWidth: 240,
originalHeight: 180,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
function GenerationSurfaceHarness() {
const [layers, setLayers] = useState<CanvasLayer[]>([createLayer()]);
const [activeTool, setActiveTool] = useState<CanvasTool>('select');
const [activeSidebarPanel, setActiveSidebarPanel] =
useState<SidebarPanel | null>('assets');
const [, setMetadataLayer] = useState<CanvasLayer | null>(null);
const [, setImageContextMenu] = useState<ImageContextMenuState | null>(null);
const layerCounterRef = useRef(0);
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
const [viewport, setViewport] = useState({ x: 10, y: 20, scale: 2 });
const dialogs = useCanvasGenerationDialogs();
const activeDialog = dialogs.generateDialog;
const activeCanvasDialog =
activeDialog && 'id' in activeDialog
? (activeDialog as CanvasGenerationDialogState)
: null;
const surface = useImageCanvasGenerationSurface({
layers,
canvasSize: { width: 900, height: 640 },
viewport,
setViewport,
layerCounterRef,
specToolWrapRef,
characterSpecButtonRef,
characterReferenceButtonRef,
iconSpecButtonRef,
generationReferenceButtonRef,
generateDialog: dialogs.generateDialog,
setGenerateDialog: dialogs.setGenerateDialog,
activeCanvasGenerationDialog: activeCanvasDialog,
canvasGenerationDialogs: dialogs.canvasGenerationDialogs,
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
removeCanvasGenerationDialogById: dialogs.removeCanvasGenerationDialogById,
removeCanvasGenerationDialogsByLayerId:
dialogs.removeCanvasGenerationDialogsByLayerId,
getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources: (nextLayers) =>
setLayers((currentLayers) => [...currentLayers, ...nextLayers]),
selectSingleLayer: () => {},
fitLayers: () => {},
setActiveTool,
setActiveSidebarPanel,
setMetadataLayer,
setImageContextMenu,
requestUpload: () => {},
});
return (
<div>
<span ref={specToolWrapRef}>规范入口</span>
<span data-testid="tool">{activeTool}</span>
<span data-testid="sidebar">{activeSidebarPanel ?? '-'}</span>
<span data-testid="dialog">
{activeDialog
? `${activeDialog.mode}:${activeDialog.composerOpen !== false ? 'open' : 'closed'}:${activeDialog.placeholder ? 'placeholder' : '-'}`
: '-'}
</span>
<span data-testid="composer-left">
{surface.generationComposerStyle?.left ?? '-'}
</span>
<span data-testid="composer-top">
{surface.generationComposerStyle?.top ?? '-'}
</span>
<button
type="button"
onClick={() => surface.switchGenerationTool('generate')}
>
切换生成
</button>
<button type="button" onClick={() => surface.switchGenerationTool('video')}>
切换视频
</button>
<button type="button" onClick={() => surface.switchGenerationTool('spec')}>
切换规范
</button>
<button
type="button"
onClick={() => surface.switchGenerationTool('character')}
>
切换角色
</button>
<button type="button" onClick={() => surface.switchGenerationTool('icon')}>
切换图标
</button>
<button
type="button"
onClick={() => surface.switchGenerationTool('ui-design')}
>
切换UI设计
</button>
<button type="button" onClick={() => surface.switchGenerationTool('text')}>
切换文字
</button>
{surface.generationComposerNode}
</div>
);
}
describe('useImageCanvasGenerationSurface', () => {
it('switches generation tools while leaving non-generation tools untouched', () => {
render(<GenerationSurfaceHarness />);
fireEvent.click(screen.getByRole('button', { name: '切换文字' }));
expect(screen.getByTestId('tool').textContent).toBe('select');
fireEvent.click(screen.getByRole('button', { name: '切换生成' }));
expect(screen.getByTestId('tool').textContent).toBe('generate');
expect(screen.getByTestId('dialog').textContent).toBe(
'generate:open:placeholder',
);
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
});
it('derives composer position and closes the generated image composer', () => {
render(<GenerationSurfaceHarness />);
fireEvent.click(screen.getByRole('button', { name: '切换生成' }));
expect(screen.getByTestId('composer-left').textContent).toBe('450');
expect(screen.getByTestId('composer-top').textContent).toBe('750');
fireEvent.click(screen.getByRole('button', { name: '关闭生成图片' }));
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
expect(screen.getByTestId('tool').textContent).toBe('select');
expect(screen.getByTestId('dialog').textContent).toBe(
'generate:closed:placeholder',
);
});
it('opens the spec menu through the generation surface', () => {
render(<GenerationSurfaceHarness />);
fireEvent.click(screen.getByRole('button', { name: '切换规范' }));
expect(screen.getByTestId('tool').textContent).toBe('spec');
const menu = screen.getByRole('menu', { name: '生成规范类型' });
expect(within(menu).getByRole('menuitem', { name: '角色形象规范' })).toBeTruthy();
});
it('opens video and UI design generation dialogs through the generation surface', () => {
render(<GenerationSurfaceHarness />);
fireEvent.click(screen.getByRole('button', { name: '切换视频' }));
expect(screen.getByTestId('tool').textContent).toBe('video');
expect(screen.getByTestId('dialog').textContent).toBe(
'video:open:placeholder',
);
expect(screen.getByRole('dialog', { name: '生成视频' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '切换UI设计' }));
expect(screen.getByTestId('tool').textContent).toBe('ui-design');
expect(screen.getByTestId('dialog').textContent).toBe(
'ui-design:open:placeholder',
);
expect(screen.getByRole('dialog', { name: '生成UI设计图' })).toBeTruthy();
});
});