946308b75e
将生成器对话框作为画布布局项序列化和恢复 生成成功后保留生成器快照并锚定到成品图层 图片类生成结果同步写入账号素材库 补充生成器持久化测试和浏览器回归相关文档
261 lines
8.1 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|