diff --git a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx index 83de3752be37..1f19c7944fd7 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -25,6 +25,33 @@ import DateWithFormatter from '../src/utils/DateWithFormatter'; import testData from './testData'; import { ProviderWrapper } from './testHelpers'; +const expectValidAriaLabels = (container: HTMLElement) => { + const allCells = container.querySelectorAll('tbody td'); + const cellsWithLabels = container.querySelectorAll( + 'tbody td[aria-labelledby]', + ); + + // Table must render data cells (catch empty table regression) + expect(allCells.length).toBeGreaterThan(0); + + // ALL data cells must have aria-labelledby (no unlabeled cells) + expect(cellsWithLabels.length).toBe(allCells.length); + + // ALL aria-labelledby values should be valid + cellsWithLabels.forEach(cell => { + const labelledBy = cell.getAttribute('aria-labelledby'); + expect(labelledBy).not.toBeNull(); + expect(labelledBy).toEqual(expect.stringMatching(/\S/)); + const labelledByValue = labelledBy as string; + expect(labelledByValue).not.toMatch(/\s/); + expect(labelledByValue).not.toMatch(/[%#△]/); + const referencedHeader = container.querySelector( + `#${CSS.escape(labelledByValue)}`, + ); + expect(referencedHeader).toBeTruthy(); + }); +}; + test('sanitizeHeaderId should sanitize percent sign', () => { expect(sanitizeHeaderId('%pct_nice')).toBe('percentpct_nice'); }); @@ -602,7 +629,7 @@ describe('plugin-chart-table', () => { // Uses originalLabel (e.g., "metric_1") which is sanitized for CSS safety const props = transformProps(testData.comparison); - const { container } = render(); + render(); const headers = screen.getAllByRole('columnheader'); @@ -632,25 +659,16 @@ describe('plugin-chart-table', () => { // IDs should only contain valid characters: alphanumeric, underscore, hyphen expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/); }); + }); - // CRITICAL: Verify ALL cells reference valid headers (no broken ARIA) - const cellsWithLabels = container.querySelectorAll( - 'td[aria-labelledby]', - ); - cellsWithLabels.forEach(cell => { - const labelledBy = cell.getAttribute('aria-labelledby'); - if (labelledBy) { - // Check that the ID doesn't contain spaces (would be interpreted as multiple IDs) - expect(labelledBy).not.toMatch(/\s/); - // Check that the ID doesn't contain special characters - expect(labelledBy).not.toMatch(/[%#△]/); - // Verify the referenced header actually exists - const referencedHeader = container.querySelector( - `#${CSS.escape(labelledBy)}`, - ); - expect(referencedHeader).toBeTruthy(); - } - }); + test('should validate ARIA references for time-comparison table cells', () => { + // Test that ALL cells with aria-labelledby have valid references + // This is critical for screen reader accessibility + const props = transformProps(testData.comparison); + + const { container } = render(); + + expectValidAriaLabels(container); }); test('should set meaningful header IDs for regular table columns', () => { @@ -711,25 +729,20 @@ describe('plugin-chart-table', () => { // IDs should only contain valid CSS selector characters expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/); }); + }); - // Test 6: Verify ALL cells reference valid headers (no broken ARIA) - const cellsWithLabels = container.querySelectorAll( - 'td[aria-labelledby]', + test('should validate ARIA references for regular table cells', () => { + // Test that ALL cells with aria-labelledby have valid references + // This is critical for screen reader accessibility + const props = transformProps(testData.advanced); + + const { container } = render( + ProviderWrapper({ + children: , + }), ); - cellsWithLabels.forEach(cell => { - const labelledBy = cell.getAttribute('aria-labelledby'); - if (labelledBy) { - // Verify no spaces (would be interpreted as multiple IDs) - expect(labelledBy).not.toMatch(/\s/); - // Verify no special characters - expect(labelledBy).not.toMatch(/[%#△]/); - // Verify the referenced header actually exists - const referencedHeader = container.querySelector( - `#${CSS.escape(labelledBy)}`, - ); - expect(referencedHeader).toBeTruthy(); - } - }); + + expectValidAriaLabels(container); }); test('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {