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.
Add 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 = `
`;
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);
});
});
});