a11yanalyze
Version:
A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance
677 lines (588 loc) • 21.7 kB
text/typescript
/**
* ConsoleReporter unit tests
* Tests comprehensive console output with progress indicators
*/
import { ConsoleReporter, ConsoleConfig } from './console-reporter';
import { ScanResult, AccessibilityIssue } from '../types';
import { CrawlSession } from '../types/crawler';
import { ScoreBreakdown, SiteScore } from '../scoring/accessibility-scorer';
import chalk from 'chalk';
// Mock external dependencies
jest.mock('chalk', () => {
const mockFn = (text: any) => text;
return {
default: mockFn,
bold: mockFn,
gray: mockFn,
blue: mockFn,
yellow: mockFn,
red: mockFn,
green: mockFn,
magenta: mockFn,
cyan: mockFn,
white: mockFn,
hex: () => mockFn,
level: 1,
};
});
jest.mock('cli-progress', () => ({
MultiBar: jest.fn().mockImplementation(() => ({
create: jest.fn(() => ({
update: jest.fn(),
getTotal: jest.fn(() => 100),
})),
stop: jest.fn(),
})),
}));
jest.mock('figures', () => ({
play: '▶',
arrowRight: '→',
pointer: '❯',
hamburger: '☰',
tick: '✓',
checkboxOn: '☑',
bullet: '•',
cross: '✖',
warning: '⚠',
info: 'ℹ',
star: '★',
circle: '●',
}));
jest.mock('ora', () => {
const mockSpinner = {
start: jest.fn().mockReturnThis(),
succeed: jest.fn(),
fail: jest.fn(),
stop: jest.fn(),
text: '',
};
return jest.fn(() => mockSpinner);
});
// Spy on console methods
const consoleSpy = {
log: jest.spyOn(console, 'log').mockImplementation(() => {}),
error: jest.spyOn(console, 'error').mockImplementation(() => {}),
warn: jest.spyOn(console, 'warn').mockImplementation(() => {}),
};
describe('ConsoleReporter', () => {
let reporter: ConsoleReporter;
beforeEach(() => {
jest.clearAllMocks();
Object.values(consoleSpy).forEach(spy => spy.mockClear());
reporter = new ConsoleReporter();
});
afterEach(() => {
reporter.cleanup();
});
// Helper functions
const createMockIssue = (overrides: Partial<AccessibilityIssue> = {}): AccessibilityIssue => ({
id: 'issue-1',
wcagReference: '1.1.1',
level: 'AA',
severity: 'serious',
element: '<img src="test.jpg">',
selector: 'img',
message: 'Image missing alt text',
remediation: 'Add descriptive alt text to the image',
helpUrl: 'https://help.example.com/alt-text',
impact: 'Users with screen readers cannot understand the image content',
tags: ['wcag2aa', 'section508'],
...overrides,
});
const createMockScanResult = (overrides: Partial<ScanResult> = {}): ScanResult => ({
url: 'https://example.com',
timestamp: new Date().toISOString(),
score: 85,
issues: [createMockIssue()],
metadata: {
scanDuration: 1500,
pageLoadTime: 800,
totalElements: 100,
testedElements: 95,
userAgent: 'test-agent',
viewport: { width: 1280, height: 720 },
url: {
original: 'https://example.com',
final: 'https://example.com',
redirects: 0,
},
},
errors: [],
compliance: {
compliant: false,
primaryLevelIssues: 1,
warningIssues: 0,
totalIssues: 1,
levelBreakdown: { A: 0, AA: 1, AAA: 0, ARIA: 0 },
},
...overrides,
});
const createMockCrawlSession = (): CrawlSession => ({
id: 'crawl-session-123',
startUrls: ['https://example.com'],
config: {
maxDepth: 2,
maxPages: 10,
allowedDomains: ['example.com'],
excludedDomains: [],
excludedPaths: [],
includedPaths: [],
requestDelay: 1000,
maxConcurrency: 3,
respectRobotsTxt: true,
discoverExternalLinks: false,
useSitemaps: true,
userAgent: 'a11yanalyze/1.0.0',
requestTimeout: 30000,
},
scanOptions: {},
startTime: new Date('2024-01-01T10:00:00Z'),
endTime: new Date('2024-01-01T10:05:00Z'),
status: 'completed',
urls: new Map(),
results: new Map(),
stats: {
urlCounts: {
total: 2,
pending: 0,
processing: 0,
completed: 2,
failed: 0,
skipped: 0,
excluded: 0,
},
pagesScanned: 2,
totalIssues: 2,
issuesBySeverity: {
critical: 0,
serious: 1,
moderate: 1,
minor: 0,
warning: 0,
},
averageScore: 80,
performance: {
avgPageLoadTime: 800,
avgScanTime: 1500,
totalCrawlTime: 300000,
pagesPerMinute: 0.4,
},
wcagCompliance: {
compliantPages: 0,
nonCompliantPages: 2,
complianceRate: 0,
levelBreakdown: { A: 0, AA: 2, AAA: 0, ARIA: 0 },
},
},
});
const createMockScoreBreakdown = (): ScoreBreakdown => ({
finalScore: 85,
baseScore: 100,
issueDeductions: [{
issueId: 'issue-1',
wcagReference: '1.1.1',
severity: 'serious',
level: 'AA',
pointsDeducted: 10,
weight: 2.0,
reason: 'serious AA issue: 1.1.1',
}],
bonuses: [{
type: 'performance',
points: 2,
description: 'Fast page load time',
criteria: 'Load time: 800ms < 2000ms',
}],
penalties: [],
coverage: {
testCoverage: 95,
totalElements: 100,
testedElements: 95,
failedElements: 5,
qualityScore: 90,
uncoveredAreas: [],
},
metadata: {
algorithmVersion: '1.0.0',
profile: 'balanced',
calculatedAt: new Date().toISOString(),
calculationTime: 5,
configChecksum: 'abc123',
},
});
const createMockSiteScore = (): SiteScore => ({
overallScore: 82,
pageScores: [
{ url: 'https://example.com', score: 85, weight: 1.0, issueCount: 1, importance: 'critical' },
{ url: 'https://example.com/page2', score: 79, weight: 0.8, issueCount: 2, importance: 'medium' },
],
aggregationMethod: 'weighted',
siteWideBonuses: [{
type: 'coverage',
points: 5,
description: 'Comprehensive site scanning',
criteria: '2/2 pages scanned (100%)',
}],
distribution: {
average: 82,
median: 82,
minimum: 79,
maximum: 85,
standardDeviation: 3,
ranges: {
excellent: 0,
good: 2,
fair: 0,
poor: 0,
critical: 0,
},
},
consistency: {
consistencyScore: 90,
outlierPages: [],
commonIssues: ['1.1.1', '2.4.6'],
scoreVariance: 9,
},
});
describe('Configuration', () => {
it('should create with default configuration', () => {
const defaultReporter = ConsoleReporter.createDefault();
expect(defaultReporter).toBeInstanceOf(ConsoleReporter);
});
it('should create verbose reporter', () => {
const verboseReporter = ConsoleReporter.createVerbose();
expect(verboseReporter).toBeInstanceOf(ConsoleReporter);
});
it('should create quiet reporter', () => {
const quietReporter = ConsoleReporter.createQuiet();
expect(quietReporter).toBeInstanceOf(ConsoleReporter);
});
it('should create debug reporter', () => {
const debugReporter = ConsoleReporter.createDebug();
expect(debugReporter).toBeInstanceOf(ConsoleReporter);
});
it('should accept custom configuration', () => {
const config: Partial<ConsoleConfig> = {
colors: false,
verbose: true,
maxWidth: 120,
};
const customReporter = new ConsoleReporter(config);
expect(customReporter).toBeInstanceOf(ConsoleReporter);
});
});
describe('Initialization', () => {
it('should initialize without errors', () => {
expect(() => reporter.init()).not.toThrow();
});
it('should show header on initialization', () => {
reporter.init();
expect(consoleSpy.log).toHaveBeenCalled();
});
it('should not show header in quiet mode', () => {
const quietReporter = new ConsoleReporter({ quiet: true });
quietReporter.init();
expect(consoleSpy.log).not.toHaveBeenCalled();
});
});
describe('Phase Management', () => {
it('should update phase correctly', () => {
expect(() => reporter.updatePhase('crawling', 'Discovering pages', 10)).not.toThrow();
expect(consoleSpy.log).toHaveBeenCalled();
});
it('should not output in quiet mode', () => {
const quietReporter = new ConsoleReporter({ quiet: true });
quietReporter.updatePhase('scanning', 'Processing pages');
expect(consoleSpy.log).not.toHaveBeenCalled();
});
it('should handle all phase types', () => {
const phases = ['initializing', 'crawling', 'scanning', 'analyzing', 'reporting', 'complete'] as const;
phases.forEach(phase => {
expect(() => reporter.updatePhase(phase, `${phase} operation`)).not.toThrow();
});
});
});
describe('Progress Tracking', () => {
it('should create progress bar', () => {
expect(() => reporter.createProgressBar('test', 'Test Progress', 100)).not.toThrow();
});
it('should update progress', () => {
reporter.createProgressBar('test', 'Test Progress', 100);
expect(() => reporter.updateProgress('test', 50, 'Halfway done')).not.toThrow();
});
it('should complete progress', () => {
reporter.createProgressBar('test', 'Test Progress', 100);
expect(() => reporter.completeProgress('test', 'Finished')).not.toThrow();
});
it('should handle missing progress bar gracefully', () => {
expect(() => reporter.updateProgress('nonexistent', 50)).not.toThrow();
expect(() => reporter.completeProgress('nonexistent')).not.toThrow();
});
});
describe('Spinner Management', () => {
it('should create spinner', () => {
expect(() => reporter.createSpinner('test', 'Loading...')).not.toThrow();
});
it('should update spinner text', () => {
reporter.createSpinner('test', 'Loading...');
expect(() => reporter.updateSpinner('test', 'Processing...')).not.toThrow();
});
it('should succeed spinner', () => {
reporter.createSpinner('test', 'Loading...');
expect(() => reporter.succeedSpinner('test', 'Success!')).not.toThrow();
});
it('should fail spinner', () => {
reporter.createSpinner('test', 'Loading...');
expect(() => reporter.failSpinner('test', 'Failed!')).not.toThrow();
});
});
describe('Page Scan Logging', () => {
it('should log page scan start', () => {
expect(() => reporter.logPageScanStart('https://example.com', 1, 5)).not.toThrow();
});
it('should log page scan completion', () => {
const scanResult = createMockScanResult();
expect(() => reporter.logPageScanComplete('https://example.com', scanResult)).not.toThrow();
});
it('should handle page with no issues', () => {
const scanResult = createMockScanResult({ issues: [] });
expect(() => reporter.logPageScanComplete('https://example.com', scanResult)).not.toThrow();
});
it('should highlight critical issues', () => {
const scanResult = createMockScanResult({
issues: [createMockIssue({ severity: 'critical' })],
});
expect(() => reporter.logPageScanComplete('https://example.com', scanResult)).not.toThrow();
});
});
describe('Page Results Display', () => {
it('should show page results', () => {
const scanResult = createMockScanResult();
expect(() => reporter.showPageResults(scanResult)).not.toThrow();
expect(consoleSpy.log).toHaveBeenCalled();
});
it('should show page results with score breakdown', () => {
const scanResult = createMockScanResult();
const scoreBreakdown = createMockScoreBreakdown();
const verboseReporter = new ConsoleReporter({ verbose: true });
expect(() => verboseReporter.showPageResults(scanResult, scoreBreakdown)).not.toThrow();
});
it('should handle perfect page (no issues)', () => {
const perfectScanResult = createMockScanResult({ issues: [], score: 100 });
expect(() => reporter.showPageResults(perfectScanResult)).not.toThrow();
});
it('should not show results in quiet mode', () => {
const quietReporter = new ConsoleReporter({ quiet: true });
const scanResult = createMockScanResult();
quietReporter.showPageResults(scanResult);
expect(consoleSpy.log).not.toHaveBeenCalled();
});
});
describe('Site Results Display', () => {
it('should show site results', () => {
const siteScore = createMockSiteScore();
const crawlSession = createMockCrawlSession();
expect(() => reporter.showSiteResults(siteScore, crawlSession)).not.toThrow();
expect(consoleSpy.log).toHaveBeenCalled();
});
it('should show detailed site results in verbose mode', () => {
const verboseReporter = new ConsoleReporter({ verbose: true });
const siteScore = createMockSiteScore();
const crawlSession = createMockCrawlSession();
expect(() => verboseReporter.showSiteResults(siteScore, crawlSession)).not.toThrow();
});
it('should handle site with many pages', () => {
const siteScore = createMockSiteScore();
// Add more pages to test pagination
for (let i = 3; i <= 15; i++) {
siteScore.pageScores.push({
url: `https://example.com/page${i}`,
score: 75 + (i % 10),
weight: 0.5,
issueCount: i % 3,
importance: 'low',
});
}
const verboseReporter = new ConsoleReporter({ verbose: true });
expect(() => verboseReporter.showSiteResults(siteScore, createMockCrawlSession())).not.toThrow();
});
});
describe('Final Summary', () => {
it('should show final summary', () => {
const siteScore = createMockSiteScore();
const crawlSession = createMockCrawlSession();
expect(() => reporter.showFinalSummary(siteScore, crawlSession)).not.toThrow();
expect(consoleSpy.log).toHaveBeenCalled();
});
it('should show different recommendations based on score', () => {
const crawlSession = createMockCrawlSession();
// Test excellent score
const excellentScore = createMockSiteScore();
excellentScore.overallScore = 95;
expect(() => reporter.showFinalSummary(excellentScore, crawlSession)).not.toThrow();
// Test good score
const goodScore = createMockSiteScore();
goodScore.overallScore = 75;
expect(() => reporter.showFinalSummary(goodScore, crawlSession)).not.toThrow();
// Test poor score
const poorScore = createMockSiteScore();
poorScore.overallScore = 45;
expect(() => reporter.showFinalSummary(poorScore, crawlSession)).not.toThrow();
});
it('should highlight critical issues in recommendations', () => {
const siteScore = createMockSiteScore();
const crawlSession = createMockCrawlSession();
crawlSession.stats.issuesBySeverity.critical = 5;
expect(() => reporter.showFinalSummary(siteScore, crawlSession)).not.toThrow();
});
it('should warn about inconsistency', () => {
const siteScore = createMockSiteScore();
siteScore.consistency.consistencyScore = 50; // Low consistency
const crawlSession = createMockCrawlSession();
expect(() => reporter.showFinalSummary(siteScore, crawlSession)).not.toThrow();
});
});
describe('Crawl Progress Logging', () => {
it('should log crawl progress in verbose mode', () => {
const verboseReporter = new ConsoleReporter({ verbose: true });
const crawlSession = createMockCrawlSession();
expect(() => verboseReporter.logCrawlProgress(crawlSession)).not.toThrow();
});
it('should not log crawl progress in non-verbose mode', () => {
const crawlSession = createMockCrawlSession();
reporter.logCrawlProgress(crawlSession);
// Should not output in non-verbose mode
});
it('should handle empty crawl session', () => {
const emptyCrawlSession = createMockCrawlSession();
emptyCrawlSession.stats.pagesScanned = 0;
expect(() => reporter.logCrawlProgress(emptyCrawlSession)).not.toThrow();
});
});
describe('Error and Warning Logging', () => {
it('should log errors', () => {
reporter.logError('Test error message');
expect(consoleSpy.error).toHaveBeenCalled();
});
it('should log errors with stack trace in debug mode', () => {
const debugReporter = new ConsoleReporter({ debug: true });
const error = new Error('Test error');
debugReporter.logError('Test error message', error);
expect(consoleSpy.error).toHaveBeenCalledTimes(2); // Message + stack trace
});
it('should log warnings', () => {
reporter.logWarning('Test warning message');
expect(consoleSpy.warn).toHaveBeenCalled();
});
it('should not log warnings in quiet mode', () => {
const quietReporter = new ConsoleReporter({ quiet: true });
quietReporter.logWarning('Test warning');
expect(consoleSpy.warn).not.toHaveBeenCalled();
});
it('should log info in verbose mode', () => {
const verboseReporter = new ConsoleReporter({ verbose: true });
verboseReporter.logInfo('Test info message');
expect(consoleSpy.log).toHaveBeenCalled();
});
it('should log debug messages in debug mode', () => {
const debugReporter = new ConsoleReporter({ debug: true });
debugReporter.logDebug('Test debug message');
expect(consoleSpy.log).toHaveBeenCalled();
});
});
describe('Score Coloring', () => {
it('should use different colors for different score ranges', () => {
const scanResults = [
createMockScanResult({ score: 95 }), // Excellent (green bold)
createMockScanResult({ score: 85 }), // Good (green)
createMockScanResult({ score: 75 }), // Fair (yellow)
createMockScanResult({ score: 65 }), // Poor (orange)
createMockScanResult({ score: 45 }), // Critical (red)
];
scanResults.forEach(result => {
expect(() => reporter.showPageResults(result)).not.toThrow();
});
});
});
describe('Issue Severity Display', () => {
it('should display all severity types correctly', () => {
const severities = ['critical', 'serious', 'moderate', 'minor', 'warning'];
severities.forEach(severity => {
const scanResult = createMockScanResult({
issues: [createMockIssue({ severity: severity as any })],
});
expect(() => reporter.showPageResults(scanResult)).not.toThrow();
});
});
it('should show top issues in verbose mode', () => {
const verboseReporter = new ConsoleReporter({ verbose: true });
const manyIssues = Array.from({ length: 10 }, (_, i) =>
createMockIssue({ id: `issue-${i}`, message: `Issue ${i}` })
);
const scanResult = createMockScanResult({ issues: manyIssues });
expect(() => verboseReporter.showPageResults(scanResult)).not.toThrow();
});
});
describe('Performance Metrics Display', () => {
it('should show performance metrics in verbose mode', () => {
const verboseReporter = new ConsoleReporter({ verbose: true });
const scanResult = createMockScanResult({
metadata: {
pageLoadTime: 1500,
scanDuration: 800,
totalElements: 150,
testedElements: 140,
userAgent: 'test-agent',
viewport: { width: 1280, height: 720 },
url: {
original: 'https://example.com',
final: 'https://example.com',
redirects: 0,
},
},
});
expect(() => verboseReporter.showPageResults(scanResult)).not.toThrow();
});
it('should handle missing performance metadata', () => {
const verboseReporter = new ConsoleReporter({ verbose: true });
const scanResult = createMockScanResult({ metadata: undefined });
expect(() => verboseReporter.showPageResults(scanResult)).not.toThrow();
});
});
describe('Resource Cleanup', () => {
it('should cleanup resources without errors', () => {
reporter.createSpinner('test', 'Loading...');
reporter.createProgressBar('test', 'Progress', 100);
expect(() => reporter.cleanup()).not.toThrow();
});
it('should handle cleanup with no active resources', () => {
expect(() => reporter.cleanup()).not.toThrow();
});
});
describe('Duration Formatting', () => {
it('should format various durations correctly', () => {
// Test through different summary displays that use formatDuration
const siteScore = createMockSiteScore();
const crawlSession = createMockCrawlSession();
// Test short duration
expect(() => reporter.showFinalSummary(siteScore, crawlSession)).not.toThrow();
});
});
describe('Color Configuration', () => {
it('should disable colors when configured', () => {
const noColorReporter = new ConsoleReporter({ colors: false });
const scanResult = createMockScanResult();
expect(() => noColorReporter.showPageResults(scanResult)).not.toThrow();
expect(chalk.level).toBe(0); // Colors disabled
});
});
describe('Width Limiting', () => {
it('should respect maximum width configuration', () => {
const narrowReporter = new ConsoleReporter({ maxWidth: 40 });
const scanResult = createMockScanResult({
url: 'https://example.com/very/long/path/that/exceeds/normal/width',
});
expect(() => narrowReporter.showPageResults(scanResult)).not.toThrow();
});
});
});