Files
Genarrative/src/components/image-editor/useImageCanvasViewportControls.test.tsx
kdletters 31cc1f0473 拆分图片画布视口控制
新增视口控制 hook 管理缩放、滚轮、坐标和小地图

从主视图移除视口尺寸与滚轮绑定逻辑

补充视口控制单测并更新拆分记录
2026-06-17 09:17:04 +08:00

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);
});
});