import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import '../../../__tests__/test-utils.js'; import './usa-button.js'; import type { USAButton } from './usa-button.js'; import type { ButtonVariant, ButtonType } from '../../types/index.js'; import { testComponentAccessibility, USWDS_A11Y_CONFIG, } from '../../../__tests__/accessibility-utils.js'; import { quickUSWDSComplianceTest } from '../../../__tests__/uswds-compliance-utils.js'; import { validateComponentJavaScript } from '../../../__tests__/test-utils.js'; import { testKeyboardNavigation, testActivationKeys, verifyKeyboardOnlyUsable, } from '../../../__tests__/keyboard-navigation-utils.js'; import { testPointerAccessibility, testTargetSize, testLabelInName, } from '../../../__tests__/touch-pointer-utils.js'; // Performance testing utility (commented out - not currently used) // const measurePerformance = (fn: () => void): number => { // const start = performance.now(); // fn(); // return performance.now() - start; // }; describe('USAButton', () => { let element: USAButton; let container: HTMLDivElement; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { container?.remove(); }); describe('Component Initialization', () => { beforeEach(() => { element = document.createElement('usa-button') as USAButton; container.appendChild(element); }); it('should create button element', () => { expect(element).toBeInstanceOf(HTMLElement); expect(element.tagName).toBe('USA-BUTTON'); }); it('should have default properties', () => { expect(element.variant).toBe('primary'); expect(element.size).toBe('medium'); expect(element.disabled).toBe(false); expect(element.type).toBe('button'); expect(element.ariaPressed).toBeNull(); }); it('should render light DOM for USWDS compatibility', () => { expect(element.shadowRoot).toBeNull(); }); it('should render button element with proper role', async () => { await element.updateComplete; // Now we have a nested button element inside the host const buttonElement = element.querySelector('button'); expect(buttonElement).toBeTruthy(); expect(buttonElement?.getAttribute('type')).toBe('button'); }); it('should be focusable through button element', async () => { await element.updateComplete; // The nested button element should be focusable const buttonElement = element.querySelector('button'); expect(buttonElement).toBeTruthy(); expect(buttonElement?.tabIndex).toBeGreaterThanOrEqual(0); }); it('should render button element for content', async () => { await element.updateComplete; const button = element.querySelector('button'); expect(button).toBeTruthy(); }); }); describe('USWDS HTML Structure and Classes', () => { beforeEach(async () => { element = document.createElement('usa-button') as USAButton; container.appendChild(element); await element.updateComplete; }); it('should have usa-button class by default', async () => { await element.updateComplete; // Button element should have the USWDS class const buttonElement = element.querySelector('button'); expect(buttonElement?.classList.contains('usa-button')).toBe(true); }); it('should apply variant classes correctly', async () => { const variants: ButtonVariant[] = [ 'secondary', 'accent-cool', 'accent-warm', 'base', 'outline', 'inverse', ]; for (const variant of variants) { element.variant = variant; await element.updateComplete; // Button element should have the USWDS variant class const buttonElement = element.querySelector('button'); expect(buttonElement?.classList.contains(`usa-button--${variant}`)).toBe(true); } }); it('should not add variant class for primary', async () => { element.variant = 'primary'; await element.updateComplete; // Button element should have only base class for primary variant const buttonElement = element.querySelector('button'); expect(buttonElement?.className).toBe('usa-button'); expect(buttonElement?.classList.contains('usa-button--primary')).toBe(false); }); it('should apply size classes correctly', async () => { element.size = 'small'; await element.updateComplete; // Button element should have the USWDS size class const buttonElement = element.querySelector('button'); expect(buttonElement?.classList.contains('usa-button--small')).toBe(true); element.size = 'big'; await element.updateComplete; expect(buttonElement?.classList.contains('usa-button--big')).toBe(true); expect(buttonElement?.classList.contains('usa-button--small')).toBe(false); element.size = 'medium'; await element.updateComplete; expect(buttonElement?.classList.contains('usa-button--big')).toBe(false); expect(buttonElement?.classList.contains('usa-button--small')).toBe(false); }); it('should combine variant and size classes', async () => { element.variant = 'secondary'; element.size = 'big'; await element.updateComplete; // Button element should have combined USWDS classes const buttonElement = element.querySelector('button'); expect(buttonElement?.classList.contains('usa-button')).toBe(true); expect(buttonElement?.classList.contains('usa-button--secondary')).toBe(true); expect(buttonElement?.classList.contains('usa-button--big')).toBe(true); }); }); describe('Disabled State', () => { beforeEach(async () => { element = document.createElement('usa-button') as USAButton; element.textContent = 'Test Button'; container.appendChild(element); await element.updateComplete; }); it('should update disabled state on button element', async () => { await element.updateComplete; // Button element should reflect disabled state const buttonElement = element.querySelector('button'); expect(buttonElement?.hasAttribute('disabled')).toBe(false); element.disabled = true; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(true); element.disabled = false; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(false); }); it('should handle focus correctly based on disabled state', async () => { await element.updateComplete; // Button element should handle focus management const buttonElement = element.querySelector('button'); expect(buttonElement?.tabIndex).toBeGreaterThanOrEqual(0); element.disabled = true; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(true); element.disabled = false; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(false); }); it('should not trigger click when disabled', async () => { const clickSpy = vi.fn(); element.addEventListener('click', clickSpy); element.disabled = true; await element.updateComplete; // Disabled native button should not trigger click element.click(); expect(clickSpy).not.toHaveBeenCalled(); element.disabled = false; await element.updateComplete; element.click(); expect(clickSpy).toHaveBeenCalledTimes(2); // Native + forwarded event }); it('should reflect disabled property as attribute', async () => { const buttonElement = element.querySelector('button'); element.disabled = true; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(true); element.disabled = false; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(false); }); }); describe('Click Handling', () => { beforeEach(async () => { element = document.createElement('usa-button') as USAButton; element.textContent = 'Click Me'; container.appendChild(element); await element.updateComplete; }); it('should dispatch click event on host element click', async () => { await element.updateComplete; const clickSpy = vi.fn(); element.addEventListener('click', clickSpy); // Dispatch native click event on host element const nativeClickEvent = new MouseEvent('click', { bubbles: true }); element.dispatchEvent(nativeClickEvent); expect(clickSpy).toHaveBeenCalled(); }); it('should dispatch custom click event', () => { const clickSpy = vi.fn(); document.addEventListener('click', clickSpy); element.click(); expect(clickSpy).toHaveBeenCalled(); const event = clickSpy.mock.calls[0][0]; expect(event.bubbles).toBe(true); expect(event.cancelable).toBe(true); document.removeEventListener('click', clickSpy); }); it('should handle programmatic click()', () => { const clickSpy = vi.fn(); element.addEventListener('click', clickSpy); element.click(); expect(clickSpy).toHaveBeenCalledTimes(2); // Native + forwarded event }); it('should not handle clicks when disabled', async () => { const clickSpy = vi.fn(); element.addEventListener('click', clickSpy); element.disabled = true; await element.updateComplete; element.click(); expect(clickSpy).not.toHaveBeenCalled(); }); }); describe('Keyboard Navigation', () => { beforeEach(async () => { element = document.createElement('usa-button') as USAButton; element.textContent = 'Keyboard Button'; container.appendChild(element); await element.updateComplete; }); it('should handle keyboard events through button element', async () => { await element.updateComplete; // Button element should act like a button const buttonElement = element.querySelector('button'); expect(buttonElement?.getAttribute('type')).toBe('button'); // Verify button is focusable expect(buttonElement?.tabIndex).toBeGreaterThanOrEqual(0); }); it('should handle focus and blur events', async () => { await element.updateComplete; // Button element should be focusable const buttonElement = element.querySelector('button'); expect(buttonElement?.tabIndex).toBeGreaterThanOrEqual(0); // Should have proper type attribute expect(buttonElement?.getAttribute('type')).toBe('button'); }); it('should not trigger on other keys', async () => { await element.updateComplete; const clickSpy = vi.fn(); element.addEventListener('click', clickSpy); // Dispatch Tab key event on button element const buttonElement = element.querySelector('button'); const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' }); buttonElement?.dispatchEvent(tabEvent); expect(clickSpy).not.toHaveBeenCalled(); }); it('should not trigger when disabled', async () => { const clickSpy = vi.fn(); element.addEventListener('click', clickSpy); element.disabled = true; await element.updateComplete; // Dispatch Enter key event on button element const buttonElement = element.querySelector('button'); const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); buttonElement?.dispatchEvent(enterEvent); expect(clickSpy).not.toHaveBeenCalled(); }); }); describe('Form Integration', () => { let form: HTMLFormElement; beforeEach(() => { form = document.createElement('form'); container.appendChild(form); }); it('should submit form when type="submit"', async () => { element = document.createElement('usa-button') as USAButton; element.type = 'submit'; element.textContent = 'Submit'; form.appendChild(element); await element.updateComplete; const submitSpy = vi.fn((e) => e.preventDefault()); form.addEventListener('submit', submitSpy); // Click the actual button element to trigger form submission const buttonElement = element.querySelector('button'); buttonElement?.click(); expect(submitSpy).toHaveBeenCalled(); }); it('should reset form when type="reset"', async () => { const input = document.createElement('input'); input.type = 'text'; input.value = 'test value'; input.defaultValue = 'default value'; form.appendChild(input); element = document.createElement('usa-button') as USAButton; element.type = 'reset'; element.textContent = 'Reset'; form.appendChild(element); await element.updateComplete; // Verify initial state expect(input.value).toBe('test value'); // Click the actual button element to trigger form reset const buttonElement = element.querySelector('button'); buttonElement?.click(); // Check if the form was actually reset by checking input value expect(input.value).toBe('default value'); }); it('should not interact with form when type="button"', () => { element = document.createElement('usa-button') as USAButton; element.type = 'button'; element.textContent = 'Button'; form.appendChild(element); const submitSpy = vi.fn((e) => e.preventDefault()); const resetSpy = vi.spyOn(form, 'reset'); form.addEventListener('submit', submitSpy); element.click(); expect(submitSpy).not.toHaveBeenCalled(); expect(resetSpy).not.toHaveBeenCalled(); }); it('should handle form submission outside of form', () => { element = document.createElement('usa-button') as USAButton; element.type = 'submit'; element.textContent = 'Submit'; container.appendChild(element); // Should not throw when no form is found expect(() => element.click()).not.toThrow(); }); }); describe('ARIA Attributes', () => { beforeEach(async () => { element = document.createElement('usa-button') as USAButton; container.appendChild(element); await element.updateComplete; }); it('should set aria-pressed on button element when provided', async () => { await element.updateComplete; // When ariaPressed is null, attribute should not be set const buttonElement = element.querySelector('button'); expect(buttonElement?.hasAttribute('aria-pressed')).toBe(false); element.ariaPressed = 'true'; await element.updateComplete; expect(buttonElement?.getAttribute('aria-pressed')).toBe('true'); element.ariaPressed = 'false'; await element.updateComplete; expect(buttonElement?.getAttribute('aria-pressed')).toBe('false'); element.ariaPressed = null; await element.updateComplete; expect(buttonElement?.hasAttribute('aria-pressed')).toBe(false); }); it('should maintain disabled state on button element', async () => { await element.updateComplete; // Button element should reflect disabled state const buttonElement = element.querySelector('button'); expect(buttonElement?.hasAttribute('disabled')).toBe(false); element.disabled = true; await element.updateComplete; expect(buttonElement?.hasAttribute('disabled')).toBe(true); }); it('should have correct type attribute on button element', async () => { await element.updateComplete; // Button element should have button attributes (role is implicit for