import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Checkbox, RadioGroup, Switch } from './choice'; import { CHECKBOX_CHANGE_EVENT, RADIO_CHANGE_EVENT, SWITCH_CHANGE_EVENT, type CheckboxChangeEvent, type RadioChangeEvent, type SwitchChangeEvent, } from './choice.types'; describe('Checkbox', () => { beforeEach(() => { document.body.innerHTML = ''; }); function el(): HTMLElement { return document.body.firstElementChild as HTMLElement; } it('wires role and unchecked state by default', () => { const node = el(); new Checkbox(node); expect(node.getAttribute('role')).toBe('checkbox'); expect(node.getAttribute('aria-checked')).toBe('false'); expect(node.dataset.state).toBe('unchecked'); expect(node.getAttribute('tabindex')).toBe('0'); }); it('toggles on click', () => { const node = el(); new Checkbox(node); node.click(); expect(node.getAttribute('aria-checked')).toBe('true'); expect(node.dataset.state).toBe('checked'); }); it('toggles on Space', () => { const node = el(); new Checkbox(node); node.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); expect(node.getAttribute('aria-checked')).toBe('true'); }); it('renders indeterminate as mixed and resolves to checked on toggle', () => { const node = el(); const cb = new Checkbox(node, { indeterminate: true }); expect(node.getAttribute('aria-checked')).toBe('mixed'); expect(node.dataset.state).toBe('indeterminate'); cb.toggle(); expect(node.getAttribute('aria-checked')).toBe('true'); }); it('is a no-op when disabled', () => { const node = el(); new Checkbox(node, { disabled: true }); node.click(); expect(node.getAttribute('aria-checked')).toBe('false'); expect(node.getAttribute('aria-disabled')).toBe('true'); expect(node.hasAttribute('tabindex')).toBe(false); }); it('emits change events', () => { const node = el(); const cb = new Checkbox(node); const spy = vi.fn(); cb.on(CHECKBOX_CHANGE_EVENT, spy); cb.toggle(); expect(spy.mock.calls[0][0].detail).toEqual({ checked: true, indeterminate: false }); }); it('removes listeners on destroy', () => { const node = el(); const cb = new Checkbox(node); cb.destroy(); node.click(); expect(node.getAttribute('aria-checked')).toBe('false'); }); }); describe('Switch', () => { beforeEach(() => { document.body.innerHTML = ''; }); function el(): HTMLElement { return document.body.firstElementChild as HTMLElement; } it('wires switch role and off state by default', () => { const node = el(); new Switch(node); expect(node.getAttribute('role')).toBe('switch'); expect(node.getAttribute('aria-checked')).toBe('false'); expect(node.dataset.state).toBe('off'); }); it('toggles on click', () => { const node = el(); new Switch(node); node.click(); expect(node.getAttribute('aria-checked')).toBe('true'); expect(node.dataset.state).toBe('on'); }); it('respects initial checked option', () => { const node = el(); const sw = new Switch(node, { checked: true }); expect(sw.checked).toBe(true); expect(node.dataset.state).toBe('on'); }); it('is a no-op when disabled', () => { const node = el(); new Switch(node, { disabled: true }); node.click(); expect(node.getAttribute('aria-checked')).toBe('false'); }); it('emits change events', () => { const node = el(); const sw = new Switch(node); const spy = vi.fn(); sw.on(SWITCH_CHANGE_EVENT, spy); sw.toggle(); expect(spy.mock.calls[0][0].detail).toEqual({ checked: true }); }); }); describe('RadioGroup', () => { const MARKUP = `
A
B
C
`; beforeEach(() => { document.body.innerHTML = MARKUP; }); function root(): HTMLElement { return document.body.firstElementChild as HTMLElement; } function radios(): HTMLElement[] { return Array.from(document.querySelectorAll('[data-c42-radio]')); } it('wires radiogroup and radio roles', () => { const r = root(); new RadioGroup(r); expect(r.getAttribute('role')).toBe('radiogroup'); expect(radios()[0].getAttribute('role')).toBe('radio'); expect(radios()[0].getAttribute('aria-checked')).toBe('false'); }); it('throws when there are no radios', () => { document.body.innerHTML = '
'; expect(() => new RadioGroup(document.body.firstElementChild as HTMLElement)).toThrow( /at least one/, ); }); it('makes only the first radio focusable when none selected', () => { new RadioGroup(root()); const [first, second] = radios(); expect(first.getAttribute('tabindex')).toBe('0'); expect(second.getAttribute('tabindex')).toBe('-1'); }); it('selects a radio on click', () => { const group = new RadioGroup(root()); radios()[1].click(); expect(group.value).toBe('b'); expect(radios()[1].getAttribute('aria-checked')).toBe('true'); expect(radios()[1].getAttribute('tabindex')).toBe('0'); }); it('honors defaultValue', () => { const group = new RadioGroup(root(), { defaultValue: 'c' }); expect(group.value).toBe('c'); expect(radios()[2].getAttribute('aria-checked')).toBe('true'); }); it('moves and selects with arrow keys (wrapping)', () => { const group = new RadioGroup(root()); const [first] = radios(); first.focus(); first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); expect(group.value).toBe('b'); expect(document.activeElement).toBe(radios()[1]); }); it('is a no-op when disabled', () => { const group = new RadioGroup(root(), { disabled: true }); radios()[0].click(); expect(group.value).toBeNull(); expect(root().getAttribute('aria-disabled')).toBe('true'); }); it('emits change events with the selected value', () => { const group = new RadioGroup(root()); const spy = vi.fn(); group.on(RADIO_CHANGE_EVENT, spy); group.setValue('a'); expect(spy.mock.calls[0][0].detail).toEqual({ value: 'a' }); }); });