/* @vitest-environment jsdom */ import type { CanvasLayer } from '@genarrative/image-canvas-core'; import { act, render, renderHook, screen } from '@testing-library/react'; import { createRef } from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { CanvasWorld } from './CanvasWorld'; import { CANVAS_RENDER_SUPERSAMPLE, canvasViewportToWorldTransform, } from './canvasWorldRendering'; import { useImageCanvasViewportControls } from './useImageCanvasViewportControls'; function layer(id: string, x: number): CanvasLayer { return { id, resourceId: `resource-${id}`, title: id, src: `media:${id}`, x, y: 0, width: 100, height: 100, originalWidth: 100, originalHeight: 100, zIndex: 1, sourceType: 'uploaded', }; } function createViewport() { const viewport = document.createElement('div'); Object.defineProperties(viewport, { clientWidth: { configurable: true, value: 800 }, clientHeight: { configurable: true, value: 600 }, }); viewport.getBoundingClientRect = () => ({ left: 0, top: 0, right: 800, bottom: 600, width: 800, height: 600, x: 0, y: 0, toJSON: () => undefined, }); document.body.append(viewport); return viewport; } afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe('shared canvas React lifecycle', () => { it('renders a supersampled world while preserving the public viewport scale', () => { render( content , ); const world = screen.getByText('content').parentElement?.parentElement; const content = screen.getByText('content').parentElement; expect(world?.style.width).toBe('12000px'); expect(world?.style.height).toBe('12000px'); expect(world?.style.transform).toBe( canvasViewportToWorldTransform({ x: 12, y: 24, scale: 2 }), ); expect( world?.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'), ).toBe('0.5'); expect(content?.style.zoom).toBe(String(CANVAS_RENDER_SUPERSAMPLE)); }); it('removes every native listener across repeated mounts', () => { const viewport = createViewport(); const addListener = vi.spyOn(viewport, 'addEventListener'); const removeListener = vi.spyOn(viewport, 'removeEventListener'); const ref = createRef(); ref.current = viewport; const options = { canvasViewportRef: ref, layers: [layer('a', 0)], captureCanvasHistory: vi.fn(), }; const first = renderHook(() => useImageCanvasViewportControls(options)); first.unmount(); const second = renderHook(() => useImageCanvasViewportControls(options)); second.unmount(); expect( addListener.mock.calls.filter(([type]) => type === 'wheel'), ).toHaveLength(2); expect( removeListener.mock.calls.filter(([type]) => type === 'wheel'), ).toHaveLength(2); }); it('disconnects the instance ResizeObserver on unmount', () => { const viewport = createViewport(); const disconnect = vi.fn(); const observe = vi.fn(); vi.stubGlobal( 'ResizeObserver', class { observe = observe; disconnect = disconnect; }, ); const ref = createRef(); ref.current = viewport; const hook = renderHook(() => useImageCanvasViewportControls({ canvasViewportRef: ref, layers: [layer('a', 0)], captureCanvasHistory: vi.fn(), }), ); expect(observe).toHaveBeenCalledWith(viewport); hook.unmount(); expect(disconnect).toHaveBeenCalledTimes(1); }); it('uses the minimap inside each viewport instead of another canvas instance', () => { const firstViewport = createViewport(); const secondViewport = createViewport(); const firstMinimap = document.createElement('button'); const secondMinimap = document.createElement('button'); firstMinimap.className = 'genarrative-image-canvas__minimap'; secondMinimap.className = 'genarrative-image-canvas__minimap'; firstMinimap.getBoundingClientRect = () => ({ left: 10, top: 10, width: 132, height: 84 }) as DOMRect; secondMinimap.getBoundingClientRect = () => ({ left: 300, top: 200, width: 132, height: 84 }) as DOMRect; firstViewport.append(firstMinimap); secondViewport.append(secondMinimap); const firstRef = createRef(); const secondRef = createRef(); firstRef.current = firstViewport; secondRef.current = secondViewport; const first = renderHook(() => useImageCanvasViewportControls({ canvasViewportRef: firstRef, layers: [layer('a', 0), layer('b', 500)], captureCanvasHistory: vi.fn(), }), ); const second = renderHook(() => useImageCanvasViewportControls({ canvasViewportRef: secondRef, layers: [layer('c', 1000), layer('d', 1600)], captureCanvasHistory: vi.fn(), }), ); const firstBefore = first.result.current.viewport; const secondBefore = second.result.current.viewport; act(() => first.result.current.moveViewportFromMinimapPointer(130, 80)); expect(first.result.current.viewport).not.toEqual(firstBefore); expect(second.result.current.viewport).toEqual(secondBefore); }); });