diff --git a/assets/test/controls.test.ts b/assets/test/controls.test.ts new file mode 100644 index 0000000..0a0f59f --- /dev/null +++ b/assets/test/controls.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createModeControl, createMetricControl } from '../src/ui/controls'; + +describe('createModeControl', () => { + const modes = [ + { key: 'year', label: 'Year' }, + { key: 'week', label: 'Week' }, + ]; + + it('returns a nav element with nav-segmented class and tablist role', () => { + const nav = createModeControl('year', modes, () => {}); + expect(nav.tagName).toBe('NAV'); + expect(nav.className).toBe('nav nav-segmented'); + expect(nav.getAttribute('role')).toBe('tablist'); + }); + + it('renders buttons for each mode', () => { + const nav = createModeControl('year', modes, () => {}); + const buttons = nav.querySelectorAll('button'); + expect(buttons.length).toBe(2); + expect(buttons[0].textContent).toBe('Year'); + expect(buttons[1].textContent).toBe('Week'); + }); + + it('marks the active mode with active class and aria-selected', () => { + const nav = createModeControl('year', modes, () => {}); + const buttons = nav.querySelectorAll('button'); + expect(buttons[0].classList.contains('active')).toBe(true); + expect(buttons[0].getAttribute('aria-selected')).toBe('true'); + expect(buttons[1].classList.contains('active')).toBe(false); + expect(buttons[1].getAttribute('aria-selected')).toBe('false'); + }); + + it('all buttons have nav-link class and role=tab', () => { + const nav = createModeControl('year', modes, () => {}); + const buttons = nav.querySelectorAll('button'); + buttons.forEach((btn) => { + expect(btn.classList.contains('nav-link')).toBe(true); + expect(btn.getAttribute('role')).toBe('tab'); + }); + }); + + it('clicking inactive button updates active state and calls onChange', () => { + const onChange = vi.fn(); + const nav = createModeControl('year', modes, onChange); + const buttons = nav.querySelectorAll('button'); + + buttons[1].click(); + + expect(onChange).toHaveBeenCalledWith('week'); + expect(buttons[1].classList.contains('active')).toBe(true); + expect(buttons[1].getAttribute('aria-selected')).toBe('true'); + expect(buttons[0].classList.contains('active')).toBe(false); + expect(buttons[0].getAttribute('aria-selected')).toBe('false'); + }); + + it('clicking already active button still calls onChange', () => { + const onChange = vi.fn(); + const nav = createModeControl('year', modes, onChange); + const buttons = nav.querySelectorAll('button'); + + buttons[0].click(); + + expect(onChange).toHaveBeenCalledWith('year'); + }); +}); + +describe('createMetricControl', () => { + it('returns a nav element with nav-segmented nav-sm classes', () => { + const nav = createMetricControl('hours', () => {}); + expect(nav.tagName).toBe('NAV'); + expect(nav.className).toBe('nav nav-segmented nav-sm'); + expect(nav.getAttribute('role')).toBe('tablist'); + }); + + it('renders Hours and Count buttons', () => { + const nav = createMetricControl('hours', () => {}); + const buttons = nav.querySelectorAll('button'); + expect(buttons.length).toBe(2); + expect(buttons[0].textContent).toBe('Hours'); + expect(buttons[1].textContent).toBe('Count'); + }); + + it('marks hours as active by default', () => { + const nav = createMetricControl('hours', () => {}); + const buttons = nav.querySelectorAll('button'); + expect(buttons[0].classList.contains('active')).toBe(true); + expect(buttons[1].classList.contains('active')).toBe(false); + }); + + it('clicking Count calls onChange with count', () => { + const onChange = vi.fn(); + const nav = createMetricControl('hours', onChange); + const buttons = nav.querySelectorAll('button'); + + buttons[1].click(); + + expect(onChange).toHaveBeenCalledWith('count'); + expect(buttons[1].classList.contains('active')).toBe(true); + expect(buttons[0].classList.contains('active')).toBe(false); + }); + + it('buttons have proper ARIA attributes', () => { + const nav = createMetricControl('count', () => {}); + const buttons = nav.querySelectorAll('button'); + expect(buttons[0].getAttribute('role')).toBe('tab'); + expect(buttons[0].getAttribute('aria-selected')).toBe('false'); + expect(buttons[1].getAttribute('role')).toBe('tab'); + expect(buttons[1].getAttribute('aria-selected')).toBe('true'); + }); +});