UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

554 lines 27.2 kB
import { createInjector } from '@furystack/inject'; import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'; import { usingAsync } from '@furystack/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { InputNumber } from './input-number.js'; describe('InputNumber', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; vi.restoreAllMocks(); }); it('should render as custom element', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, null), }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el).not.toBeNull(); }); }); it('should render the inner input, decrement and increment buttons', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5 }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); const buttons = wrapper.querySelectorAll('.step-button'); expect(input).not.toBeNull(); expect(input.value).toBe('5'); expect(buttons.length).toBe(2); }); }); it('should render the label title', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { labelTitle: "Quantity" }), }); await flushUpdates(); const label = document.querySelector('shade-input-number label'); expect(label).not.toBeNull(); expect(label.textContent).toContain('Quantity'); }); }); describe('increment and decrement', () => { it('should increment value when + button is clicked', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(6); }); }); it('should decrement value when - button is clicked', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const decrementBtn = wrapper.querySelectorAll('.step-button')[0]; decrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(4); }); }); it('should use custom step value', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 10, step: 5, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(15); }); }); }); describe('min and max', () => { it('should clamp value to max', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 9, max: 10, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(10); }); }); it('should clamp value to min', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 1, min: 0, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const decrementBtn = wrapper.querySelectorAll('.step-button')[0]; decrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(0); }); }); it('should disable decrement button when value equals min', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 0, min: 0 }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const decrementBtn = wrapper.querySelectorAll('.step-button')[0]; expect(decrementBtn.disabled).toBe(true); }); }); it('should disable increment button when value equals max', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 100, max: 100 }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; expect(incrementBtn.disabled).toBe(true); }); }); }); describe('precision', () => { it('should display value with specified precision', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 3.1, precision: 2 }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); expect(input.value).toBe('3.10'); }); }); it('should round to precision when stepping', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 1, step: 0.01, precision: 2, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(1.01); }); }); }); describe('keyboard support', () => { it('should increment on ArrowUp', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(6); }); }); it('should decrement on ArrowDown', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(4); }); }); it('should not respond to keyboard when disabled', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, disabled: true, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); await flushUpdates(); expect(onValueChange).not.toHaveBeenCalled(); }); }); }); describe('formatter and parser', () => { it('should display formatted value', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 1000, formatter: (v) => (v !== undefined ? `$${v.toLocaleString()}` : '') }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); expect(input.value).toBe('$1,000'); }); }); it('should use parser to interpret input text', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(InputNumber, { value: 0, parser: (text) => { const cleaned = text.replace(/[^0-9.-]/g, ''); const num = Number(cleaned); return isNaN(num) ? undefined : num; }, onValueChange: onValueChange })), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); input.value = '$500'; input.dispatchEvent(new Event('change', { bubbles: true })); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(500); }); }); }); describe('direct text input', () => { it('should parse typed value on blur', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 0, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); input.value = '42'; input.dispatchEvent(new Event('blur', { bubbles: true })); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(42); }); }); it('should handle empty input on blur', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); input.value = ''; input.dispatchEvent(new Event('blur', { bubbles: true })); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(undefined); }); }); }); describe('disabled state', () => { it('should set data-disabled attribute when disabled', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { disabled: true }), }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.hasAttribute('data-disabled')).toBe(true); }); }); it('should disable both step buttons when disabled', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, disabled: true }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const buttons = wrapper.querySelectorAll('.step-button'); expect(buttons[0].disabled).toBe(true); expect(buttons[1].disabled).toBe(true); }); }); it('should not change value when clicking buttons while disabled', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, disabled: true, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).not.toHaveBeenCalled(); }); }); }); describe('variants', () => { it('should set data-variant for outlined', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { variant: "outlined" }), }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.getAttribute('data-variant')).toBe('outlined'); }); }); it('should set data-variant for contained', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { variant: "contained" }), }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.getAttribute('data-variant')).toBe('contained'); }); }); }); describe('helper text', () => { it('should render helper text', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { helperText: "Enter a quantity" }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const helperText = wrapper.querySelector('.helperText'); expect(helperText).not.toBeNull(); expect(helperText.textContent).toBe('Enter a quantity'); }); }); it('should not render helper text container when no helper text', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, null), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const helperText = wrapper.querySelector('.helperText'); expect(helperText).toBeNull(); }); }); }); describe('accessibility', () => { it('should set role=spinbutton on the input', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { value: 5, min: 0, max: 10 }), }); // Wait for render + requestAnimationFrame await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const input = wrapper.querySelector('input'); expect(input.getAttribute('role')).toBe('spinbutton'); expect(input.getAttribute('aria-valuemin')).toBe('0'); expect(input.getAttribute('aria-valuemax')).toBe('10'); expect(input.getAttribute('aria-valuenow')).toBe('5'); }); }); it('should set aria-label on step buttons', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, null), }); // Wait for render + requestAnimationFrame await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const buttons = wrapper.querySelectorAll('.step-button'); expect(buttons[0].getAttribute('aria-label')).toBe('Decrease value'); expect(buttons[1].getAttribute('aria-label')).toBe('Increase value'); }); }); }); describe('no initial value', () => { it('should start from min when incrementing with no value', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { min: 1, onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(2); }); }); it('should start from 0 when incrementing with no value and no min', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onValueChange = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { onValueChange: onValueChange }), }); await flushUpdates(); const wrapper = document.querySelector('shade-input-number'); const incrementBtn = wrapper.querySelectorAll('.step-button')[1]; incrementBtn.click(); await flushUpdates(); expect(onValueChange).toHaveBeenCalledWith(1); }); }); }); describe('size', () => { it('should not set data-size when size is not specified', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, null) }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.getAttribute('data-size')).toBeNull(); }); }); it('should not set data-size for medium size (default)', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { size: "medium" }) }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.getAttribute('data-size')).toBeNull(); }); }); it('should set data-size="small" for small size', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { size: "small" }) }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.getAttribute('data-size')).toBe('small'); }); }); it('should set data-size="large" for large size', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(InputNumber, { size: "large" }) }); await flushUpdates(); const el = document.querySelector('shade-input-number'); expect(el.getAttribute('data-size')).toBe('large'); }); }); }); }); //# sourceMappingURL=input-number.spec.js.map