import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import './usa-table.ts'; import type { USATable } from './usa-table.js'; import { testComponentAccessibility, USWDS_A11Y_CONFIG, } from '../../../__tests__/accessibility-utils.js'; import { validateComponentJavaScript } from '../../../__tests__/test-utils.js'; import { testTextResize, testReflow, testTextSpacing, testMobileAccessibility, } from '../../../__tests__/responsive-accessibility-utils.js'; // Helper function to wait for USWDS initialization - MUST be at top level for all tests const waitForUSWDS = async (el: USATable) => { await el.updateComplete; // Wait for firstUpdated() to complete (includes requestAnimationFrame and USWDS init) await new Promise(resolve => setTimeout(resolve, 100)); }; describe('USATable', () => { let element: USATable; beforeEach(() => { element = document.createElement('usa-table') as USATable; document.body.appendChild(element); }); afterEach(() => { element.remove(); }); describe('Default Properties', () => { it('should have correct default properties', async () => { await element.updateComplete; expect(element.caption).toBe(''); expect(element.headers).toEqual([]); expect(element.data).toEqual([]); expect(element.striped).toBe(false); expect(element.borderless).toBe(false); expect(element.compact).toBe(false); expect(element.stacked).toBe(false); expect(element.stackedHeader).toBe(false); expect(element.stickyHeader).toBe(false); expect(element.scrollable).toBe(false); expect(element.sortColumn).toBe(''); expect(element.sortDirection).toBe('asc'); }); }); describe('Basic Rendering', () => { it('should render a table element', async () => { await element.updateComplete; const table = element.querySelector('table'); expect(table).toBeTruthy(); expect(table?.classList.contains('usa-table')).toBe(true); }); it('should render empty table with no data message', async () => { element.headers = [ { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, ]; await element.updateComplete; const emptyCell = element.querySelector('td[colspan="2"]'); expect(emptyCell).toBeTruthy(); expect(emptyCell?.textContent?.trim()).toBe('No data available'); }); it('should render caption when provided', async () => { element.caption = 'Employee Data'; await element.updateComplete; const caption = element.querySelector('caption'); expect(caption).toBeTruthy(); expect(caption?.textContent?.trim()).toBe('Employee Data'); }); it('should render default caption when not provided', async () => { await element.updateComplete; const caption = element.querySelector('caption'); expect(caption).toBeTruthy(); expect(caption?.classList.contains('usa-sr-only')).toBe(true); expect(caption?.textContent?.trim()).toBe('Data table'); }); }); describe('Column and Data Rendering', () => { beforeEach(() => { element.headers = [ { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, { key: 'department', label: 'Department' }, ]; element.data = [ { name: 'John Doe', email: 'john@example.gov', department: 'IT' }, { name: 'Jane Smith', email: 'jane@example.gov', department: 'HR' }, ]; }); it('should render table headers', async () => { await element.updateComplete; const headers = element.querySelectorAll('th[scope="col"]'); expect(headers.length).toBe(3); expect(headers[0].textContent?.trim()).toBe('Name'); expect(headers[1].textContent?.trim()).toBe('Email'); expect(headers[2].textContent?.trim()).toBe('Department'); }); it('should render table data', async () => { await element.updateComplete; const rows = element.querySelectorAll('tbody tr'); expect(rows.length).toBe(2); const firstRowCells = rows[0].querySelectorAll('th, td'); expect(firstRowCells[0].textContent?.trim()).toBe('John Doe'); expect(firstRowCells[1].textContent?.trim()).toBe('john@example.gov'); expect(firstRowCells[2].textContent?.trim()).toBe('IT'); }); it('should use first column as row header', async () => { await element.updateComplete; const rowHeaders = element.querySelectorAll('th[scope="row"]'); expect(rowHeaders.length).toBe(2); expect(rowHeaders[0].textContent?.trim()).toBe('John Doe'); expect(rowHeaders[1].textContent?.trim()).toBe('Jane Smith'); }); it('should add data-label attributes for responsive stacking', async () => { await element.updateComplete; const dataCells = element.querySelectorAll('td[data-label]'); expect(dataCells.length).toBe(4); // 2 rows × 2 data columns expect(dataCells[0].getAttribute('data-label')).toBe('Email'); expect(dataCells[1].getAttribute('data-label')).toBe('Department'); }); }); describe('Table Variants and Classes', () => { it('should apply striped class when striped is true', async () => { element.striped = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--striped')).toBe(true); }); it('should apply borderless class when borderless is true', async () => { element.borderless = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--borderless')).toBe(true); }); it('should apply compact class when compact is true', async () => { element.compact = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--compact')).toBe(true); }); it('should apply stacked class when stacked is true', async () => { element.stacked = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--stacked')).toBe(true); }); it('should apply stacked-header class when stackedHeader is true', async () => { element.stackedHeader = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--stacked-header')).toBe(true); }); it('should apply sticky-header class when stickyHeader is true', async () => { element.stickyHeader = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--sticky-header')).toBe(true); }); it('should apply multiple classes when multiple variants are true', async () => { element.striped = true; element.compact = true; element.borderless = true; await element.updateComplete; const table = element.querySelector('table'); expect(table?.classList.contains('usa-table--striped')).toBe(true); expect(table?.classList.contains('usa-table--compact')).toBe(true); expect(table?.classList.contains('usa-table--borderless')).toBe(true); }); }); describe('Scrollable Table', () => { it('should wrap table in scrollable container when scrollable is true', async () => { element.scrollable = true; await element.updateComplete; const container = element.querySelector('.usa-table-container--scrollable'); expect(container).toBeTruthy(); expect(container?.getAttribute('tabindex')).toBe('0'); expect(container?.getAttribute('role')).toBe('region'); }); it('should not wrap table when scrollable is false', async () => { element.scrollable = false; await element.updateComplete; const container = element.querySelector('.usa-table-container--scrollable'); expect(container).toBeNull(); }); }); describe('Sorting Functionality', () => { beforeEach(() => { element.headers = [ { key: 'name', label: 'Name', sortable: true, sortType: 'text' }, { key: 'age', label: 'Age', sortable: true, sortType: 'number' }, { key: 'email', label: 'Email', sortable: false }, ]; element.data = [ { name: 'John Doe', age: 30, email: 'john@example.gov' }, { name: 'Alice Brown', age: 25, email: 'alice@example.gov' }, { name: 'Bob Smith', age: 35, email: 'bob@example.gov' }, ]; }); it('should render sortable headers as clickable elements', async () => { await element.updateComplete; const sortableHeaders = element.querySelectorAll('th[data-sortable]'); expect(sortableHeaders.length).toBe(2); // Name and Age are sortable const nonSortableHeader = element.querySelector('th:nth-child(3)'); expect(nonSortableHeader?.textContent?.trim()).toBe('Email'); // No sortable attribute expect(nonSortableHeader?.hasAttribute('data-sortable')).toBe(false); }); it('should set appropriate ARIA attributes for sortable columns', async () => { await element.updateComplete; const sortableHeaders = element.querySelectorAll('th[data-sortable]'); expect(sortableHeaders.length).toBe(2); // Initially no sorting applied - aria-sort should NOT be set until after first sort // This matches USWDS behavior exactly (see usa-table-behavior.ts sortRows() line 157) sortableHeaders.forEach((header) => { const ariaSort = header.getAttribute('aria-sort'); expect(ariaSort).toBeNull(); // No attribute until sorting occurs }); }); it('should handle sort click and dispatch event', async () => { await waitForUSWDS(element); const sortEventSpy = vi.fn(); element.addEventListener('table-sort', sortEventSpy); const sortableHeader = element.querySelector('th[data-sortable]') as HTMLElement; const sortButton = sortableHeader?.querySelector('.usa-table__header__button') as HTMLElement; // Click button if it exists (USWDS created it), otherwise dispatch click event on header if (sortButton) { sortButton.click(); } else { // Dispatch a proper click event that bubbles const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); sortableHeader.dispatchEvent(clickEvent); } expect(sortEventSpy).toHaveBeenCalledOnce(); // Check the event detail const event = sortEventSpy.mock.calls[0][0] as CustomEvent; expect(event.detail).toEqual({ column: 'name', direction: 'asc', sortType: 'text', }); }); // Skipped in jsdom - requires Cypress for USWDS JavaScript interaction // Coverage: src/components/table/usa-table.component.cy.ts (comprehensive sorting tests) it('should update ARIA attributes after sorting', async () => { element.sortColumn = 'name'; element.sortDirection = 'asc'; await element.updateComplete; const sortedHeader = element.querySelector('th[scope="col"]'); expect(sortedHeader?.getAttribute('aria-sort')).toBe('ascending'); element.sortDirection = 'desc'; await element.updateComplete; expect(sortedHeader?.getAttribute('aria-sort')).toBe('descending'); }); it('should sort text data correctly', async () => { await waitForUSWDS(element); const sortableHeader = element.querySelector('th[data-sortable]') as HTMLElement; const sortButton = sortableHeader?.querySelector('.usa-table__header__button') as HTMLElement; // Dispatch click event properly if (sortButton) { sortButton.click(); } else { const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); sortableHeader.dispatchEvent(clickEvent); } // Wait for sort to complete await element.updateComplete; // Verify data array is sorted correctly (test the sorting logic, not DOM reactivity) // Light DOM has known reactivity limitations with Lit expect(element.data[0].name).toBe('Alice Brown'); // Should be first alphabetically expect(element.data[1].name).toBe('Bob Smith'); expect(element.data[2].name).toBe('John Doe'); }); it('should sort numeric data correctly', async () => { await waitForUSWDS(element); const ageSortableHeader = element.querySelectorAll('th[data-sortable]')[1] as HTMLElement; const ageButton = ageSortableHeader?.querySelector('.usa-table__header__button') as HTMLElement; // Dispatch click event properly if (ageButton) { ageButton.click(); } else { const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); ageSortableHeader.dispatchEvent(clickEvent); } // Wait for sort to complete await element.updateComplete; // Verify data array is sorted correctly by age (test the sorting logic, not DOM reactivity) // Light DOM has known reactivity limitations with Lit expect(element.data[0].age).toBe(25); // Alice Brown - youngest expect(element.data[1].age).toBe(30); // John Doe expect(element.data[2].age).toBe(35); // Bob Smith - oldest }); }); describe('Data Type Formatting', () => { beforeEach(() => { element.headers = [ { key: 'percentage', label: 'Completion', sortType: 'percentage' }, { key: 'date', label: 'Due Date', sortType: 'date' }, { key: 'count', label: 'Count', sortType: 'number' }, ]; }); it('should format percentage values', async () => { element.data = [{ percentage: 85.5, date: '2024-01-15', count: 42 }]; await element.updateComplete; const firstRow = element.querySelector('tbody tr'); const percentageCell = firstRow?.querySelector('th[scope="row"]'); // First column is row header expect(percentageCell?.textContent?.trim()).toBe('85.5%'); }); it('should format date values from string', async () => { element.data = [{ percentage: 85, date: '2024-01-15', count: 42 }]; await element.updateComplete; const dateCell = element.querySelector('td[data-label="Due Date"]'); const dateValue = dateCell?.textContent?.trim(); expect(dateValue).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/); // MM/DD/YYYY format }); it('should format date values from Date object', async () => { const testDate = new Date('2024-01-15'); element.data = [{ percentage: 85, date: testDate, count: 42 }]; await element.updateComplete; const dateCell = element.querySelector('td[data-label="Due Date"]'); const dateValue = dateCell?.textContent?.trim(); expect(dateValue).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/); }); it('should handle number formatting', async () => { element.data = [{ percentage: 85, date: '2024-01-15', count: 42 }]; await element.updateComplete; const countCell = element.querySelector('td[data-label="Count"]'); expect(countCell?.textContent?.trim()).toBe('42'); }); }); describe('Sticky Header', () => { it('should apply sticky header class when stickyHeader is enabled', async () => { element.stickyHeader = true; element.headers = [ { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, ]; element.data = [{ name: 'John Doe', email: 'john@example.gov' }]; await element.updateComplete; const stickyTable = element.querySelector('.usa-table--sticky-header'); expect(stickyTable).toBeTruthy(); }); }); describe('Screen Reader Announcements', () => { it('should have announcement region for sort feedback', async () => { await element.updateComplete; const announcement = element.querySelector('.usa-table__announcement-region'); expect(announcement).toBeTruthy(); expect(announcement?.getAttribute('aria-live')).toBe('polite'); }); // NOTE: Announcement content test is covered in Cypress (usa-table-announcements.component.cy.ts) // where it works reliably in a real browser environment. Vitest has timing issues with Light DOM // rendering and USWDS behavior updates that make this test flaky. it('should clear announcement when no sort is applied', async () => { element.sortColumn = ''; await element.updateComplete; const announcement = element.querySelector('.usa-table__announcement-region'); expect(announcement?.textContent?.trim()).toBe(''); }); }); describe('Accessibility Features', () => { beforeEach(() => { element.headers = [ { key: 'name', label: 'Name', sortable: true }, { key: 'email', label: 'Email' }, ]; element.data = [{ name: 'John Doe', email: 'john@example.gov' }]; }); it('should have proper table roles', async () => { await element.updateComplete; const table = element.querySelector('table'); expect(table?.getAttribute('role')).toBe('table'); const columnHeaders = element.querySelectorAll('th[scope="col"]'); columnHeaders.forEach((header) => { expect(header.getAttribute('role')).toBe('columnheader'); }); const rowHeaders = element.querySelectorAll('th[scope="row"]'); rowHeaders.forEach((header) => { expect(header.getAttribute('role')).toBe('rowheader'); }); }); it('should have accessible sortable headers', async () => { await waitForUSWDS(element); const sortableHeader = element.querySelector('th[data-sortable]') as HTMLElement; const sortButton = sortableHeader?.querySelector('.usa-table__header__button') as HTMLElement; // If USWDS has created the button, it should have tabindex='0' // Otherwise, the header itself should be accessible if (sortButton) { expect(sortButton.getAttribute('tabindex')).toBe('0'); } // USWDS adds 'usa-table__header--sortable' class after initialization expect(sortableHeader).toBeTruthy(); expect(sortableHeader?.textContent?.trim()).toContain('Name'); // After sorting, aria-sort should be set element.sortColumn = 'name'; element.sortDirection = 'asc'; await element.updateComplete; const updatedAriaSort = sortableHeader?.getAttribute('aria-sort'); expect(updatedAriaSort).toBe('ascending'); }); it('should provide proper scope attributes', async () => { await element.updateComplete; const columnHeaders = element.querySelectorAll('th[scope="col"]'); expect(columnHeaders.length).toBe(2); const rowHeaders = element.querySelectorAll('th[scope="row"]'); expect(rowHeaders.length).toBe(1); }); }); describe('Edge Cases and Error Handling', () => { it('should handle empty columns array', async () => { element.headers = []; element.data = []; // When columns are empty, treat as no data scenario await element.updateComplete; const thead = element.querySelector('thead'); expect(thead).toBeNull(); const emptyMessage = element.querySelector('tbody tr td'); expect(emptyMessage?.textContent?.trim()).toBe('No data available'); expect(emptyMessage?.getAttribute('colspan')).toBe('1'); }); it('should handle mismatched data and columns', async () => { element.headers = [ { key: 'name', label: 'Name' }, { key: 'missing', label: 'Missing Field' }, ]; element.data = [{ name: 'John Doe' }]; // missing 'missing' field await element.updateComplete; const cells = element.querySelectorAll('tbody td, tbody th'); expect(cells[1].textContent?.trim()).toBe('undefined'); // Missing field shows as 'undefined' }); it('should handle sort on non-sortable column gracefully', async () => { element.headers = [{ key: 'name', label: 'Name', sortable: false }]; element.data = [{ name: 'John' }]; await element.updateComplete; const sortEventSpy = vi.fn(); element.addEventListener('table-sort', sortEventSpy); // Try to trigger sort on non-sortable column (should not work) const headerCell = element.querySelector('th'); const clickEvent = new Event('click'); headerCell?.dispatchEvent(clickEvent); expect(sortEventSpy).not.toHaveBeenCalled(); }); it('should handle invalid date values', async () => { element.headers = [{ key: 'date', label: 'Date', sortType: 'date' }]; element.data = [{ date: 'invalid-date' }]; await element.updateComplete; const firstRow = element.querySelector('tbody tr'); const dateCell = firstRow?.querySelector('th[scope="row"]'); // First column becomes row header expect(dateCell?.textContent?.trim()).toBe('Invalid Date'); }); it('should handle invalid numeric values', async () => { element.headers = [{ key: 'number', label: 'Number', sortType: 'number' }]; element.data = [{ number: 'not-a-number' }]; await element.updateComplete; const firstRow = element.querySelector('tbody tr'); const numberCell = firstRow?.querySelector('th[scope="row"]'); // First column becomes row header expect(numberCell?.textContent?.trim()).toBe('not-a-number'); // Shows as string }); }); describe('Comprehensive Slotted Content Validation', () => { beforeEach(() => { element.headers = [ { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' } ]; }); it('should render custom empty state content', async () => { const customEmptyMessage = document.createElement('div'); customEmptyMessage.slot = 'empty'; customEmptyMessage.className = 'test-empty-message'; customEmptyMessage.textContent = 'Custom no data message'; element.appendChild(customEmptyMessage); element.data = []; await element.updateComplete; const slotContent = element.querySelector('[slot="empty"]'); expect(slotContent?.textContent).toBe('Custom no data message'); // Verify slot renders in table cell const emptyCell = element.querySelector('tbody td'); expect(emptyCell).toBeTruthy(); }); it('should render complex empty state with actions', async () => { const emptyState = document.createElement('div'); emptyState.slot = 'empty'; emptyState.innerHTML = `

