testaro 60.7.3 → 60.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.7.3",
3
+ "version": "60.8.0",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -372,121 +372,6 @@ const launch = exports.launch = async (
372
372
  browserContext.setDefaultTimeout(0);
373
373
  // When a page (i.e. tab) is added to the browser context (i.e. browser window):
374
374
  browserContext.on('page', async page => {
375
- const isTestaroTest = act.type === 'test' && act.which === 'testaro';
376
- // Mask automation detection.
377
- await page.addInitScript(isTestaroTest => {
378
- Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
379
- window.chrome = {runtime: {}};
380
- Object.defineProperty(navigator, 'plugins', {
381
- get: () => [1, 2, 3, 4, 5]
382
- });
383
- Object.defineProperty(navigator, 'languages', {
384
- get: () => ['en-US', 'en']
385
- });
386
- // If the act is a testaro test act:
387
- if (isTestaroTest) {
388
- // Add a window method to return an instance.
389
- window.getInstance = (
390
- element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
391
- ) => {
392
- // If the element exists:
393
- if (element) {
394
- // Get its properties.
395
- const boxData = element.getBoundingClientRect();
396
- ['x', 'y', 'width', 'height'].forEach(dimension => {
397
- boxData[dimension] = Math.round(boxData[dimension]);
398
- });
399
- const {x, y, width, height} = boxData;
400
- const {tagName, id = ''} = element;
401
- // Return an itemized instance.
402
- return {
403
- ruleID,
404
- what,
405
- count,
406
- ordinalSeverity,
407
- tagName,
408
- id,
409
- location: {
410
- doc: 'dom',
411
- type: 'box',
412
- spec: {
413
- x,
414
- y,
415
- width,
416
- height
417
- }
418
- },
419
- excerpt: element.textContent.trim().replace(/\s+/g, ' ').slice(0, 100),
420
- boxID: [x, y, width, height].join(':'),
421
- pathID: window.getXPath(element)
422
- };
423
- }
424
- // Otherwise, i.e. if no element exists, return a summary instance.
425
- return {
426
- ruleID,
427
- what,
428
- count,
429
- ordinalSeverity,
430
- tagName: summaryTagName,
431
- id: '',
432
- location: {
433
- doc: '',
434
- type: '',
435
- spec: ''
436
- },
437
- excerpt: '',
438
- boxID: '',
439
- pathID: ''
440
- };
441
- };
442
- // Add a window method to get the XPath of an element.
443
- window.getXPath = element => {
444
- if (!element || element.nodeType !== Node.ELEMENT_NODE) {
445
- return '';
446
- }
447
- const segments = [];
448
- let el = element;
449
- // As long as the current node is an element:
450
- while (el && el.nodeType === Node.ELEMENT_NODE) {
451
- const tag = el.tagName.toLowerCase();
452
- // If it is the html element:
453
- if (el === document.documentElement) {
454
- // Prepend it to the segment array
455
- segments.unshift('html');
456
- // Stop traversing.
457
- break;
458
- }
459
- // Otherwise, get its parent node.
460
- const parent = el.parentNode;
461
- // If (abnormally) the parent node is not an element:
462
- if (!parent || parent.nodeType !== Node.ELEMENT_NODE) {
463
- // Prepend it to the segment array.
464
- segments.unshift(tag);
465
- // Stop traversing, leaving the segment array partial.
466
- break;
467
- }
468
- let subscript = '';
469
- let sameTagCount = 0;
470
- // Get the subscript of the element.
471
- const cohort = Array
472
- .from(parent.childNodes)
473
- .filter(
474
- childNode => childNode.nodeType === Node.ELEMENT_NODE
475
- && childNode.tagName === el.tagName
476
- );
477
- if (cohort.length > 1) {
478
- subscript = `[${cohort.indexOf(el) + 1}]`;
479
- }
480
- // Prepend the element identifier to the segment array.
481
- segments.unshift(`${tag}${subscript}`);
482
- // Continue the traversal with the parent of the current element.
483
- el = parent;
484
- }
485
- // Return the XPath.
486
- return `/${segments.join('/')}`;
487
- };
488
- }
489
- }, isTestaroTest);
490
375
  // Ensure the report has a jobData property.
491
376
  report.jobData ??= {};
492
377
  const {jobData} = report;
@@ -550,6 +435,122 @@ const launch = exports.launch = async (
550
435
  page = await browserContext.newPage();
551
436
  // Wait until it is stable.
552
437
  await page.waitForLoadState('domcontentloaded', {timeout: 5000});
438
+ const isTestaroTest = act.type === 'test' && act.which === 'testaro';
439
+ // Add a script to the page to:
440
+ await page.addInitScript(isTestaroTest => {
441
+ // Mask automation detection.
442
+ Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
443
+ window.chrome = {runtime: {}};
444
+ Object.defineProperty(navigator, 'plugins', {
445
+ get: () => [1, 2, 3, 4, 5]
446
+ });
447
+ Object.defineProperty(navigator, 'languages', {
448
+ get: () => ['en-US', 'en']
449
+ });
450
+ // If the act is a testaro test act:
451
+ if (isTestaroTest) {
452
+ // Add a window method to return an instance.
453
+ window.getInstance = (
454
+ element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
455
+ ) => {
456
+ // If the element exists:
457
+ if (element) {
458
+ // Get its properties.
459
+ const boxData = element.getBoundingClientRect();
460
+ ['x', 'y', 'width', 'height'].forEach(dimension => {
461
+ boxData[dimension] = Math.round(boxData[dimension]);
462
+ });
463
+ const {x, y, width, height} = boxData;
464
+ const {tagName, id = ''} = element;
465
+ // Return an itemized instance.
466
+ return {
467
+ ruleID,
468
+ what,
469
+ count,
470
+ ordinalSeverity,
471
+ tagName,
472
+ id,
473
+ location: {
474
+ doc: 'dom',
475
+ type: 'box',
476
+ spec: {
477
+ x,
478
+ y,
479
+ width,
480
+ height
481
+ }
482
+ },
483
+ excerpt: element.textContent.trim().replace(/\s+/g, ' ').slice(0, 100),
484
+ boxID: [x, y, width, height].join(':'),
485
+ pathID: window.getXPath(element)
486
+ };
487
+ }
488
+ // Otherwise, i.e. if no element exists, return a summary instance.
489
+ return {
490
+ ruleID,
491
+ what,
492
+ count,
493
+ ordinalSeverity,
494
+ tagName: summaryTagName,
495
+ id: '',
496
+ location: {
497
+ doc: '',
498
+ type: '',
499
+ spec: ''
500
+ },
501
+ excerpt: '',
502
+ boxID: '',
503
+ pathID: ''
504
+ };
505
+ };
506
+ // Add a window method to get the XPath of an element.
507
+ window.getXPath = element => {
508
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
509
+ return '';
510
+ }
511
+ const segments = [];
512
+ let el = element;
513
+ // As long as the current node is an element:
514
+ while (el && el.nodeType === Node.ELEMENT_NODE) {
515
+ const tag = el.tagName.toLowerCase();
516
+ // If it is the html element:
517
+ if (el === document.documentElement) {
518
+ // Prepend it to the segment array
519
+ segments.unshift('html');
520
+ // Stop traversing.
521
+ break;
522
+ }
523
+ // Otherwise, get its parent node.
524
+ const parent = el.parentNode;
525
+ // If (abnormally) the parent node is not an element:
526
+ if (!parent || parent.nodeType !== Node.ELEMENT_NODE) {
527
+ // Prepend it to the segment array.
528
+ segments.unshift(tag);
529
+ // Stop traversing, leaving the segment array partial.
530
+ break;
531
+ }
532
+ let subscript = '';
533
+ let sameTagCount = 0;
534
+ // Get the subscript of the element.
535
+ const cohort = Array
536
+ .from(parent.childNodes)
537
+ .filter(
538
+ childNode => childNode.nodeType === Node.ELEMENT_NODE
539
+ && childNode.tagName === el.tagName
540
+ );
541
+ if (cohort.length > 1) {
542
+ subscript = `[${cohort.indexOf(el) + 1}]`;
543
+ }
544
+ // Prepend the element identifier to the segment array.
545
+ segments.unshift(`${tag}${subscript}`);
546
+ // Continue the traversal with the parent of the current element.
547
+ el = parent;
548
+ }
549
+ // Return the XPath.
550
+ return `/${segments.join('/')}`;
551
+ };
552
+ }
553
+ }, isTestaroTest);
553
554
  // Navigate to the specified URL.
554
555
  const navResult = await goTo(report, page, url, 15000, 'domcontentloaded');
555
556
  // If the navigation succeeded:
@@ -0,0 +1,98 @@
1
+ /*
2
+ © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
4
+
5
+ MIT License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+ */
25
+
26
+ /*
27
+ allHidden
28
+ This test reports a page that is entirely or mainly hidden.
29
+ */
30
+
31
+ // FUNCTIONS
32
+
33
+ // Runs the test and returns the result.
34
+ exports.reporter = async page => {
35
+ // Get data on violations of the rule.
36
+ const violationData = await page.evaluate(() => {
37
+ // Get all candidates, i.e. the regions that should never be hidden.
38
+ const regions = {
39
+ html: {
40
+ element: document.documentElement,
41
+ severity: 3
42
+ },
43
+ body: {
44
+ element: document.body,
45
+ severity: 2
46
+ },
47
+ main: {
48
+ element: document.querySelector('main, [role=main]'),
49
+ severity: 1
50
+ }
51
+ };
52
+ let violationCounts = [0, 0, 0, 0];
53
+ const instances = [];
54
+ // For each candidate:
55
+ Object.keys(regions).forEach(regionName => {
56
+ const region = regions[regionName];
57
+ const {element, severity} = region;
58
+ console.log(`XXX ${regionName}`);
59
+ const tagName = regionName.toUpperCase();
60
+ // If it is not main and does not exist:
61
+ if (! element && regionName !== 'main') {
62
+ console.log(`XXX ${regionName} is not main anddoes not exist`);
63
+ // Increment the violation count.
64
+ violationCounts[3]++;
65
+ // Add an instance to the instances.
66
+ const what = `The ${regionName} region does not exist`;
67
+ // Add a summary instance to the instances.
68
+ instances.push(window.getInstance(null, 'allHidden', what, 1, 3, tagName));
69
+ }
70
+ // Otherwise, if it exists:
71
+ else if (element) {
72
+ console.log(`XXX ${regionName} exists`);
73
+ const styleDec = window.getComputedStyle(element);
74
+ const {display, visibility} = styleDec;
75
+ // If it is hidden:
76
+ if (display === 'none' || visibility === 'hidden' || element.ariaHidden === 'true') {
77
+ // Increment the violation count.
78
+ violationCounts[severity]++;
79
+ // Add an instance to the instances.
80
+ const what = `The ${regionName} region is hidden`;
81
+ // Add an instance to the instances.
82
+ instances.push(window.getInstance(element, 'allHidden', what, 1, severity, tagName));
83
+ }
84
+ }
85
+ });
86
+ return {
87
+ violationCounts,
88
+ instances
89
+ };
90
+ });
91
+ const {violationCounts, instances} = violationData;
92
+ // Return the result.
93
+ return {
94
+ data: {},
95
+ totals: violationCounts,
96
+ standardInstances: instances
97
+ };
98
+ };
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
3
4
 
4
5
  MIT License
5
6
 
@@ -26,52 +27,35 @@
26
27
  allHidden
27
28
  This test reports a page that is entirely or mainly hidden.
28
29
  */
30
+
31
+ // FUNCTIONS
32
+
33
+ // Runs the test and returns the result.
29
34
  exports.reporter = async page => {
30
- // Gets the hiddennesses of the document, body, and main region.
31
- const data = await page.evaluate(() => {
32
- // For each region:
33
- const {body} = document;
34
- const main = body && body.querySelector('main, [role=main]');
35
- const data = [];
36
- [document.documentElement, body, main].forEach(region => {
37
- if (! region) {
38
- data.push(null);
39
- }
40
- else if (region.hidden || region.ariaHidden) {
41
- data.push(true);
42
- }
43
- else {
44
- const styleDec = window.getComputedStyle(region);
45
- const {display, visibility} = styleDec;
46
- data.push(display === 'none' || visibility === 'hidden');
47
- }
48
- });
49
- return data;
50
- });
51
- // Get the severity totals.
52
- const totals = [0, data[2] ? 1 : 0, data[1] ? 1 : 0, data[0] ? 1 : 0];
53
- const standardInstances = [];
54
- data.forEach((isHidden, index) => {
55
- const region = [['Document', 'HTML'], ['Body', 'BODY'], ['Main region', 'MAIN']][index];
56
- if (isHidden) {
57
- standardInstances.push({
58
- ruleID: 'allHidden',
59
- what: `${region[0]} is hidden`,
60
- ordinalSeverity: 3 - index,
61
- tagName: region[1],
62
- id: '',
63
- location: {
64
- doc: '',
65
- type: '',
66
- spec: ''
67
- },
68
- excerpt: ''
69
- });
35
+ // Get a count of elements deemed visible by Playwright.
36
+ const visibleElementCount = await page.locator('body :visible').count();
37
+ // Get a violation count and an instance.
38
+ const violationData = await page.evaluate(visibleElementCount => {
39
+ let violationCount = 0;
40
+ const instances = [];
41
+ // If no element is visible:
42
+ if (! visibleElementCount) {
43
+ // Increment the violation count.
44
+ violationCount = 1;
45
+ const what = `The entire page body is hidden or empty`;
46
+ // Add a summary instance to the instances.
47
+ instances.push(window.getInstance(null, 'allHidden', what, 1, 3));
70
48
  }
71
- });
49
+ return {
50
+ violationCount,
51
+ instances
52
+ };
53
+ }, visibleElementCount);
54
+ const {violationCount, instances} = violationData;
55
+ // Return the result.
72
56
  return {
73
- data,
74
- totals,
75
- standardInstances
57
+ data: {},
58
+ totals: [0, 0, 0, violationCount],
59
+ standardInstances: instances
76
60
  };
77
61
  };
package/testaro/bulk.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2021–2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
3
4
 
4
5
  MIT License
5
6
 
@@ -32,37 +33,31 @@
32
33
  if the page is cluttered with content.
33
34
  */
34
35
  exports.reporter = async page => {
35
- const data = {};
36
- await page.waitForSelector('body', {timeout: 10000})
37
- .catch(error => {
38
- console.log(`ERROR (${error.message})`);
39
- data.prevented = true;
40
- data.error = 'ERROR: bulk timed out';
41
- return {result: data};
42
- });
43
- const visiblesLoc = await page.locator('body :visible');
44
- const visibleLocs = await visiblesLoc.all();
45
- data.visibleElements = visibleLocs.length;
46
- const severity = Math.min(4, Math.round(data.visibleElements / 400));
47
- const totals = [0, 0, 0, 0];
48
- if (severity) {
49
- totals[severity - 1] = 1;
50
- }
36
+ // Get a count of elements deemed visible by Playwright.
37
+ const visibleElementCount = await page.locator(':visible').count();
38
+ // Get totals and an instance.
39
+ const violationData = await page.evaluate(visibleElementCount => {
40
+ // Convert the count to a severity level, treating up to 400 as non-reportable.
41
+ const severity = Math.min(4, Math.round(visibleElementCount / 400)) - 1;
42
+ const totals = [0, 0, 0, 0];
43
+ const instances = [];
44
+ // If the severity is reportable:
45
+ if (severity > -1) {
46
+ totals[severity] = 1;
47
+ const what = `Page contains ${visibleElementCount} visible elements`;
48
+ // Create an instance reporting it.
49
+ instances.push(window.getInstance(document.documentElement, 'bulk', what, 1, severity));
50
+ }
51
+ return {
52
+ totals,
53
+ instances
54
+ };
55
+ }, visibleElementCount);
56
+ const {totals, instances} = violationData;
57
+ // Return the result.
51
58
  return {
52
- data,
59
+ data: {},
53
60
  totals,
54
- standardInstances: data.visibleElements < 200 ? [] : [{
55
- ruleID: 'bulk',
56
- what: 'Page contains a large number of visible elements',
57
- ordinalSeverity: severity - 1,
58
- tagName: 'HTML',
59
- id: '',
60
- location: {
61
- doc: '',
62
- type: '',
63
- spec: ''
64
- },
65
- excerpt: ''
66
- }]
61
+ standardInstances: instances
67
62
  };
68
63
  };
package/tests/testaro.js CHANGED
@@ -607,7 +607,7 @@ exports.reporter = async (page, report, actIndex) => {
607
607
  // Add data about the test, including its prevention, to the result.
608
608
  const endTime = Date.now();
609
609
  testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
610
- data.rulePreventions.push(rule);
610
+ data.rulePreventions.push(ruleID);
611
611
  data.rulePreventionMessages[ruleID] = 'Timeout';
612
612
  result[ruleID].totals = [0, 0, 0, 0];
613
613
  result[ruleID].standardInstances = [];
@@ -33,16 +33,16 @@
33
33
  <main>
34
34
  <h1>Page with good and bad ARIA description references</h1>
35
35
  <p>WAVE is an application that tests web pages for <span aria-describedby="a11yWhat">accessibility</span></p>
36
- <p>Defect 1: MDN Web Docs is a website that documents the standards underlying <span aria-describedby="wwwWhat">the World Wide Web</span></p>
36
+ <p>Defect 1: MDN Web Docs is a website that documents the standards underlying <span aria-describedby="wwwWhat">the World Wide Web</span></p>
37
37
  <p>The three largest countries in North America are:</p>
38
38
  <ul>
39
39
  <li>Canada</li>
40
40
  <li aria-describedby="usaAlt">Defect 2: United States of America</li>
41
41
  <li>Mexico</li>
42
42
  </ul>
43
- <p id="usaALT">The United States of America is often called <q>America</q>.</p>
43
+ <p id="usaAlt">The United States of America is often called <q>America</q>.</p>
44
44
  <p id="a11yWhat">Accessibility is the quality of being designed and implemented for universal usability for users with a range of abilities and disabilities.</p>
45
- <p id="usaALT">The United States of America is often called <q>USA</q>.</p>
45
+ <p id="usaAlt">The United States of America is often called <q>USA</q>.</p>
46
46
  </main>
47
47
  </body>
48
48
  </html>
@@ -29,7 +29,7 @@
29
29
  <meta name="description" content="tester">
30
30
  <meta name="viewport" content="width=device-width, initial-scale=1">
31
31
  </head>
32
- <body aria-hidden="true">
32
+ <body aria-hidden="true">
33
33
  <main>
34
34
  <h1>Page with an ARIA-hidden body</h1>
35
35
  <p>This page has an ARIA-hidden body.</p>
@@ -22,7 +22,7 @@
22
22
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
23
  SOFTWARE.
24
24
  -->
25
- <html lang="en-US" aria-hidden="true">
25
+ <html lang="en-US" aria-hidden="true">
26
26
  <head>
27
27
  <meta charset="utf-8">
28
28
  <title>Page with mixture of hiddenness</title>
@@ -1,6 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <!--
3
3
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
4
+ © 2025 Jonathan Robert Pool. All rights reserved.
4
5
 
5
6
  MIT License
6
7
 
@@ -30,5 +31,5 @@
30
31
  <meta name="viewport" content="width=device-width, initial-scale=1">
31
32
  </head>
32
33
  <h1>Page with no body</h1>
33
- <p>The content is direct children of the <code>html</code> element.</p>
34
+ <p>The content is direct children of the <code>html</code> element. However, the browser will supply the missing <code>body</code> element, allowing the page to pass the test.</p>
34
35
  </html>
@@ -1,6 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <!--
3
3
  © 2023 CVS Health and/or one of its affiliates. All rights reserved.
4
+ © 2025 Jonathan Robert Pool. All rights reserved.
4
5
 
5
6
  MIT License
6
7
 
@@ -31,6 +32,6 @@
31
32
  </head>
32
33
  <body>
33
34
  <h1>Page with no main landmark</h1>
34
- <p>This page has no main landmark.</p>
35
+ <p>This page has no main landmark. But the <code>body</code> element has visible descendants, so this page will pass the test.</p>
35
36
  </body>
36
37
  </html>
@@ -1,51 +0,0 @@
1
- /*
2
- reproInitScripts.js
3
- Minimal example to demonstrate an apparent Playwright bug reported as issue 38442
4
- (https://github.com/microsoft/playwright/issues/38442).
5
- */
6
-
7
- // save as e.g. reproInitScripts.js
8
- // run with: node reproInitScripts.js
9
-
10
- const {chromium} = require('playwright');
11
-
12
- (async () => {
13
-
14
- const browser = await chromium.launch({headless: true});
15
- const context = await browser.newContext();
16
-
17
- context.on('page', async page => {
18
- console.log('context.on("page") fired');
19
-
20
- // First init script
21
- await page.addInitScript(() => {
22
- window.helperOne = () => 'one';
23
- });
24
-
25
- // Second init script
26
- await page.addInitScript(() => {
27
- window.helperTwo = () => 'two';
28
- });
29
-
30
- // Third init script
31
- await page.addInitScript(() => {
32
- window.helperThree = () => 'three';
33
- });
34
-
35
- });
36
-
37
- const page = await context.newPage();
38
-
39
- await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
40
-
41
- const result = await page.evaluate(() => ({
42
- helperOne: typeof window.helperOne,
43
- helperTwo: typeof window.helperTwo,
44
- helperThree: typeof window.helperThree
45
- }));
46
-
47
- console.log('Helper types:', result);
48
-
49
- await browser.close();
50
-
51
- })();