docs: enterprise refactor plan with ralph specs
This commit is contained in:
197
tests/plugins/accessibility.test.ts
Normal file
197
tests/plugins/accessibility.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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: '<img src="logo.png">' }],
|
||||
},
|
||||
]);
|
||||
|
||||
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: '<p>text</p>' }],
|
||||
},
|
||||
]);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user