import { describe, test, expect, vi } from 'vitest'; import React from 'react'; import { createRoot, Root } from 'react-dom/client'; import { act } from 'react-dom/test-utils'; import { NumericUnitInput, parseCssLength, SizeControl, AspectRatioControl, parseAspectRatioInput, FocalPointGrid, FOCAL_POINTS, SpacingControl, BorderControl, buildBorderShorthand, AnimationControl, ANIMATIONS, VisibilityControl, } from './shared'; /* Same DOM-harness pattern as AssetPicker.test.tsx / ImageBlock.render.test.tsx (no @testing-library/react in this repo). */ 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 click(el: Element | null) { if (!el) throw new Error('element not found'); act(() => { (el as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); }); } function setValue(input: HTMLInputElement | HTMLSelectElement, value: string) { const proto = input instanceof HTMLSelectElement ? window.HTMLSelectElement.prototype : window.HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set!; setter.call(input, value); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } function blur(el: Element) { act(() => { el.dispatchEvent(new FocusEvent('blur', { bubbles: true })); el.dispatchEvent(new FocusEvent('focusout', { bubbles: true })); }); } function q(testId: string): T | null { return container.querySelector(`[data-testid="${testId}"]`); } /* ---------- NumericUnitInput / parseCssLength ---------- */ describe('parseCssLength', () => { test('parses "16px" into number 16 + unit px', () => { expect(parseCssLength('16px')).toEqual({ num: '16', unit: 'px' }); }); test('parses "50%" into number 50 + unit %', () => { expect(parseCssLength('50%', ['px', '%'])).toEqual({ num: '50', unit: '%' }); }); test('treats "auto" and "" as empty', () => { expect(parseCssLength('auto')).toEqual({ num: '', unit: 'px' }); expect(parseCssLength('')).toEqual({ num: '', unit: 'px' }); expect(parseCssLength(undefined)).toEqual({ num: '', unit: 'px' }); }); test('falls back to the first configured unit for an unrecognized unit', () => { expect(parseCssLength('16vh', ['px', '%'])).toEqual({ num: '16', unit: 'px' }); }); }); describe('NumericUnitInput', () => { test('renders the parsed number and unit from a "16px" value', () => { render(); expect(q('numeric-unit-number')!.value).toBe('16'); expect(q('numeric-unit-unit')!.value).toBe('px'); unmount(); }); test('changing the number emits the recombined string', () => { const onChange = vi.fn(); render(); setValue(q('numeric-unit-number')!, '32'); expect(onChange).toHaveBeenCalledWith('32px'); unmount(); }); test('changing the unit re-emits with the current number', () => { const onChange = vi.fn(); render(); setValue(q('numeric-unit-unit')!, '%'); expect(onChange).toHaveBeenCalledWith('16%'); unmount(); }); test('clearing the number emits empty string, not "0px"', () => { const onChange = vi.fn(); render(); setValue(q('numeric-unit-number')!, ''); expect(onChange).toHaveBeenCalledWith(''); unmount(); }); test('omits the unit