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/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.8.4",
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
  }
@@ -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
- 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.
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
- // If the act is a testaro test act:
451
- if (isTestaroTest) {
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
- }, isTestaroTest);
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 doActsIndex in acts) {
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]
@@ -0,0 +1,5 @@
1
+ // src/nameComputation.js
2
+
3
+ const {computeAccessibleName} = require('dom-accessibility-api');
4
+
5
+ window.computeAccessibleName = computeAccessibleName;
@@ -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
- // ########## IMPORTS
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
- // Initialize the locators and result.
48
- const all = await init(100, page, 'input[type=text], input[type=email], input:not([type])');
49
- // For each locator:
50
- const autoValues = {
51
- 'given-name': givenLabels,
52
- 'family-name': familyLabels,
53
- 'email': emailLabels
54
- };
55
- for (const loc of all.allLocs) {
56
- // Get which autocomplete value, if any, its element needs.
57
- const elData = await getLocatorData(loc);
58
- const lcText = elData.excerpt.toLowerCase();
59
- const neededAutos = Object.keys(autoValues)
60
- .filter(autoValue => autoValues[autoValue].some(typeLabel => lcText.includes(typeLabel)));
61
- let neededAuto;
62
- if (neededAutos.length === 1) {
63
- neededAuto = neededAutos[0];
64
- }
65
- else if (! neededAutos.length && await loc.getAttribute('type') === 'email') {
66
- neededAuto = 'email';
67
- }
68
- // If it needs one:
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