/**
* Tests for AccessibilityCollector with mocked AxeBuilder and violations.
*/
import { AccessibilityCollector } from '../../src/plugins/collectors/AccessibilityCollector';
import type { IAction } from '../../src/core/interfaces';
// ─── Helpers ──────────────────────────────────────────────────────────────────
function makeAction(): IAction {
return { id: 'a1', type: 'click', timestamp: Date.now(), seed: 42, stateId: 'state1' };
}
interface MockViolation {
id: string;
impact?: string;
description: string;
helpUrl: string;
nodes: unknown[];
}
// ─── Mock @axe-core/playwright ────────────────────────────────────────────────
const analyzeMock = jest.fn();
jest.mock('@axe-core/playwright', () => {
return {
AxeBuilder: jest.fn().mockImplementation(() => ({
withTags: jest.fn().mockReturnThis(),
analyze: analyzeMock,
})),
};
});
function makeViolations(violations: MockViolation[]) {
analyzeMock.mockResolvedValue({ violations });
}
// A minimal mock page (AxeBuilder receives the page, evaluate is only used as fallback)
const mockPage = {
evaluate: jest.fn().mockResolvedValue([]),
};
// ─── Tests ────────────────────────────────────────────────────────────────────
beforeEach(() => {
analyzeMock.mockReset();
mockPage.evaluate.mockReset();
mockPage.evaluate.mockResolvedValue([]);
});
describe('AccessibilityCollector', () => {
it('returns empty array when disabled', async () => {
const collector = new AccessibilityCollector({ enabled: false });
const result = await collector.collect(mockPage as never, 'state1', 'sess1', []);
expect(result).toHaveLength(0);
expect(analyzeMock).not.toHaveBeenCalled();
});
it('returns empty array when no violations found', async () => {
makeViolations([]);
const collector = new AccessibilityCollector({ enabled: true });
const result = await collector.collect(mockPage as never, 'state1', 'sess1', [makeAction()]);
expect(result).toHaveLength(0);
});
it('converts serious violations to high severity anomalies', async () => {
makeViolations([
{
id: 'image-alt',
impact: 'serious',
description: 'Images must have alternate text',
helpUrl: 'https://dequeuniversity.com/rules/axe/image-alt',
nodes: [{ html: '
' }],
},
]);
const collector = new AccessibilityCollector({ enabled: true, minImpact: 'minor' });
const result = await collector.collect(mockPage as never, 'state1', 'sess1', [makeAction()]);
expect(result).toHaveLength(1);
expect(result[0]!.type).toBe('accessibility_violation');
expect(result[0]!.severity).toBe('high');
expect(result[0]!.description).toContain('Images must have alternate text');
});
it('converts critical violations to critical severity', async () => {
makeViolations([
{
id: 'color-contrast',
impact: 'critical',
description: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/color-contrast',
nodes: [{ html: '
text
' }], }, ]); const collector = new AccessibilityCollector({ enabled: true, minImpact: 'minor' }); const result = await collector.collect(mockPage as never, 'state1', 'sess1', []); expect(result[0]!.severity).toBe('critical'); }); it('converts moderate violations to medium severity', async () => { makeViolations([ { id: 'label', impact: 'moderate', description: 'Form elements must have labels', helpUrl: 'https://dequeuniversity.com/rules/axe/label', nodes: [{}], }, ]); const collector = new AccessibilityCollector({ enabled: true, minImpact: 'minor' }); const result = await collector.collect(mockPage as never, 'state1', 'sess1', []); expect(result[0]!.severity).toBe('medium'); }); it('filters out violations below minImpact', async () => { makeViolations([ { id: 'link-name', impact: 'minor', description: 'Links must have discernible text', helpUrl: 'https://dequeuniversity.com/rules/axe/link-name', nodes: [{}], }, ]); // minImpact = 'serious' → minor is ignored const collector = new AccessibilityCollector({ enabled: true, minImpact: 'serious' }); const result = await collector.collect(mockPage as never, 'state1', 'sess1', []); expect(result).toHaveLength(0); }); it('returns multiple anomalies for multiple violations above threshold', async () => { makeViolations([ { id: 'v1', impact: 'serious', description: 'Violation 1', helpUrl: 'h1', nodes: [{}] }, { id: 'v2', impact: 'critical', description: 'Violation 2', helpUrl: 'h2', nodes: [{}, {}] }, { id: 'v3', impact: 'moderate', description: 'Violation 3', helpUrl: 'h3', nodes: [{}] }, ]); const collector = new AccessibilityCollector({ enabled: true, minImpact: 'moderate' }); const result = await collector.collect(mockPage as never, 'state1', 'sess1', []); expect(result).toHaveLength(3); const severities = result.map((a) => a.severity); expect(severities).toContain('high'); expect(severities).toContain('critical'); expect(severities).toContain('medium'); }); it('handles AxeBuilder failure gracefully (returns empty)', async () => { analyzeMock.mockRejectedValue(new Error('axe internal error')); // Also make the fallback page.evaluate fail mockPage.evaluate.mockRejectedValue(new Error('page not available')); const collector = new AccessibilityCollector({ enabled: true }); const result = await collector.collect(mockPage as never, 'state1', 'sess1', []); expect(Array.isArray(result)).toBe(true); expect(result).toHaveLength(0); }); it('includes rule id, node count, and helpUrl in evidence rawErrors', async () => { makeViolations([ { id: 'button-name', impact: 'critical', description: 'Buttons must have discernible text', helpUrl: 'https://dequeuniversity.com/rules/axe/button-name', nodes: [{}, {}, {}], }, ]); const collector = new AccessibilityCollector({ enabled: true, minImpact: 'minor' }); const result = await collector.collect(mockPage as never, 'state1', 'sess1', []); const evidence = result[0]!.evidence.rawErrors ?? []; expect(evidence.some((e) => e.includes('button-name'))).toBe(true); expect(evidence.some((e) => e.includes('3'))).toBe(true); expect(evidence.some((e) => e.includes('dequeuniversity'))).toBe(true); }); it('sets correct observationId and actionTrace on anomaly', async () => { makeViolations([ { id: 'v', impact: 'serious', description: 'Desc', helpUrl: 'h', nodes: [{}] }, ]); const actions = [makeAction()]; const collector = new AccessibilityCollector({ enabled: true, minImpact: 'minor' }); const result = await collector.collect(mockPage as never, 'my-state-id', 'sess1', actions); expect(result[0]!.observationId).toBe('my-state-id'); expect(result[0]!.actionTrace).toBe(actions); }); });