合并主分支

解决合并冲突
This commit is contained in:
2026-08-08 10:47:31 +08:00
140 changed files with 11920 additions and 2046 deletions
@@ -44,6 +44,7 @@ const renderUiDesignAssetExtractionMarkedImageMock = vi.hoisted(() => vi.fn());
const generateEditorCharacterAnimationMock = vi.hoisted(() => vi.fn());
const generateEditorIconSpritesheetMock = vi.hoisted(() => vi.fn());
const generateEditorImageMock = vi.hoisted(() => vi.fn());
const generateEditorSceneMock = vi.hoisted(() => vi.fn());
const generateEditorVideoMock = vi.hoisted(() => vi.fn());
const generateEditorSoundEffectMock = vi.hoisted(() => vi.fn());
const generateEditorBackgroundMusicMock = vi.hoisted(() => vi.fn());
@@ -67,6 +68,7 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
generateEditorCharacterAnimation: generateEditorCharacterAnimationMock,
generateEditorIconSpritesheet: generateEditorIconSpritesheetMock,
generateEditorImage: generateEditorImageMock,
generateEditorScene: generateEditorSceneMock,
generateEditorVideo: generateEditorVideoMock,
generateEditorSoundEffect: generateEditorSoundEffectMock,
generateEditorBackgroundMusic: generateEditorBackgroundMusicMock,
@@ -575,6 +577,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
generateEditorCharacterAnimationMock.mockReset();
generateEditorIconSpritesheetMock.mockReset();
generateEditorImageMock.mockReset();
generateEditorSceneMock.mockReset();
generateEditorVideoMock.mockReset();
generateEditorSoundEffectMock.mockReset();
generateEditorBackgroundMusicMock.mockReset();
@@ -1723,6 +1726,263 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
});
});
it('submits scene intent through the dedicated scene endpoint', async () => {
generateEditorSceneMock.mockResolvedValueOnce(
createGenerated({
width: 1024,
height: 576,
prompt: '雨夜中的欧洲小镇街道',
}),
);
render(
<SubmissionWorkflowHarness
initialDialog={{
id: 'dialog-scene',
mode: 'scene',
prompt: '雨夜中的欧洲小镇街道',
status: 'idle',
composerOpen: true,
sceneStylePreset: 'anime',
sceneCustomStyle: '',
generationReferences: [],
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
placeholder: {
x: 100,
y: 100,
width: 1024,
height: 576,
originalWidth: 1024,
originalHeight: 576,
},
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(generateEditorSceneMock).toHaveBeenCalledWith(
expect.objectContaining({
sceneContent: '雨夜中的欧洲小镇街道',
stylePreset: 'anime',
model: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
assetLabel: '游戏场景 1',
generationInputs: {
fields: [
{ title: '画面内容', value: '雨夜中的欧洲小镇街道' },
{ title: '视觉风格', value: '日系动画' },
],
references: [],
},
}),
);
});
expect(generateEditorImageMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByTestId('layers').textContent).toContain(
'layer-generated-1:游戏场景 1:-:scene',
);
});
});
it('forwards inline scene generation warnings before adding the result', async () => {
generateEditorSceneMock.mockResolvedValueOnce(
createGenerated({
width: 1024,
height: 576,
warning: {
code: 'delivery-size-normalization-failed',
reason: '尺寸恢复失败,已保留生成服务返回的实际尺寸。',
},
}),
);
render(
<SubmissionWorkflowHarness
initialDialog={{
id: 'dialog-scene-warning',
mode: 'scene',
prompt: '雨夜海边车站',
status: 'idle',
composerOpen: true,
sceneStylePreset: 'anime',
sceneCustomStyle: '',
generationReferences: [],
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(screen.getByTestId('generation-warning').textContent).toBe(
'尺寸恢复失败,已保留生成服务返回的实际尺寸。',
);
});
expect(screen.getByTestId('layers').textContent).toContain(
'layer-generated-1:游戏场景 1:-:scene',
);
});
it('reloads the authoritative scene project when the first queued snapshot is unresolved', async () => {
vi.useFakeTimers();
try {
const applyProjectSnapshot = vi.fn();
const unresolvedProject = {
projectId: 'editor-project-1',
title: '场景队列项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [
{
layerId: 'generation-dialog-scene-queued',
resourceId: 'generation-dialog-scene-queued',
itemType: 'generation-dialog',
dialog: {
id: 'dialog-scene-queued',
mode: 'scene',
status: 'generating',
},
},
],
resources: [],
updatedAt: '2026-08-06T08:00:00.000Z',
};
const completedProject = {
...unresolvedProject,
layers: [
{
layerId: 'layer-scene-result',
resourceId: 'resource-scene-result',
title: '游戏场景 1',
assetKind: 'scene',
},
{
layerId: 'generation-dialog-scene-queued',
resourceId: 'generation-dialog-scene-queued',
itemType: 'generation-dialog',
dialog: {
id: 'dialog-scene-queued',
mode: 'scene',
status: 'idle',
generatedLayerId: 'layer-scene-result',
},
},
],
updatedAt: '2026-08-06T08:00:01.000Z',
};
generateEditorSceneMock.mockResolvedValueOnce({
...createGenerated({ width: 1024, height: 576 }),
queueState: createQueueState(),
});
loadEditorProjectMock
.mockResolvedValueOnce(unresolvedProject)
.mockResolvedValueOnce(completedProject);
render(
<SubmissionWorkflowHarness
projectId="editor-project-1"
applyProjectSnapshot={applyProjectSnapshot}
initialDialog={{
id: 'dialog-scene-queued',
mode: 'scene',
prompt: '雨夜海边车站',
status: 'idle',
composerOpen: true,
sceneStylePreset: 'anime',
sceneCustomStyle: '',
generationReferences: [],
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
placeholder: {
x: 100,
y: 100,
width: 1024,
height: 576,
originalWidth: 1024,
originalHeight: 576,
},
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(loadEditorProjectMock).toHaveBeenCalledTimes(1);
await act(async () => {
await vi.advanceTimersByTimeAsync(650);
});
expect(loadEditorProjectMock).toHaveBeenCalledTimes(2);
expect(applyProjectSnapshot).toHaveBeenNthCalledWith(
1,
unresolvedProject,
);
expect(applyProjectSnapshot).toHaveBeenNthCalledWith(2, completedProject);
} finally {
vi.useRealTimers();
}
});
it('does not add a local scene layer when the backend omits its project snapshot', async () => {
generateEditorSceneMock.mockResolvedValueOnce(
createGenerated({
width: 1024,
height: 576,
project: null,
}),
);
render(
<SubmissionWorkflowHarness
projectId="editor-project-1"
initialDialog={{
id: 'dialog-scene-authority',
mode: 'scene',
prompt: '雨夜海边车站',
status: 'idle',
composerOpen: true,
sceneStylePreset: 'anime',
sceneCustomStyle: '',
generationReferences: [],
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
placeholder: {
x: 100,
y: 100,
width: 1024,
height: 576,
originalWidth: 1024,
originalHeight: 576,
},
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(screen.getByTestId('dialog').textContent).toBe(
'scene:idle:open:-:placeholder:-',
);
});
expect(screen.getByTestId('layers').textContent).not.toContain(
'layer-generated-1',
);
});
it('uses the latest moved canvas placeholder for local-only generation fallback', async () => {
const callOrder: string[] = [];
const captureCanvasHistory = vi.fn((action: CanvasHistoryAction) => {