Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

194 lines
6.7 KiB
TypeScript

// @vitest-environment jsdom
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import { fireEvent } from '@testing-library/dom';
import { ResizeHandles } from '../../../src/editing/handles/ResizeHandles';
import { RotateHandle } from '../../../src/editing/handles/RotateHandle';
import { RESIZE_HANDLES, type PPTBoxElement } from '../../../src/editing/core/resize';
const element = {
id: 'a',
type: 'image',
left: 100,
top: 100,
width: 200,
height: 80,
rotate: 0,
fixedRatio: false,
src: 'x.png',
} as unknown as PPTBoxElement;
const viewportStyles = { left: 0, top: 0, width: 1000, height: 562.5 };
describe('ResizeHandles', () => {
it('renders one box per requested handle at the un-rotated frame points', () => {
const { container } = render(
<ResizeHandles
element={element}
handles={RESIZE_HANDLES}
viewportStyles={viewportStyles}
canvasScale={1}
onHandlePointerDown={vi.fn()}
/>,
);
const boxes = container.querySelectorAll('[data-resize-handle]');
expect(boxes).toHaveLength(8);
const at = (h: string) => container.querySelector(`[data-resize-handle="${h}"]`) as HTMLElement;
expect(at('left-top').style.left).toBe('100px');
expect(at('left-top').style.top).toBe('100px');
expect(at('top').style.left).toBe('200px');
expect(at('top').style.top).toBe('100px');
expect(at('right').style.left).toBe('300px');
expect(at('right').style.top).toBe('140px');
expect(at('right-bottom').style.left).toBe('300px');
expect(at('right-bottom').style.top).toBe('180px');
});
it('renders only the requested subset (text-style two handles)', () => {
const { container } = render(
<ResizeHandles
element={element}
handles={['left', 'right']}
viewportStyles={viewportStyles}
canvasScale={1}
onHandlePointerDown={vi.fn()}
/>,
);
const boxes = container.querySelectorAll('[data-resize-handle]');
expect(boxes).toHaveLength(2);
});
it('positions handles at the ROTATED frame points (not the axis-aligned box)', () => {
// 100x50 box at (100, 100) rotated 90°: center (150, 125), the rotated
// top-center point sits at (175, 125).
const rotated = { ...element, left: 100, top: 100, width: 100, height: 50, rotate: 90 };
const { container } = render(
<ResizeHandles
element={rotated as PPTBoxElement}
handles={RESIZE_HANDLES}
viewportStyles={viewportStyles}
canvasScale={1}
onHandlePointerDown={vi.fn()}
/>,
);
const top = container.querySelector('[data-resize-handle="top"]') as HTMLElement;
expect(parseFloat(top.style.left)).toBeCloseTo(175, 5);
expect(parseFloat(top.style.top)).toBeCloseTo(125, 5);
const leftTop = container.querySelector('[data-resize-handle="left-top"]') as HTMLElement;
expect(parseFloat(leftTop.style.left)).toBeCloseTo(175, 5);
expect(parseFloat(leftTop.style.top)).toBeCloseTo(75, 5);
});
it('applies the centering offset and canvas scale like the other overlays', () => {
const { container } = render(
<ResizeHandles
element={element}
handles={['left-top']}
viewportStyles={{ ...viewportStyles, left: 160 }}
canvasScale={0.5}
onHandlePointerDown={vi.fn()}
/>,
);
const lt = container.querySelector('[data-resize-handle="left-top"]') as HTMLElement;
expect(lt.style.left).toBe('210px'); // 160 + 100 * 0.5
expect(lt.style.top).toBe('50px'); // 0 + 100 * 0.5
});
it('carries a rotation-aware directional cursor', () => {
const { container: c0 } = render(
<ResizeHandles
element={element}
handles={['left-top']}
viewportStyles={viewportStyles}
canvasScale={1}
onHandlePointerDown={vi.fn()}
/>,
);
expect((c0.querySelector('[data-resize-handle="left-top"]') as HTMLElement).style.cursor).toBe(
'nwse-resize',
);
const { container: c45 } = render(
<ResizeHandles
element={{ ...element, rotate: 45 } as PPTBoxElement}
handles={['left-top']}
viewportStyles={viewportStyles}
canvasScale={1}
onHandlePointerDown={vi.fn()}
/>,
);
expect((c45.querySelector('[data-resize-handle="left-top"]') as HTMLElement).style.cursor).toBe(
'ns-resize',
);
});
it('reports the pressed handle through onHandlePointerDown', () => {
const onDown = vi.fn();
const { container } = render(
<ResizeHandles
element={element}
handles={RESIZE_HANDLES}
viewportStyles={viewportStyles}
canvasScale={1}
onHandlePointerDown={onDown}
/>,
);
fireEvent.pointerDown(
container.querySelector('[data-resize-handle="left-bottom"]') as HTMLElement,
{ pointerId: 1 },
);
expect(onDown).toHaveBeenCalledTimes(1);
expect(onDown.mock.calls[0][0]).toBe('left-bottom');
});
});
describe('RotateHandle', () => {
it('floats 25 screen px above the top-center point at rotation 0', () => {
const { container } = render(
<RotateHandle
element={element}
viewportStyles={viewportStyles}
canvasScale={1}
onPointerDown={vi.fn()}
/>,
);
const handle = container.querySelector('[data-rotate-handle]') as HTMLElement;
// Top-center (200, 100), pushed 25px along (0, -1).
expect(parseFloat(handle.style.left)).toBeCloseTo(200, 5);
expect(parseFloat(handle.style.top)).toBeCloseTo(75, 5);
expect(handle.style.cursor).toBe('grab');
});
it('orbits with the element: at 90° it sits 25px to the right of the rotated top point', () => {
const rotated = { ...element, left: 100, top: 100, width: 100, height: 50, rotate: 90 };
const { container } = render(
<RotateHandle
element={rotated as PPTBoxElement}
viewportStyles={viewportStyles}
canvasScale={1}
onPointerDown={vi.fn()}
/>,
);
const handle = container.querySelector('[data-rotate-handle]') as HTMLElement;
// Rotated top point (175, 125), up vector (sin 90, -cos 90) = (1, 0).
expect(parseFloat(handle.style.left)).toBeCloseTo(200, 5);
expect(parseFloat(handle.style.top)).toBeCloseTo(125, 5);
});
it('keeps the 25px gap constant on screen regardless of canvas scale', () => {
const { container } = render(
<RotateHandle
element={element}
viewportStyles={viewportStyles}
canvasScale={0.5}
onPointerDown={vi.fn()}
/>,
);
const handle = container.querySelector('[data-rotate-handle]') as HTMLElement;
// Top-center (200, 100) * 0.5 = (100, 50), minus the un-scaled 25px gap.
expect(parseFloat(handle.style.left)).toBeCloseTo(100, 5);
expect(parseFloat(handle.style.top)).toBeCloseTo(25, 5);
});
});