snap-ally 0.0.2 β†’ 0.1.0-beta

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
@@ -1,4 +1,4 @@
1
- # snap-ally <span aria-hidden="true">πŸ“Έβ™Ώ</span>
1
+ # snap-ally πŸ“Έβ™Ώ
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)
@@ -7,17 +7,13 @@ A powerful, developer-friendly Playwright reporter for **Accessibility testing**
7
7
 
8
8
  ---
9
9
 
10
- ## <span aria-hidden="true">πŸ“Ί</span> Demo
10
+ ## πŸ“Ί Demo
11
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>
12
+ **[▢️ Watch the Demo Video](https://www.loom.com/share/853c04f1f76242a699e8f82e54733007)**
17
13
 
18
14
  ---
19
15
 
20
- ## <span aria-hidden="true">✨</span> Features
16
+ ## ✨ Features
21
17
 
22
18
  - **Beautiful HTML Reporting**: Comprehensive summary and detail pages.
23
19
  - **Visual Overlays**: Highlights violations directly on the page in screenshots.
@@ -28,7 +24,7 @@ A powerful, developer-friendly Playwright reporter for **Accessibility testing**
28
24
 
29
25
  ---
30
26
 
31
- ## <span aria-hidden="true">πŸš€</span> Installation
27
+ ## πŸš€ Installation
32
28
 
33
29
  ```bash
34
30
  npm install snap-ally --save-dev
@@ -36,30 +32,33 @@ npm install snap-ally --save-dev
36
32
 
37
33
  ---
38
34
 
39
- ## <span aria-hidden="true">πŸ› οΈ</span> Setup
35
+ ## πŸ› οΈ Setup
40
36
 
41
37
  Add `snap-ally` to your `playwright.config.ts`:
42
38
 
43
39
  ```typescript
44
- import { defineConfig } from '@playwright/test';
40
+ import { defineConfig } from "@playwright/test";
45
41
 
46
42
  export default defineConfig({
47
43
  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',
44
+ [
45
+ "snap-ally",
46
+ {
47
+ outputFolder: "a11y-report",
48
+ // Optional: Visual Customization
49
+ colors: {
50
+ critical: "#dc2626",
51
+ serious: "#ea580c",
52
+ moderate: "#f59e0b",
53
+ minor: "#0ea5e9",
54
+ },
55
+ // Optional: Azure DevOps Integration
56
+ ado: {
57
+ organization: "your-org",
58
+ project: "your-project",
59
+ },
56
60
  },
57
- // Optional: Azure DevOps Integration
58
- ado: {
59
- organization: 'your-org',
60
- project: 'your-project'
61
- }
62
- }]
61
+ ],
63
62
  ],
64
63
  });
65
64
  ```
@@ -71,23 +70,24 @@ export default defineConfig({
71
70
  Import and use `scanA11y` within your Playwright tests:
72
71
 
73
72
  ```typescript
