snap-ally 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 apis3445
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # snap-ally <span aria-hidden="true">πŸ“Έβ™Ώ</span>
2
+
3
+ [![npm version](https://img.shields.io/npm/v/snap-ally.svg)](https://www.npmjs.com/package/snap-ally)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ 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
+ ---
9
+
10
+ ## <span aria-hidden="true">πŸ“Ί</span> Demo
11
+
12
+ <div align="center">
13
+ <video src="video.webm" width="800" controls aria-label="Snap-Ally accessibility reporter demonstration video showing HTML reports and visual overlays">
14
+ Your browser does not support the video tag. You can <a href="video.webm">download the video</a> to view it.
15
+ </video>
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## <span aria-hidden="true">✨</span> Features
21
+
22
+ - **Beautiful HTML Reporting**: Comprehensive summary and detail pages.
23
+ - **Visual Overlays**: Highlights violations directly on the page in screenshots.
24
+ - **Automated Bug Preview**: Generates bug-like reports for each violation with clear technical details.
25
+ - **Azure DevOps (ADO) Integration**: Link directly to your ADO project to create/manage accessibility bugs.
26
+ - **Video & Screenshots**: Automatically captures and attaches video/screenshots of the failing state.
27
+ - **Configurable Axe Rules**: Enable/Disable specific rules or filter by WCAG tags.
28
+
29
+ ---
30
+
31
+ ## <span aria-hidden="true">πŸš€</span> Installation
32
+
33
+ ```bash
34
+ npm install snap-ally --save-dev
35
+ ```
36
+
37
+ ---
38
+
39
+ ## <span aria-hidden="true">πŸ› οΈ</span> Setup
40
+
41
+ Add `snap-ally` to your `playwright.config.ts`:
42
+
43
+ ```typescript
44
+ import { defineConfig } from '@playwright/test';
45
+
46
+ export default defineConfig({
47
+ reporter: [
48
+ ['snap-ally', {
49
+ outputFolder: 'a11y-report',
50
+ // Optional: Visual Customization
51
+ colors: {
52
+ critical: '#dc2626',
53
+ serious: '#ea580c',
54
+ moderate: '#f59e0b',
55
+ minor: '#0ea5e9',
56
+ },
57
+ // Optional: Azure DevOps Integration
58
+ ado: {
59
+ organization: 'your-org',
60
+ project: 'your-project'
61
+ }
62
+ }]
63
+ ],
64
+ });
65
+ ```
66
+
67
+ ---
68
+
69
+ ## <span aria-hidden="true">πŸ“–</span> Usage
70
+
71
+ Import and use `scanA11y` within your Playwright tests:
72
+
73
+ ```typescript
74
+ import { test } from '@playwright/test';
75
+ import { scanA11y } from 'snap-ally';
76
+
77
+ test('verify page accessibility', async ({ page }, testInfo) => {
78
+ await page.goto('https://example.com');
79
+
80
+ // Basic scan
81
+ await scanA11y(page, testInfo);
82
+
83
+ // Advanced scan with configuration
84
+ await scanA11y(page, testInfo, {
85
+ rules: {
86
+ 'color-contrast': { enabled: false }, // Disable specific rule
87
+ },
88
+ tags: ['wcag2a', 'wcag2aa'], // Focus on specific WCAG levels
89
+ verbose: true,
90
+ pageKey: 'Homepage' // Custom name for the report file
91
+ });
92
+ });
93
+ ```
94
+
95
+ ---
96
+
97
+ ## <span aria-hidden="true">βš™οΈ</span> Configuration Options
98
+
99
+ ### Reporter Options (in `playwright.config.ts`)
100
+
101
+ | Option | Type | Description |
102
+ | --- | --- | --- |
103
+ | `outputFolder` | `string` | Where to save the reports. Defaults to `steps-report`. |
104
+ | `colors` | `object` | Customize severity colors (critical, serious, moderate, minor). |
105
+ | `ado` | `object` | Azure DevOps configuration for deep linking. |
106
+ | `ado.organization` | `string` | Your Azure DevOps organization name. |
107
+ | `ado.project` | `string` | Your Azure DevOps project name. |
108
+
109
+ ### `scanA11y` Options
110
+
111
+ | Option | Type | Description |
112
+ | --- | --- | --- |
113
+ | `include` | `string` | CSS selector to limit the scan to a specific element. |
114
+ | `verbose` | `boolean` | Log violations to the console. Defaults to `true`. |
115
+ | `rules` | `object` | Axe-core rule configuration. |
116
+ | `tags` | `string[]` | List of Axe-core tags to run (e.g., `['wcag2aa']`). |
117
+ | `pageKey` | `string` | Custom identifier for the report file name. |
118
+
119
+ ---
120
+
121
+ ## <span aria-hidden="true">πŸ›‘οΈ</span> License
122
+
123
+ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
124
+
125
+ ---
126
+
127
+ ## <span aria-hidden="true">🀝</span> Contributing
128
+
129
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,42 @@
1
+ import { Page, TestInfo } from '@playwright/test';
2
+ export interface AuditAnnotation {
3
+ type: string;
4
+ description: string;
5
+ keyPage: string;
6
+ }
7
+ /**
8
+ * Handles visual feedback and Playwright annotations during an accessibility audit.
9
+ */
10
+ export declare class A11yAuditOverlay {
11
+ protected page: Page;
12
+ protected keyPage: string;
13
+ private readonly overlayRootId;
14
+ private auditAnnotations;
15
+ constructor(page: Page, keyPage: string);
16
+ reset(): void;
17
+ /**
18
+ * Shows a compact, modern banner at the bottom of the page describing the violation.
19
+ */
20
+ showViolationOverlay(violation: {
21
+ id: string;
22
+ help: string;
23
+ }, color: string): Promise<void>;
24
+ /**
25
+ * Removes the violation description overlay.
26
+ */
27
+ hideViolationOverlay(): Promise<void>;
28
+ /**
29
+ * Attaches accessibility data to the Playwright test report.
30
+ */
31
+ addTestAttachment(testInfo: TestInfo, name: string, description: string): Promise<void>;
32
+ getAuditAnnotations(): AuditAnnotation[];
33
+ /**
34
+ * Captures a screenshot and attaches it to the test report.
35
+ */
36
+ captureAndAttachScreenshot(fileName: string, testInfo: TestInfo): Promise<Buffer>;
37
+ highlightElement(selector: string, color: string): Promise<void>;
38
+ /**
39
+ * Removes highlighting from an element.
40
+ */
41
+ unhighlightElement(): Promise<void>;
42
+ }
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.A11yAuditOverlay = void 0;
4
+ const test_1 = require("@playwright/test");
5
+ /**
6
+ * Handles visual feedback and Playwright annotations during an accessibility audit.
7
+ */
8
+ class A11yAuditOverlay {
9
+ constructor(page, keyPage) {
10
+ this.page = page;
11
+ this.keyPage = keyPage;
12
+ this.overlayRootId = 'a11y-audit-overlay-root';
13
+ this.auditAnnotations = [];
14
+ }
15
+ reset() {
16
+ this.auditAnnotations = [];
17
+ }
18
+ /**
19
+ * Shows a compact, modern banner at the bottom of the page describing the violation.
20
+ */
21
+ async showViolationOverlay(violation, color) {
22
+ await this.page.evaluate(([v, color, rootId]) => {
23
+ let root = document.getElementById(rootId);
24
+ if (!root) {
25
+ root = document.createElement('div');
26
+ root.id = rootId;
27
+ root.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647;';
28
+ document.body.appendChild(root);
29
+ root.attachShadow({ mode: 'open' });
30
+ }
31
+ const shadow = root.shadowRoot;
32
+ let container = shadow.getElementById('a11y-banner');
33
+ if (!container) {
34
+ const style = document.createElement('style');
35
+ style.textContent = `
36
+ #a11y-banner {
37
+ position: fixed;
38
+ left: 50%;
39
+ bottom: 24px;
40
+ transform: translateX(-50%);
41
+ width: calc(100% - 40px);
42
+ max-width: 600px;
43
+ padding: 12px 18px;
44
+ border-radius: 12px;
45
+ color: white;
46
+ font-family: system-ui, -apple-system, sans-serif;
47
+ font-size: 14px;
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 12px;
51
+ box-shadow: 0 12px 40px rgba(0,0,0,0.3);
52
+ backdrop-filter: blur(16px) saturate(180%);
53
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
54
+ border: 1px solid rgba(255,255,255,0.15);
55
+ z-index: 10000;
56
+ }
57
+ .badge {
58
+ background: rgba(255, 255, 255, 0.2);
59
+ padding: 2px 8px;
60
+ border-radius: 6px;
61
+ font-size: 11px;
62
+ font-weight: 700;
63
+ text-transform: uppercase;
64
+ letter-spacing: 0.5px;
65
+ border: 1px solid rgba(255,255,255,0.2);
66
+ }
67
+ .content {
68
+ flex: 1;
69
+ line-height: 1.4;
70
+ font-weight: 500;
71
+ overflow: hidden;
72
+ }
73
+ `;
74
+ shadow.appendChild(style);
75
+ container = document.createElement('div');
76
+ container.id = 'a11y-banner';
77
+ shadow.appendChild(container);
78
+ }
79
+ const alphaColor = color.includes('rgba') ? color :
80
+ (color.includes('rgb') ? color.replace('rgb', 'rgba').replace(')', ', 0.85)') : color + 'E6');
81
+ container.style.backgroundColor = alphaColor;
82
+ container.innerHTML = `
83
+ <div style="font-size: 20px;">⚠️</div>
84
+ <div class="content">
85
+ <div style="margin-bottom: 4px; display: flex; align-items: center; gap: 8px;">
86
+ <span class="badge">${v.id}</span>
87
+ <span style="opacity: 0.9;">${v.help}</span>
88
+ </div>
89
+ </div>
90
+ `;
91
+ }, [violation, color, this.overlayRootId]);
92
+ }
93
+ /**
94
+ * Removes the violation description overlay.
95
+ */
96
+ async hideViolationOverlay() {
97
+ await this.page.evaluate((rootId) => {
98
+ const el = document.getElementById(rootId);
99
+ if (el)
100
+ el.remove();
101
+ }, this.overlayRootId);
102
+ }
103
+ /**
104
+ * Attaches accessibility data to the Playwright test report.
105
+ */
106
+ async addTestAttachment(testInfo, name, description) {
107
+ await testInfo.attach(name, {
108
+ contentType: 'application/json',
109
+ body: Buffer.from(description)
110
+ });
111
+ }
112
+ getAuditAnnotations() {
113
+ return this.auditAnnotations;
114
+ }
115
+ /**
116
+ * Captures a screenshot and attaches it to the test report.
117
+ */
118
+ async captureAndAttachScreenshot(fileName, testInfo) {
119
+ return await test_1.test.step('Capture A11y screenshot', async () => {
120
+ // Use viewport screenshot instead of fullPage to avoid browser resizing flashes
121
+ const screenshot = await this.page.screenshot({ fullPage: false });
122
+ await testInfo.attach(fileName, { contentType: 'image/png', body: screenshot });
123
+ return screenshot;
124
+ });
125
+ }
126
+ async highlightElement(selector, color) {
127
+ await this.page.evaluate(([sel, color, rootId]) => {
128
+ const target = document.querySelector(sel);
129
+ if (!target)
130
+ return;
131
+ // Scroll FIRST to ensure accurate coordinates after scroll
132
+ target.scrollIntoView({ behavior: 'auto', block: 'center' });
133
+ let root = document.getElementById(rootId);
134
+ if (!root) {
135
+ root = document.createElement('div');
136
+ root.id = rootId;
137
+ root.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647;';
138
+ document.body.appendChild(root);
139
+ root.attachShadow({ mode: 'open' });
140
+ }
141
+ const shadow = root.shadowRoot;
142
+ let highlight = shadow.getElementById('a11y-highlight');
143
+ if (!highlight) {
144
+ const style = document.createElement('style');
145
+ style.textContent = `
146
+ #a11y-highlight {
147
+ position: absolute;
148
+ pointer-events: none;
149
+ border-radius: 8px;
150
+ box-sizing: border-box;
151
+ z-index: 9999;
152
+ box-shadow: 0 0 0 4px var(--c-alpha), 0 0 20px var(--c-alpha);
153
+ }
154
+ .glow {
155
+ position: absolute;
156
+ top: 0;
157
+ left: 0;
158
+ right: 0;
159
+ bottom: 0;
160
+ border-radius: inherit;
161
+ border: 2px solid var(--c);
162
+ }
163
+ `;
164
+ shadow.appendChild(style);
165
+ highlight = document.createElement('div');
166
+ highlight.id = 'a11y-highlight';
167
+ highlight.innerHTML = '<div class="glow"></div>';
168
+ shadow.appendChild(highlight);
169
+ }
170
+ const rect = target.getBoundingClientRect();
171
+ highlight.style.left = `${rect.left + window.scrollX - 4}px`;
172
+ highlight.style.top = `${rect.top + window.scrollY - 4}px`;
173
+ highlight.style.width = `${rect.width + 8}px`;
174
+ highlight.style.height = `${rect.height + 8}px`;
175
+ highlight.style.border = `3px solid ${color}`;
176
+ highlight.style.setProperty('--c', color);
177
+ highlight.style.setProperty('--c-alpha', color.includes('rgba') ? color.replace(/[\d.]+\)$/, '0.3)') : color + '4D');
178
+ }, [selector, color, this.overlayRootId]);
179
+ }
180
+ /**
181
+ * Removes highlighting from an element.
182
+ */
183
+ async unhighlightElement() {
184
+ await this.page.evaluate((rootId) => {
185
+ const root = document.getElementById(rootId);
186
+ if (root && root.shadowRoot) {
187
+ const highlight = root.shadowRoot.getElementById('a11y-highlight');
188
+ if (highlight)
189
+ highlight.remove();
190
+ }
191
+ }, this.overlayRootId);
192
+ }
193
+ }
194
+ exports.A11yAuditOverlay = A11yAuditOverlay;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Handles the rendering of HTML reports using EJS templates.
3
+ */
4
+ export declare class A11yHtmlRenderer {
5
+ /**
6
+ * Renders an HTML template and saves it to the specified file.
7
+ * @param templateName The template file name in the templates folder.
8
+ * @param data The data object to pass to EJS.
9
+ * @param outputFolder The folder where the rendered file will be saved.
10
+ * @param outputFileName The full path of the output file.
11
+ */
12
+ render(templateName: string, data: Record<string, unknown>, outputFolder: string, outputFileName: string): Promise<void>;
13
+ /**
14
+ * Converts ANSI color codes to HTML spans for nicer error display.
15
+ */
16
+ ansiToHtml(text: string): string;
17
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.A11yHtmlRenderer = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const ejs = __importStar(require("ejs"));
40
+ /**
41
+ * Handles the rendering of HTML reports using EJS templates.
42
+ */
43
+ class A11yHtmlRenderer {
44
+ /**
45
+ * Renders an HTML template and saves it to the specified file.
46
+ * @param templateName The template file name in the templates folder.
47
+ * @param data The data object to pass to EJS.
48
+ * @param outputFolder The folder where the rendered file will be saved.
49
+ * @param outputFileName The full path of the output file.
50
+ */
51
+ async render(templateName, data, outputFolder, outputFileName) {
52
+ // Resolve path relative to this file (dist/A11yHtmlRenderer.js)
53
+ const templatePath = path.join(__dirname, 'templates', templateName);
54
+ let templateContent = '';
55
+ try {
56
+ templateContent = fs.readFileSync(templatePath, 'utf8');
57
+ }
58
+ catch {
59
+ throw new Error(`[A11yHtmlRenderer] Template not found: ${templatePath}`);
60
+ }
61
+ let html = '';
62
+ try {
63
+ html = ejs.render(templateContent, data);
64
+ }
65
+ catch (error) {
66
+ console.error(`[A11yHtmlRenderer] EJS Render Error (${templateName}):`, error);
67
+ throw error;
68
+ }
69
+ if (!fs.existsSync(outputFolder)) {
70
+ fs.mkdirSync(outputFolder, { recursive: true });
71
+ }
72
+ fs.writeFileSync(outputFileName, html);
73
+ }
74
+ /**
75
+ * Converts ANSI color codes to HTML spans for nicer error display.
76
+ */
77
+ ansiToHtml(text) {
78
+ const map = {
79
+ '\u001b[30m': '<span style="color:black">',
80
+ '\u001b[31m': '<span style="color:red">',
81
+ '\u001b[32m': '<span style="color:green">',
82
+ '\u001b[33m': '<span style="color:yellow">',
83
+ '\u001b[34m': '<span style="color:blue">',
84
+ '\u001b[35m': '<span style="color:magenta">',
85
+ '\u001b[36m': '<span style="color:cyan">',
86
+ '\u001b[37m': '<span style="color:white">',
87
+ '\u001b[0m': '</span>',
88
+ '\u001b[2m': '<span style="opacity:0.5">',
89
+ '\u001b[22m': '</span>',
90
+ '\u001b[39m': '</span>',
91
+ };
92
+ let result = text
93
+ .replace(/&/g, '&amp;')
94
+ .replace(/</g, '&lt;')
95
+ .replace(/>/g, '&gt;');
96
+ for (const [code, tag] of Object.entries(map)) {
97
+ result = result.split(code).join(tag);
98
+ }
99
+ return result;
100
+ }
101
+ }
102
+ exports.A11yHtmlRenderer = A11yHtmlRenderer;
@@ -0,0 +1,37 @@
1
+ import { TestResult } from '@playwright/test/reporter';
2
+ /**
3
+ * Utilities for managing and copying report assets like videos and screenshots.
4
+ */
5
+ export declare class A11yReportAssets {
6
+ /**
7
+ * Copies a file from source to a destination folder.
8
+ */
9
+ copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
10
+ /**
11
+ * Copies the test video if available.
12
+ * Includes a small retry to ensure Playwright has finished flushing the file.
13
+ */
14
+ copyTestVideo(result: TestResult, destFolder: string): Promise<string>;
15
+ /**
16
+ * Copies all screenshots found in the test attachments.
17
+ */
18
+ copyScreenshots(result: TestResult, destFolder: string): string[];
19
+ /**
20
+ * Copies all PNG attachments to the report folder and returns their new names.
21
+ */
22
+ copyPngAttachments(result: TestResult, destFolder: string): {
23
+ path: string;
24
+ name: string;
25
+ }[];
26
+ /**
27
+ * Copies all other attachments (traces, logs, etc.) to the report folder.
28
+ */
29
+ copyAllOtherAttachments(result: TestResult, destFolder: string): {
30
+ path: string;
31
+ name: string;
32
+ }[];
33
+ /**
34
+ * Writes a buffer to a file in the destination folder.
35
+ */
36
+ writeBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
37
+ }