/* @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([ createLayer('first', 10), ]); const [viewport, setViewport] = useState({ x: 1, y: 2, scale: 1, }); const [generateDialog, setGenerateDialog] = useState({ 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('first'); const [selectedLayerIds, setSelectedLayerIds] = useState(['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 (
{layers.map((layer) => `${layer.id}:${layer.x}`).join(',')} {viewport.x},{viewport.y},{viewport.scale} {generateDialog?.prompt ?? '-'} {inactiveGenerateDialogs.map((dialog) => dialog.prompt).join(',')} {selectedLayerIds.join(',')} {String(history.canUndo)} {String(history.canRedo)}
); } describe('useCanvasHistory', () => { it('captures, restores, and replays canvas history snapshots', () => { const clearDragState = vi.fn(); render(); 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'); }); });