74
- import { test } from '@playwright/test';
75
- import { scanA11y } from 'snap-ally';
73
+ import { test } from "@playwright/test";
74
+ import { scanA11y } from "snap-ally";
75
+
76
+ test("verify page accessibility", async ({ page }, testInfo) => {
77
+ await page.goto("https://example.com");
76
78
 
77
- test('verify page accessibility', async ({ page }, testInfo) => {
78
- await page.goto('https://example.com');
79
-
80
79
  // Basic scan
81
80
  await scanA11y(page, testInfo);
82
81
 
83
82
  // Advanced scan with configuration
84
83
  await scanA11y(page, testInfo, {
84
+ verbose: true, // Log results to terminal
85
+ consoleLog: true, // Log results to browser console
86
+ pageKey: 'Homepage', // Custom name for the report file
87
+ tags: ['wcag2a', 'wcag2aa'],
85
88
  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
89
+ 'color-contrast': { enabled: false },
90
+ }
91
91
  });
92
92
  });
93
93
  ```
@@ -98,20 +98,21 @@ test('verify page accessibility', async ({ page }, testInfo) => {
98
98
 
99
99
  ### Reporter Options (in `playwright.config.ts`)
100
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. |
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
108
 
109
109
  ### `scanA11y` Options
110
110
 
111
111
  | Option | Type | Description |
112
112
  | --- | --- | --- |
113
113
  | `include` | `string` | CSS selector to limit the scan to a specific element. |
114
- | `verbose` | `boolean` | Log violations to the console. Defaults to `true`. |
114
+ | `verbose` | `boolean` | **Terminal Logs**: Print violations to terminal. Defaults to `true`. |
115
+ | `consoleLog` | `boolean` | **Browser Logs**: Print violations to browser console. Defaults to `true`. |
115
116
  | `rules` | `object` | Axe-core rule configuration. |
116
117
  | `tags` | `string[]` | List of Axe-core tags to run (e.g., `['wcag2aa']`). |
117
118
  | `pageKey` | `string` | Custom identifier for the report file name. |
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Handles the rendering of HTML reports using EJS templates.
2
+ * Handles the rendering of HTML reports using static templates and JSON data injection.
3
3
  */
4
4
  export declare class A11yHtmlRenderer {
5
5
  /**
6
- * Renders an HTML template and saves it to the specified file.
6
+ * Renders a static HTML template by copying it and generating the accompanied data payload.
7
7
  * @param templateName The template file name in the templates folder.
8
- * @param data The data object to pass to EJS.
8
+ * @param data The data object to pass to the client-side JS app.
9
9
  * @param outputFolder The folder where the rendered file will be saved.
10
10
  * @param outputFileName The full path of the output file.
11
11
  */
@@ -36,40 +36,53 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.A11yHtmlRenderer = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const ejs = __importStar(require("ejs"));
40
39
  /**
41
- * Handles the rendering of HTML reports using EJS templates.
40
+ * Handles the rendering of HTML reports using static templates and JSON data injection.
42
41
  */
43
42
  class A11yHtmlRenderer {
44
43
  /**
45
- * Renders an HTML template and saves it to the specified file.
44
+ * Renders a static HTML template by copying it and generating the accompanied data payload.
46
45
  * @param templateName The template file name in the templates folder.
47
- * @param data The data object to pass to EJS.
46
+ * @param data The data object to pass to the client-side JS app.
48
47
  * @param outputFolder The folder where the rendered file will be saved.
49
48
  * @param outputFileName The full path of the output file.
50
49
  */
51
50
  async render(templateName, data, outputFolder, outputFileName) {
52
51
  // 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 {
52
+ const templatesDir = path.join(__dirname, 'templates');
53
+ const templatePath = path.join(templatesDir, templateName);
54
+ const cssPath = path.join(templatesDir, 'global-report-styles.css');
55
+ const jsPath = path.join(templatesDir, 'report-app.js');
56
+ if (!fs.existsSync(templatePath)) {
59
57
  throw new Error(`[A11yHtmlRenderer] Template not found: ${templatePath}`);
60
58
  }
61
- let html = '';
59
+ if (!fs.existsSync(outputFolder)) {
60
+ fs.mkdirSync(outputFolder, { recursive: true });
61
+ }
62
+ // 1. Copy the pure HTML template to the output location
63
+ fs.copyFileSync(templatePath, outputFileName);
64
+ // 2. Wrap the report data in a JS variable and write data.js next to the HTML file
65
+ const outputDir = path.dirname(outputFileName);
66
+ const dataJsPath = path.join(outputDir, 'data.js');
67
+ const jsContent = `window.snapAllyData = ${JSON.stringify(data)};`;
68
+ fs.writeFileSync(dataJsPath, jsContent, 'utf8');
69
+ // 3. Copy the global CSS and JS rendering engine next to the HTML file
70
+ const outCssPath = path.join(outputDir, 'global-report-styles.css');
71
+ const outJsPath = path.join(outputDir, 'report-app.js');
62
72
  try {
63
- html = ejs.render(templateContent, data);
73
+ if (fs.existsSync(cssPath))
74
+ fs.copyFileSync(cssPath, outCssPath);
64
75
  }
65
- catch (error) {
66
- console.error(`[A11yHtmlRenderer] EJS Render Error (${templateName}):`, error);
67
- throw error;
76
+ catch (e) {
77
+ console.error('Error copying CSS:', e);
68
78
  }
69
- if (!fs.existsSync(outputFolder)) {
70
- fs.mkdirSync(outputFolder, { recursive: true });
79
+ try {
80
+ if (fs.existsSync(jsPath))
81
+ fs.copyFileSync(jsPath, outJsPath);
82
+ }
83
+ catch (e) {
84
+ console.error('Error copying JS:', e);
71
85
  }
72
- fs.writeFileSync(outputFileName, html);
73
86
  }
74
87
  /**
75
88
  * Converts ANSI color codes to HTML spans for nicer error display.
@@ -9,7 +9,7 @@ export declare class A11yReportAssets {
9
9
  copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
10
10
  /**
11
11
  * Copies the test video if available.
12
- * Includes a small retry to ensure Playwright has finished flushing the file.
12
+ * Includes a more robust retry to ensure Playwright has finished flushing the file.
13
13
  */
14
14
  copyTestVideo(result: TestResult, destFolder: string): Promise<string>;
15
15
  /**
@@ -57,22 +57,24 @@ class A11yReportAssets {
57
57
  }
58
58
  /**
59
59
  * Copies the test video if available.
60
- * Includes a small retry to ensure Playwright has finished flushing the file.
60
+ * Includes a more robust retry to ensure Playwright has finished flushing the file.
61
61
  */
62
62
  async copyTestVideo(result, destFolder) {
63
- const videoAttachments = result.attachments.filter(a => a.name === 'video');
63
+ // More flexible matching for video attachments
64
+ const videoAttachments = result.attachments.filter(a => a.name === 'video' || (a.contentType || '').startsWith('video/'));
64
65
  let bestVideo = null;
65
66
  let maxSize = -1;
66
67
  for (const attachment of videoAttachments) {
67
68
  if (!attachment.path)
68
69
  continue;
69
- // Retry logic: Wait for file to exist and have non-zero size (up to 2 seconds)
70
+ // Retry logic: Wait for file to exist and have non-zero size (up to 3 seconds)
70
71
  let attempts = 0;
71
72
  let isReady = false;
72
- while (attempts < 10) {
73
+ while (attempts < 15) {
73
74
  if (fs.existsSync(attachment.path)) {
74
75
  try {
75
- if (fs.statSync(attachment.path).size > 0) {
76
+ const stats = fs.statSync(attachment.path);
77
+ if (stats.size > 0) {
76
78
  isReady = true;
77
79
  break;
78
80
  }
@@ -97,12 +99,12 @@ class A11yReportAssets {
97
99
  }
98
100
  }
99
101
  else {
100
- console.warn(`[SnapAlly] Video attachment found but file is missing or empty: ${attachment.path}`);
102
+ console.warn(`[SnapAlly] Video attachment found but file is missing or empty after retry: ${attachment.path}`);
101
103
  }
102
104
  }
103
105
  if (bestVideo) {
104
106
  try {
105
- return this.copyToFolder(destFolder, bestVideo);
107
+ return this.copyToFolder(destFolder, bestVideo, 'video.webm');
106
108
  }
107
109
  catch (e) {
108
110
  console.error(`[SnapAlly] Failed to copy video: ${e}`);
@@ -116,13 +118,15 @@ class A11yReportAssets {
116
118
  */
117
119
  copyScreenshots(result, destFolder) {
118
120
  return result.attachments
119
- .filter(a => a.name === 'screenshot')
121
+ .filter(a => a.name === 'screenshot' || (a.contentType || '').startsWith('image/'))
120
122
  .map(a => {
121
123
  if (a.path) {
122
124
  return this.copyToFolder(destFolder, a.path);
123
125
  }
124
126
  else if (a.body) {
125
- return this.writeBuffer(destFolder, `screenshot-${Date.now()}.png`, a.body);
127
+ const timestamp = Date.now();
128
+ const name = a.name === 'screenshot' ? `screenshot-${timestamp}.png` : (a.name.endsWith('.png') ? a.name : `${a.name}.png`);
129
+ return this.writeBuffer(destFolder, name, a.body);
126
130
  }
127
131
  return '';
128
132
  })
@@ -133,14 +137,15 @@ class A11yReportAssets {
133
137
  */
134
138
  copyPngAttachments(result, destFolder) {
135
139
  return result.attachments
136
- .filter(a => a.name.endsWith('.png') && a.name !== 'screenshot')
140
+ .filter(a => (a.name.endsWith('.png') || (a.contentType || '') === 'image/png') && a.name !== 'screenshot')
137
141
  .map(a => {
138
142
  let name = '';
139
143
  if (a.path) {
140
144
  name = this.copyToFolder(destFolder, a.path, a.name);
141
145
  }
142
146
  else if (a.body) {
143
- name = this.writeBuffer(destFolder, a.name, a.body);
147
+ const safeName = a.name.endsWith('.png') ? a.name : `${a.name}.png`;
148
+ name = this.writeBuffer(destFolder, safeName, a.body);
144
149
  }
145
150
  return name ? { path: name, name: a.name } : null;
146
151
  })
@@ -152,7 +157,10 @@ class A11yReportAssets {
152
157
  copyAllOtherAttachments(result, destFolder) {
153
158
  const excludedNames = ['screenshot', 'video', 'A11y'];
154
159
  return result.attachments
155
- .filter(a => !excludedNames.includes(a.name) && !a.name.endsWith('.png'))
160
+ .filter(a => !excludedNames.includes(a.name) &&
161
+ !a.name.endsWith('.png') &&
162
+ !(a.contentType || '').startsWith('image/') &&
163
+ !(a.contentType || '').startsWith('video/'))
156
164
  .map(a => {
157
165
  let name = '';
158
166
  if (a.path) {
@@ -16,6 +16,8 @@ export interface A11yScannerOptions {
16
16
  tags?: string[];
17
17
  /** Any other Axe-core options to pass to the builder. */
18
18
  axeOptions?: Record<string, unknown>;
19
+ /** Custom identifier for the report file name. */
20
+ pageKey?: string;
19
21
  }
20
22
  /**
21
23
  * Performs an accessibility audit using Axe and Lighthouse.
@@ -9,13 +9,39 @@ const playwright_1 = __importDefault(require("@axe-core/playwright"));
9
9
  const test_1 = require("@playwright/test");
10
10
  const A11yAuditOverlay_1 = require("./A11yAuditOverlay");
11
11
  const models_1 = require("./models");
12
+ const A11yTimeUtils_1 = require("./A11yTimeUtils");
13
+ /**
14
+ * Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
15
+ * Removes or replaces dangerous characters and path separators.
16
+ */
17
+ function sanitizePageKey(input) {
18
+ return input
19
+ // Remove protocol
20
+ .replace(/^https?:\/\//, '')
21
+ // Remove or replace path separators and dangerous characters
22
+ .replace(/[\/\\:*?"<>|]/g, '-')
23
+ // Remove any remaining path traversal attempts
24
+ .replace(/\.\./g, '')
25
+ // Replace multiple consecutive dashes with a single dash
26
+ .replace(/-+/g, '-')
27
+ // Remove leading/trailing dashes
28
+ .replace(/^-+|-+$/g, '')
29
+ // Convert to lowercase for consistency
30
+ .toLowerCase()
31
+ // Limit length to prevent filesystem issues
32
+ .substring(0, 200);
33
+ }
12
34
  /**
13
35
  * Performs an accessibility audit using Axe and Lighthouse.
14
36
  */
15
37
  async function scanA11y(page, testInfo, options = {}) {
16
- var _a;
17
- const verbose = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
18
- const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page, page.url());
38
+ var _a, _b;
39
+ const showTerminal = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
40
+ const showBrowser = (_b = options.consoleLog) !== null && _b !== void 0 ? _b : true;
41
+ // Sanitize pageKey to prevent path traversal attacks
42
+ const rawPageKey = options.pageKey || page.url();
43
+ const pageKey = sanitizePageKey(rawPageKey);
44
+ const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page, pageKey);
19
45
  // Configure Axe
20
46
  let axeBuilder = new playwright_1.default({ page });
21
47
  const target = options.include || options.box;
@@ -37,13 +63,34 @@ async function scanA11y(page, testInfo, options = {}) {
37
63
  if (options.axeOptions) {
38
64
  axeBuilder = axeBuilder.options(options.axeOptions);
39
65
  }
40
- const axeResults = await axeBuilder.analyze();
66
+ let axeResults;
67
+ try {
68
+ axeResults = await axeBuilder.analyze();
69
+ }
70
+ catch (error) {
71
+ if (error.message && (error.message.includes('Test ended') || error.message.includes('Target page, context or browser has been closed'))) {
72
+ console.warn(`[SnapAlly] Accessibility scan skipped: ${error.message}`);
73
+ return;
74
+ }
75
+ throw error;
76
+ }
41
77
  const violationCount = axeResults.violations.length;
42
- if (verbose && violationCount > 0) {
43
- console.log(`\n[A11yScanner] Violations found: ${violationCount}`);
44
- axeResults.violations.forEach((v, i) => {
45
- console.log(` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
46
- });
78
+ if ((showTerminal || showBrowser) && violationCount > 0) {
79
+ const mainMsg = `[A11yScanner] Violations found: ${violationCount}`;
80
+ // Prepare all detail messages
81
+ const detailMessages = axeResults.violations.map((v, i) => ` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
82
+ // Log to terminal
83
+ if (showTerminal) {
84
+ console.log(`\n${mainMsg}`);
85
+ detailMessages.forEach(msg => console.log(msg));
86
+ }
87
+ // Batch log to Browser Console in a single evaluate call
88
+ if (showBrowser) {
89
+ await page.evaluate(([mainMsg, details]) => {
90
+ console.log(`%c ${mainMsg}`, 'color: #ea580c; font-weight: bold; font-size: 12px;');
91
+ details.forEach((msg) => console.log(msg));
92
+ }, [mainMsg, detailMessages]);
93
+ }
47
94
  }
48
95
  // Fail the test if violations found (softly)
49
96
  test_1.expect.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`).toBe(0);
@@ -67,9 +114,9 @@ async function scanA11y(page, testInfo, options = {}) {
67
114
  await overlay.showViolationOverlay({ id: violation.id, help: violation.help }, severityColor);
68
115
  if (await locator.isVisible()) {
69
116
  await overlay.highlightElement(elementSelector, severityColor);
70
- // Allow time for video capture or manual inspection during debug
117
+ // Allow a small time for overlay highlight to be visible in video
71
118
  // eslint-disable-next-line playwright/no-wait-for-timeout
72
- await page.waitForTimeout(2000);
119
+ await page.waitForTimeout(100);
73
120
  const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
74
121
  const buffer = await overlay.captureAndAttachScreenshot(screenshotName, testInfo);
75
122
  // Capture execution steps for context
@@ -106,16 +153,18 @@ async function scanA11y(page, testInfo, options = {}) {
106
153
  }
107
154
  // Prepare data for the reporter
108
155
  const reportData = {
109
- pageKey: page.url(),
156
+ pageKey,
157
+ pageUrl: page.url(),
110
158
  accessibilityScore: 0, // No longer used, derivation from Lighthouse removed
111
- errors,
159
+ a11yErrors: errors,
112
160
  video: 'a11y-scan-video.webm', // Reference name for reporter
113
161
  criticalColor: models_1.Severity.critical,
114
162
  seriousColor: models_1.Severity.serious,
115
163
  moderateColor: models_1.Severity.moderate,
116
164
  minorColor: models_1.Severity.minor,
117
165
  adoOrganization: process.env.ADO_ORGANIZATION || '',
118
- adoProject: process.env.ADO_PROJECT || ''
166
+ adoProject: process.env.ADO_PROJECT || '',
167
+ timestamp: A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date())
119
168
  };
120
169
  await overlay.addTestAttachment(testInfo, 'A11y', JSON.stringify(reportData));
121
170
  await overlay.hideViolationOverlay();
@@ -6,4 +6,8 @@ export declare class A11yTimeUtils {
6
6
  * Formats milliseconds into a human-readable duration string.
7
7
  */
8
8
  static formatDuration(ms: number): string;
9
+ /**
10
+ * Formats a Date object into a human-readable string.
11
+ */
12
+ static formatDate(date: Date): string;
9
13
  }
@@ -20,5 +20,20 @@ class A11yTimeUtils {
20
20
  const remainingSeconds = seconds % 60;
21
21
  return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
22
22
  }
23
+ /**
24
+ * Formats a Date object into a human-readable string.
25
+ */
26
+ static formatDate(date) {
27
+ const day = String(date.getDate()).padStart(2, '0');
28
+ const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
29
+ const month = monthNames[date.getMonth()];
30
+ const year = date.getFullYear();
31
+ let hours = date.getHours();
32
+ const ampm = hours >= 12 ? 'PM' : 'AM';
33
+ hours = hours % 12 || 12;
34
+ const formattedHours = String(hours).padStart(2, '0');
35
+ const minutes = String(date.getMinutes()).padStart(2, '0');
36
+ return `${day} ${month} ${year}, ${formattedHours}:${minutes} ${ampm}`;
37
+ }
23
38
  }
24
39
  exports.A11yTimeUtils = A11yTimeUtils;
@@ -33,6 +33,7 @@ declare class SnapAllyReporter implements Reporter {
33
33
  private renderer;
34
34
  private options;
35
35
  private projectRoot;
36
+ printsToStdio(): boolean;
36
37
  private executionSummary;
37
38
  private tasks;
38
39
  constructor(options?: AccessibilityReporterOptions);