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 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(testId: string): T | null { return container.querySelector(`[data-testid="${testId}"]`); } function qAll(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(); 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(); 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(); 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(); // 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(); 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(); 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(); clickPresetByLabel(q('aspect-ratio-control'), '9:16'); expect(lastProps.style.aspectRatio).toBe('9 / 16'); expect(lastProps.style.height).toBe(''); }); });