/** * 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); }); });