testaro 60.7.4 → 60.8.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/VALIDATION_README.md +10 -8
- package/package.json +1 -1
- package/run.js +116 -115
- package/testaro/adbID.js +2 -1
- package/testaro/allHidden-old.js +98 -0
- package/testaro/allHidden.js +28 -44
- package/testaro/altScheme.js +1 -1
- package/testaro/bulk.js +25 -30
- package/testaro/captionLoc.js +1 -1
- package/testaro/datalistRef.js +1 -1
- package/testaro/imageLink.js +1 -1
- package/testaro/legendLoc.js +1 -1
- package/testaro/optRoleSel.js +1 -1
- package/testaro/phOnly.js +1 -1
- package/testaro/secHeading.js +1 -1
- package/testaro/textSem.js +1 -1
- package/tests/testaro.js +1 -4
- package/validation/tests/targets/allHidden/ariaHiddenBody.html +1 -1
- package/validation/tests/targets/allHidden/mixedHidden.html +1 -1
- package/validation/tests/targets/allHidden/noBody.html +2 -1
- package/validation/tests/targets/allHidden/noMain.html +2 -1
- package/reproInitScripts.js +0 -51
package/VALIDATION_README.md
CHANGED
|
@@ -4,28 +4,30 @@ Quick notes to run the Testaro validation tests locally (Windows PowerShell):
|
|
|
4
4
|
|
|
5
5
|
1. Install project dependencies
|
|
6
6
|
|
|
7
|
-
```powershell
|
|
8
|
-
npm install
|
|
9
|
-
```
|
|
7
|
+
```powershell
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
1. Install Playwright browsers (required)
|
|
12
12
|
|
|
13
|
-
```powershell
|
|
14
|
-
npx playwright install
|
|
15
|
-
```
|
|
13
|
+
```powershell
|
|
14
|
+
npx playwright install
|
|
15
|
+
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
1. Run a validation for a specific rule (example: `altScheme`)
|
|
18
18
|
|
|
19
19
|
```powershell
|
|
20
20
|
npm test altScheme
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Notes:
|
|
24
|
+
|
|
24
25
|
- If a validator job is stored under `validation/tests/jobProperties/pending`, copy it to `validation/tests/jobProperties/` or run the validator via the provided filenames. `altScheme` was copied already.
|
|
25
26
|
- If a test fails its expectations, read the JSON output printed by the validation harness for `standardResult` and `expectations` to identify missing instances.
|
|
26
27
|
- After making changes to rule implementations in `testaro/`, re-run the specific `npm test <ruleID>` until the validator reports success.
|
|
27
28
|
|
|
28
29
|
Preparing a PR:
|
|
30
|
+
|
|
29
31
|
- Create a branch (example `feature/add-training-rules`), commit your changes, push to remote, and open a PR describing which rules are training vs clean-room.
|
|
30
32
|
|
|
31
33
|
## License
|
package/package.json
CHANGED
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:
|
package/testaro/adbID.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
© 2025 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
© 2025 Juan S. Casado. All rights reserved.
|
|
3
4
|
© 2025 Jonathan Robert Pool. All rights reserved.
|
|
4
5
|
|
|
5
6
|
MIT License
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
|
|
26
27
|
/*
|
|
27
28
|
adbID
|
|
28
|
-
This test reports elements referencing aria-describedby targets that are missing
|
|
29
|
+
Clean-room rule:This test reports elements referencing aria-describedby targets that are missing
|
|
29
30
|
or, because of duplicate IDs, ambiguous. An earlier version of this test was
|
|
30
31
|
originally developed under a clean-room procedure to ensure its independence from
|
|
31
32
|
the implementation of a test for a similar rule in the Tenon tool.
|
|
@@ -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
|
+
};
|
package/testaro/allHidden.js
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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/altScheme.js
CHANGED
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
|
-
|
|
36
|
-
await page.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
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/testaro/captionLoc.js
CHANGED
package/testaro/datalistRef.js
CHANGED
package/testaro/imageLink.js
CHANGED
package/testaro/legendLoc.js
CHANGED
package/testaro/optRoleSel.js
CHANGED
package/testaro/phOnly.js
CHANGED
package/testaro/secHeading.js
CHANGED
package/testaro/textSem.js
CHANGED
package/tests/testaro.js
CHANGED
|
@@ -448,9 +448,6 @@ const allRules = [
|
|
|
448
448
|
defaultOn: false
|
|
449
449
|
}
|
|
450
450
|
];
|
|
451
|
-
const headedBrowser = process.env.HEADED_BROWSER === 'true';
|
|
452
|
-
const debug = process.env.DEBUG === 'true';
|
|
453
|
-
const waits = Number.parseInt(process.env.WAITS) || 0;
|
|
454
451
|
const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
|
|
455
452
|
|
|
456
453
|
// ERROR HANDLER
|
|
@@ -607,7 +604,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
607
604
|
// Add data about the test, including its prevention, to the result.
|
|
608
605
|
const endTime = Date.now();
|
|
609
606
|
testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
|
|
610
|
-
data.rulePreventions.push(
|
|
607
|
+
data.rulePreventions.push(ruleID);
|
|
611
608
|
data.rulePreventionMessages[ruleID] = 'Timeout';
|
|
612
609
|
result[ruleID].totals = [0, 0, 0, 0];
|
|
613
610
|
result[ruleID].standardInstances = [];
|
|
@@ -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
|
|
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
|
-
|
|
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>
|
package/reproInitScripts.js
DELETED
|
@@ -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
|
-
})();
|