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.slides.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 } }), })); vi.mock('../../../utils/assets', () => ({ uploadAsset: vi.fn(), listAssets: vi.fn(), })); import { MediaStylePanel } from './MediaStylePanel'; 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}"]`)); } function setValue(input: HTMLInputElement, value: string) { const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')!.set!; setter.call(input, value); input.dispatchEvent(new Event('input', { bubbles: true })); } beforeEach(() => { setPropSpy.mockClear(); }); afterEach(() => { if (container) unmount(); }); describe('MediaStylePanel Video source uses AssetPicker (upload/browse/paste-URL), not a plain text input', () => { test('renders a full-variant AssetPicker (not the old plain input) for videoUrl', () => { lastProps = { videoUrl: '', poster: '', style: {} }; render(); expect(q('asset-picker-full')).not.toBeNull(); }); test('applying a pasted URL via the AssetPicker writes videoUrl (not some other key)', () => { lastProps = { videoUrl: '', poster: '', style: {} }; render(); // The Video Source AssetPicker is the first url-input on the page (the // Poster picker is a second, separate AssetPicker instance below it). const urlInput = qAll('asset-picker-url-input')[0]; expect(urlInput).toBeTruthy(); setValue(urlInput, 'https://example.com/clip.mp4'); const applyBtn = qAll('asset-picker-apply')[0]; act(() => { applyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(setPropSpy).toHaveBeenCalled(); expect(lastProps.videoUrl).toBe('https://example.com/clip.mp4'); }); test('a video-shaped selection also renders a Poster AssetPicker (image) that writes `poster`', () => { lastProps = { videoUrl: 'https://example.com/clip.mp4', poster: '', style: {} }; render(); const pickers = qAll('asset-picker-full'); expect(pickers.length).toBe(2); // Video Source + Poster Image const urlInputs = qAll('asset-picker-url-input'); const posterInput = urlInputs[1]; setValue(posterInput, 'https://example.com/poster.jpg'); const applyBtns = qAll('asset-picker-apply'); act(() => { applyBtns[1].dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(lastProps.poster).toBe('https://example.com/poster.jpg'); }); }); describe('MediaStylePanel Video size controls (Width + Aspect Ratio) are gated on videoUrl', () => { test('a video-shaped selection renders SizeControl + AspectRatioControl', () => { lastProps = { videoUrl: '', poster: '', style: {} }; render(); expect(q('size-control')).not.toBeNull(); expect(q('aspect-ratio-control')).not.toBeNull(); }); test('a Map-shaped selection (no videoUrl) does NOT render the video size controls', () => { lastProps = { address: 'New York, NY', zoom: 14, height: '400px', style: {} }; render(); expect(q('size-control')).toBeNull(); expect(q('aspect-ratio-control')).toBeNull(); }); }); function openCollapsibleByTitle(title: string) { const btn = Array.from(container.querySelectorAll('button')).find((b) => b.textContent?.includes(title)); expect(btn).toBeTruthy(); act(() => { btn!.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); } describe('MediaStylePanel box-model + animation/visibility rollout renders for every media type', () => { test('a Map-shaped selection still gets Spacing / Border & Effects / Animation & Visibility sections (collapsed by default, open on click)', () => { lastProps = { address: 'New York, NY', zoom: 14, height: '400px', style: {}, animation: '', hideOnDesktop: false }; render(); openCollapsibleByTitle('Spacing'); expect(q('spacing-control')).not.toBeNull(); openCollapsibleByTitle('Border & Effects'); expect(q('border-control')).not.toBeNull(); openCollapsibleByTitle('Animation & Visibility'); expect(q('animation-control')).not.toBeNull(); expect(q('visibility-control')).not.toBeNull(); }); });