0502c5c5df
移除普通生图、快速编辑和改造面板的独立关闭按钮 统一空白画布点击后的生成面板收起逻辑 避免快速编辑和改造误移除最近生成器 补充生成面板交互回归测试
805 lines
27 KiB
TypeScript
805 lines
27 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import {
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from '@testing-library/react';
|
|
import { useRef, useState } from 'react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
CanvasTool,
|
|
ImageContextMenuState,
|
|
SidebarPanel,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
|
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
|
|
|
const generateEditorImageMock = vi.hoisted(() => vi.fn());
|
|
const generateEditorCharacterAnimationMock = vi.hoisted(() => vi.fn());
|
|
const generateEditorIconSpritesheetMock = vi.hoisted(() => vi.fn());
|
|
const generateEditorSoundEffectMock = vi.hoisted(() => vi.fn());
|
|
const generateEditorBackgroundMusicMock = vi.hoisted(() => vi.fn());
|
|
const editEditorImageMock = vi.hoisted(() => vi.fn());
|
|
const renderCropExpandImageMock = vi.hoisted(() => vi.fn());
|
|
|
|
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: editEditorImageMock,
|
|
generateEditorBackgroundMusic: generateEditorBackgroundMusicMock,
|
|
generateEditorCharacterAnimation: generateEditorCharacterAnimationMock,
|
|
generateEditorIconSpritesheet: generateEditorIconSpritesheetMock,
|
|
generateEditorImage: generateEditorImageMock,
|
|
generateEditorSoundEffect: generateEditorSoundEffectMock,
|
|
};
|
|
});
|
|
|
|
vi.mock('./ImageCanvasRasterEditModel', async () => {
|
|
const actual = await vi.importActual<
|
|
typeof import('./ImageCanvasRasterEditModel')
|
|
>('./ImageCanvasRasterEditModel');
|
|
return {
|
|
...actual,
|
|
renderCropExpandImage: renderCropExpandImageMock,
|
|
};
|
|
});
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-source',
|
|
resourceId: 'resource-source',
|
|
title: '源图',
|
|
src: 'data:image/png;base64,source',
|
|
x: 120,
|
|
y: 140,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 1024,
|
|
originalHeight: 768,
|
|
zIndex: 2,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createGenerated(overrides = {}) {
|
|
return {
|
|
imageSrc: 'data:image/png;base64,generated',
|
|
width: 1024,
|
|
height: 1024,
|
|
sourceType: 'generated' as const,
|
|
prompt: '生成提示词',
|
|
actualPrompt: '生成提示词',
|
|
model: 'gpt-image-2',
|
|
provider: 'VectorEngine',
|
|
taskId: 'task-generated',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function GenerationWorkflowHarness({
|
|
initialLayers = [createLayer()],
|
|
initialViewport = { x: 10, y: 20, scale: 2 },
|
|
}: {
|
|
initialLayers?: CanvasLayer[];
|
|
initialViewport?: { x: number; y: number; scale: number };
|
|
}) {
|
|
const [layers, setLayers] = useState<CanvasLayer[]>(initialLayers);
|
|
const [viewport, setViewport] = useState(initialViewport);
|
|
const [activeTool, setActiveTool] = useState<CanvasTool>('select');
|
|
const [activeSidebarPanel, setActiveSidebarPanel] =
|
|
useState<SidebarPanel | null>('assets');
|
|
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
|
const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
|
|
const [imageContextMenu, setImageContextMenu] =
|
|
useState<ImageContextMenuState | null>({
|
|
layerId: 'layer-source',
|
|
x: 1,
|
|
y: 2,
|
|
});
|
|
const fitLayersMockRef = useRef(vi.fn());
|
|
const layerCounterRef = useRef(0);
|
|
const dialogs = useCanvasGenerationDialogs();
|
|
const workflow = useImageCanvasGenerationWorkflow({
|
|
layers,
|
|
canvasSize: { width: 900, height: 640 },
|
|
viewport,
|
|
setViewport,
|
|
setLayers,
|
|
canvasGenerationDialogs: dialogs.canvasGenerationDialogs,
|
|
layerCounterRef,
|
|
generateDialog: dialogs.generateDialog,
|
|
setGenerateDialog: dialogs.setGenerateDialog,
|
|
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById,
|
|
removeCanvasGenerationDialogsByLayerId:
|
|
dialogs.removeCanvasGenerationDialogsByLayerId,
|
|
getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder,
|
|
archiveActiveCanvasGenerationDialog:
|
|
dialogs.archiveActiveCanvasGenerationDialog,
|
|
appendCanvasLayersWithResources: (nextLayers) =>
|
|
setLayers((currentLayers) => [...currentLayers, ...nextLayers]),
|
|
selectSingleLayer: setSelectedLayerId,
|
|
fitLayers: fitLayersMockRef.current,
|
|
captureCanvasHistory: () => {},
|
|
setActiveTool,
|
|
setActiveSidebarPanel,
|
|
setMetadataLayer,
|
|
setImageContextMenu,
|
|
});
|
|
|
|
const activeDialog = dialogs.generateDialog;
|
|
const activeCanvasDialog =
|
|
activeDialog && 'id' in activeDialog
|
|
? (activeDialog as CanvasGenerationDialogState)
|
|
: null;
|
|
|
|
return (
|
|
<div>
|
|
<span data-testid="tool">{activeTool}</span>
|
|
<span data-testid="sidebar">{activeSidebarPanel ?? '-'}</span>
|
|
<span data-testid="selected">{selectedLayerId ?? '-'}</span>
|
|
<span data-testid="metadata">{metadataLayer?.id ?? '-'}</span>
|
|
<span data-testid="image-context">{imageContextMenu ? 'open' : '-'}</span>
|
|
<span data-testid="viewport">{`${viewport.x}:${viewport.y}:${viewport.scale}`}</span>
|
|
<span data-testid="placeholder">
|
|
{activeDialog?.placeholder
|
|
? `${activeDialog.placeholder.x}:${activeDialog.placeholder.y}:${activeDialog.placeholder.width}:${activeDialog.placeholder.height}`
|
|
: '-'}
|
|
</span>
|
|
<span data-testid="layers">
|
|
{layers
|
|
.map(
|
|
(layer) =>
|
|
`${layer.id}:${layer.title}:${layer.sourceResourceId ?? '-'}:${layer.assetKind ?? '-'}`,
|
|
)
|
|
.join('|')}
|
|
</span>
|
|
<span data-testid="dialog">
|
|
{activeDialog
|
|
? `${activeDialog.mode}:${activeDialog.status}:${activeDialog.composerOpen !== false ? 'open' : 'closed'}:${activeDialog.generatedLayerId ?? '-'}:${activeDialog.placeholder ? 'placeholder' : '-'}`
|
|
: '-'}
|
|
</span>
|
|
<span data-testid="generation-dialogs">
|
|
{dialogs.canvasGenerationDialogs
|
|
.map(
|
|
(dialog) =>
|
|
`${dialog.id}:${dialog.mode}:${dialog.composerOpen !== false ? 'open' : 'closed'}`,
|
|
)
|
|
.join('|') || '-'}
|
|
</span>
|
|
<span data-testid="generation-references">
|
|
{activeDialog?.generationReferences
|
|
?.map((reference) => reference.label)
|
|
.join('|') ?? '-'}
|
|
</span>
|
|
<span data-testid="quick-edit">
|
|
{workflow.quickEditPanel
|
|
? `${workflow.quickEditPanel.sourceLayerId}:${workflow.quickEditPanel.status}:${workflow.quickEditPanel.prompt || '-'}`
|
|
: '-'}
|
|
</span>
|
|
<span data-testid="crop-expand">
|
|
{workflow.cropExpandPanel
|
|
? `${workflow.cropExpandPanel.sourceLayerId}:${workflow.cropExpandPanel.ratio}:${workflow.cropExpandPanel.frame.x}:${workflow.cropExpandPanel.frame.y}:${workflow.cropExpandPanel.frame.width}:${workflow.cropExpandPanel.frame.height}:${workflow.cropExpandPanel.status}`
|
|
: '-'}
|
|
</span>
|
|
<span data-testid="character-animation">
|
|
{workflow.characterAnimationPanel
|
|
? `${workflow.characterAnimationPanel.sourceLayerId}:${workflow.characterAnimationPanel.status}`
|
|
: '-'}
|
|
</span>
|
|
<span data-testid="character-animation-price">
|
|
{workflow.characterAnimationPrice}
|
|
</span>
|
|
<span data-testid="fit-count">
|
|
{fitLayersMockRef.current.mock.calls.length}
|
|
</span>
|
|
<button type="button" onClick={workflow.openGenerateDialog}>
|
|
打开生成
|
|
</button>
|
|
<button type="button" onClick={() => workflow.openSpecDialog('ui')}>
|
|
打开UI规范
|
|
</button>
|
|
<button type="button" onClick={workflow.openVideoGenerationDialog}>
|
|
打开视频生成
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.openRedrawPanel(
|
|
createLayer({
|
|
id: 'layer-sound',
|
|
title: '游戏音效',
|
|
src: 'data:audio/mpeg;base64,sound',
|
|
mediaType: 'audio',
|
|
assetKind: 'sound-effect',
|
|
width: 420,
|
|
height: 120,
|
|
originalWidth: 420,
|
|
originalHeight: 120,
|
|
prompt: '金币跳出音',
|
|
generationInputs: {
|
|
fields: [
|
|
{ title: 'prompt', value: '金币跳出音' },
|
|
{ title: 'model', value: 'audio1.0' },
|
|
{ title: 'duration', value: '8秒' },
|
|
],
|
|
references: [],
|
|
},
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
打开音效改造
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
dialogs.setGenerateDialog({
|
|
mode: 'edit',
|
|
prompt: '',
|
|
status: 'idle',
|
|
sourceLayerId: 'layer-source',
|
|
})
|
|
}
|
|
>
|
|
打开修改状态
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => workflow.openQuickEditPanel(layers[0]!)}
|
|
>
|
|
打开快速编辑
|
|
</button>
|
|
<button type="button" onClick={() => workflow.openRedrawPanel(layers[0]!)}>
|
|
打开图片改造
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => workflow.openCropExpandPanel(layers[0]!)}
|
|
>
|
|
打开裁扩
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.setCropExpandPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
frame: {
|
|
x: currentPanel.frame.x - 10,
|
|
y: currentPanel.frame.y - 20,
|
|
width: currentPanel.frame.width + 60,
|
|
height: currentPanel.frame.height + 40,
|
|
},
|
|
}
|
|
: currentPanel,
|
|
)
|
|
}
|
|
>
|
|
拖拽裁扩边界
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.setCropExpandPanel((currentPanel) =>
|
|
currentPanel ? { ...currentPanel, ratio: '1:1' } : currentPanel,
|
|
)
|
|
}
|
|
>
|
|
选择1比1
|
|
</button>
|
|
<button type="button" onClick={() => void workflow.submitCropExpand()}>
|
|
完成裁扩
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.openCharacterAnimationPanel(
|
|
createLayer({ id: 'layer-character', assetKind: 'character' }),
|
|
)
|
|
}
|
|
>
|
|
打开角色动画
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.openCharacterAnimationPanel(
|
|
createLayer({ id: 'layer-plain' }),
|
|
)
|
|
}
|
|
>
|
|
打开普通动画
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
dialogs.setGenerateDialog((currentDialog) =>
|
|
currentDialog
|
|
? { ...currentDialog, prompt: '一张生成图' }
|
|
: currentDialog,
|
|
)
|
|
}
|
|
>
|
|
填写生成提示词
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (activeDialog) {
|
|
void workflow.submitImageGeneration(activeDialog);
|
|
}
|
|
}}
|
|
>
|
|
提交生成
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.pickGenerationReferenceFromLayer(
|
|
createLayer({ id: 'layer-reference', title: '画布参考图' }),
|
|
)
|
|
}
|
|
>
|
|
选择画布参考图
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
dialogs.setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'spec'
|
|
? {
|
|
...currentDialog,
|
|
specReference: {
|
|
id: 'spec-ref',
|
|
label: '参考.png',
|
|
src: 'data:image/png;base64,ref',
|
|
},
|
|
}
|
|
: currentDialog,
|
|
)
|
|
}
|
|
>
|
|
添加规范参考图
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.setQuickEditPanel((currentPanel) =>
|
|
currentPanel
|
|
? { ...currentPanel, prompt: '快速修图' }
|
|
: currentPanel,
|
|
)
|
|
}
|
|
>
|
|
填写快速编辑
|
|
</button>
|
|
<button type="button" onClick={() => void workflow.submitQuickEdit()}>
|
|
提交快速编辑
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
workflow.clearDeletedLayerGenerationState('layer-source')
|
|
}
|
|
>
|
|
清理源图状态
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (activeCanvasDialog) {
|
|
dialogs.updateCanvasGenerationDialogById(
|
|
activeCanvasDialog.id,
|
|
(dialog) => ({
|
|
...dialog,
|
|
generatedLayerId: 'layer-source',
|
|
}),
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
绑定生成图层
|
|
</button>
|
|
<button type="button" onClick={workflow.hideGeneratedLayerPanelAfterBlur}>
|
|
隐藏生成面板
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe('useImageCanvasGenerationWorkflow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('opens a movable canvas generation placeholder and keeps toolbar state active', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
|
|
expect(screen.getByTestId('tool').textContent).toBe('generate');
|
|
expect(screen.getByTestId('selected').textContent).toBe('-');
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'generate:idle:open:-:placeholder',
|
|
);
|
|
});
|
|
|
|
it('calculates character animation button price from the model pricing config', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开角色动画' }));
|
|
|
|
expect(screen.getByTestId('character-animation').textContent).toBe(
|
|
'layer-character:idle',
|
|
);
|
|
expect(screen.getByTestId('character-animation-price').textContent).toBe(
|
|
'40',
|
|
);
|
|
});
|
|
|
|
it('places a new generation placeholder away from existing canvas images and centers the viewport on it', () => {
|
|
render(
|
|
<GenerationWorkflowHarness
|
|
initialLayers={[
|
|
createLayer({
|
|
id: 'center-layer',
|
|
x: 0,
|
|
y: -80,
|
|
width: 460,
|
|
height: 460,
|
|
}),
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
|
|
expect(screen.getByTestId('placeholder').textContent).toBe(
|
|
'-1056:-362:1024:1024',
|
|
);
|
|
expect(screen.getByTestId('viewport').textContent).toBe('1538:20:2');
|
|
});
|
|
|
|
it('places a new video generation placeholder away from existing canvas images and centers the viewport on it', () => {
|
|
render(
|
|
<GenerationWorkflowHarness
|
|
initialLayers={[
|
|
createLayer({
|
|
id: 'center-layer',
|
|
x: 0,
|
|
y: -80,
|
|
width: 460,
|
|
height: 460,
|
|
}),
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开视频生成' }));
|
|
|
|
expect(screen.getByTestId('tool').textContent).toBe('video');
|
|
expect(screen.getByTestId('selected').textContent).toBe('-');
|
|
expect(screen.getByTestId('placeholder').textContent).toBe(
|
|
'-207:412:854:480',
|
|
);
|
|
expect(screen.getByTestId('viewport').textContent).toBe('10:-984:2');
|
|
});
|
|
|
|
it('opens audio remodel as an audio generation dialog next to the source layer', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开音效改造' }));
|
|
|
|
expect(screen.getByTestId('tool').textContent).toBe('music');
|
|
expect(screen.getByTestId('selected').textContent).toBe('layer-sound');
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'audio-sound-effect:idle:open:-:placeholder',
|
|
);
|
|
expect(screen.getByTestId('placeholder').textContent).toBe(
|
|
'572:140:420:120',
|
|
);
|
|
});
|
|
|
|
it('keeps the active canvas generator when opening quick edit for another layer', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
|
|
|
|
expect(screen.getByTestId('dialog').textContent).toBe('-');
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
|
'layer-source:idle:-',
|
|
);
|
|
expect(screen.getByTestId('generation-dialogs').textContent).toBe(
|
|
'generation-dialog-1:generate:closed',
|
|
);
|
|
});
|
|
|
|
it('keeps the active canvas generator when opening redraw for another layer', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '打开图片改造' }));
|
|
|
|
expect(screen.getByTestId('dialog').textContent).toBe('-');
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
|
'layer-source:idle:-',
|
|
);
|
|
expect(screen.getByTestId('generation-dialogs').textContent).toBe(
|
|
'generation-dialog-1:generate:closed',
|
|
);
|
|
});
|
|
|
|
it('submits audio remodel with restored sound parameters and appends the new audio layer', async () => {
|
|
generateEditorSoundEffectMock.mockResolvedValueOnce({
|
|
audioSrc: 'data:audio/mpeg;base64,generated',
|
|
width: 420,
|
|
height: 120,
|
|
sourceType: 'generated',
|
|
prompt: '金币跳出音',
|
|
actualPrompt: '金币跳出音',
|
|
model: 'audio1.0',
|
|
provider: 'VectorEngine',
|
|
taskId: 'task-sound',
|
|
priceMudPoints: 10,
|
|
audioKind: 'sound-effect',
|
|
durationSeconds: 4,
|
|
});
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开音效改造' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
|
|
|
await waitFor(() => {
|
|
expect(generateEditorSoundEffectMock).toHaveBeenCalledWith({
|
|
prompt: '金币跳出音',
|
|
model: 'audio1.0',
|
|
duration: 8,
|
|
priceMudPoints: 10,
|
|
});
|
|
});
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('layers').textContent).toContain(
|
|
'layer-audio-1:游戏音效 1:-:sound-effect',
|
|
);
|
|
});
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'audio-sound-effect:idle:open:layer-audio-1:placeholder',
|
|
);
|
|
expect(screen.getByTestId('placeholder').textContent).toBe(
|
|
'572:140:420:120',
|
|
);
|
|
});
|
|
|
|
it('submits a normal generation, appends the generated layer, and keeps the composer anchored', async () => {
|
|
generateEditorImageMock.mockResolvedValueOnce(
|
|
createGenerated({ prompt: '一张生成图' }),
|
|
);
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '填写生成提示词' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
|
|
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
|
prompt: '一张生成图',
|
|
model: 'gemini-3.1-flash-image-preview',
|
|
aspectRatio: '1:1',
|
|
imageSize: '1K',
|
|
});
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'generate:generating:closed:-:placeholder',
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('layers').textContent).toContain(
|
|
'layer-generated-1:生成图片 1',
|
|
);
|
|
});
|
|
expect(screen.getByTestId('sidebar').textContent).toBe('layers');
|
|
expect(screen.getByTestId('selected').textContent).toBe(
|
|
'layer-generated-1',
|
|
);
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'generate:idle:open:layer-generated-1:placeholder',
|
|
);
|
|
});
|
|
|
|
it('adds picked canvas layers as normal generation references and submits them', async () => {
|
|
generateEditorImageMock.mockResolvedValueOnce(
|
|
createGenerated({ prompt: '参考生成图' }),
|
|
);
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '选择画布参考图' }));
|
|
|
|
expect(screen.getByTestId('generation-references').textContent).toBe(
|
|
'画布参考图',
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '填写生成提示词' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
|
|
|
await waitFor(() => {
|
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
|
prompt: '一张生成图',
|
|
model: 'gemini-3.1-flash-image-preview',
|
|
aspectRatio: '1:1',
|
|
imageSize: '1K',
|
|
referenceImageSrcs: ['data:image/png;base64,source'],
|
|
});
|
|
});
|
|
});
|
|
|
|
it('submits spec generation with reference image and reference prompt semantics', async () => {
|
|
generateEditorImageMock.mockResolvedValueOnce(
|
|
createGenerated({ prompt: 'UI规范图' }),
|
|
);
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开UI规范' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '添加规范参考图' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
|
|
|
await waitFor(() => {
|
|
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
size: '2048x1152',
|
|
kind: 'spec',
|
|
model: 'gpt-image-2',
|
|
aspectRatio: '16:9',
|
|
imageSize: '2K',
|
|
referenceImageSrcs: ['data:image/png;base64,ref'],
|
|
prompt: expect.stringContaining('参考图生成规范'),
|
|
}),
|
|
);
|
|
});
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('layers').textContent).toContain(
|
|
'layer-generated-1:图标规范 1',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('submits quick edits beside the source and fits source plus result', async () => {
|
|
generateEditorImageMock.mockResolvedValueOnce(
|
|
createGenerated({ prompt: '快速修图' }),
|
|
);
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
|
'layer-source:idle:-',
|
|
);
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '填写快速编辑' }));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
|
'layer-source:idle:快速修图',
|
|
);
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
|
|
|
await waitFor(() => {
|
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
|
prompt: '快速修图',
|
|
size: '1024x768',
|
|
kind: 'quick-edit',
|
|
model: 'gemini-3.1-flash-image-preview',
|
|
referenceImageSrcs: ['data:image/png;base64,source'],
|
|
});
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('layers').textContent).toContain(
|
|
'layer-quick-edit-1:源图 快速编辑:resource-source',
|
|
);
|
|
});
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
|
expect(screen.getByTestId('tool').textContent).toBe('select');
|
|
expect(screen.getByTestId('fit-count').textContent).toBe('1');
|
|
});
|
|
|
|
it('opens crop-expand in free ratio and completes as a new beside layer', async () => {
|
|
renderCropExpandImageMock.mockResolvedValueOnce({
|
|
imageSrc: 'data:image/png;base64,crop-expanded',
|
|
width: 380,
|
|
height: 280,
|
|
});
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开裁扩' }));
|
|
expect(screen.getByTestId('crop-expand').textContent).toBe(
|
|
'layer-source:free:120:140:320:240:idle',
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拖拽裁扩边界' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '完成裁扩' }));
|
|
|
|
await waitFor(() => {
|
|
expect(renderCropExpandImageMock).toHaveBeenCalledWith({
|
|
source: 'data:image/png;base64,source',
|
|
insets: {
|
|
left: 32,
|
|
top: 64,
|
|
right: 160,
|
|
bottom: 64,
|
|
},
|
|
});
|
|
});
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('layers').textContent).toContain(
|
|
'layer-crop-expand-1:源图 裁扩:resource-source',
|
|
);
|
|
});
|
|
expect(screen.getByTestId('selected').textContent).toBe(
|
|
'layer-crop-expand-1',
|
|
);
|
|
expect(screen.getByTestId('crop-expand').textContent).toBe('-');
|
|
expect(screen.getByTestId('fit-count').textContent).toBe('1');
|
|
});
|
|
|
|
it('clears generation side panels and linked edit dialogs when a source layer is deleted', async () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
|
'layer-source:idle:-',
|
|
);
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '打开修改状态' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '清理源图状态' }));
|
|
|
|
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
|
expect(screen.getByTestId('dialog').textContent).toBe('-');
|
|
});
|
|
|
|
it('creates a placed character action generation dialog for character layers', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开普通动画' }));
|
|
expect(screen.getByTestId('character-animation').textContent).toBe('-');
|
|
expect(screen.getByTestId('dialog').textContent).toBe('-');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开角色动画' }));
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'character-animation:idle:open:-:placeholder',
|
|
);
|
|
expect(screen.getByTestId('character-animation').textContent).toBe(
|
|
'layer-character:idle',
|
|
);
|
|
expect(screen.getByTestId('image-context').textContent).toBe('-');
|
|
});
|
|
|
|
it('hides non-generating canvas generation composers without deleting placeholders', () => {
|
|
render(<GenerationWorkflowHarness />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '隐藏生成面板' }));
|
|
|
|
expect(screen.getByTestId('dialog').textContent).toBe(
|
|
'generate:idle:closed:-:placeholder',
|
|
);
|
|
});
|
|
});
|