Files
astrolabe/src/app/components/ColorField.test.tsx
T

81 lines
3.0 KiB
TypeScript

/**
* ColorField — the shared color-input primitive (swatch + optional hex field).
* Covers the render shapes and the hex-entry commit/normalize behavior; the
* Theme Builder and Chart Builder integrations are exercised in their own tests.
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { ColorField } from './ColorField';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
act(() => {
root = createRoot(container);
});
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
/** Drive an input's value through the native setter so React's onChange fires. */
function setNativeValue(el: HTMLInputElement, value: string) {
// eslint-disable-next-line @typescript-eslint/unbound-method -- invoked immediately via .call
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')!.set!;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
}
const swatch = () => container.querySelector<HTMLInputElement>('input[type="color"]')!;
const hexField = () => container.querySelector<HTMLInputElement>('input[type="text"]');
describe('ColorField', () => {
test('renders just the swatch by default (no hex field)', () => {
act(() => root.render(<ColorField value="#112233" label="Fill" onChange={() => {}} />));
expect(swatch().getAttribute('aria-label')).toBe('Fill');
expect(swatch().value).toBe('#112233');
expect(hexField()).toBeNull();
});
test('falls back to #000000 in the picker for a non-hex value', () => {
act(() => root.render(<ColorField value="steelblue" label="Fill" onChange={() => {}} />));
expect(swatch().value).toBe('#000000');
});
test('the picker reports its raw value on change', () => {
const onChange = vi.fn();
act(() => root.render(<ColorField value="#112233" label="Fill" onChange={onChange} />));
act(() => setNativeValue(swatch(), '#ff0000'));
expect(onChange).toHaveBeenCalledWith('#ff0000');
});
test('hex mode commits a valid hex and normalizes an unprefixed one', () => {
const onChange = vi.fn();
act(() => root.render(<ColorField value="#112233" label="Fill" onChange={onChange} hex />));
const hex = hexField()!;
expect(hex.getAttribute('aria-label')).toBe('Fill hex value');
act(() => setNativeValue(hex, '#abcdef'));
expect(onChange).toHaveBeenLastCalledWith('#abcdef');
act(() => setNativeValue(hex, 'ABCDEF'));
expect(onChange).toHaveBeenLastCalledWith('#abcdef');
});
test('hex mode does not commit an incomplete value', () => {
const onChange = vi.fn();
act(() => root.render(<ColorField value="#112233" label="Fill" onChange={onChange} hex />));
act(() => setNativeValue(hexField()!, '#abc'));
expect(onChange).not.toHaveBeenCalled();
});
});