150 lines
4.0 KiB
TypeScript
150 lines
4.0 KiB
TypeScript
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { CropOverlay } from '../../../src/components/CropOverlay';
|
|
|
|
describe('CropOverlay', () => {
|
|
afterEach(cleanup);
|
|
|
|
it('maps a clockwise-oriented preview back to encoded source pixels', () => {
|
|
const onChange = vi.fn();
|
|
const { container } = render(
|
|
<CropOverlay
|
|
sourceWidth={100}
|
|
sourceHeight={80}
|
|
orientation={90}
|
|
value={{ x: 10, y: 20, width: 30, height: 40 }}
|
|
onChange={onChange}
|
|
>
|
|
<img src="blob:local-preview" alt="Local preview" />
|
|
</CropOverlay>
|
|
);
|
|
|
|
const rectangle = container.querySelector('.crop-editor__rectangle');
|
|
expect(rectangle).toHaveStyle({
|
|
left: '25%',
|
|
top: '10%',
|
|
width: '50%',
|
|
height: '30%',
|
|
});
|
|
|
|
fireEvent.keyDown(
|
|
screen.getByRole('button', { name: 'Move crop rectangle' }),
|
|
{ key: 'ArrowRight' }
|
|
);
|
|
|
|
expect(onChange).toHaveBeenLastCalledWith({
|
|
x: 10,
|
|
y: 19,
|
|
width: 30,
|
|
height: 40,
|
|
});
|
|
});
|
|
|
|
it('supports pointer dragging with bounded percentage-to-pixel math', () => {
|
|
const onChange = vi.fn();
|
|
const { container } = render(
|
|
<CropOverlay
|
|
sourceWidth={200}
|
|
sourceHeight={100}
|
|
value={{ x: 20, y: 10, width: 100, height: 60 }}
|
|
onChange={onChange}
|
|
>
|
|
<div>Preview</div>
|
|
</CropOverlay>
|
|
);
|
|
const stage = container.querySelector('.crop-editor__stage');
|
|
if (!(stage instanceof HTMLElement)) throw new Error('Missing crop stage');
|
|
vi.spyOn(stage, 'getBoundingClientRect').mockReturnValue({
|
|
x: 0,
|
|
y: 0,
|
|
left: 0,
|
|
top: 0,
|
|
right: 200,
|
|
bottom: 100,
|
|
width: 200,
|
|
height: 100,
|
|
toJSON: () => ({}),
|
|
});
|
|
const move = screen.getByRole('button', { name: 'Move crop rectangle' });
|
|
|
|
fireEvent.pointerDown(move, {
|
|
pointerId: 7,
|
|
clientX: 10,
|
|
clientY: 10,
|
|
});
|
|
fireEvent.pointerMove(move, {
|
|
pointerId: 7,
|
|
clientX: 60,
|
|
clientY: 30,
|
|
});
|
|
fireEvent.pointerUp(move, { pointerId: 7 });
|
|
|
|
expect(onChange).toHaveBeenLastCalledWith({
|
|
x: 70,
|
|
y: 30,
|
|
width: 100,
|
|
height: 60,
|
|
});
|
|
});
|
|
|
|
it('locks common display ratios without confusing rotated dimensions', async () => {
|
|
const user = userEvent.setup();
|
|
const onChange = vi.fn();
|
|
render(
|
|
<CropOverlay
|
|
sourceWidth={100}
|
|
sourceHeight={80}
|
|
orientation={90}
|
|
value={{ x: 0, y: 0, width: 100, height: 80 }}
|
|
onChange={onChange}
|
|
>
|
|
<div>Preview</div>
|
|
</CropOverlay>
|
|
);
|
|
|
|
await user.selectOptions(
|
|
screen.getByRole('combobox', { name: 'Common ratio' }),
|
|
'16:9'
|
|
);
|
|
|
|
const crop = onChange.mock.lastCall?.[0] as
|
|
{ width: number; height: number } | undefined;
|
|
expect(crop).toBeDefined();
|
|
expect((crop?.width ?? 0) / (crop?.height ?? 1)).toBeCloseTo(9 / 16, 2);
|
|
expect(
|
|
screen.getByRole('checkbox', { name: 'Lock aspect ratio' })
|
|
).toBeChecked();
|
|
});
|
|
|
|
it('rejects invalid numeric edits and emits even reset bounds when required', async () => {
|
|
const user = userEvent.setup();
|
|
const onChange = vi.fn();
|
|
render(
|
|
<CropOverlay
|
|
sourceWidth={101}
|
|
sourceHeight={99}
|
|
requireEvenDimensions
|
|
value={{ x: 0, y: 0, width: 100, height: 98 }}
|
|
onChange={onChange}
|
|
>
|
|
<div>Preview</div>
|
|
</CropOverlay>
|
|
);
|
|
|
|
await user.clear(screen.getByRole('spinbutton', { name: 'Width' }));
|
|
expect(screen.getByRole('alert')).toHaveTextContent(
|
|
'All crop fields are required.'
|
|
);
|
|
expect(onChange).not.toHaveBeenCalled();
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Reset crop' }));
|
|
expect(onChange).toHaveBeenLastCalledWith({
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 98,
|
|
});
|
|
});
|
|
});
|