Files
av-tools/tests/unit/components/crop-overlay.test.tsx

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