No Data Available

Get started by adding your first item.

`; element.appendChild(emptyState); element.data = []; await element.updateComplete; expect(element.querySelector('.test-empty-state')).toBeTruthy(); expect(element.querySelector('.test-empty-state h3')).toBeTruthy(); expect(element.querySelector('.test-empty-state button')).toBeTruthy(); }); it('should render additional table content via default slot', async () => { const customContent = document.createElement('tfoot'); customContent.className = 'test-custom-footer'; customContent.innerHTML = 'Custom footer'; element.appendChild(customContent); element.data = []; await element.updateComplete; const tfoot = element.querySelector('tfoot.test-custom-footer'); expect(tfoot).toBeTruthy(); expect(tfoot?.textContent?.trim()).toBe('Custom footer'); }); it('should support caption via default slot', async () => { const caption = document.createElement('caption'); caption.className = 'test-table-caption'; caption.textContent = 'User Data Table'; element.appendChild(caption); element.data = [{ name: 'John', email: 'john@example.com' }]; await element.updateComplete; const renderedCaption = element.querySelector('caption.test-table-caption'); expect(renderedCaption).toBeTruthy(); expect(renderedCaption?.textContent).toBe('User Data Table'); }); it('should handle multiple slotted elements together', async () => { // Add caption const caption = document.createElement('caption'); caption.className = 'multi-caption'; caption.textContent = 'Data Table'; // Add custom footer const footer = document.createElement('tfoot'); footer.className = 'multi-footer'; footer.innerHTML = 'Total: 0 items'; element.appendChild(caption); element.appendChild(footer); element.data = []; await element.updateComplete; expect(element.querySelector('.multi-caption')).toBeTruthy(); expect(element.querySelector('.multi-footer')).toBeTruthy(); }); it('should maintain slotted content when data changes', async () => { const footer = document.createElement('tfoot'); footer.className = 'persistent-footer'; footer.innerHTML = 'Footer'; element.appendChild(footer); // Start with empty data element.data = []; await element.updateComplete; expect(element.querySelector('.persistent-footer')).toBeTruthy(); // Add data element.data = [{ name: 'John', email: 'john@example.com' }]; await element.updateComplete; expect(element.querySelector('.persistent-footer')).toBeTruthy(); // Clear data again element.data = []; await element.updateComplete; expect(element.querySelector('.persistent-footer')).toBeTruthy(); }); it('should not show empty slot when data exists', async () => { const emptyMessage = document.createElement('div'); emptyMessage.slot = 'empty'; emptyMessage.className = 'should-not-show'; emptyMessage.textContent = 'No data'; element.appendChild(emptyMessage); // With data, empty slot should not be visible element.data = [{ name: 'John', email: 'john@example.com' }]; await element.updateComplete; // Slot element exists in DOM but shouldn't be rendered in table const slotElement = element.querySelector('[slot="empty"]'); expect(slotElement).toBeTruthy(); // Element exists // But it shouldn't be visible in table body const tableBody = element.querySelector('tbody'); // The slot might be present but not actually rendered in tbody expect(tableBody?.querySelectorAll('tr').length).toBeGreaterThan(0); }); it('should support complex footer with totals and summaries', async () => { const footer = document.createElement('tfoot'); footer.className = 'test-summary-footer'; footer.innerHTML = ` Total Users: 3 `; element.appendChild(footer); element.data = [ { name: 'User 1', email: 'user1@example.com' }, { name: 'User 2', email: 'user2@example.com' }, { name: 'User 3', email: 'user3@example.com' } ]; await element.updateComplete; const summaryFooter = element.querySelector('.test-summary-footer'); expect(summaryFooter).toBeTruthy(); expect(summaryFooter?.querySelectorAll('tr').length).toBe(2); expect(summaryFooter?.querySelector('button')).toBeTruthy(); }); }); describe('Light DOM Rendering', () => { it('should render in light DOM for USWDS compatibility', async () => { await element.updateComplete; expect(element.shadowRoot).toBeNull(); expect(element.querySelector('table')).toBeTruthy(); }); }); describe('Property Updates and Re-rendering', () => { it('should re-render when data changes', async () => { element.headers = [{ key: 'name', label: 'Name' }]; element.data = [{ name: 'John' }]; await element.updateComplete; let rows = element.querySelectorAll('tbody tr'); expect(rows.length).toBe(1); element.data = [{ name: 'John' }, { name: 'Jane' }]; await element.updateComplete; rows = element.querySelectorAll('tbody tr'); expect(rows.length).toBe(2); }); it('should re-render when columns change', async () => { element.headers = [{ key: 'name', label: 'Name' }]; await element.updateComplete; let headers = element.querySelectorAll('th[scope="col"]'); expect(headers.length).toBe(1); element.headers = [ { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, ]; await element.updateComplete; headers = element.querySelectorAll('th[scope="col"]'); expect(headers.length).toBe(2); }); it('should re-render when table variant properties change', async () => { await element.updateComplete; let table = element.querySelector('table'); expect(table?.classList.contains('usa-table--striped')).toBe(false); element.striped = true; await element.updateComplete; table = element.querySelector('table'); expect(table?.classList.contains('usa-table--striped')).toBe(true); }); }); describe('Performance Considerations', () => { it('should handle large datasets efficiently', async () => { const largeDataset = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `User ${i}`, email: `user${i}@example.gov`, })); element.headers = [ { key: 'id', label: 'ID' }, { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, ]; element.data = largeDataset; const startTime = performance.now(); await element.updateComplete; const endTime = performance.now(); const rows = element.querySelectorAll('tbody tr'); expect(rows.length).toBe(1000); // Should complete rendering within reasonable time (5 seconds for large dataset in test environment) expect(endTime - startTime).toBeLessThan(5000); }); }); // 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.caption = 'Test Table'; element.headers = [ { key: 'id', label: 'ID', sortable: true }, { key: 'name', label: 'Name', sortable: true }, { key: 'status', label: 'Status', sortable: false }, ]; element.data = [ { id: 1, name: 'John Doe', status: 'Active' }, { id: 2, name: 'Jane Smith', status: 'Inactive' }, { id: 3, name: 'Bob Johnson', status: 'Pending' }, ]; element.striped = true; element.compact = false; element.borderless = false; element.stickyHeader = true; element.scrollable = true; 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.caption = `Table ${i}`; element.striped = i % 2 === 0; element.compact = i % 3 === 0; element.borderless = i % 4 === 0; element.stacked = i % 5 === 0; element.stickyHeader = i % 2 === 1; element.scrollable = i % 3 === 1; element.headers = [ { key: 'col1', label: `Column ${i}A`, sortable: i % 2 === 0 }, { key: 'col2', label: `Column ${i}B`, sortable: i % 3 === 0 }, ]; element.data = [ { col1: `Data ${i}1`, col2: `Data ${i}2` }, { col1: `Data ${i}3`, col2: `Data ${i}4` }, ]; await element.updateComplete; } expect(element.parentNode).toBe(initialParent); expect(element.isConnected).toBe(true); }); it('should handle complex table operations without disconnection', async () => { // Complex table operations const complexData = Array.from({ length: 50 }, (_, i) => ({ id: i, name: `User ${i}`, department: i % 3 === 0 ? 'IT' : i % 3 === 1 ? 'HR' : 'Finance', salary: 50000 + i * 1000, active: i % 2 === 0, })); element.headers = [ { key: 'id', label: 'ID', sortable: true }, { key: 'name', label: 'Name', sortable: true }, { key: 'department', label: 'Department', sortable: true }, { key: 'salary', label: 'Salary', sortable: true }, { key: 'active', label: 'Status', sortable: false }, ]; element.data = complexData; await element.updateComplete; // Test sorting operations element.sortColumn = 'name'; element.sortDirection = 'desc'; await element.updateComplete; element.sortColumn = 'salary'; element.sortDirection = 'asc'; await element.updateComplete; // Test variant changes element.striped = true; element.compact = true; await element.updateComplete; expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); }); describe('Event System Stability (CRITICAL)', () => { it('should not interfere with other components after event handling', async () => { const eventsSpy = vi.fn(); element.addEventListener('table-sort', eventsSpy); element.headers = [ { key: 'name', label: 'Name', sortable: true }, { key: 'age', label: 'Age', sortable: true }, { key: 'city', label: 'City', sortable: false }, ]; element.data = [ { name: 'Alice', age: 30, city: 'New York' }, { name: 'Bob', age: 25, city: 'Boston' }, { name: 'Charlie', age: 35, city: 'Chicago' }, ]; await element.updateComplete; // Trigger sorting events const sortableHeaders = element.querySelectorAll( 'th[data-sortable]' ) as NodeListOf; const nameButton = sortableHeaders[0]?.querySelector('.usa-table__header__button') as HTMLElement; const ageButton = sortableHeaders[1]?.querySelector('.usa-table__header__button') as HTMLElement; nameButton?.click(); // Name header ageButton?.click(); // Age header expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); it('should handle rapid sorting operations without component removal', async () => { element.headers = [ { key: 'col1', label: 'Column 1', sortable: true }, { key: 'col2', label: 'Column 2', sortable: true }, { key: 'col3', label: 'Column 3', sortable: true }, ]; element.data = [ { col1: 'A', col2: 'X', col3: '1' }, { col1: 'B', col2: 'Y', col3: '2' }, { col1: 'C', col2: 'Z', col3: '3' }, ]; await element.updateComplete; const sortableHeaders = element.querySelectorAll( 'th[data-sortable]' ) as NodeListOf; // Rapid sorting simulation for (let i = 0; i < 20; i++) { const header = sortableHeaders[i % sortableHeaders.length]; header.click(); await element.updateComplete; } expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); it('should handle event pollution without component removal', async () => { // Create potential event pollution for (let i = 0; i < 20; i++) { const customEvent = new CustomEvent(`test-event-${i}`, { bubbles: true }); element.dispatchEvent(customEvent); } element.caption = 'Event Test Table'; element.headers = [{ key: 'test', label: 'Test Column', sortable: true }]; element.data = [{ test: 'Test Data 1' }, { test: 'Test Data 2' }]; await element.updateComplete; expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); }); describe('Data Rendering Stability (CRITICAL)', () => { it('should handle large dataset changes without disconnection', async () => { // Test large dataset operations const datasets = [ Array.from({ length: 100 }, (_, i) => ({ id: i, value: `Value ${i}` })), Array.from({ length: 500 }, (_, i) => ({ id: i, value: `Large ${i}` })), Array.from({ length: 10 }, (_, i) => ({ id: i, value: `Small ${i}` })), Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `Huge ${i}` })), ]; element.headers = [ { key: 'id', label: 'ID', sortable: true }, { key: 'value', label: 'Value', sortable: true }, ]; for (const dataset of datasets) { element.data = dataset; await element.updateComplete; // Verify table renders correctly const table = element.querySelector('table'); expect(table).toBeTruthy(); } expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); it('should handle complex column structure changes', async () => { // Test complex column configurations const columnConfigs = [ [ { key: 'name', label: 'Name', sortable: true }, { key: 'age', label: 'Age', sortable: true }, ], [ { key: 'id', label: 'ID', sortable: false }, { key: 'first', label: 'First Name', sortable: true }, { key: 'last', label: 'Last Name', sortable: true }, { key: 'email', label: 'Email', sortable: false }, ], [{ key: 'single', label: 'Single Column', sortable: true }], ]; const baseData = [ { name: 'John', age: 30, id: 1, first: 'John', last: 'Doe', email: 'john@gov', single: 'Data', }, { name: 'Jane', age: 25, id: 2, first: 'Jane', last: 'Smith', email: 'jane@gov', single: 'More', }, ]; for (const columns of columnConfigs) { element.headers = columns; element.data = baseData; await element.updateComplete; const headers = element.querySelectorAll('th[scope="col"]'); expect(headers.length).toBe(columns.length); } expect(document.body.contains(element)).toBe(true); expect(element.isConnected).toBe(true); }); }); describe('Accessibility Compliance (CRITICAL)', () => { it('should pass comprehensive accessibility tests (same as Storybook)', async () => { // Setup table with comprehensive test data element.caption = 'Employee Directory - Accessibility Testing'; element.headers = [ { key: 'name', label: 'Employee Name', sortable: true }, { key: 'position', label: 'Position Title', sortable: true }, { key: 'department', label: 'Department', sortable: false }, { key: 'email', label: 'Email Address', sortable: false }, ]; element.data = [ { name: 'Sarah Johnson', position: 'Software Engineer', department: 'Information Technology', email: 'sarah.johnson@example.gov', }, { name: 'Michael Rodriguez', position: 'HR Specialist', department: 'Human Resources', email: 'michael.rodriguez@example.gov', }, { name: 'Emily Chen', position: 'Budget Analyst', department: 'Finance', email: 'emily.chen@example.gov', }, ]; await element.updateComplete; // Run comprehensive accessibility audit await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); }); it('should pass accessibility tests with sorting enabled', async () => { element.headers = [ { key: 'id', label: 'Employee ID', sortable: true }, { key: 'name', label: 'Full Name', sortable: true }, { key: 'status', label: 'Employment Status', sortable: false }, ]; element.data = [ { id: 'EMP001', name: 'John Doe', status: 'Active' }, { id: 'EMP002', name: 'Jane Smith', status: 'On Leave' }, ]; element.sortColumn = 'name'; element.sortDirection = 'asc'; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); }); it('should pass accessibility tests with scrollable variant', async () => { element.scrollable = true; element.headers = [ { key: 'col1', label: 'Column 1', sortable: true }, { key: 'col2', label: 'Column 2', sortable: false }, { key: 'col3', label: 'Column 3', sortable: true }, ]; element.data = [ { col1: 'Data A1', col2: 'Data A2', col3: 'Data A3' }, { col1: 'Data B1', col2: 'Data B2', col3: 'Data B3' }, ]; await element.updateComplete; await testComponentAccessibility(element, USWDS_A11Y_CONFIG.FULL_COMPLIANCE); }); describe('JavaScript Implementation Validation', () => { it('should pass JavaScript implementation validation', async () => { // Validate USWDS JavaScript implementation patterns const componentPath = `${process.cwd()}/src/components/table/usa-table.ts`; const validation = validateComponentJavaScript(componentPath, 'table'); if (!validation.isValid) { console.warn('JavaScript validation issues:', validation.issues); } // JavaScript validation should pass for critical integration patterns expect(validation.score).toBeGreaterThanOrEqual(50); // Allow some non-critical issues // Critical USWDS integration should be present (table may be presentational) const criticalIssues = validation.issues.filter((issue) => issue.includes('Missing USWDS JavaScript integration') ); expect(criticalIssues.length).toBeLessThanOrEqual(1); // Table may be classified as presentational }); }); }); describe('Storybook Integration (CRITICAL)', () => { it('should render in Storybook without auto-dismissing', async () => { element.caption = 'Storybook Test Table - Federal Employee Data'; element.striped = true; element.compact = false; element.borderless = false; element.stickyHeader = true; element.scrollable = true; element.headers = [ { key: 'id', label: 'Employee ID', sortable: true }, { key: 'name', label: 'Full Name', sortable: true }, { key: 'department', label: 'Department', sortable: true }, { key: 'grade', label: 'GS Grade', sortable: true }, { key: 'location', label: 'Duty Station', sortable: false }, { key: 'startDate', label: 'Start Date', sortable: true }, { key: 'clearance', label: 'Security Clearance', sortable: false }, ]; element.data = [ { id: 'EMP001', name: 'Sarah Johnson', department: 'Information Technology', grade: 'GS-13', location: 'Washington, DC', startDate: '2018-03-15', clearance: 'Secret', }, { id: 'EMP002', name: 'Michael Rodriguez', department: 'Human Resources', grade: 'GS-12', location: 'Denver, CO', startDate: '2019-07-22', clearance: 'Public Trust', }, { id: 'EMP003', name: 'Emily Chen', department: 'Finance', grade: 'GS-14', location: 'San Francisco, CA', startDate: '2017-11-08', clearance: 'Top Secret', }, { id: 'EMP004', name: 'David Thompson', department: 'Operations', grade: 'GS-11', location: 'Atlanta, GA', startDate: '2020-01-14', clearance: 'Public Trust', }, { id: 'EMP005', name: 'Lisa Park', department: 'Legal Affairs', grade: 'GS-15', location: 'Washington, DC', startDate: '2016-05-03', clearance: 'Secret', }, ]; element.sortColumn = 'name'; element.sortDirection = 'asc'; await element.updateComplete; expect(element.isConnected).toBe(true); expect(element.querySelector('table')).toBeTruthy(); expect(element.querySelector('caption')?.textContent).toContain('Storybook Test Table'); expect(element.querySelector('.usa-table--striped')).toBeTruthy(); expect(element.querySelector('.usa-table--sticky-header')).toBeTruthy(); // Verify headers render correctly const headers = element.querySelectorAll('th[scope="col"]'); expect(headers.length).toBe(7); // Verify data rows render correctly const dataRows = element.querySelectorAll('tbody tr'); expect(dataRows.length).toBe(5); // Verify sorting functionality works const sortableHeaders = element.querySelectorAll('th[data-sortable]'); expect(sortableHeaders.length).toBe(5); // 5 sortable columns // Test sorting interaction const sortableHeadersList = element.querySelectorAll( 'th[data-sortable]' ) as NodeListOf; const nameHeader = sortableHeadersList[1]; // Name is second sortable column expect(nameHeader).toBeTruthy(); nameHeader.click(); await element.updateComplete; expect(element.isConnected).toBe(true); expect(document.body.contains(element)).toBe(true); }); }); describe('Responsive/Reflow Accessibility (WCAG 1.4)', () => { beforeEach(() => { element.headers = [ { key: 'name', label: 'Name', sortable: true }, { key: 'department', label: 'Department', sortable: true }, ]; element.data = [ { name: 'John Smith', department: 'Engineering' }, { name: 'Jane Doe', department: 'Design' }, ]; }); it('should resize text properly up to 200% (WCAG 1.4.4)', async () => { element.caption = 'Responsive Table'; await element.updateComplete; const header = element.querySelector('th'); expect(header).toBeTruthy(); const result = testTextResize(header as Element, 200); expect(result).toBeDefined(); expect(result.violations).toBeDefined(); }); it('should support horizontal scroll for wide tables (WCAG 1.4.10)', async () => { element.scrollable = true; await element.updateComplete; const container = element.querySelector('.usa-table-container--scrollable'); expect(container).toBeTruthy(); const result = testReflow(container as Element, 320); // Scrollable tables are allowed to have horizontal scroll expect(result).toBeDefined(); expect(result.contentWidth).toBeGreaterThanOrEqual(0); }); it('should support text spacing adjustments (WCAG 1.4.12)', async () => { await element.updateComplete; const cell = element.querySelector('td'); expect(cell).toBeTruthy(); const result = testTextSpacing(cell as Element); expect(result.readable).toBe(true); expect(result.violations.length).toBe(0); }); it('should be accessible on mobile devices (comprehensive)', async () => { element.stacked = true; // Use stacked layout for mobile await element.updateComplete; const table = element.querySelector('table'); expect(table).toBeTruthy(); const result = await testMobileAccessibility(table as Element); expect(result).toBeDefined(); expect(result.details.reflowWorks).toBeDefined(); expect(result.details.textResizable).toBeDefined(); }); it('should maintain responsive behavior with stacked layout (WCAG 1.4.10)', async () => { element.stacked = true; element.stackedHeader = true; await element.updateComplete; const table = element.querySelector('.usa-table--stacked'); expect(table).toBeTruthy(); const result = testReflow(table as Element, 320); expect(result).toBeDefined(); expect(result.contentWidth).toBeGreaterThanOrEqual(0); }); it('should maintain accessibility with sticky header (WCAG 1.4.10)', async () => { element.stickyHeader = true; await element.updateComplete; const table = element.querySelector('.usa-table--sticky-header'); expect(table).toBeTruthy(); const result = testReflow(table as Element, 320); expect(result).toBeDefined(); expect(result.contentWidth).toBeGreaterThanOrEqual(0); }); }); });