Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down Expand Up @@ -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(<TableChart {...props} sticky={false} />);
render(<TableChart {...props} sticky={false} />);

const headers = screen.getAllByRole('columnheader');

Expand Down Expand Up @@ -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(<TableChart {...props} sticky={false} />);

expectValidAriaLabels(container);
});

test('should set meaningful header IDs for regular table columns', () => {
Expand Down Expand Up @@ -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: <TableChart {...props} sticky={false} />,
}),
);
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', () => {
Expand Down
Loading