218 lines
6.1 KiB
TypeScript
218 lines
6.1 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
import { useRef } from 'react';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
|
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer>): CanvasLayer {
|
|
const id = overrides.id ?? 'layer-a';
|
|
return {
|
|
id,
|
|
resourceId: `resource-${id}`,
|
|
title: id,
|
|
src: `data:image/png;base64,${id}`,
|
|
x: 100,
|
|
y: 120,
|
|
width: 200,
|
|
height: 100,
|
|
originalWidth: 200,
|
|
originalHeight: 100,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function setElementBox(
|
|
element: HTMLElement,
|
|
box: { width: number; height: number; left?: number; top?: number },
|
|
) {
|
|
Object.defineProperty(element, 'clientWidth', {
|
|
configurable: true,
|
|
value: box.width,
|
|
});
|
|
Object.defineProperty(element, 'clientHeight', {
|
|
configurable: true,
|
|
value: box.height,
|
|
});
|
|
element.getBoundingClientRect = () =>
|
|
({
|
|
x: box.left ?? 0,
|
|
y: box.top ?? 0,
|
|
left: box.left ?? 0,
|
|
top: box.top ?? 0,
|
|
right: (box.left ?? 0) + box.width,
|
|
bottom: (box.top ?? 0) + box.height,
|
|
width: box.width,
|
|
height: box.height,
|
|
toJSON: () => ({}),
|
|
}) as DOMRect;
|
|
}
|
|
|
|
function ViewportHarness({
|
|
captureCanvasHistory = vi.fn(),
|
|
}: {
|
|
captureCanvasHistory?: () => void;
|
|
}) {
|
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
const layers = [
|
|
createLayer({ id: 'one', x: 0, y: 0, width: 400, height: 300 }),
|
|
createLayer({ id: 'two', x: 600, y: 100, width: 200, height: 200 }),
|
|
];
|
|
const controls = useImageCanvasViewportControls({
|
|
canvasViewportRef: viewportRef,
|
|
layers,
|
|
captureCanvasHistory,
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
ref={(element) => {
|
|
viewportRef.current = element;
|
|
if (element) {
|
|
setElementBox(element, { width: 900, height: 640 });
|
|
}
|
|
}}
|
|
data-testid="viewport-element"
|
|
/>
|
|
<div
|
|
className="image-canvas-editor__minimap"
|
|
data-testid="minimap"
|
|
ref={(element) => {
|
|
if (element) {
|
|
setElementBox(element, {
|
|
left: 20,
|
|
top: 30,
|
|
width: 160,
|
|
height: 120,
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
<span data-testid="viewport">
|
|
{controls.viewport.x.toFixed(2)},{controls.viewport.y.toFixed(2)},
|
|
{controls.viewport.scale.toFixed(2)}
|
|
</span>
|
|
<span data-testid="canvas-size">
|
|
{controls.canvasSize.width}x{controls.canvasSize.height}
|
|
</span>
|
|
<span data-testid="drop-point">
|
|
{JSON.stringify(controls.getCanvasDropPoint(260, 190))}
|
|
</span>
|
|
<span data-testid="world-point">
|
|
{JSON.stringify(controls.getCanvasPointFromClient(260, 190))}
|
|
</span>
|
|
<span data-testid="minimap-count">
|
|
{controls.minimapModel?.layers.length ?? 0}
|
|
</span>
|
|
<button type="button" onClick={() => controls.fitLayers()}>
|
|
fit
|
|
</button>
|
|
<button type="button" onClick={() => controls.updateScaleFromCenter(2)}>
|
|
zoom center
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
controls.updateViewportFromMinimapDrag(
|
|
{
|
|
kind: 'minimap',
|
|
pointerId: 1,
|
|
startClientX: 100,
|
|
startClientY: 100,
|
|
startViewport: { x: -100, y: -50, scale: 1 },
|
|
minimapScale: controls.minimapModel?.scale ?? 1,
|
|
moved: true,
|
|
},
|
|
104,
|
|
103,
|
|
);
|
|
}}
|
|
>
|
|
minimap drag
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => controls.moveViewportFromMinimapPointer(100, 90)}
|
|
>
|
|
minimap click
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe('useImageCanvasViewportControls', () => {
|
|
it('owns canvas size, fit view, center zoom and canvas point helpers', () => {
|
|
const captureCanvasHistory = vi.fn();
|
|
render(
|
|
<ViewportHarness captureCanvasHistory={captureCanvasHistory} />,
|
|
);
|
|
|
|
expect(screen.getByTestId('canvas-size').textContent).toBe('900x640');
|
|
expect(screen.getByTestId('minimap-count').textContent).toBe('2');
|
|
expect(screen.getByTestId('drop-point').textContent).toBe(
|
|
'{"x":260,"y":190}',
|
|
);
|
|
const worldPoint = JSON.parse(
|
|
screen.getByTestId('world-point').textContent ?? '{}',
|
|
) as { x: number; y: number };
|
|
expect(worldPoint.x).toBeCloseTo(634.1463);
|
|
expect(worldPoint.y).toBeCloseTo(146.3415);
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'fit' }).click();
|
|
});
|
|
expect(screen.getByTestId('viewport').textContent).toBe(
|
|
'50.00,170.00,1.00',
|
|
);
|
|
expect(captureCanvasHistory).toHaveBeenCalledTimes(1);
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'zoom center' }).click();
|
|
});
|
|
expect(screen.getByTestId('viewport').textContent).toBe(
|
|
'-350.00,20.00,2.00',
|
|
);
|
|
expect(captureCanvasHistory).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('handles vertical wheel scroll, ctrl wheel zoom and minimap movement', () => {
|
|
render(<ViewportHarness />);
|
|
const viewportElement = screen.getByTestId('viewport-element');
|
|
|
|
act(() => {
|
|
fireEvent.wheel(viewportElement, { deltaY: 120, clientX: 260, clientY: 190 });
|
|
});
|
|
expect(screen.getByTestId('viewport').textContent).toBe(
|
|
'-260.00,-50.00,0.82',
|
|
);
|
|
|
|
act(() => {
|
|
fireEvent.wheel(viewportElement, {
|
|
ctrlKey: true,
|
|
deltaY: -120,
|
|
clientX: 260,
|
|
clientY: 190,
|
|
});
|
|
});
|
|
expect(screen.getByTestId('viewport').textContent).toBe(
|
|
'-312.00,-74.00,0.90',
|
|
);
|
|
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'minimap drag' }).click();
|
|
});
|
|
expect(screen.getByTestId('viewport').textContent).toContain('-');
|
|
|
|
const beforeClick = screen.getByTestId('viewport').textContent;
|
|
act(() => {
|
|
screen.getByRole('button', { name: 'minimap click' }).click();
|
|
});
|
|
expect(screen.getByTestId('viewport').textContent).not.toBe(beforeClick);
|
|
});
|
|
});
|