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

711 lines (585 loc) 25.3 kB
/** * AccessibilityScorer unit tests * Tests comprehensive scoring algorithm with weighted calculations */ import { AccessibilityScorer, ScoringConfig, ScoringProfile, ScoreBreakdown, SiteScore } from './accessibility-scorer'; import { AccessibilityIssue, ScanResult } from '../types'; import { CrawlSession } from '../types/crawler'; describe('AccessibilityScorer', () => { let scorer: AccessibilityScorer; beforeEach(() => { scorer = new AccessibilityScorer(); }); // Helper functions to create mock data 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([ ['https://example.com', { url: 'https://example.com', depth: 0, source: 'initial', priority: 100, discoveredAt: new Date('2024-01-01T10:00:00Z'), attempts: 1, status: 'completed', }], ['https://example.com/page2', { url: 'https://example.com/page2', depth: 1, source: 'page', priority: 90, discoveredAt: new Date('2024-01-01T10:01:00Z'), attempts: 1, status: 'completed', }], ]), 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 }, }, }, }); describe('Scoring Profiles', () => { it('should provide predefined scoring profiles', () => { const profiles = AccessibilityScorer.getAvailableProfiles(); expect(profiles).toContain('strict'); expect(profiles).toContain('balanced'); expect(profiles).toContain('lenient'); expect(profiles).toContain('enterprise'); expect(profiles).toContain('custom'); }); it('should create scorer with specific profile', () => { const strictScorer = AccessibilityScorer.withProfile('strict'); const config = strictScorer.getConfig(); expect(config.profile).toBe('strict'); expect(config.wcagLevelWeights.A).toBe(3.0); expect(config.severityPenalties.critical).toBe(15); expect(config.enableBonuses).toBe(false); }); it('should allow profile configuration overrides', () => { const customScorer = AccessibilityScorer.withProfile('strict', { enableBonuses: true, maxScore: 120, }); const config = customScorer.getConfig(); expect(config.enableBonuses).toBe(true); expect(config.maxScore).toBe(120); expect(config.profile).toBe('strict'); // Profile should remain }); it('should get profile configuration', () => { const strictConfig = AccessibilityScorer.getProfileConfig('strict'); expect(strictConfig.wcagLevelWeights?.A).toBe(3.0); expect(strictConfig.enableBonuses).toBe(false); }); }); describe('Page Score Calculation', () => { it('should calculate basic page score', () => { const scanResult = createMockScanResult(); const breakdown = scorer.calculatePageScore(scanResult); expect(breakdown.finalScore).toBeGreaterThan(0); expect(breakdown.finalScore).toBeLessThanOrEqual(100); expect(breakdown.baseScore).toBe(100); expect(breakdown.issueDeductions).toHaveLength(1); expect(breakdown.metadata.algorithmVersion).toBe('1.0.0'); }); it('should apply WCAG level weights correctly', () => { const levelAIssue = createMockIssue({ level: 'A', severity: 'serious' }); const levelAAAIssue = createMockIssue({ level: 'AAA', severity: 'serious' }); const resultA = createMockScanResult({ issues: [levelAIssue] }); const resultAAA = createMockScanResult({ issues: [levelAAAIssue] }); const scoreA = scorer.calculatePageScore(resultA); const scoreAAA = scorer.calculatePageScore(resultAAA); // Level A should have higher penalty (lower score) than AAA expect(scoreA.finalScore).toBeLessThan(scoreAAA.finalScore); }); it('should apply severity penalties correctly', () => { const criticalIssue = createMockIssue({ severity: 'critical' }); const minorIssue = createMockIssue({ severity: 'minor' }); const criticalResult = createMockScanResult({ issues: [criticalIssue] }); const minorResult = createMockScanResult({ issues: [minorIssue] }); const criticalScore = scorer.calculatePageScore(criticalResult); const minorScore = scorer.calculatePageScore(minorResult); expect(criticalScore.finalScore).toBeLessThan(minorScore.finalScore); }); it('should respect maximum penalty per issue', () => { const highPenaltyIssue = createMockIssue({ severity: 'critical', level: 'A', wcagReference: '2.1.1' // Has multiplier }); const result = createMockScanResult({ issues: [highPenaltyIssue] }); const breakdown = scorer.calculatePageScore(result); const deduction = breakdown.issueDeductions[0]; expect(deduction?.pointsDeducted).toBeLessThanOrEqual(scorer.getConfig().maxPenaltyPerIssue); }); it('should apply issue type multipliers', () => { const keyboardIssue = createMockIssue({ wcagReference: '2.1.1' }); // 1.3x multiplier const genericIssue = createMockIssue({ wcagReference: '3.1.1' }); // 1.0x multiplier const keyboardResult = createMockScanResult({ issues: [keyboardIssue] }); const genericResult = createMockScanResult({ issues: [genericIssue] }); const keyboardScore = scorer.calculatePageScore(keyboardResult); const genericScore = scorer.calculatePageScore(genericResult); expect(keyboardScore.finalScore).toBeLessThan(genericScore.finalScore); }); it('should enforce minimum and maximum scores', () => { const manyIssues = Array.from({ length: 50 }, (_, i) => createMockIssue({ id: `issue-${i}`, severity: 'critical' }) ); const perfectScanResult = createMockScanResult({ issues: [] }); const terribleScanResult = createMockScanResult({ issues: manyIssues }); const perfectScore = scorer.calculatePageScore(perfectScanResult); const terribleScore = scorer.calculatePageScore(terribleScanResult); expect(perfectScore.finalScore).toBeLessThanOrEqual(scorer.getConfig().maxScore); expect(terribleScore.finalScore).toBeGreaterThanOrEqual(scorer.getConfig().minScore); }); }); describe('Coverage Assessment', () => { it('should calculate test coverage correctly', () => { const scanResult = createMockScanResult({ metadata: { ...createMockScanResult().metadata!, totalElements: 100, testedElements: 90, }, }); const breakdown = scorer.calculatePageScore(scanResult); expect(breakdown.coverage.testCoverage).toBe(90); expect(breakdown.coverage.totalElements).toBe(100); expect(breakdown.coverage.testedElements).toBe(90); expect(breakdown.coverage.failedElements).toBe(10); }); it('should handle perfect coverage', () => { const scanResult = createMockScanResult({ metadata: { ...createMockScanResult().metadata!, totalElements: 100, testedElements: 100, }, }); const breakdown = scorer.calculatePageScore(scanResult); expect(breakdown.coverage.testCoverage).toBe(100); expect(breakdown.coverage.failedElements).toBe(0); }); it('should handle empty pages gracefully', () => { const scanResult = createMockScanResult({ metadata: { ...createMockScanResult().metadata!, totalElements: 0, testedElements: 0, }, }); const breakdown = scorer.calculatePageScore(scanResult); expect(breakdown.coverage.testCoverage).toBe(100); // Empty page = perfect coverage }); }); describe('Bonus System', () => { it('should award performance bonus for fast loading', () => { const fastResult = createMockScanResult({ metadata: { ...createMockScanResult().metadata!, pageLoadTime: 1500, // Under 2000ms }, }); const breakdown = scorer.calculatePageScore(fastResult); expect(breakdown.bonuses.some(b => b.type === 'performance' && b.description.includes('Fast page load') )).toBe(true); }); it('should award coverage bonus for excellent coverage', () => { const excellentCoverageResult = createMockScanResult({ metadata: { ...createMockScanResult().metadata!, totalElements: 100, testedElements: 96, // >95% }, }); const breakdown = scorer.calculatePageScore(excellentCoverageResult); expect(breakdown.bonuses.some(b => b.type === 'coverage' && b.description.includes('Excellent') )).toBe(true); }); it('should award no critical issues bonus', () => { const noCriticalResult = createMockScanResult({ issues: [createMockIssue({ severity: 'moderate' })], }); const breakdown = scorer.calculatePageScore(noCriticalResult); expect(breakdown.bonuses.some(b => b.type === 'accessibility' && b.description.includes('No critical') )).toBe(true); }); it('should award perfect accessibility bonus', () => { const perfectResult = createMockScanResult({ issues: [] }); const breakdown = scorer.calculatePageScore(perfectResult); expect(breakdown.bonuses.some(b => b.type === 'accessibility' && b.description.includes('Perfect') )).toBe(true); }); it('should not award bonuses when disabled', () => { const noBonusScorer = new AccessibilityScorer({ enableBonuses: false }); const perfectResult = createMockScanResult({ issues: [] }); const breakdown = noBonusScorer.calculatePageScore(perfectResult); expect(breakdown.bonuses).toHaveLength(0); }); }); describe('Penalty System', () => { it('should apply timeout penalties', () => { const timeoutResult = createMockScanResult({ errors: [{ type: 'timeout', message: 'Page load timeout' }], }); const breakdown = scorer.calculatePageScore(timeoutResult); expect(breakdown.penalties.some(p => p.type === 'timeout' && p.points === 10 )).toBe(true); }); it('should apply scan error penalties', () => { const errorResult = createMockScanResult({ errors: [{ type: 'parsing', message: 'HTML parsing error' }], }); const breakdown = scorer.calculatePageScore(errorResult); expect(breakdown.penalties.some(p => p.type === 'error' && p.points === 5 )).toBe(true); }); it('should apply coverage penalties for low coverage', () => { const lowCoverageResult = createMockScanResult({ metadata: { ...createMockScanResult().metadata!, totalElements: 100, testedElements: 70, // 70% < 80% }, }); const breakdown = scorer.calculatePageScore(lowCoverageResult); expect(breakdown.penalties.some(p => p.type === 'coverage' )).toBe(true); }); it('should apply critical issue penalties for many critical issues', () => { const manyCriticalIssues = Array.from({ length: 5 }, (_, i) => createMockIssue({ id: `critical-${i}`, severity: 'critical' }) ); const criticalResult = createMockScanResult({ issues: manyCriticalIssues }); const breakdown = scorer.calculatePageScore(criticalResult); expect(breakdown.penalties.some(p => p.type === 'critical' && p.points === 10 // (5-3)*5 )).toBe(true); }); it('should not apply penalties when disabled', () => { const noPenaltyScorer = new AccessibilityScorer({ enablePenalties: false }); const errorResult = createMockScanResult({ errors: [{ type: 'timeout', message: 'Timeout' }], }); const breakdown = noPenaltyScorer.calculatePageScore(errorResult); expect(breakdown.penalties).toHaveLength(0); }); }); describe('Site Score Calculation', () => { it('should calculate weighted site score', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ url: 'https://example.com', score: 90 }), // Home page, high weight createMockScanResult({ url: 'https://example.com/page2', score: 70 }), // Depth 1, lower weight ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults, 'weighted'); expect(siteScore.overallScore).toBeGreaterThan(70); expect(siteScore.overallScore).toBeLessThan(90); expect(siteScore.aggregationMethod).toBe('weighted'); expect(siteScore.pageScores).toHaveLength(2); }); it('should calculate average site score', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ score: 80 }), createMockScanResult({ score: 90 }), ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults, 'average'); expect(siteScore.overallScore).toBe(85); // (80+90)/2 }); it('should calculate median site score', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ score: 60 }), createMockScanResult({ score: 80 }), createMockScanResult({ score: 90 }), ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults, 'median'); expect(siteScore.overallScore).toBe(80); // Middle value }); it('should calculate worst-case site score', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ score: 60 }), createMockScanResult({ score: 80 }), createMockScanResult({ score: 90 }), ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults, 'worst'); expect(siteScore.overallScore).toBe(60); // Minimum value }); it('should assign page importance correctly', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ url: 'https://example.com' }), // Home page createMockScanResult({ url: 'https://example.com/contact' }), // Important page createMockScanResult({ url: 'https://example.com/deep/nested/page' }), // Deep page ]; // Mock crawl session with depth information crawlSession.urls.set('https://example.com/contact', { url: 'https://example.com/contact', depth: 1, source: 'page', priority: 90, discoveredAt: new Date(), attempts: 1, status: 'completed', }); crawlSession.urls.set('https://example.com/deep/nested/page', { url: 'https://example.com/deep/nested/page', depth: 3, source: 'page', priority: 70, discoveredAt: new Date(), attempts: 1, status: 'completed', }); const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.pageScores[0]?.importance).toBe('critical'); // Home page expect(siteScore.pageScores[1]?.importance).toBe('high'); // Contact page expect(siteScore.pageScores[2]?.importance).toBe('low'); // Deep page }); }); describe('Site-wide Bonuses', () => { it('should award comprehensive scanning bonus', () => { const crawlSession = createMockCrawlSession(); // Mock 100% scan rate const scanResults = Array.from(crawlSession.urls.keys()).map(url => createMockScanResult({ url }) ); const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.siteWideBonuses.some(b => b.type === 'coverage' && b.description.includes('Comprehensive') )).toBe(true); }); it('should award consistency bonus for consistent scores', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ score: 85 }), createMockScanResult({ score: 87 }), ]; // Low variance const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.siteWideBonuses.some(b => b.type === 'accessibility' && b.description.includes('Consistent') )).toBe(true); }); }); describe('Score Distribution', () => { it('should calculate score distribution correctly', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ score: 95 }), // excellent createMockScanResult({ score: 85 }), // good createMockScanResult({ score: 75 }), // fair createMockScanResult({ score: 65 }), // poor createMockScanResult({ score: 55 }), // critical ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.distribution.ranges.excellent).toBe(1); expect(siteScore.distribution.ranges.good).toBe(1); expect(siteScore.distribution.ranges.fair).toBe(1); expect(siteScore.distribution.ranges.poor).toBe(1); expect(siteScore.distribution.ranges.critical).toBe(1); expect(siteScore.distribution.average).toBe(75); expect(siteScore.distribution.median).toBe(75); expect(siteScore.distribution.minimum).toBe(55); expect(siteScore.distribution.maximum).toBe(95); }); }); describe('Consistency Metrics', () => { it('should identify outlier pages', () => { const crawlSession = createMockCrawlSession(); const scanResults = [ createMockScanResult({ url: 'https://example.com/good1', score: 85 }), createMockScanResult({ url: 'https://example.com/good2', score: 87 }), createMockScanResult({ url: 'https://example.com/outlier', score: 45 }), // Outlier ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.consistency.outlierPages).toContain('https://example.com/outlier'); expect(siteScore.consistency.consistencyScore).toBeLessThan(100); }); it('should identify common issues across site', () => { const crawlSession = createMockCrawlSession(); const commonIssue = createMockIssue({ wcagReference: '1.1.1' }); const scanResults = [ createMockScanResult({ issues: [commonIssue] }), createMockScanResult({ issues: [commonIssue] }), ]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.consistency.commonIssues).toContain('1.1.1'); }); }); describe('Configuration Management', () => { it('should update configuration dynamically', () => { const newConfig = { maxScore: 120, enableBonuses: false }; scorer.updateConfig(newConfig); const config = scorer.getConfig(); expect(config.maxScore).toBe(120); expect(config.enableBonuses).toBe(false); }); it('should generate configuration checksum', () => { const breakdown1 = scorer.calculatePageScore(createMockScanResult()); const checksum1 = breakdown1.metadata.configChecksum; scorer.updateConfig({ maxScore: 120 }); const breakdown2 = scorer.calculatePageScore(createMockScanResult()); const checksum2 = breakdown2.metadata.configChecksum; expect(checksum1).not.toBe(checksum2); }); it('should include calculation timing', () => { const breakdown = scorer.calculatePageScore(createMockScanResult()); expect(breakdown.metadata.calculationTime).toBeGreaterThan(0); expect(breakdown.metadata.calculatedAt).toBeDefined(); }); }); describe('Edge Cases', () => { it('should handle empty scan results', () => { const emptyScanResult = createMockScanResult({ issues: [] }); const breakdown = scorer.calculatePageScore(emptyScanResult); expect(breakdown.finalScore).toBe(100); // Perfect score expect(breakdown.issueDeductions).toHaveLength(0); }); it('should handle missing metadata gracefully', () => { const noMetadataResult = createMockScanResult({ metadata: undefined }); const breakdown = scorer.calculatePageScore(noMetadataResult); expect(breakdown.coverage.totalElements).toBe(0); expect(breakdown.coverage.testCoverage).toBe(100); }); it('should handle empty site scan results', () => { const crawlSession = createMockCrawlSession(); const siteScore = scorer.calculateSiteScore(crawlSession, []); expect(siteScore.overallScore).toBe(0); expect(siteScore.pageScores).toHaveLength(0); }); it('should handle single page sites', () => { const crawlSession = createMockCrawlSession(); const scanResults = [createMockScanResult({ score: 85 })]; const siteScore = scorer.calculateSiteScore(crawlSession, scanResults); expect(siteScore.overallScore).toBe(85); expect(siteScore.distribution.median).toBe(85); expect(siteScore.consistency.scoreVariance).toBe(0); }); }); describe('Score Transparency', () => { it('should provide detailed score breakdown', () => { const scanResult = createMockScanResult({ issues: [ createMockIssue({ severity: 'critical', wcagReference: '1.1.1' }), createMockIssue({ severity: 'minor', wcagReference: '2.4.6' }), ], }); const breakdown = scorer.calculatePageScore(scanResult); expect(breakdown.issueDeductions).toHaveLength(2); expect(breakdown.issueDeductions[0]?.reason).toContain('critical'); expect(breakdown.issueDeductions[1]?.reason).toContain('minor'); expect(breakdown.metadata).toBeDefined(); expect(breakdown.coverage).toBeDefined(); }); it('should explain score calculations', () => { const scanResult = createMockScanResult(); const breakdown = scorer.calculatePageScore(scanResult); // Should provide enough information to understand the score expect(breakdown.baseScore).toBeDefined(); expect(breakdown.issueDeductions.length).toBeGreaterThan(0); expect(breakdown.issueDeductions[0]?.pointsDeducted).toBeGreaterThan(0); expect(breakdown.issueDeductions[0]?.weight).toBeGreaterThan(0); expect(breakdown.issueDeductions[0]?.reason).toBeTruthy(); }); }); });