Files
Genarrative/src/components/image-editor/useCanvasGenerationDialogs.test.tsx
T
kdletters 946308b75e 保存图片画布生成器快照
将生成器对话框作为画布布局项序列化和恢复

生成成功后保留生成器快照并锚定到成品图层

图片类生成结果同步写入账号素材库

补充生成器持久化测试和浏览器回归相关文档
2026-06-17 23:57:25 +08:00

261 lines
8.1 KiB
TypeScript

/* @vitest-environment jsdom */
import { act, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { CanvasGenerationDialogState } from './ImageCanvasEditorTypes';
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
function createDialog(
mode: CanvasGenerationDialogState['mode'],
prompt: string,
): Omit<CanvasGenerationDialogState, 'id'> {
return {
mode,
prompt,
status: 'idle',
composerOpen: true,
placeholder: {
x: mode === 'character' ? 30 : 10,
y: mode === 'character' ? 40 : 20,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
},
};
}
function GenerationDialogsHarness({ onActivate }: { onActivate: () => void }) {
const dialogs = useCanvasGenerationDialogs({ onActivate });
const activeId = dialogs.activeCanvasGenerationDialog?.id ?? '-';
const activePrompt = dialogs.activeCanvasGenerationDialog?.prompt ?? '-';
const inactiveIds = dialogs.inactiveGenerateDialogs
.map((dialog) => `${dialog.id}:${dialog.prompt}:${dialog.composerOpen}`)
.join('|');
const allPrompts = dialogs.canvasGenerationDialogs
.map((dialog) => dialog.prompt)
.join(',');
return (
<div>
<span data-testid="active-id">{activeId}</span>
<span data-testid="active-prompt">{activePrompt}</span>
<span data-testid="inactive">{inactiveIds}</span>
<span data-testid="all-prompts">{allPrompts}</span>
<span data-testid="placeholder-x">
{String(
dialogs.getGeneratingDialogPlaceholder(
dialogs.activeCanvasGenerationDialog ?? {
mode: 'generate',
prompt: 'fallback',
status: 'idle',
},
)?.x ?? '-',
)}
</span>
<button
type="button"
onClick={() => dialogs.openCanvasGenerationDialog(createDialog('generate', 'first'))}
>
open first
</button>
<button
type="button"
onClick={() => dialogs.openCanvasGenerationDialog(createDialog('character', 'second'))}
>
open second
</button>
<button
type="button"
onClick={() => {
const target = dialogs.inactiveGenerateDialogs[0];
if (target) {
dialogs.activateCanvasGenerationDialog(target);
}
}}
>
activate inactive
</button>
<button
type="button"
onClick={() => {
const target = dialogs.activeCanvasGenerationDialog;
if (target) {
dialogs.updateCanvasGenerationDialogById(target.id, (dialog) => ({
...dialog,
status: 'generating',
placeholder: dialog.placeholder
? {
...dialog.placeholder,
x: 99,
}
: dialog.placeholder,
}));
}
}}
>
update active
</button>
<button
type="button"
onClick={() => {
const target = dialogs.activeCanvasGenerationDialog;
if (target) {
dialogs.removeCanvasGenerationDialogById(target.id);
}
}}
>
remove active
</button>
<button
type="button"
onClick={() => {
const target = dialogs.activeCanvasGenerationDialog;
if (target) {
dialogs.updateCanvasGenerationDialogById(target.id, (dialog) => ({
...dialog,
sourceLayerId: 'layer-a',
}));
}
}}
>
bind layer
</button>
<button
type="button"
onClick={() => dialogs.removeCanvasGenerationDialogsByLayerId('layer-a')}
>
remove layer dialogs
</button>
<button
type="button"
onClick={() =>
dialogs.restoreCanvasGenerationDialogs([
{
id: 'generation-dialog-12',
mode: 'generate',
prompt: 'restored',
status: 'idle',
composerOpen: true,
placeholder: {
x: 12,
y: 24,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
},
},
])
}
>
restore saved
</button>
<button
type="button"
onClick={() =>
dialogs.restoreCanvasGenerationDialogs([
{
id: 'generation-dialog-2',
mode: 'generate',
prompt: 'inactive saved',
status: 'idle',
composerOpen: false,
},
{
id: 'generation-dialog-3',
mode: 'character',
prompt: 'active saved',
status: 'idle',
composerOpen: true,
},
])
}
>
restore active
</button>
</div>
);
}
describe('useCanvasGenerationDialogs', () => {
it('archives, activates, updates, and removes canvas generation dialogs', () => {
const onActivate = vi.fn();
render(<GenerationDialogsHarness onActivate={onActivate} />);
act(() => screen.getByRole('button', { name: 'open first' }).click());
expect(screen.getByTestId('active-prompt').textContent).toBe('first');
expect(screen.getByTestId('all-prompts').textContent).toBe('first');
act(() => screen.getByRole('button', { name: 'open second' }).click());
expect(screen.getByTestId('active-prompt').textContent).toBe('second');
expect(screen.getByTestId('inactive').textContent).toContain(
'generation-dialog-1:first:false',
);
expect(screen.getByTestId('all-prompts').textContent).toBe(
'first,second',
);
act(() =>
screen.getByRole('button', { name: 'activate inactive' }).click(),
);
expect(onActivate).toHaveBeenCalledTimes(1);
expect(screen.getByTestId('active-prompt').textContent).toBe('first');
expect(screen.getByTestId('inactive').textContent).toContain(
'generation-dialog-2:second:false',
);
act(() => screen.getByRole('button', { name: 'update active' }).click());
expect(screen.getByTestId('placeholder-x').textContent).toBe('99');
act(() => screen.getByRole('button', { name: 'bind layer' }).click());
act(() =>
screen.getByRole('button', { name: 'remove layer dialogs' }).click(),
);
expect(screen.getByTestId('active-prompt').textContent).toBe('-');
expect(screen.getByTestId('inactive').textContent).toContain('second');
act(() => screen.getByRole('button', { name: 'activate inactive' }).click());
act(() => screen.getByRole('button', { name: 'remove active' }).click());
expect(screen.getByTestId('active-prompt').textContent).toBe('-');
expect(screen.getByTestId('inactive').textContent).toBe('');
});
it('restores saved dialogs and continues ids after the saved maximum', () => {
const onActivate = vi.fn();
render(<GenerationDialogsHarness onActivate={onActivate} />);
act(() => screen.getByRole('button', { name: 'restore saved' }).click());
expect(screen.getByTestId('active-id').textContent).toBe(
'generation-dialog-12',
);
expect(screen.getByTestId('active-prompt').textContent).toBe('restored');
act(() => screen.getByRole('button', { name: 'open second' }).click());
expect(screen.getByTestId('active-id').textContent).toBe(
'generation-dialog-13',
);
expect(screen.getByTestId('inactive').textContent).toContain(
'generation-dialog-12:restored:false',
);
});
it('keeps the saved open dialog active when restoring multiple dialogs', () => {
const onActivate = vi.fn();
render(<GenerationDialogsHarness onActivate={onActivate} />);
act(() => screen.getByRole('button', { name: 'restore active' }).click());
expect(screen.getByTestId('active-id').textContent).toBe(
'generation-dialog-3',
);
expect(screen.getByTestId('active-prompt').textContent).toBe(
'active saved',
);
expect(screen.getByTestId('inactive').textContent).toContain(
'generation-dialog-2:inactive saved:false',
);
});
});