snap-ally 0.3.0 → 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/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/snap-ally.svg)](https://www.npmjs.com/package/snap-ally)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/U7U1U2V3V)
5
6
 
6
7
  A powerful, developer-friendly Playwright reporter for **Accessibility testing** using Axe-core. Beyond just reporting, it provides visual evidence to help developers fix accessibility issues faster.
7
8
 
@@ -60,15 +61,18 @@ export default defineConfig({
60
61
  outputFolder: 'a11y-report',
61
62
  // Optional: Visual Customization
62
63
  colors: {
63
- critical: '#dc2626',
64
- serious: '#ea580c',
65
- moderate: '#f59e0b',
66
- minor: '#0ea5e9',
64
+ critical: '#b91c1c',
65
+ serious: '#c2410c',
66
+ moderate: '#a16207',
67
+ minor: '#1e40af',
67
68
  },
69
+ verbose: true, // Show in terminal
70
+ consoleLog: true, // Show in browser console
68
71
  // Optional: Azure DevOps Integration
69
72
  ado: {
70
73
  organization: 'your-org',
71
74
  project: 'your-project',
75
+ areaPath: 'your-project\\your-team', // Optional: Define where bugs should be created
72
76
  },
73
77
  },
74
78
  ],
@@ -80,20 +84,20 @@ export default defineConfig({
80
84
 
81
85
  ## <span aria-hidden="true">📖</span> Usage
82
86
 
83
- Import and use `scanA11y` within your Playwright tests:
87
+ Import and use `checkAccessibility` within your Playwright tests:
84
88
 
85
89
  ```typescript
86
90
  import { test } from '@playwright/test';
87
- import { scanA11y } from 'snap-ally';
91
+ import { checkAccessibility } from 'snap-ally';
88
92
 
89
93
  test('verify page accessibility', async ({ page }, testInfo) => {
90
94
  await page.goto('https://example.com');
91
95
 
92
96
  // Basic scan
93
- await scanA11y(page, testInfo);
97
+ await checkAccessibility(page, testInfo);
94
98
 
95
99
  // Advanced scan with configuration
96
- await scanA11y(page, testInfo, {
100
+ await checkAccessibility(page, testInfo, {
97
101
  verbose: true, // Log results to terminal
98
102
  consoleLog: true, // Log results to browser console
99
103
  pageKey: 'Homepage', // Custom name for the report file
@@ -113,13 +117,16 @@ test('verify page accessibility', async ({ page }, testInfo) => {
113
117
 
114
118
  | Option | Type | Description |
115
119
  | ------------------ | -------- | --------------------------------------------------------------- |
116
- | `outputFolder` | `string` | Where to save the reports. Defaults to `steps-report`. |
117
- | `colors` | `object` | Customize severity colors (critical, serious, moderate, minor). |
118
- | `ado` | `object` | Azure DevOps configuration for deep linking. |
120
+ | `outputFolder` | `string` | Where to save the reports. Defaults to `steps-report`. |
121
+ | `colors` | `object` | Customize severity colors (critical, serious, moderate, minor). |
122
+ | `verbose` | `boolean` | **Default**: `true`. Show violations in terminal. |
123
+ | `consoleLog` | `boolean` | **Default**: `true`. Show violations in browser console. |
124
+ | `ado` | `object` | Azure DevOps configuration for deep linking. |
119
125
  | `ado.organization` | `string` | Your Azure DevOps organization name. |
120
126
  | `ado.project` | `string` | Your Azure DevOps project name. |
127
+ | `ado.areaPath` | `string` | Optional: The Area Path where bugs should be created. |
121
128
 
122
- ### `scanA11y` Options
129
+ ### `checkAccessibility` Options
123
130
 
124
131
  | Option | Type | Description |
125
132
  | ------------ | ---------- | -------------------------------------------------------------------------- |
@@ -68,7 +68,7 @@ class SnapAllyReporter {
68
68
  };
69
69
  this.testRuleCounts = {};
70
70
  this.testGlobalCounts = {};
71
- this.options = options;
71
+ this.options = { verbose: true, consoleLog: true, ...options };
72
72
  this.outputFolder = path.resolve(process.cwd(), options.outputFolder || 'steps-report');
73
73
  this.validateOutputFolder(this.outputFolder);
74
74
  this.colors = {
@@ -92,23 +92,23 @@ class SnapAllyReporter {
92
92
  const normalizedCwd = path.normalize(cwd);
93
93
  // Prevent deletion of current working directory
94
94
  if (normalizedPath === normalizedCwd) {
95
- throw new Error(`[SnapAlly] Invalid outputFolder: Cannot delete the current working directory. ` +
95
+ throw new Error('[SnapAlly] Invalid outputFolder: Cannot delete the current working directory. ' +
96
96
  `Resolved path: "${resolvedPath}"`);
97
97
  }
98
98
  // Prevent deletion of parent directories
99
99
  if (normalizedCwd.startsWith(normalizedPath + path.sep) || normalizedCwd.startsWith(normalizedPath + '/')) {
100
- throw new Error(`[SnapAlly] Invalid outputFolder: Cannot delete a parent directory of the current working directory. ` +
100
+ throw new Error('[SnapAlly] Invalid outputFolder: Cannot delete a parent directory of the current working directory. ' +
101
101
  `Resolved path: "${resolvedPath}"`);
102
102
  }
103
103
  // Prevent deletion of root or near-root directories
104
104
  const pathSegments = normalizedPath.split(path.sep).filter(s => s.length > 0);
105
105
  if (pathSegments.length <= 1) {
106
- throw new Error(`[SnapAlly] Invalid outputFolder: Path is too close to root directory. ` +
106
+ throw new Error('[SnapAlly] Invalid outputFolder: Path is too close to root directory. ' +
107
107
  `Resolved path: "${resolvedPath}"`);
108
108
  }
109
109
  // Ensure the path is within the current working directory (safest approach)
110
110
  if (!normalizedPath.startsWith(normalizedCwd + path.sep) && !normalizedPath.startsWith(normalizedCwd + '/')) {
111
- throw new Error(`[SnapAlly] Invalid outputFolder: Path must be within the current working directory. ` +
111
+ throw new Error('[SnapAlly] Invalid outputFolder: Path must be within the current working directory. ' +
112
112
  `Resolved path: "${resolvedPath}", CWD: "${cwd}"`);
113
113
  }
114
114
  }
@@ -134,17 +134,14 @@ class SnapAllyReporter {
134
134
  console.log(`\n[SnapAlly] Report generated: ${summaryPath}`);
135
135
  }
136
136
  async processTestResult(test, result, index) {
137
- var _a, _b, _c, _d;
137
+ var _a, _b, _c;
138
138
  const testFolderName = `test-${index}`;
139
139
  const testFolder = path.join(this.outputFolder, testFolderName);
140
140
  const videoPath = this.assetsManager.copyVideos(result, testFolder);
141
141
  const screenshotPaths = this.assetsManager.copyScreenshots(result, testFolder);
142
142
  const attachments = this.assetsManager.copyAllOtherAttachments(result, testFolder);
143
143
  const a11yAttachment = result.attachments.find((a) => a.name === 'A11y');
144
- if (a11yAttachment) {
145
- console.log(`[SnapAlly] Found A11y attachment for project: ${test._projectId}`);
146
- }
147
- else {
144
+ if (!a11yAttachment && this.options.verbose) {
148
145
  console.warn(`[SnapAlly] A11y attachment missing for test: ${test.title}. Available: ${result.attachments.map(a => a.name).join(', ')}`);
149
146
  }
150
147
  let a11yData = null;
@@ -157,16 +154,14 @@ class SnapAllyReporter {
157
154
  }
158
155
  }
159
156
  // Handle cases where a11yData might be the direct ReportData or wrapped in a data property
160
- const actualData = (a11yData === null || a11yData === void 0 ? void 0 : a11yData.data) || a11yData;
157
+ const actualData = (a11yData && typeof a11yData === 'object' && 'data' in a11yData ? a11yData.data : a11yData);
161
158
  const violations = (actualData === null || actualData === void 0 ? void 0 : actualData.a11yErrors) || (actualData === null || actualData === void 0 ? void 0 : actualData.violations) || [];
162
159
  const a11yErrorCount = violations.reduce((acc, curr) => { var _a, _b; return acc + (curr.total || ((_a = curr.target) === null || _a === void 0 ? void 0 : _a.length) || ((_b = curr.nodes) === null || _b === void 0 ? void 0 : _b.length) || 0); }, 0);
163
160
  const filteredSteps = (() => {
164
- const sRaw = result.steps.map(s => s.title);
165
161
  const blocklist = ['Evaluate', 'Create page', 'Close page', 'Before Hooks', 'After Hooks', 'Worker Teardown', 'Worker Cleanup', 'Attach', 'Wait for timeout', 'Capture A11y screenshot'];
166
162
  const filtered = result.steps
167
163
  .filter((s) => !blocklist.some(b => s.title.includes(b)))
168
164
  .map((s) => s.title);
169
- console.log(`[SnapAlly] Steps for ${test.title}: Raw=${sRaw.length}, Filtered=${filtered.length}`);
170
165
  return filtered;
171
166
  })();
172
167
  const testResults = {
@@ -181,13 +176,11 @@ class SnapAllyReporter {
181
176
  statusIcon: this.getStatusIcon(result.status),
182
177
  browser: (() => {
183
178
  var _a, _b;
184
- if (test.outcome() === 'skipped')
185
- return 'n/a';
186
179
  const bName = test._projectId ||
187
180
  ((_b = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project()) === null || _b === void 0 ? void 0 : _b.name) ||
188
181
  test.projectName ||
189
182
  'chromium';
190
- return bName;
183
+ return test.outcome() === 'skipped' ? 'n/a' : bName;
191
184
  })(),
192
185
  adoOrganization: ((_a = this.options.ado) === null || _a === void 0 ? void 0 : _a.organization) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoOrganization),
193
186
  adoProject: ((_b = this.options.ado) === null || _b === void 0 ? void 0 : _b.project) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoProject),
@@ -222,7 +215,7 @@ class SnapAllyReporter {
222
215
  console.log(`[SnapAlly] Generating A11y report for ${test.title} (Browser: ${testResults.browser})`);
223
216
  await this.renderer.render('accessibility-report.html', testResults, testFolder, a11yReportPath);
224
217
  }
225
- console.log(`[SnapAlly] Data state for "${test.title}": browser=${testResults.browser}, a11yErrors=${((_d = testResults.a11yErrors) === null || _d === void 0 ? void 0 : _d.length) || 0}`);
218
+ // Removed debug logging of internal data state
226
219
  const reportPath = path.join(testFolder, reportFileName);
227
220
  await this.renderer.render('test-execution-report.html', testResults, testFolder, reportPath);
228
221
  this.updateSummary(test, testResults);
@@ -299,7 +292,7 @@ class SnapAllyReporter {
299
292
  if (!this.executionSummary.wcagErrors[ruleId]) {
300
293
  this.executionSummary.wcagErrors[ruleId] = {
301
294
  count: 0,
302
- severity: err.severity || err.impact,
295
+ severity: err.severity || err.impact || 'minor',
303
296
  helpUrl: err.helpUrl,
304
297
  description: err.description,
305
298
  };
@@ -315,7 +308,7 @@ class SnapAllyReporter {
315
308
  if (!bSummary.wcagErrors[ruleId]) {
316
309
  bSummary.wcagErrors[ruleId] = {
317
310
  count: 0,
318
- severity: err.severity || err.impact,
311
+ severity: err.severity || err.impact || 'minor',
319
312
  helpUrl: err.helpUrl,
320
313
  description: err.description,
321
314
  };
@@ -27,9 +27,15 @@ function sanitizePageKey(input) {
27
27
  * Performs an accessibility audit using Axe and Lighthouse.
28
28
  */
29
29
  async function scanA11y(page, testInfo, options = {}) {
30
- var _a, _b, _c, _d, _e, _f;
31
- const showTerminal = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
32
- const showBrowser = (_b = options.consoleLog) !== null && _b !== void 0 ? _b : true;
30
+ var _a, _b, _c, _d, _e, _f, _g, _h;
31
+ // 1. Find reporter config for global defaults
32
+ const reporterConfig = testInfo.config.reporter.find((r) => Array.isArray(r) &&
33
+ (typeof r[0] === 'string' &&
34
+ (r[0].includes('SnapAllyReporter') || r[0].endsWith('SnapAllyReporter.ts'))));
35
+ const globalOptions = (Array.isArray(reporterConfig) ? ((_a = reporterConfig[1]) !== null && _a !== void 0 ? _a : {}) : {});
36
+ // 2. Resolve final options (local > global > default)
37
+ const showTerminal = (_c = (_b = options.verbose) !== null && _b !== void 0 ? _b : globalOptions.verbose) !== null && _c !== void 0 ? _c : true;
38
+ const showBrowser = (_e = (_d = options.consoleLog) !== null && _d !== void 0 ? _d : globalOptions.consoleLog) !== null && _e !== void 0 ? _e : true;
33
39
  const rawPageKey = options.pageKey || page.url();
34
40
  const pageKey = sanitizePageKey(rawPageKey);
35
41
  const overlay = new VisualReporter_1.VisualReporter(page);
@@ -82,14 +88,10 @@ async function scanA11y(page, testInfo, options = {}) {
82
88
  }, [mainMsg, detailMessages, models_1.DEFAULT_COLORS.serious]);
83
89
  }
84
90
  }
85
- test_1.expect
86
- .soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`)
87
- .toBe(0);
88
- const reporterConfig = testInfo.config.reporter.find((r) => Array.isArray(r) &&
89
- (typeof r[0] === 'string' &&
90
- (r[0].includes('SnapAllyReporter') || r[0].endsWith('src/SnapAllyReporter.ts'))));
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
- const customColors = reporterConfig && Array.isArray(reporterConfig) ? (_c = reporterConfig[1]) === null || _c === void 0 ? void 0 : _c.colors : undefined;
91
+ await test_1.test.step('Check Accessibility', async () => {
92
+ test_1.expect.soft(violationCount).toBe(0);
93
+ });
94
+ const customColors = globalOptions === null || globalOptions === void 0 ? void 0 : globalOptions.colors;
93
95
  const errors = [];
94
96
  for (const violation of axeResults.violations) {
95
97
  let errorIdx = 0;
@@ -151,9 +153,9 @@ async function scanA11y(page, testInfo, options = {}) {
151
153
  seriousColor: (customColors === null || customColors === void 0 ? void 0 : customColors.serious) || models_1.DEFAULT_COLORS.serious,
152
154
  moderateColor: (customColors === null || customColors === void 0 ? void 0 : customColors.moderate) || models_1.DEFAULT_COLORS.moderate,
153
155
  minorColor: (customColors === null || customColors === void 0 ? void 0 : customColors.minor) || models_1.DEFAULT_COLORS.minor,
154
- adoOrganization: ((_d = options.ado) === null || _d === void 0 ? void 0 : _d.organization) || process.env.ADO_ORGANIZATION || '',
155
- adoProject: ((_e = options.ado) === null || _e === void 0 ? void 0 : _e.project) || process.env.ADO_PROJECT || '',
156
- adoAreaPath: ((_f = options.ado) === null || _f === void 0 ? void 0 : _f.areaPath) || process.env.ADO_AREA_PATH || '',
156
+ adoOrganization: ((_f = options.ado) === null || _f === void 0 ? void 0 : _f.organization) || process.env.ADO_ORGANIZATION || '',
157
+ adoProject: ((_g = options.ado) === null || _g === void 0 ? void 0 : _g.project) || process.env.ADO_PROJECT || '',
158
+ adoAreaPath: ((_h = options.ado) === null || _h === void 0 ? void 0 : _h.areaPath) || process.env.ADO_AREA_PATH || '',
157
159
  timestamp: TimeUtils_1.TimeUtils.formatDate(new Date()),
158
160
  };
159
161
  await overlay.attachJsonData(testInfo, 'A11y', JSON.stringify(reportData));
@@ -13,6 +13,16 @@ export interface ReporterOptions {
13
13
  moderate?: string;
14
14
  minor?: string;
15
15
  };
16
+ /**
17
+ * Whether to log violations to the terminal.
18
+ * @default true
19
+ */
20
+ verbose?: boolean;
21
+ /**
22
+ * Whether to log violations to the browser console.
23
+ * @default true
24
+ */
25
+ consoleLog?: boolean;
16
26
  /**
17
27
  * Azure DevOps integration options.
18
28
  */
@@ -4,10 +4,10 @@ exports.FALLBACK_GRAY = exports.DEFAULT_COLORS = void 0;
4
4
  exports.getSeverityColor = getSeverityColor;
5
5
  /** Default severity colors used when the user doesn't override them. */
6
6
  exports.DEFAULT_COLORS = {
7
- critical: '#dc2626', // Power Red
8
- serious: '#ea580c', // Deep Orange
9
- moderate: '#f59e0b', // Amber/Honey
10
- minor: '#f0f06f', // Ocean Blue (Updated to Yellow per user request)
7
+ critical: '#b91c1c', // Deep Red
8
+ serious: '#c2410c', // Deep Orange
9
+ moderate: '#a16207', // Dark Amber
10
+ minor: '#1e40af', // Royal Blue
11
11
  };
12
12
  /** Default fallback color for unknown severities. */
13
13
  exports.FALLBACK_GRAY = '#757575';
@@ -711,8 +711,10 @@ function renderBarChart(elementId, wcagData, colors) {
711
711
 
712
712
  const chartColors = wcagEntries.map((e) => (colors && colors[e[1].severity]) || '#ef4444');
713
713
 
714
+ const chartHeight = 80 + (wcagEntries.length * 50);
715
+
714
716
  new ApexCharts(el, {
715
- chart: { type: 'bar', height: 350, toolbar: { show: false } },
717
+ chart: { type: 'bar', height: chartHeight, toolbar: { show: false } },
716
718
  series: [{ name: 'Violations', data: wcagEntries.map((e) => e[1].count) }],
717
719
  xaxis: {
718
720
  categories: wcagEntries.map((e) => e[0]),
@@ -733,7 +735,7 @@ function renderBarChart(elementId, wcagData, colors) {
733
735
  bar: {
734
736
  borderRadius: 6,
735
737
  horizontal: true,
736
- barHeight: '70%',
738
+ barHeight: wcagEntries.length === 1 ? '35%' : wcagEntries.length <= 3 ? '50%' : '75%',
737
739
  distributed: true,
738
740
  dataLabels: { position: 'top' },
739
741
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snap-ally",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "A custom Playwright reporter for Accessibility testing using Axe, with HTML reporting and Azure DevOps integration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,4 +48,4 @@
48
48
  "prettier": "^3.8.1",
49
49
  "typescript": "^5.0.0"
50
50
  }
51
- }
51
+ }