Refs #32 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
134 lines
5.1 KiB
TypeScript
134 lines
5.1 KiB
TypeScript
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import React from 'react';
|
|
import { createRoot, Root } from 'react-dom/client';
|
|
import { act } from 'react-dom/test-utils';
|
|
|
|
/* Same DOM-harness pattern as MediaStylePanel.video.test.tsx -- mock
|
|
@craftjs/core's useEditor so setProp calls can be observed without
|
|
mounting a real <Editor> tree, and mock utils/assets so AssetPicker
|
|
doesn't hit the network. */
|
|
const setPropSpy = vi.fn((_id: string, updater: (p: any) => void) => {
|
|
updater(lastProps);
|
|
});
|
|
let lastProps: any;
|
|
|
|
vi.mock('@craftjs/core', () => ({
|
|
useEditor: () => ({ actions: { setProp: setPropSpy, history: { merge: () => ({ setProp: setPropSpy }) } } }),
|
|
}));
|
|
|
|
vi.mock('../../../utils/assets', () => ({
|
|
uploadAsset: vi.fn(),
|
|
listAssets: vi.fn(),
|
|
}));
|
|
|
|
import { ImageStylePanel } from './ImageStylePanel';
|
|
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
|
|
function render(ui: React.ReactElement) {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
act(() => {
|
|
root = createRoot(container);
|
|
root.render(ui);
|
|
});
|
|
}
|
|
|
|
function unmount() {
|
|
act(() => { root.unmount(); });
|
|
container.remove();
|
|
}
|
|
|
|
function q<T extends Element = Element>(testId: string): T | null {
|
|
return container.querySelector(`[data-testid="${testId}"]`);
|
|
}
|
|
|
|
function qAll<T extends Element = Element>(testId: string): T[] {
|
|
return Array.from(container.querySelectorAll(`[data-testid="${testId}"]`));
|
|
}
|
|
|
|
/** Click the preset button with this exact label inside a given data-testid
|
|
* root (AspectRatioControl / PresetButtonGrid render plain buttons keyed by
|
|
* label, no per-button testid). */
|
|
function clickPresetByLabel(root: Element | null, label: string) {
|
|
const btn = Array.from(root?.querySelectorAll('button') ?? []).find((b) => b.textContent === label);
|
|
expect(btn).toBeTruthy();
|
|
act(() => { btn!.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
setPropSpy.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (container) unmount();
|
|
});
|
|
|
|
describe('ImageStylePanel crop-fills-by-default (fix-anim-image B)', () => {
|
|
test('applying a non-empty aspect ratio with objectFit unset also sets objectFit to cover', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: {} };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
|
|
clickPresetByLabel(q('aspect-ratio-control'), '1:1');
|
|
|
|
expect(lastProps.style.aspectRatio).toBe('1 / 1');
|
|
expect(lastProps.style.objectFit).toBe('cover');
|
|
});
|
|
|
|
test('applying a ratio when objectFit is already "contain" leaves it as contain (no forced override)', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: { objectFit: 'contain' } };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
|
|
clickPresetByLabel(q('aspect-ratio-control'), '16:9');
|
|
|
|
expect(lastProps.style.aspectRatio).toBe('16 / 9');
|
|
expect(lastProps.style.objectFit).toBe('contain');
|
|
});
|
|
|
|
test('clearing the ratio (Original) does not force-clear objectFit', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: { aspectRatio: '1 / 1', objectFit: 'cover' } };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
|
|
clickPresetByLabel(q('aspect-ratio-control'), 'Original');
|
|
|
|
expect(lastProps.style.aspectRatio).toBe('');
|
|
expect(lastProps.style.objectFit).toBe('cover');
|
|
});
|
|
|
|
test('the Object Fit control (Cover/Contain/Fill/None) remains present and usable', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: { aspectRatio: '1 / 1', objectFit: 'cover' } };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
|
|
// PresetButtonGrid buttons have no dedicated per-button testid; find by label text.
|
|
const containBtn = Array.from(container.querySelectorAll('button')).find((b) => b.textContent === 'Contain');
|
|
expect(containBtn).toBeTruthy();
|
|
act(() => { containBtn!.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
|
expect(lastProps.style.objectFit).toBe('contain');
|
|
});
|
|
});
|
|
|
|
describe('ImageStylePanel Height control gated on aspect-ratio (fix-anim-image C)', () => {
|
|
test('no aspect-ratio set: both Width and Height SizeControls render', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: {} };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
expect(qAll('size-control').length).toBe(2);
|
|
});
|
|
|
|
test('aspect-ratio set: only the Width SizeControl renders (Height is hidden)', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: { aspectRatio: '1 / 1', objectFit: 'cover' } };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
expect(qAll('size-control').length).toBe(1);
|
|
});
|
|
|
|
test('applying an aspect ratio clears a stale height so it cannot linger and conflict', () => {
|
|
lastProps = { src: '/uploads/photo.jpg', alt: '', style: { height: '50%' } };
|
|
render(<ImageStylePanel selectedId="img-1" nodeProps={lastProps} />);
|
|
|
|
clickPresetByLabel(q('aspect-ratio-control'), '9:16');
|
|
|
|
expect(lastProps.style.aspectRatio).toBe('9 / 16');
|
|
expect(lastProps.style.height).toBe('');
|
|
});
|
|
});
|