UNPKG

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