ally-a11y 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCESSIBILITY.md +205 -0
- package/LICENSE +21 -0
- package/README.md +940 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +528 -0
- package/dist/commands/audit-palette.d.ts +18 -0
- package/dist/commands/audit-palette.js +613 -0
- package/dist/commands/auto-pr.d.ts +19 -0
- package/dist/commands/auto-pr.js +434 -0
- package/dist/commands/badge.d.ts +11 -0
- package/dist/commands/badge.js +143 -0
- package/dist/commands/completion.d.ts +4 -0
- package/dist/commands/completion.js +185 -0
- package/dist/commands/crawl.d.ts +12 -0
- package/dist/commands/crawl.js +249 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +233 -0
- package/dist/commands/explain.d.ts +12 -0
- package/dist/commands/explain.js +233 -0
- package/dist/commands/fix.d.ts +13 -0
- package/dist/commands/fix.js +668 -0
- package/dist/commands/health.d.ts +11 -0
- package/dist/commands/health.js +367 -0
- package/dist/commands/history.d.ts +10 -0
- package/dist/commands/history.js +191 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +164 -0
- package/dist/commands/learn.d.ts +8 -0
- package/dist/commands/learn.js +592 -0
- package/dist/commands/pr-check.d.ts +12 -0
- package/dist/commands/pr-check.js +270 -0
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.js +375 -0
- package/dist/commands/scan-storybook.d.ts +18 -0
- package/dist/commands/scan-storybook.js +402 -0
- package/dist/commands/scan.d.ts +25 -0
- package/dist/commands/scan.js +673 -0
- package/dist/commands/stats.d.ts +5 -0
- package/dist/commands/stats.js +137 -0
- package/dist/commands/tree.d.ts +12 -0
- package/dist/commands/tree.js +635 -0
- package/dist/commands/triage.d.ts +13 -0
- package/dist/commands/triage.js +327 -0
- package/dist/commands/watch.d.ts +17 -0
- package/dist/commands/watch.js +302 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/baseline.d.ts +62 -0
- package/dist/utils/baseline.js +169 -0
- package/dist/utils/browser.d.ts +78 -0
- package/dist/utils/browser.js +239 -0
- package/dist/utils/cache.d.ts +76 -0
- package/dist/utils/cache.js +178 -0
- package/dist/utils/config.d.ts +102 -0
- package/dist/utils/config.js +237 -0
- package/dist/utils/converters.d.ts +77 -0
- package/dist/utils/converters.js +200 -0
- package/dist/utils/copilot.d.ts +36 -0
- package/dist/utils/copilot.js +139 -0
- package/dist/utils/detect.d.ts +22 -0
- package/dist/utils/detect.js +197 -0
- package/dist/utils/enhanced-errors.d.ts +46 -0
- package/dist/utils/enhanced-errors.js +295 -0
- package/dist/utils/errors.d.ts +31 -0
- package/dist/utils/errors.js +149 -0
- package/dist/utils/fix-patterns.d.ts +56 -0
- package/dist/utils/fix-patterns.js +529 -0
- package/dist/utils/history-tracking.d.ts +94 -0
- package/dist/utils/history-tracking.js +230 -0
- package/dist/utils/history.d.ts +42 -0
- package/dist/utils/history.js +255 -0
- package/dist/utils/impact-scores.d.ts +44 -0
- package/dist/utils/impact-scores.js +257 -0
- package/dist/utils/retry.d.ts +24 -0
- package/dist/utils/retry.js +76 -0
- package/dist/utils/scanner.d.ts +74 -0
- package/dist/utils/scanner.js +606 -0
- package/dist/utils/scanner.test.d.ts +4 -0
- package/dist/utils/scanner.test.js +162 -0
- package/dist/utils/ui.d.ts +44 -0
- package/dist/utils/ui.js +276 -0
- package/mcp-server/dist/index.d.ts +8 -0
- package/mcp-server/dist/index.js +1923 -0
- package/package.json +88 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the accessibility scanner
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, before, after } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { AccessibilityScanner, calculateScore, generateSummary, createReport, findHtmlFiles, } from '../../dist/utils/scanner.js';
|
|
8
|
+
describe('AccessibilityScanner', () => {
|
|
9
|
+
let scanner;
|
|
10
|
+
before(async () => {
|
|
11
|
+
scanner = new AccessibilityScanner();
|
|
12
|
+
await scanner.init();
|
|
13
|
+
});
|
|
14
|
+
after(async () => {
|
|
15
|
+
await scanner.close();
|
|
16
|
+
});
|
|
17
|
+
it('should scan HTML file with violations', async () => {
|
|
18
|
+
const testFile = resolve(process.cwd(), 'test-fixtures/bad-a11y.html');
|
|
19
|
+
const result = await scanner.scanHtmlFile(testFile);
|
|
20
|
+
assert.ok(result.violations.length > 0, 'Should find violations');
|
|
21
|
+
assert.ok(result.timestamp, 'Should have timestamp');
|
|
22
|
+
assert.ok(result.passes > 0, 'Should have some passing rules');
|
|
23
|
+
});
|
|
24
|
+
it('should scan clean HTML file', async () => {
|
|
25
|
+
const testFile = resolve(process.cwd(), 'test-fixtures/good-a11y.html');
|
|
26
|
+
const result = await scanner.scanHtmlFile(testFile);
|
|
27
|
+
assert.strictEqual(result.violations.length, 0, 'Should find no violations');
|
|
28
|
+
});
|
|
29
|
+
it('should scan HTML string', async () => {
|
|
30
|
+
const html = `
|
|
31
|
+
<!DOCTYPE html>
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head><title>Test</title></head>
|
|
34
|
+
<body><main><h1>Test</h1></main></body>
|
|
35
|
+
</html>
|
|
36
|
+
`;
|
|
37
|
+
const result = await scanner.scanHtmlString(html, 'test-string');
|
|
38
|
+
assert.ok(result, 'Should return result');
|
|
39
|
+
assert.strictEqual(result.file, 'test-string');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('calculateScore', () => {
|
|
43
|
+
it('should return 100 for no violations', () => {
|
|
44
|
+
const results = [{
|
|
45
|
+
url: 'test',
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
violations: [],
|
|
48
|
+
passes: 50,
|
|
49
|
+
incomplete: 0,
|
|
50
|
+
}];
|
|
51
|
+
const score = calculateScore(results);
|
|
52
|
+
assert.strictEqual(score, 100);
|
|
53
|
+
});
|
|
54
|
+
it('should penalize critical violations heavily', () => {
|
|
55
|
+
const criticalViolation = {
|
|
56
|
+
id: 'test',
|
|
57
|
+
impact: 'critical',
|
|
58
|
+
description: 'Test',
|
|
59
|
+
help: 'Test help',
|
|
60
|
+
helpUrl: 'https://example.com',
|
|
61
|
+
tags: [],
|
|
62
|
+
nodes: [{ html: '<div>', target: ['div'], failureSummary: '' }],
|
|
63
|
+
};
|
|
64
|
+
const results = [{
|
|
65
|
+
url: 'test',
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
violations: [criticalViolation],
|
|
68
|
+
passes: 50,
|
|
69
|
+
incomplete: 0,
|
|
70
|
+
}];
|
|
71
|
+
const score = calculateScore(results);
|
|
72
|
+
assert.ok(score < 100, 'Score should be reduced');
|
|
73
|
+
assert.ok(score <= 75, 'Critical violation should reduce score significantly');
|
|
74
|
+
});
|
|
75
|
+
it('should penalize minor violations lightly', () => {
|
|
76
|
+
const minorViolation = {
|
|
77
|
+
id: 'test',
|
|
78
|
+
impact: 'minor',
|
|
79
|
+
description: 'Test',
|
|
80
|
+
help: 'Test help',
|
|
81
|
+
helpUrl: 'https://example.com',
|
|
82
|
+
tags: [],
|
|
83
|
+
nodes: [{ html: '<div>', target: ['div'], failureSummary: '' }],
|
|
84
|
+
};
|
|
85
|
+
const results = [{
|
|
86
|
+
url: 'test',
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
violations: [minorViolation],
|
|
89
|
+
passes: 50,
|
|
90
|
+
incomplete: 0,
|
|
91
|
+
}];
|
|
92
|
+
const score = calculateScore(results);
|
|
93
|
+
assert.ok(score >= 95, 'Minor violation should only reduce score slightly');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('generateSummary', () => {
|
|
97
|
+
it('should count violations by severity', () => {
|
|
98
|
+
const results = [{
|
|
99
|
+
url: 'test',
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
violations: [
|
|
102
|
+
{ id: 'a', impact: 'critical', description: '', help: '', helpUrl: '', tags: [], nodes: [] },
|
|
103
|
+
{ id: 'b', impact: 'serious', description: '', help: '', helpUrl: '', tags: [], nodes: [] },
|
|
104
|
+
{ id: 'c', impact: 'serious', description: '', help: '', helpUrl: '', tags: [], nodes: [] },
|
|
105
|
+
],
|
|
106
|
+
passes: 0,
|
|
107
|
+
incomplete: 0,
|
|
108
|
+
}];
|
|
109
|
+
const summary = generateSummary(results);
|
|
110
|
+
assert.strictEqual(summary.bySeverity.critical, 1);
|
|
111
|
+
assert.strictEqual(summary.bySeverity.serious, 2);
|
|
112
|
+
assert.strictEqual(summary.totalViolations, 3);
|
|
113
|
+
});
|
|
114
|
+
it('should identify top issues', () => {
|
|
115
|
+
const results = [{
|
|
116
|
+
url: 'test',
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
violations: [
|
|
119
|
+
{ id: 'image-alt', impact: 'critical', description: '', help: 'Images need alt', helpUrl: '', tags: [], nodes: [] },
|
|
120
|
+
{ id: 'image-alt', impact: 'critical', description: '', help: 'Images need alt', helpUrl: '', tags: [], nodes: [] },
|
|
121
|
+
{ id: 'button-name', impact: 'critical', description: '', help: 'Buttons need name', helpUrl: '', tags: [], nodes: [] },
|
|
122
|
+
],
|
|
123
|
+
passes: 0,
|
|
124
|
+
incomplete: 0,
|
|
125
|
+
}];
|
|
126
|
+
const summary = generateSummary(results);
|
|
127
|
+
assert.ok(summary.topIssues.length > 0);
|
|
128
|
+
assert.strictEqual(summary.topIssues[0].id, 'image-alt');
|
|
129
|
+
assert.strictEqual(summary.topIssues[0].count, 2);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('findHtmlFiles', () => {
|
|
133
|
+
it('should find HTML files in test-fixtures', async () => {
|
|
134
|
+
const files = await findHtmlFiles(resolve(process.cwd(), 'test-fixtures'));
|
|
135
|
+
assert.ok(files.length >= 2, 'Should find at least 2 HTML files');
|
|
136
|
+
assert.ok(files.some(f => f.includes('bad-a11y.html')));
|
|
137
|
+
assert.ok(files.some(f => f.includes('good-a11y.html')));
|
|
138
|
+
});
|
|
139
|
+
it('should ignore node_modules', async () => {
|
|
140
|
+
const files = await findHtmlFiles(process.cwd());
|
|
141
|
+
const inNodeModules = files.filter(f => f.includes('node_modules'));
|
|
142
|
+
assert.strictEqual(inNodeModules.length, 0, 'Should not include node_modules');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('createReport', () => {
|
|
146
|
+
it('should create valid report structure', () => {
|
|
147
|
+
const results = [{
|
|
148
|
+
url: 'test.html',
|
|
149
|
+
file: 'test.html',
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
violations: [],
|
|
152
|
+
passes: 10,
|
|
153
|
+
incomplete: 0,
|
|
154
|
+
}];
|
|
155
|
+
const report = createReport(results);
|
|
156
|
+
assert.ok(report.version);
|
|
157
|
+
assert.ok(report.scanDate);
|
|
158
|
+
assert.strictEqual(report.totalFiles, 1);
|
|
159
|
+
assert.ok(report.summary);
|
|
160
|
+
assert.strictEqual(report.summary.score, 100);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI utilities for polished output
|
|
3
|
+
*/
|
|
4
|
+
import { type Ora } from 'ora';
|
|
5
|
+
import type { ReportSummary, Violation } from '../types/index.js';
|
|
6
|
+
import type { ImpactScore } from './impact-scores.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a terminal hyperlink using OSC 8 escape sequences
|
|
9
|
+
* Format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
|
|
10
|
+
*
|
|
11
|
+
* For VS Code and other editors, use file:// URLs with line and column:
|
|
12
|
+
* file://path/to/file:line:column
|
|
13
|
+
*
|
|
14
|
+
* @param text - The visible text to display
|
|
15
|
+
* @param url - The URL to link to
|
|
16
|
+
* @returns The text wrapped in hyperlink escape sequences, or plain text if unsupported
|
|
17
|
+
*/
|
|
18
|
+
export declare function hyperlink(text: string, url: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Create a clickable file path link that opens in the editor
|
|
21
|
+
*
|
|
22
|
+
* @param filePath - Absolute or relative path to the file
|
|
23
|
+
* @param line - Optional line number (1-indexed)
|
|
24
|
+
* @param column - Optional column number (1-indexed)
|
|
25
|
+
* @returns Hyperlinked file path or plain text if unsupported
|
|
26
|
+
*/
|
|
27
|
+
export declare function fileLink(filePath: string, line?: number, column?: number): string;
|
|
28
|
+
export declare function printBanner(): void;
|
|
29
|
+
export declare function createSpinner(text: string): Ora;
|
|
30
|
+
export declare function printViolation(violation: Violation, file?: string, absolutePath?: string, impactScore?: ImpactScore): void;
|
|
31
|
+
export declare function printSummary(summary: ReportSummary): void;
|
|
32
|
+
export declare function printSuccess(message: string): void;
|
|
33
|
+
export declare function printError(message: string): void;
|
|
34
|
+
export declare function printWarning(message: string): void;
|
|
35
|
+
export declare function printInfo(message: string): void;
|
|
36
|
+
export declare function printDiff(oldCode: string, newCode: string): void;
|
|
37
|
+
export declare function printFixPrompt(file: string, line: number, issue: string): void;
|
|
38
|
+
export declare function printScoreChange(before: number, after: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Animate score counting up - creates memorable visual feedback
|
|
41
|
+
*/
|
|
42
|
+
export declare function animateScoreUp(from: number, to: number): Promise<void>;
|
|
43
|
+
export declare function printScoreBadge(score: number): void;
|
|
44
|
+
export declare function printFileHeader(file: string, violationCount: number, absolutePath?: string): void;
|
package/dist/utils/ui.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI utilities for polished output
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import boxen from 'boxen';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
// Honor NO_COLOR environment variable (https://no-color.org/)
|
|
8
|
+
const noColor = process.env.NO_COLOR !== undefined || process.env.ALLY_NO_COLOR !== undefined;
|
|
9
|
+
if (noColor) {
|
|
10
|
+
chalk.level = 0;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if terminal hyperlinks should be disabled
|
|
14
|
+
* Disabled in NO_COLOR mode, CI environments, or non-TTY
|
|
15
|
+
*/
|
|
16
|
+
function supportsHyperlinks() {
|
|
17
|
+
if (noColor)
|
|
18
|
+
return false;
|
|
19
|
+
if (process.env.CI)
|
|
20
|
+
return false;
|
|
21
|
+
if (!process.stdout.isTTY)
|
|
22
|
+
return false;
|
|
23
|
+
// Some terminals advertise hyperlink support
|
|
24
|
+
if (process.env.TERM_PROGRAM === 'vscode')
|
|
25
|
+
return true;
|
|
26
|
+
if (process.env.TERM_PROGRAM === 'iTerm.app')
|
|
27
|
+
return true;
|
|
28
|
+
if (process.env.WT_SESSION)
|
|
29
|
+
return true; // Windows Terminal
|
|
30
|
+
if (process.env.KONSOLE_VERSION)
|
|
31
|
+
return true;
|
|
32
|
+
// Default to true for modern terminals
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a terminal hyperlink using OSC 8 escape sequences
|
|
37
|
+
* Format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
|
|
38
|
+
*
|
|
39
|
+
* For VS Code and other editors, use file:// URLs with line and column:
|
|
40
|
+
* file://path/to/file:line:column
|
|
41
|
+
*
|
|
42
|
+
* @param text - The visible text to display
|
|
43
|
+
* @param url - The URL to link to
|
|
44
|
+
* @returns The text wrapped in hyperlink escape sequences, or plain text if unsupported
|
|
45
|
+
*/
|
|
46
|
+
export function hyperlink(text, url) {
|
|
47
|
+
if (!supportsHyperlinks()) {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a clickable file path link that opens in the editor
|
|
54
|
+
*
|
|
55
|
+
* @param filePath - Absolute or relative path to the file
|
|
56
|
+
* @param line - Optional line number (1-indexed)
|
|
57
|
+
* @param column - Optional column number (1-indexed)
|
|
58
|
+
* @returns Hyperlinked file path or plain text if unsupported
|
|
59
|
+
*/
|
|
60
|
+
export function fileLink(filePath, line, column) {
|
|
61
|
+
// Build the file URL with optional line and column
|
|
62
|
+
let url = `file://${filePath}`;
|
|
63
|
+
if (line !== undefined) {
|
|
64
|
+
url += `:${line}`;
|
|
65
|
+
if (column !== undefined) {
|
|
66
|
+
url += `:${column}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Build the display text
|
|
70
|
+
let displayText = filePath;
|
|
71
|
+
if (line !== undefined) {
|
|
72
|
+
displayText += `:${line}`;
|
|
73
|
+
if (column !== undefined) {
|
|
74
|
+
displayText += `:${column}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return hyperlink(displayText, url);
|
|
78
|
+
}
|
|
79
|
+
// Severity colors
|
|
80
|
+
const severityColors = {
|
|
81
|
+
critical: chalk.red.bold,
|
|
82
|
+
serious: chalk.red,
|
|
83
|
+
moderate: chalk.yellow,
|
|
84
|
+
minor: chalk.blue,
|
|
85
|
+
};
|
|
86
|
+
const severityIcons = {
|
|
87
|
+
critical: '!!!',
|
|
88
|
+
serious: '!!',
|
|
89
|
+
moderate: '!',
|
|
90
|
+
minor: 'i',
|
|
91
|
+
};
|
|
92
|
+
export function printBanner() {
|
|
93
|
+
const banner = chalk.cyan.bold(`
|
|
94
|
+
__ _ | | _ _
|
|
95
|
+
/ _\` | | || | | |
|
|
96
|
+
| (_| | | || |_| |
|
|
97
|
+
\\__,_| |_| \\__, |
|
|
98
|
+
|___/ v1.0.0
|
|
99
|
+
`);
|
|
100
|
+
console.log(banner);
|
|
101
|
+
console.log(chalk.dim(' Your codebase\'s accessibility ally\n'));
|
|
102
|
+
}
|
|
103
|
+
export function createSpinner(text) {
|
|
104
|
+
return ora({
|
|
105
|
+
text,
|
|
106
|
+
color: 'cyan',
|
|
107
|
+
spinner: 'dots',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function printViolation(violation, file, absolutePath, impactScore) {
|
|
111
|
+
const color = severityColors[violation.impact];
|
|
112
|
+
const icon = severityIcons[violation.impact];
|
|
113
|
+
// Print severity and impact score on the same line
|
|
114
|
+
if (impactScore) {
|
|
115
|
+
const scoreColor = impactScore.score >= 95 ? chalk.red.bold :
|
|
116
|
+
impactScore.score >= 75 ? chalk.red :
|
|
117
|
+
impactScore.score >= 40 ? chalk.yellow :
|
|
118
|
+
chalk.blue;
|
|
119
|
+
console.log(color(` [${icon}] ${violation.impact.toUpperCase()}`) +
|
|
120
|
+
scoreColor(` Impact: ${impactScore.score}/100`) +
|
|
121
|
+
chalk.dim(` (WCAG ${impactScore.wcagLevel})`));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(color(` [${icon}] ${violation.impact.toUpperCase()}`));
|
|
125
|
+
}
|
|
126
|
+
console.log(chalk.white(` ${violation.help}`));
|
|
127
|
+
// Print impact reasoning if available
|
|
128
|
+
if (impactScore) {
|
|
129
|
+
console.log(chalk.dim(` 💡 ${impactScore.reasoning}`));
|
|
130
|
+
console.log(chalk.dim(` 👥 Affects: `) +
|
|
131
|
+
chalk.cyan(impactScore.affectedUsers.join(', ')));
|
|
132
|
+
console.log(chalk.dim(` 📊 Estimated: `) +
|
|
133
|
+
chalk.cyan(impactScore.estimatedUsers));
|
|
134
|
+
}
|
|
135
|
+
if (file) {
|
|
136
|
+
// Use absolute path for the hyperlink if available, display relative path
|
|
137
|
+
const linkPath = absolutePath || file;
|
|
138
|
+
const linkedFile = fileLink(linkPath);
|
|
139
|
+
// Apply dim styling to the linked text
|
|
140
|
+
console.log(chalk.dim(` File: `) + chalk.dim(linkedFile));
|
|
141
|
+
}
|
|
142
|
+
violation.nodes.forEach((node, i) => {
|
|
143
|
+
if (i < 3) { // Limit to 3 nodes for readability
|
|
144
|
+
const target = node.target.join(' > ');
|
|
145
|
+
console.log(chalk.dim(` → ${target}`));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (violation.nodes.length > 3) {
|
|
149
|
+
console.log(chalk.dim(` ... and ${violation.nodes.length - 3} more occurrences`));
|
|
150
|
+
}
|
|
151
|
+
// Make the help URL clickable too
|
|
152
|
+
const helpLink = hyperlink(violation.helpUrl, violation.helpUrl);
|
|
153
|
+
console.log(chalk.dim(` Learn more: `) + chalk.dim(helpLink));
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
export function printSummary(summary) {
|
|
157
|
+
const { totalViolations, bySeverity, score } = summary;
|
|
158
|
+
// Score color
|
|
159
|
+
let scoreColor = chalk.green;
|
|
160
|
+
if (score < 50)
|
|
161
|
+
scoreColor = chalk.red;
|
|
162
|
+
else if (score < 75)
|
|
163
|
+
scoreColor = chalk.yellow;
|
|
164
|
+
const summaryText = `
|
|
165
|
+
${chalk.bold('Accessibility Score:')} ${scoreColor.bold(`${score}/100`)}
|
|
166
|
+
|
|
167
|
+
${chalk.red.bold(`!!! CRITICAL: ${bySeverity.critical || 0}`)}
|
|
168
|
+
${chalk.red(`!! SERIOUS: ${bySeverity.serious || 0}`)}
|
|
169
|
+
${chalk.yellow(`! MODERATE: ${bySeverity.moderate || 0}`)}
|
|
170
|
+
${chalk.blue(`i MINOR: ${bySeverity.minor || 0}`)}
|
|
171
|
+
|
|
172
|
+
${chalk.dim(`Total issues: ${totalViolations}`)}
|
|
173
|
+
`;
|
|
174
|
+
console.log(boxen(summaryText.trim(), {
|
|
175
|
+
padding: 1,
|
|
176
|
+
margin: 1,
|
|
177
|
+
borderStyle: 'round',
|
|
178
|
+
borderColor: score >= 75 ? 'green' : score >= 50 ? 'yellow' : 'red',
|
|
179
|
+
title: 'Scan Results',
|
|
180
|
+
titleAlignment: 'center',
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
export function printSuccess(message) {
|
|
184
|
+
console.log(chalk.green('✓ ') + message);
|
|
185
|
+
}
|
|
186
|
+
export function printError(message) {
|
|
187
|
+
console.log(chalk.red('✗ ') + message);
|
|
188
|
+
}
|
|
189
|
+
export function printWarning(message) {
|
|
190
|
+
console.log(chalk.yellow('⚠ ') + message);
|
|
191
|
+
}
|
|
192
|
+
export function printInfo(message) {
|
|
193
|
+
console.log(chalk.blue('ℹ ') + message);
|
|
194
|
+
}
|
|
195
|
+
export function printDiff(oldCode, newCode) {
|
|
196
|
+
console.log(chalk.red(` - ${oldCode}`));
|
|
197
|
+
console.log(chalk.green(` + ${newCode}`));
|
|
198
|
+
}
|
|
199
|
+
export function printFixPrompt(file, line, issue) {
|
|
200
|
+
console.log();
|
|
201
|
+
console.log(chalk.cyan.bold(`📝 Fixing: ${file}:${line}`));
|
|
202
|
+
console.log(chalk.dim(` Issue: ${issue}`));
|
|
203
|
+
console.log();
|
|
204
|
+
}
|
|
205
|
+
export function printScoreChange(before, after) {
|
|
206
|
+
const diff = after - before;
|
|
207
|
+
const diffStr = diff > 0 ? chalk.green(`+${diff}`) : chalk.red(`${diff}`);
|
|
208
|
+
console.log(boxen(`Score: ${before} → ${chalk.bold(after.toString())} (${diffStr})`, {
|
|
209
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
210
|
+
borderStyle: 'round',
|
|
211
|
+
borderColor: diff > 0 ? 'green' : 'red',
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Animate score counting up - creates memorable visual feedback
|
|
216
|
+
*/
|
|
217
|
+
export async function animateScoreUp(from, to) {
|
|
218
|
+
if (noColor || from >= to) {
|
|
219
|
+
// No animation in no-color mode or if score didn't improve
|
|
220
|
+
printScoreBadge(to);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const frames = Math.min(to - from, 20); // Max 20 frames
|
|
224
|
+
const step = (to - from) / frames;
|
|
225
|
+
const delay = 50; // 50ms per frame
|
|
226
|
+
for (let i = 0; i <= frames; i++) {
|
|
227
|
+
const current = Math.round(from + (step * i));
|
|
228
|
+
process.stdout.write(`\r${getScoreBar(current)}`);
|
|
229
|
+
await sleep(delay);
|
|
230
|
+
}
|
|
231
|
+
console.log(); // New line after animation
|
|
232
|
+
printScoreBadge(to);
|
|
233
|
+
}
|
|
234
|
+
function sleep(ms) {
|
|
235
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
236
|
+
}
|
|
237
|
+
function getScoreBar(score) {
|
|
238
|
+
const width = 30;
|
|
239
|
+
const filled = Math.round((score / 100) * width);
|
|
240
|
+
const empty = width - filled;
|
|
241
|
+
const color = score >= 90 ? chalk.green
|
|
242
|
+
: score >= 75 ? chalk.yellow
|
|
243
|
+
: score >= 50 ? chalk.hex('#FFA500')
|
|
244
|
+
: chalk.red;
|
|
245
|
+
const bar = color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
246
|
+
const scoreText = color.bold(`${score}%`);
|
|
247
|
+
return ` ${bar} ${scoreText}`;
|
|
248
|
+
}
|
|
249
|
+
export function printScoreBadge(score) {
|
|
250
|
+
const emoji = score >= 90 ? '🌟'
|
|
251
|
+
: score >= 75 ? '✅'
|
|
252
|
+
: score >= 50 ? '⚠️'
|
|
253
|
+
: '❌';
|
|
254
|
+
const color = score >= 90 ? chalk.green
|
|
255
|
+
: score >= 75 ? chalk.yellow
|
|
256
|
+
: score >= 50 ? chalk.hex('#FFA500')
|
|
257
|
+
: chalk.red;
|
|
258
|
+
console.log(boxen(`${emoji} Accessibility Score: ${color.bold(`${score}/100`)}`, {
|
|
259
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
260
|
+
borderStyle: 'round',
|
|
261
|
+
borderColor: score >= 75 ? 'green' : score >= 50 ? 'yellow' : 'red',
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
export function printFileHeader(file, violationCount, absolutePath) {
|
|
265
|
+
const color = violationCount > 0 ? chalk.yellow : chalk.green;
|
|
266
|
+
// Use absolute path for the hyperlink if available
|
|
267
|
+
const linkPath = absolutePath || file;
|
|
268
|
+
const linkedFile = fileLink(linkPath);
|
|
269
|
+
console.log(color.bold(`\n📄 `) + color.bold(linkedFile));
|
|
270
|
+
if (violationCount > 0) {
|
|
271
|
+
console.log(chalk.dim(` ${violationCount} issue${violationCount === 1 ? '' : 's'} found`));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log(chalk.green.dim(' No issues found'));
|
|
275
|
+
}
|
|
276
|
+
}
|