219 lines
6.2 KiB
TypeScript
219 lines
6.2 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { act, render, screen } from '@testing-library/react';
|
|
import { useRef, useState } from 'react';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
CanvasViewport,
|
|
GenerateDialogState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { useCanvasHistory } from './useCanvasHistory';
|
|
|
|
function createLayer(id: string, x: number): CanvasLayer {
|
|
return {
|
|
id,
|
|
resourceId: `resource-${id}`,
|
|
title: id,
|
|
src: `data:image/png;base64,${id}`,
|
|
x,
|
|
y: 20,
|
|
width: 100,
|
|
height: 80,
|
|
originalWidth: 100,
|
|
originalHeight: 80,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
};
|
|
}
|
|
|
|
function HistoryHarness({ onClearDrag }: { onClearDrag: () => void }) {
|
|
const [layers, setLayers] = useState<CanvasLayer[]>([
|
|
createLayer('first', 10),
|
|
]);
|
|
const [viewport, setViewport] = useState<CanvasViewport>({
|
|
x: 1,
|
|
y: 2,
|
|
scale: 1,
|
|
});
|
|
const [generateDialog, setGenerateDialog] =
|
|
useState<GenerateDialogState | null>({
|
|
id: 'dialog-active',
|
|
mode: 'generate',
|
|
prompt: 'active prompt',
|
|
status: 'idle',
|
|
placeholder: {
|
|
x: 10,
|
|
y: 20,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 320,
|
|
originalHeight: 240,
|
|
},
|
|
});
|
|
const [inactiveGenerateDialogs, setInactiveGenerateDialogs] = useState<
|
|
CanvasGenerationDialogState[]
|
|
>([
|
|
{
|
|
id: 'dialog-inactive',
|
|
mode: 'character',
|
|
prompt: 'archived prompt',
|
|
status: 'idle',
|
|
placeholder: {
|
|
x: 30,
|
|
y: 40,
|
|
width: 512,
|
|
height: 512,
|
|
originalWidth: 512,
|
|
originalHeight: 512,
|
|
},
|
|
},
|
|
]);
|
|
const [selectedLayerId, setSelectedLayerId] = useState<string | null>('first');
|
|
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>(['first']);
|
|
|
|
const layersRef = useRef(layers);
|
|
const viewportRef = useRef(viewport);
|
|
const generateDialogRef = useRef(generateDialog);
|
|
const inactiveGenerateDialogsRef = useRef(inactiveGenerateDialogs);
|
|
const selectedLayerIdRef = useRef(selectedLayerId);
|
|
const selectedLayerIdsRef = useRef(selectedLayerIds);
|
|
|
|
layersRef.current = layers;
|
|
viewportRef.current = viewport;
|
|
generateDialogRef.current = generateDialog;
|
|
inactiveGenerateDialogsRef.current = inactiveGenerateDialogs;
|
|
selectedLayerIdRef.current = selectedLayerId;
|
|
selectedLayerIdsRef.current = selectedLayerIds;
|
|
|
|
const history = useCanvasHistory({
|
|
refs: {
|
|
layersRef,
|
|
viewportRef,
|
|
generateDialogRef,
|
|
inactiveGenerateDialogsRef,
|
|
selectedLayerIdRef,
|
|
selectedLayerIdsRef,
|
|
},
|
|
setters: {
|
|
setLayers,
|
|
setViewport,
|
|
setGenerateDialog,
|
|
setInactiveGenerateDialogs,
|
|
setSelectedLayerId,
|
|
setSelectedLayerIds,
|
|
},
|
|
resetters: {
|
|
setHoveredLayerId: () => {},
|
|
setMetadataLayer: () => {},
|
|
setCanvasMarquee: () => {},
|
|
setSnapGuide: () => {},
|
|
setImageContextMenu: () => {},
|
|
setContextMenu: () => {},
|
|
setIsPanning: () => {},
|
|
clearDragState: onClearDrag,
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<span data-testid="layers">
|
|
{layers.map((layer) => `${layer.id}:${layer.x}`).join(',')}
|
|
</span>
|
|
<span data-testid="viewport">
|
|
{viewport.x},{viewport.y},{viewport.scale}
|
|
</span>
|
|
<span data-testid="dialog">{generateDialog?.prompt ?? '-'}</span>
|
|
<span data-testid="inactive">
|
|
{inactiveGenerateDialogs.map((dialog) => dialog.prompt).join(',')}
|
|
</span>
|
|
<span data-testid="selection">{selectedLayerIds.join(',')}</span>
|
|
<span data-testid="can-undo">{String(history.canUndo)}</span>
|
|
<span data-testid="can-redo">{String(history.canRedo)}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
history.captureCanvasHistory();
|
|
}}
|
|
>
|
|
capture
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setLayers([createLayer('second', 90)]);
|
|
setViewport({ x: 9, y: 8, scale: 2 });
|
|
setGenerateDialog({
|
|
id: 'dialog-next',
|
|
mode: 'spec',
|
|
prompt: 'next prompt',
|
|
status: 'idle',
|
|
});
|
|
setInactiveGenerateDialogs([]);
|
|
setSelectedLayerId('second');
|
|
setSelectedLayerIds(['second']);
|
|
}}
|
|
>
|
|
mutate
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
history.undoCanvasChange();
|
|
}}
|
|
>
|
|
undo
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
history.redoCanvasChange();
|
|
}}
|
|
>
|
|
redo
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe('useCanvasHistory', () => {
|
|
it('captures, restores, and replays canvas history snapshots', () => {
|
|
const clearDragState = vi.fn();
|
|
render(<HistoryHarness onClearDrag={clearDragState} />);
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'capture' }).click();
|
|
});
|
|
expect(screen.getByTestId('can-undo').textContent).toBe('true');
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'mutate' }).click();
|
|
});
|
|
expect(screen.getByTestId('layers').textContent).toBe('second:90');
|
|
expect(screen.getByTestId('viewport').textContent).toBe('9,8,2');
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'undo' }).click();
|
|
});
|
|
|
|
expect(screen.getByTestId('layers').textContent).toBe('first:10');
|
|
expect(screen.getByTestId('viewport').textContent).toBe('1,2,1');
|
|
expect(screen.getByTestId('dialog').textContent).toBe('active prompt');
|
|
expect(screen.getByTestId('inactive').textContent).toBe('archived prompt');
|
|
expect(screen.getByTestId('selection').textContent).toBe('first');
|
|
expect(screen.getByTestId('can-redo').textContent).toBe('true');
|
|
expect(clearDragState).toHaveBeenCalledTimes(1);
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'redo' }).click();
|
|
});
|
|
|
|
expect(screen.getByTestId('layers').textContent).toBe('second:90');
|
|
expect(screen.getByTestId('viewport').textContent).toBe('9,8,2');
|
|
expect(screen.getByTestId('dialog').textContent).toBe('next prompt');
|
|
expect(screen.getByTestId('selection').textContent).toBe('second');
|
|
});
|
|
});
|