testaro 60.8.4 → 60.10.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 +1 -2
- package/dist/nameComputation.js +1087 -0
- package/package.json +6 -1
- package/procs/doTestAct.js +4 -10
- package/run.js +25 -14
- package/src/nameComputation.js +5 -0
- package/testaro/autocomplete.js +62 -46
- package/tests/testaro.js +2 -0
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testaro",
|
|
3
|
-
"version": "60.
|
|
3
|
+
"version": "60.10.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": {
|
|
7
|
+
"build:nameComputation": "esbuild src/nameComputation.js --bundle --format=iife --global-name=computeAccessibleName --outfile=dist/nameComputation.js",
|
|
8
|
+
"build": "npm run build:nameComputation",
|
|
9
|
+
"deps": "npm update && npx playwright install && npm run build",
|
|
7
10
|
"tests": "node validation/executors/tests",
|
|
8
11
|
"test": "node validation/executors/test",
|
|
9
12
|
"run": "node validation/executors/run",
|
|
@@ -47,6 +50,8 @@
|
|
|
47
50
|
"puppeteer-extra-plugin-stealth": "*"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
53
|
+
"dom-accessibility-api": "*",
|
|
54
|
+
"esbuild": "*",
|
|
50
55
|
"eslint": "*"
|
|
51
56
|
}
|
|
52
57
|
}
|
package/procs/doTestAct.js
CHANGED
|
@@ -39,19 +39,12 @@ const os = require('os');
|
|
|
39
39
|
|
|
40
40
|
// CONSTANTS
|
|
41
41
|
|
|
42
|
-
const headedBrowser = process.env.HEADED_BROWSER === 'true';
|
|
43
|
-
const debug = process.env.DEBUG === 'true';
|
|
44
|
-
const waits = Number.parseInt(process.env.WAITS) || 0;
|
|
45
42
|
const tmpDir = os.tmpdir();
|
|
46
43
|
|
|
47
|
-
// VARIABLES
|
|
48
|
-
|
|
49
|
-
const actIndex = Number.parseInt(process.argv[2]);
|
|
50
|
-
|
|
51
44
|
// FUNCTIONS
|
|
52
45
|
|
|
53
46
|
// Performs the tests of the act specified by the caller.
|
|
54
|
-
const doTestAct = async
|
|
47
|
+
const doTestAct = async actIndex => {
|
|
55
48
|
const reportPath = `${tmpDir}/report.json`;
|
|
56
49
|
// Get the report from the temporary directory.
|
|
57
50
|
const reportJSON = await fs.readFile(reportPath, 'utf8');
|
|
@@ -68,6 +61,7 @@ const doTestAct = async () => {
|
|
|
68
61
|
// Launch a browser, navigate to the URL, and update the run-module page export.
|
|
69
62
|
await launch(
|
|
70
63
|
report,
|
|
64
|
+
actIndex,
|
|
71
65
|
'high',
|
|
72
66
|
browserID,
|
|
73
67
|
targetURL
|
|
@@ -88,7 +82,7 @@ const doTestAct = async () => {
|
|
|
88
82
|
page = require('../run').page;
|
|
89
83
|
}
|
|
90
84
|
}
|
|
91
|
-
// If the page exists:
|
|
85
|
+
// If the page exists or the tool is Testaro:
|
|
92
86
|
if (page || which === 'testaro') {
|
|
93
87
|
try {
|
|
94
88
|
// Make the act reporter perform the specified tests of the tool.
|
|
@@ -142,4 +136,4 @@ const doTestAct = async () => {
|
|
|
142
136
|
}
|
|
143
137
|
};
|
|
144
138
|
|
|
145
|
-
doTestAct();
|
|
139
|
+
doTestAct(Number.parseInt(process.argv[2]));
|
package/run.js
CHANGED
|
@@ -111,7 +111,6 @@ const tmpDir = os.tmpdir();
|
|
|
111
111
|
// Facts about the current session.
|
|
112
112
|
let actCount = 0;
|
|
113
113
|
// Facts about the current act.
|
|
114
|
-
let actIndex = 0;
|
|
115
114
|
let browser;
|
|
116
115
|
let cleanupInProgress = false;
|
|
117
116
|
let browserCloseIntentional = false;
|
|
@@ -290,9 +289,9 @@ const browserClose = exports.browserClose = async () => {
|
|
|
290
289
|
};
|
|
291
290
|
// Launches a browser and navigates to a URL.
|
|
292
291
|
const launch = exports.launch = async (
|
|
293
|
-
report, headEmulation, tempBrowserID, tempURL, retries = 2
|
|
292
|
+
report, actIndex, headEmulation, tempBrowserID, tempURL, retries = 2
|
|
294
293
|
) => {
|
|
295
|
-
const act = report.acts[actIndex];
|
|
294
|
+
const act = report.acts[actIndex] || {};
|
|
296
295
|
const {device} = report;
|
|
297
296
|
const deviceID = device && device.id;
|
|
298
297
|
const browserID = tempBrowserID || report.browserID || '';
|
|
@@ -435,10 +434,8 @@ const launch = exports.launch = async (
|
|
|
435
434
|
page = await browserContext.newPage();
|
|
436
435
|
// Wait until it is stable.
|
|
437
436
|
await page.waitForLoadState('domcontentloaded', {timeout: 5000});
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
await page.addInitScript(isTestaroTest => {
|
|
441
|
-
// Mask automation detection.
|
|
437
|
+
// Add a script to the page to mask automation detection.
|
|
438
|
+
await page.addInitScript(() => {
|
|
442
439
|
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
|
443
440
|
window.chrome = {runtime: {}};
|
|
444
441
|
Object.defineProperty(navigator, 'plugins', {
|
|
@@ -447,8 +444,21 @@ const launch = exports.launch = async (
|
|
|
447
444
|
Object.defineProperty(navigator, 'languages', {
|
|
448
445
|
get: () => ['en-US', 'en']
|
|
449
446
|
});
|
|
450
|
-
|
|
451
|
-
|
|
447
|
+
});
|
|
448
|
+
const isTestaroTest = act.type === 'test' && act.which === 'testaro';
|
|
449
|
+
// If the act is a testaro test act:
|
|
450
|
+
if (isTestaroTest) {
|
|
451
|
+
// Add a script to the page to compute the accessible name of an element.
|
|
452
|
+
await page.addInitScript({path: require.resolve('./dist/nameComputation.js')});
|
|
453
|
+
// Add a script to the page to:
|
|
454
|
+
await page.addInitScript(() => {
|
|
455
|
+
// Add a window method to compute the accessible name of an element.
|
|
456
|
+
window.getAccessibleName = element => {
|
|
457
|
+
const nameIsComputable = element
|
|
458
|
+
&& element.nodeType === Node.ELEMENT_NODE
|
|
459
|
+
&& typeof window.computeAccessibleName === 'function';
|
|
460
|
+
return nameIsComputable ? window.computeAccessibleName(element) : '';
|
|
461
|
+
};
|
|
452
462
|
// Add a window method to return an instance.
|
|
453
463
|
window.getInstance = (
|
|
454
464
|
element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
|
|
@@ -551,8 +561,8 @@ const launch = exports.launch = async (
|
|
|
551
561
|
// Return the XPath.
|
|
552
562
|
return `/${segments.join('/')}`;
|
|
553
563
|
};
|
|
554
|
-
}
|
|
555
|
-
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
556
566
|
// Navigate to the specified URL.
|
|
557
567
|
const navResult = await goTo(report, page, url, 15000, 'domcontentloaded');
|
|
558
568
|
// If the navigation succeeded:
|
|
@@ -594,7 +604,7 @@ const launch = exports.launch = async (
|
|
|
594
604
|
);
|
|
595
605
|
await wait(1000 * waitSeconds + 100);
|
|
596
606
|
// Then retry the launch and navigation.
|
|
597
|
-
return launch(report, headEmulation, tempBrowserID, tempURL, retries - 1);
|
|
607
|
+
return launch(report, actIndex, headEmulation, tempBrowserID, tempURL, retries - 1);
|
|
598
608
|
}
|
|
599
609
|
// Otherwise, i.e. if no retries remain:
|
|
600
610
|
else {
|
|
@@ -819,9 +829,8 @@ const doActs = async (report, opts = {}) => {
|
|
|
819
829
|
const standard = report.standard || 'only';
|
|
820
830
|
const reportPath = `${tmpDir}/report.json`;
|
|
821
831
|
// For each act in the report.
|
|
822
|
-
for (const
|
|
832
|
+
for (const actIndex in acts) {
|
|
823
833
|
if (signal && signal.aborted) throw new Error('doActs aborted');
|
|
824
|
-
actIndex = doActsIndex;
|
|
825
834
|
// If the job has not been aborted:
|
|
826
835
|
if (report.jobData && ! report.jobData.aborted) {
|
|
827
836
|
let act = acts[actIndex];
|
|
@@ -899,6 +908,7 @@ const doActs = async (report, opts = {}) => {
|
|
|
899
908
|
// Launch a browser, navigate to a page, and add the result to the act.
|
|
900
909
|
await launch(
|
|
901
910
|
report,
|
|
911
|
+
actIndex,
|
|
902
912
|
'high',
|
|
903
913
|
actLaunchSpecs[0],
|
|
904
914
|
actLaunchSpecs[1]
|
|
@@ -1649,6 +1659,7 @@ const doActs = async (report, opts = {}) => {
|
|
|
1649
1659
|
// Replace the browser and navigate to the URL.
|
|
1650
1660
|
await launch(
|
|
1651
1661
|
report,
|
|
1662
|
+
'',
|
|
1652
1663
|
'high',
|
|
1653
1664
|
specs[0],
|
|
1654
1665
|
specs[1]
|
package/testaro/autocomplete.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
|
|
|
@@ -27,59 +28,74 @@
|
|
|
27
28
|
This test reports failures to equip name and email inputs with correct autocomplete attributes.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
// Module to perform common operations.
|
|
33
|
-
const {init, getRuleResult} = require('../procs/testaro');
|
|
34
|
-
// Module to get locator data.
|
|
35
|
-
const {getLocatorData} = require('../procs/getLocatorData');
|
|
36
|
-
|
|
37
|
-
// ########## FUNCTIONS
|
|
31
|
+
// FUNCTIONS
|
|
38
32
|
|
|
39
33
|
// Runs the test and returns the result.
|
|
40
34
|
exports.reporter = async (
|
|
41
35
|
page,
|
|
42
36
|
withItems,
|
|
37
|
+
nameLabels = ['your name', 'full name', 'first and last name'],
|
|
38
|
+
emailLabels = ['email'],
|
|
43
39
|
givenLabels = ['first name', 'forename', 'given name'],
|
|
44
|
-
familyLabels = ['last name', 'surname', 'family name']
|
|
45
|
-
emailLabels = ['email']
|
|
40
|
+
familyLabels = ['last name', 'surname', 'family name']
|
|
46
41
|
) => {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (neededAuto) {
|
|
70
|
-
// If it does not have the one it needs:
|
|
71
|
-
const actualAuto = await loc.getAttribute('autocomplete');
|
|
72
|
-
const isBad = actualAuto !== neededAuto;
|
|
73
|
-
if (isBad) {
|
|
74
|
-
// Add the locator to the array of violators.
|
|
75
|
-
all.locs.push([loc, neededAuto]);
|
|
42
|
+
// Return totals and standard instances for the rule.
|
|
43
|
+
return await page.evaluate(args => {
|
|
44
|
+
const [withItems, nameLabels,givenLabels, familyLabels, emailLabels] = args;
|
|
45
|
+
// Get all candidates, i.e. text and email input elements.
|
|
46
|
+
const candidates = document.body.querySelectorAll(
|
|
47
|
+
'input[type=text], input[type=email], input:not([type])'
|
|
48
|
+
);
|
|
49
|
+
let violationCount = 0;
|
|
50
|
+
const instances = [];
|
|
51
|
+
// For each candidate:
|
|
52
|
+
candidates.forEach(candidate => {
|
|
53
|
+
// Get its lower-cased accessible name.
|
|
54
|
+
const name = window.getAccessibleName(candidate).toLowerCase();
|
|
55
|
+
// Get its required autocomplete value.
|
|
56
|
+
let requiredAuto = '';
|
|
57
|
+
if (candidate.type === 'email' || name && emailLabels.some(label => name.includes(label))) {
|
|
58
|
+
requiredAuto = 'email';
|
|
59
|
+
}
|
|
60
|
+
else if (
|
|
61
|
+
name && candidate.type === 'text' && nameLabels.some(label => name.includes(label))
|
|
62
|
+
) {
|
|
63
|
+
requiredAuto = 'name';
|
|
76
64
|
}
|
|
65
|
+
else if (
|
|
66
|
+
name && candidate.type === 'text' && givenLabels.some(label => name.includes(label))
|
|
67
|
+
) {
|
|
68
|
+
requiredAuto = 'given-name';
|
|
69
|
+
}
|
|
70
|
+
else if (
|
|
71
|
+
name && candidate.type === 'text' && familyLabels.some(label => name.includes(label))
|
|
72
|
+
) {
|
|
73
|
+
requiredAuto = 'family-name';
|
|
74
|
+
}
|
|
75
|
+
// Get its actual autocomplete value.
|
|
76
|
+
const actualAuto = candidate.getAttribute('autocomplete');
|
|
77
|
+
// If an autocomplete value is required but not present:
|
|
78
|
+
if (requiredAuto && ! (actualAuto && actualAuto.includes(requiredAuto))) {
|
|
79
|
+
// Increment the violation count.
|
|
80
|
+
violationCount++;
|
|
81
|
+
// If itemization is required:
|
|
82
|
+
if (withItems) {
|
|
83
|
+
const what = `input has no autocomplete="${requiredAuto}" attribute`;
|
|
84
|
+
// Add an instance to the instances.
|
|
85
|
+
instances.push(window.getInstance(candidate, 'autocomplete', what, 1, 2));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// If there were any violations and itemization is not required:
|
|
90
|
+
if (violationCount && ! withItems) {
|
|
91
|
+
const what = 'Inputs are missing required autocomplete attributes';
|
|
92
|
+
// Add a summary instance to the instances.
|
|
93
|
+
instances.push(window.getInstance(null, 'autocomplete', what, violationCount, 2));
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
data: {},
|
|
97
|
+
totals: [0, 0, violationCount, 0],
|
|
98
|
+
standardInstances: instances
|
|
77
99
|
}
|
|
78
|
-
}
|
|
79
|
-
// Populate and return the result.
|
|
80
|
-
const whats = [
|
|
81
|
-
'Input is missing an autocomplete attribute with value __param__',
|
|
82
|
-
'Inputs are missing applicable autocomplete attributes'
|
|
83
|
-
];
|
|
84
|
-
return await getRuleResult(withItems, all, 'autocomplete', whats, 2);
|
|
100
|
+
}, [withItems, nameLabels, givenLabels, familyLabels, emailLabels]);
|
|
85
101
|
};
|
package/tests/testaro.js
CHANGED
|
@@ -552,6 +552,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
552
552
|
// Replace the browser and the page and navigate to the target.
|
|
553
553
|
await launch(
|
|
554
554
|
report,
|
|
555
|
+
actIndex,
|
|
555
556
|
headEmulation,
|
|
556
557
|
browserID,
|
|
557
558
|
url
|
|
@@ -663,6 +664,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
663
664
|
// Replace the browser and the page in the run module and navigate to the target.
|
|
664
665
|
await launch(
|
|
665
666
|
report,
|
|
667
|
+
actIndex,
|
|
666
668
|
headEmulation,
|
|
667
669
|
report.browserID,
|
|
668
670
|
url
|