import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import './usa-time-picker.ts'; import type { USATimePicker } from './usa-time-picker.js'; import { waitForUpdate, testPropertyChanges, assertAccessibilityAttributes, assertDOMStructure, validateComponentJavaScript, runUSWDSTransformationTests, } from '../../../__tests__/test-utils.js'; import { testComponentAccessibility, USWDS_A11Y_CONFIG, } from '../../../__tests__/accessibility-utils.js'; describe('USATimePicker', () => { let element: USATimePicker; beforeEach(() => { element = document.createElement('usa-time-picker') as USATimePicker; document.body.appendChild(element); }); afterEach(async () => { // Wait for any pending async operations to complete before cleanup // Time picker initialization includes async combo-box transformation await new Promise(resolve => setTimeout(resolve, 50)); element.remove(); }); describe('Basic Functionality', () => { it('should create and render correctly', async () => { await waitForUpdate(element); expect(element).toBeTruthy(); expect(element.tagName).toBe('USA-TIME-PICKER'); }); // NOTE: Default properties test moved to Cypress (cypress/e2e/time-picker-interactions.cy.ts) // Requires USWDS-enhanced combo-box elements in browser environment it('should render correct DOM structure', async () => { await waitForUpdate(element); // Give USWDS time to initialize and transform the DOM await new Promise(resolve => setTimeout(resolve, 100)); assertDOMStructure(element, '.usa-form-group', 1, 'Should have form group container'); assertDOMStructure(element, '.usa-label', 1, 'Should have label element'); assertDOMStructure(element, '.usa-combo-box', 1, 'Should have combo box container'); assertDOMStructure(element, '.usa-combo-box__input', 1, 'Should have input element'); assertDOMStructure(element, '.usa-combo-box__toggle-list', 1, 'Should have toggle button'); }); }); describe('Properties', () => { it('should handle value changes', async () => { await testPropertyChanges(element, 'value', ['09:00', '14:30', '23:59'], (el, value) => { expect(el.value).toBe(value); }); }); it('should handle label changes', async () => { await testPropertyChanges( element, 'label', ['Select time', 'Appointment time', 'Meeting time'], async (el, value) => { await waitForUpdate(el); const label = el.querySelector('.usa-label'); expect(label?.textContent?.trim()).toContain(value); } ); }); it('should handle hint text changes', async () => { await testPropertyChanges( element, 'hint', ['', 'Select a time', 'Choose your preferred time'], async (el, value) => { await waitForUpdate(el); const hint = el.querySelector('.usa-hint'); if (value) { expect(hint?.textContent?.trim()).toBe(value); } else { expect(hint).toBeNull(); } } ); }); // NOTE: Placeholder changes test moved to Cypress (cypress/e2e/time-picker-interactions.cy.ts) // Requires USWDS-enhanced combo-box input element in browser environment it('should handle disabled state', async () => { element.disabled = true; await waitForUpdate(element); // Give USWDS time to initialize and transform the DOM await new Promise(resolve => setTimeout(resolve, 100)); const input = element.querySelector('.usa-combo-box__input') as HTMLInputElement; const toggle = element.querySelector('.usa-combo-box__toggle-list') as HTMLButtonElement; expect(input.disabled).toBe(true); expect(toggle.disabled).toBe(true); }); it('should handle required state', async () => { element.required = true; await waitForUpdate(element); // Give USWDS time to initialize and transform the DOM await new Promise(resolve => setTimeout(resolve, 100)); const input = element.querySelector('.usa-combo-box__input') as HTMLInputElement; const formGroup = element.querySelector('.usa-form-group'); expect(input.required).toBe(true); expect(formGroup?.classList.contains('usa-form-group--required')).toBe(true); }); it('should handle step changes', async () => { await testPropertyChanges(element, 'step', ['15', '30', '60'], (el, value) => { expect(el.step).toBe(value); }); }); it('should handle minTime and maxTime', async () => { element.minTime = '09:00'; element.maxTime = '17:00'; await waitForUpdate(element); expect(element.minTime).toBe('09:00'); expect(element.maxTime).toBe('17:00'); }); }); describe('Time Options Generation', () => { it('should generate default time options', async () => { await waitForUpdate(element); // Time picker delegates option generation to USWDS // Test that the component has the required structure for USWDS to generate options const timePicker = element.querySelector('.usa-time-picker'); expect(timePicker).toBeTruthy(); expect(timePicker?.getAttribute('data-step')).toBe('30'); // default step // The USWDS library will generate options during initialization // In test environment, we validate the component provides the structure USWDS needs const input = element.querySelector('.usa-input'); expect(input).toBeTruthy(); }); it('should respect step intervals', async () => { element.step = '15'; await waitForUpdate(element); // Verify step attribute is set for USWDS to use const timePicker = element.querySelector('.usa-time-picker'); expect(timePicker?.getAttribute('data-step')).toBe('15'); // USWDS will use this step value to generate appropriate time intervals expect(element.step).toBe('15'); }); }); describe('Time Conversion', () => { it('should parse time strings correctly', async () => { await waitForUpdate(element); // Test time parsing logic inline since parseTimeString method was removed const parseTime = (timeStr: string) => { if (!timeStr) return 0; const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; }; expect(parseTime('09:00')).toBe(540); // 9 * 60 expect(parseTime('15:30')).toBe(930); // 15 * 60 + 30 expect(parseTime('00:00')).toBe(0); expect(parseTime('')).toBe(0); }); }); // NOTE: Keyboard navigation tests moved to Cypress (cypress/e2e/time-picker-interactions.cy.ts) // These tests depend on USWDS-transformed DOM elements that require browser environment // NOTE: Filtering, event handling, accessibility, form integration, and performance tests // were removed because they tested internal methods (getFilteredOptions, generateTimeOptions, selectOption) // that no longer exist. The component now delegates these responsibilities to USWDS. // USWDS handles filtering, option generation, and event management - our component's job is // to render the correct structure and pass the right attributes (tested above). // CRITICAL TESTS - Component Lifecycle Stability (Auto-dismiss Prevention) describe('Component Lifecycle Stability (CRITICAL)', () => { it('should remain in DOM after property updates (not auto-dismiss)', async () => { element.value = '09:00'; element.label = 'Test Time'; element.hint = 'Select a time'; element.required = true; element.disabled = false; element.minTime = '08:00'; element.maxTime = '18:00'; element.step = '15'; await element.updateComplete; expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); it('should maintain component state during rapid property changes', async () => { const initialParent = element.parentNode; // Rapid property changes that could trigger lifecycle issues for (let i = 0; i < 10; i++) { element.value = `${9 + (i % 8)}:${(i * 5) % 60}`.padEnd(5, '0'); element.label = `Time ${i}`; element.step = i % 2 === 0 ? '15' : '30'; element.required = i % 3 === 0; element.disabled = i % 4 === 0; await element.updateComplete; } expect(element.parentNode).toBe(initialParent); expect(element.isConnected).toBe(true); }); it('should handle complex time picker state changes without disconnection', async () => { // Complex state changes including dropdown operations element.isOpen = true; element.activeIndex = 5; element.filterText = '9:00'; element.value = '09:30'; element.minTime = '08:00'; element.maxTime = '17:00'; element.step = '15'; await element.updateComplete; // Toggle dropdown state element.isOpen = false; element.activeIndex = -1; element.filterText = ''; await element.updateComplete; expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); }); // NOTE: Event system stability and dropdown lifecycle tests were removed because they tested // internal methods (getFilteredOptions, generateTimeOptions) that no longer exist. // Component lifecycle stability is already tested in the "Component Lifecycle Stability" section above. describe('Accessibility Compliance (CRITICAL)', () => { it('should pass comprehensive accessibility tests (same as Storybook)', async () => { // Use larger time steps to reduce memory usage (4 hour intervals instead of 30 min) // This reduces options from 48 to 6, significantly reducing memory footprint element.step = '240'; // 4 hours element.minTime = '08:00'; element.maxTime = '17:00'; // Test default time picker element.label = 'Appointment Time'; element.value = '09:00'; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); // Test with hint text element.hint = 'Please select your preferred time'; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); // Test required state element.required = true; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); // Test disabled state element.disabled = true; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); // Test readonly state element.disabled = false; element.readonly = true; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); }); it('should maintain accessibility during dropdown interactions', async () => { // Use memory-efficient settings element.step = '240'; // 4 hours element.minTime = '08:00'; element.maxTime = '16:00'; element.label = 'Meeting Time'; element.value = '12:00'; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); // Test with dropdown open (minimal testing to prevent memory issues) element.isOpen = true; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); }); it('should be accessible in form contexts', async () => { const form = document.createElement('form'); const fieldset = document.createElement('fieldset'); const legend = document.createElement('legend'); legend.textContent = 'Schedule Information'; fieldset.appendChild(legend); fieldset.appendChild(element); form.appendChild(fieldset); document.body.appendChild(form); // Use memory-efficient settings element.step = '240'; // 4 hours element.minTime = '08:00'; element.maxTime = '16:00'; element.label = 'Start Time'; element.value = '12:00'; element.hint = 'Select your preferred start time'; element.required = true; await element.updateComplete; await testComponentAccessibility(form, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); form.remove(); }); }); // NOTE: USWDS DOM transformation tests were removed because they tested USWDS's transformation // process, not our component. The component's responsibility is to render the minimal structure // that USWDS needs (tested in "Basic Functionality" and "USWDS Integration Requirements" above). // USWDS's transformation behavior is tested in USWDS's own test suite. /** * USWDS Integration Requirements Tests * * These tests validate critical USWDS integration patterns for the Time Picker component. * They ensure that USWDS JavaScript can properly enhance the component with: * - Default value handling via data-default-value attribute * - Component enhancement signal via data-enhanced attribute * - Placeholder display behavior * * These tests prevent regressions like: * - Default times not displaying * - Placeholder not showing when no value * - USWDS initialization failures * * See: /tmp/combo-box-complete-summary.md for pattern details */ describe('USWDS Integration Requirements', () => { it('should include data-default-value attribute on input element', async () => { element.value = '14:30'; await waitForUpdate(element); const input = element.querySelector('input') as HTMLInputElement; expect(input).toBeTruthy(); expect(input?.hasAttribute('data-default-value')).toBe(true); expect(input?.getAttribute('data-default-value')).toBe('14:30'); }); it('should include data-default-value empty string when no value', async () => { element.value = ''; await waitForUpdate(element); const input = element.querySelector('input') as HTMLInputElement; expect(input).toBeTruthy(); expect(input?.hasAttribute('data-default-value')).toBe(true); expect(input?.getAttribute('data-default-value')).toBe(''); }); it('should include data-enhanced="false" on time picker wrapper', async () => { await waitForUpdate(element); const wrapper = element.querySelector('.usa-time-picker'); expect(wrapper).toBeTruthy(); expect(wrapper?.getAttribute('data-enhanced')).toBe('false'); }); it('should render placeholder when set', async () => { element.placeholder = 'Select a time'; await waitForUpdate(element); const input = element.querySelector('input') as HTMLInputElement; expect(input).toBeTruthy(); expect(input?.getAttribute('placeholder')).toBe('Select a time'); }); it('should display placeholder when no value set', async () => { element.placeholder = 'hh:mm'; element.value = ''; await waitForUpdate(element); const input = element.querySelector('input') as HTMLInputElement; expect(input).toBeTruthy(); expect(input?.getAttribute('placeholder')).toBe('hh:mm'); expect(input?.value).toBe(''); }); it('should maintain data-default-value when value changes', async () => { element.value = '09:00'; await waitForUpdate(element); let input = element.querySelector('input') as HTMLInputElement; expect(input?.getAttribute('data-default-value')).toBe('09:00'); element.value = '17:30'; await waitForUpdate(element); input = element.querySelector('input') as HTMLInputElement; expect(input?.getAttribute('data-default-value')).toBe('17:30'); }); }); describe('Regression Prevention: USWDS Behavior Patterns (2025-10-08)', () => { // Prevents regression of bugs found in combo box and date picker components // These patterns apply to all components using USWDS-mirrored behavior files it('should render correct DOM structure for USWDS behavior enhancement', async () => { // Validates that the component renders the structure needed by usa-time-picker-behavior.ts element.label = 'Test time'; element.value = '09:00'; await waitForUpdate(element); // Structure needed for USWDS mirrored behavior: // 1. Container with .usa-time-picker class const container = element.querySelector('.usa-time-picker'); expect(container).toBeTruthy(); // 2. Input element const input = element.querySelector('input'); expect(input).toBeTruthy(); expect(input?.classList.contains('usa-input')).toBe(true); // 3. data-enhanced attribute on container expect(container?.hasAttribute('data-enhanced')).toBe(true); // 4. data-default-value on input expect(input?.hasAttribute('data-default-value')).toBe(true); // This structure allows usa-time-picker-behavior.ts to: // - Find the container via TIME_PICKER selector // - Create and show/hide the dropdown with proper attribute manipulation // - Handle time selection and updates }); it('should maintain data-enhanced as string type', async () => { // CRITICAL: data-enhanced must be a string, not boolean // Pattern found in combo box Bug #1 element.value = '09:00'; await waitForUpdate(element); const wrapper = element.querySelector('.usa-time-picker'); // Verify it's a string (starts as "false", may become "true" after enhancement) expect(typeof wrapper?.getAttribute('data-enhanced')).toBe('string'); // Verify it's not null or undefined expect(wrapper?.hasAttribute('data-enhanced')).toBe(true); }); it('should have data-default-value attribute for USWDS initialization', async () => { // CRITICAL: data-default-value is required for USWDS to set initial value // Pattern documented in USWDS_INITIAL_VALUE_PATTERN.md element.value = '14:30'; await waitForUpdate(element); const input = element.querySelector('input') as HTMLInputElement; expect(input).toBeTruthy(); // Must have data-default-value attribute expect(input?.hasAttribute('data-default-value')).toBe(true); expect(input?.getAttribute('data-default-value')).toBe('14:30'); }); }); });