testaro 64.11.0 → 65.0.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 CHANGED
@@ -725,9 +725,10 @@ Here is an example of an instance in a standard result:
725
725
  type: 'xpath',
726
726
  spec: '/html[1]/body[1]/section[3]/div[2]/div[1]/ul[1]/li[1]/button[1]'
727
727
  },
728
- excerpt: '<button type="link"></button>',
728
+ excerpt: '<button type="link">Create an account</button>',
729
729
  boxID: '12:340:46:50',
730
- pathID: '/html/body/section[3]/div[2]/div/ul/li[1]/button[1]'
730
+ pathID: '/html/body/section[3]/div[2]/div/ul/li[1]/button[1]',
731
+ text: 'Create an account'
731
732
  }
732
733
  ```
733
734
 
@@ -741,14 +742,15 @@ The element has no `id` attribute to distinguish it from other `button` elements
741
742
  - `box` (coordinates, width, and height of the element box): Editoria11y, Testaro
742
743
  - none: HTML CodeSniffer
743
744
 
744
- The tool also reproduces an excerpt of the element code.
745
+ The tool or Testaro also reproduces an excerpt of the element code.
745
746
 
746
747
  ##### Element identification
747
748
 
748
- While the above properties can help you find the offending element, Testaro makes this easier by adding, where practical, two standard element identifiers to each standard instance:
749
+ While the above properties can help you find the offending element, Testaro makes this easier by adding, where practical, three standard element identifiers to each standard instance:
749
750
 
750
751
  - `boxID`: a compact representation of the x, y, width, and height of the element bounding box, if the element can be identified and is visible.
751
752
  - `pathID`: the XPath of the element, if the element can be identified.
753
+ - `text`: the text content of the element, if the element can be identified.
752
754
 
753
755
  These standard identifiers can help you determine whether violations reported by different tools belong to the same element or different elements. The `boxID` property can also support the making of images of the violating elements.
754
756
 
@@ -757,7 +759,7 @@ Some tools limit the efficacy of the current algorithm for standard identifiers:
757
759
  - HTML CodeSniffer does not report element locations, and the reported code excerpts exclude all text content.
758
760
  - Nu Html Checker reports line and column boundaries of element start tags and truncates element text content in reported code excerpts.
759
761
 
760
- Testaro aims to overcome these limitations by inserting uniquely identifying attributes into all elements of the pages being tested by these tools. Those attribute values appear in the excerpts produced by the tools and permit Testaro to identify the elements in the tested page. Except for elements excluded from the DOM, such as descendants of `noscript` elements, this mechanism allows Testaro to provide a `pathID` property in almost all standard instances. The `boxID` property is less universal, since some elements, such as `script` elements and hidden elements, have no bounding boxes.
762
+ Testaro aims to overcome these limitations by inserting uniquely identifying attributes into all elements of the pages being tested by these tools. Those attribute values permit Testaro to identify the elements in the tested page. Except for elements excluded from the DOM, such as descendants of `noscript` elements, this mechanism allows Testaro to provide a `pathID` property in almost all standard instances. The `boxID` property is less universal, since some elements, such as `script` elements and hidden elements, have no bounding boxes.
761
763
 
762
764
  Testing can change the pages being tested, and such changes can cause a particular element to change its physical or logical location. In such cases, an element may appear multiple times in a tool report with different `boxID` or `pathID` values, even though it is, for practical purposes, the same element.
763
765
 
@@ -966,7 +968,9 @@ The rationales motivating the Testaro-defined tests can be found in comments wit
966
968
 
967
969
  On some occasions a test throws an error that cannot be handled with a `try`-`catch` structure. It has been observed, for example, that the `ibm` test does this when the page content, rather than the page URL, is given to `getCompliance()` and the target is `https://globalsolutions.org`, `https://monsido.com`, or `https://www.ambetterhealth.com/`.
968
970
 
969
- Some tools take apparently infinite time to perform their tests on some pages. To handle such stalling, Testaro subjects all tools to time limits. The limitation is implemented with forked child processes. Specifically, the `procs/doTestAct.js` module is run as a forked process with a `timeout` option for each of the 11 tools.
971
+ Some tools take apparently infinite time to perform their tests on some pages. One website whose pages prevent 5 of the tools from ever completing their tests is the site of BrowserStack.
972
+
973
+ To handle such fatal errors andstalling, Testaro runs the tests of each tool in a separate forked child process that executes the `doTestAct.js` module. The parent process subjects each tool to a time limit and kills the child if the time limit expires.
970
974
 
971
975
  ### Activation
972
976
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "64.11.0",
3
+ "version": "65.0.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": {
@@ -22,12 +22,21 @@ const {browserClose, launch} = require(`${__dirname}/../run`);
22
22
  // Module to set operating-system constants.
23
23
  const os = require('os');
24
24
 
25
- // CONSTANTS
26
-
27
- const tmpDir = os.tmpdir();
28
-
29
25
  // FUNCTIONS
30
26
 
27
+ // Sends a message to the parent process.
28
+ const sendMessage = message => {
29
+ try {
30
+ if (typeof process.send === 'function') {
31
+ process.send(message);
32
+ }
33
+ }
34
+ catch(error) {
35
+ console.log(
36
+ `ERROR: process.send threw ${error.message} trying to send message ${message} to parent`
37
+ );
38
+ }
39
+ };
31
40
  // Performs the tests of an act.
32
41
  const doTestAct = async (reportPath, actIndex) => {
33
42
  // Get the report from the temporary directory.
@@ -58,7 +67,11 @@ const doTestAct = async (reportPath, actIndex) => {
58
67
  const reportJSON = JSON.stringify(report);
59
68
  await fs.writeFile(reportPath, reportJSON);
60
69
  // Report this.
61
- process.send('ERROR: Job aborted');
70
+ sendMessage({
71
+ status: 'error',
72
+ error: 'Page launch aborted'
73
+ });
74
+ process.exit(1);
62
75
  }
63
76
  // Otherwise, i.e. if the launch did not abort the job:
64
77
  else {
@@ -85,7 +98,10 @@ const doTestAct = async (reportPath, actIndex) => {
85
98
  // Save the revised report.
86
99
  await fs.writeFile(reportPath, reportJSON);
87
100
  // Send a completion message.
88
- process.send('Act completed');
101
+ sendMessage({
102
+ status: 'ok',
103
+ });
104
+ process.exit(0);
89
105
  }
90
106
  // If the tool invocation failed:
91
107
  catch(error) {
@@ -97,7 +113,11 @@ const doTestAct = async (reportPath, actIndex) => {
97
113
  // Report the failure.
98
114
  const message = error.message.slice(0, 400);
99
115
  console.log(`ERROR: Test act ${act.which} failed (${message})`);
100
- process.send('ERROR performing the act');
116
+ sendMessage({
117
+ status: 'error',
118
+ error: 'ERROR performing the act'
119
+ });
120
+ process.exit(1);
101
121
  };
102
122
  }
103
123
  // Otherwise, i.e. if the page does not exist:
@@ -116,9 +136,32 @@ const doTestAct = async (reportPath, actIndex) => {
116
136
  // Report this.
117
137
  const message = 'ERROR: No page';
118
138
  console.log(message);
119
- process.send(message);
139
+ sendMessage({
140
+ status: 'error',
141
+ error: message
142
+ });
143
+ process.exit(1);
120
144
  }
121
145
  };
122
146
 
147
+ process.on('uncaughtException', error => {
148
+ console.log(`ERROR: uncaughtException (${error.message})`);
149
+ sendMessage({
150
+ status: 'error',
151
+ error: 'uncaughtException'
152
+ });
153
+ process.exit(1);
154
+ });
155
+
156
+ process.on('unhandledRejection', error => {
157
+ const message = error && error.message ? error.message : String(error);
158
+ console.log(`ERROR: unhandledRejection (${message})`);
159
+ sendMessage({
160
+ status: 'error',
161
+ error: 'unhandledRejection'
162
+ });
163
+ process.exit(1);
164
+ });
165
+
123
166
  const args = process.argv;
124
167
  doTestAct(args[2], Number.parseInt(args[3]));
package/run.js CHANGED
@@ -791,7 +791,7 @@ const launchSpecs = (act, report) => [
791
791
  ];
792
792
  // Performs the acts in a report and adds the results to the report.
793
793
  const doActs = async (report, opts = {}) => {
794
- const {acts} = report;
794
+ let {acts} = report;
795
795
  // Get the granular observation options, if any.
796
796
  const {onProgress = null, signal = null} = opts;
797
797
  // Get the standardization specification.
@@ -885,9 +885,7 @@ const doActs = async (report, opts = {}) => {
885
885
  // If this failed:
886
886
  if (! page) {
887
887
  // Add this to the act.
888
- act.data ??= {};
889
- act.data.prevented = true;
890
- act.data.error = page.error || '';
888
+ addError(false, false, report, actIndex, page.error || '');
891
889
  }
892
890
  }
893
891
  // Otherwise, if the act is a test act:
@@ -898,72 +896,154 @@ const doActs = async (report, opts = {}) => {
898
896
  const startTime = Date.now();
899
897
  // Add it to the act.
900
898
  act.startTime = startTime;
901
- // Save the report.
902
899
  let reportJSON = JSON.stringify(report);
900
+ // Save a copy of the report.
903
901
  await fs.writeFile(reportPath, reportJSON);
904
- // Create a process to perform the act and add the result to the saved report.
902
+ let timedOut = false;
903
+ const limitMs = timeoutMultiplier * 1000 * (timeLimits[act.which] || 15);
904
+ // Create a child process to perform the act and add the result to the saved report.
905
905
  const actResult = await new Promise(resolve => {
906
906
  let closed = false;
907
- const child = fork(
908
- `${__dirname}/procs/doTestAct`, [reportPath, actIndex], {timeout: timeoutMultiplier * 1000 * (timeLimits[act.which] || 15)}
909
- );
907
+ const child = fork(`${__dirname}/procs/doTestAct`, [reportPath, actIndex]);
908
+ let killTimer = null;
909
+ // Start a timeout timer for the child process.
910
+ const timeoutTimer = setTimeout(() => {
911
+ if (! timedOut) {
912
+ timedOut = true;
913
+ console.log(`ERROR: Timed out at ${Math.round(limitMs / 1000)} seconds`);
914
+ child.kill('SIGTERM');
915
+ killTimer = setTimeout(() => {
916
+ if (! closed) {
917
+ console.log('ERROR: Failed to exit on SIGTERM from parent')
918
+ }
919
+ child.kill('SIGKILL');
920
+ }, 2000);
921
+ }
922
+ }, limitMs);
923
+ // Clears any current timers.
924
+ const clearTimers = () => {
925
+ [timeoutTimer, killTimer].forEach(timer => {
926
+ if (timer) {
927
+ clearTimeout(timer);
928
+ }
929
+ });
930
+ };
931
+ // If the child process sends a message (normally Act completed):
910
932
  child.on('message', message => {
911
933
  if (! closed) {
912
934
  closed = true;
913
- resolve(message);
935
+ clearTimers();
936
+ // Return the message.
937
+ resolve({
938
+ kind: 'message',
939
+ message
940
+ });
941
+ }
942
+ });
943
+ // If the child process sends an error:
944
+ child.on('error', error => {
945
+ if (! closed) {
946
+ closed = true;
947
+ clearTimers();
948
+ // Return the error message.
949
+ resolve({
950
+ kind: 'error',
951
+ error: error.message
952
+ });
914
953
  }
915
954
  });
916
- child.on('close', code => {
955
+ // If the child process closes:
956
+ child.on('close', (code, signal) => {
917
957
  if (! closed) {
918
958
  closed = true;
919
- resolve(`Page closed with code ${code}`);
959
+ clearTimers();
960
+ // Return the exit code, signal, and timeout status.
961
+ resolve({
962
+ kind: 'close',
963
+ code,
964
+ signal,
965
+ timedOut
966
+ });
920
967
  }
921
968
  });
922
969
  });
923
- // Get the revised report.
924
- reportJSON = await fs.readFile(reportPath, 'utf8');
925
- report = JSON.parse(reportJSON);
926
- // Get the revised act.
927
- act = report.acts[actIndex];
928
- // If the result is an error code:
929
- if (typeof actResult === 'number') {
930
- // Add the error data to the act.
931
- act.data ??= {};
932
- act.data.prevented = true;
933
- act.data.error = actResult;
970
+ // If the child process sent a message:
971
+ if (actResult.kind === 'message') {
972
+ // Get the revised report file.
973
+ reportJSON = await fs.readFile(reportPath, 'utf8');
974
+ try {
975
+ // Convert it from JSON to an object and replace the report with the object.
976
+ report = JSON.parse(reportJSON);
977
+ // Redefine the acts as those in the revised report.
978
+ acts = report.acts;
979
+ }
980
+ // If the conversion fails, leaving the report and its acts unchanged:
981
+ catch (error) {
982
+ // Report this.
983
+ console.log(
984
+ `ERROR: Tool sent message ${actResult.message}. Report is no longer JSON (${error.message}) but is instead a(n) ${typeof reportJSON} of length ${reportJSON.length}:\n${reportJSON}`
985
+ );
986
+ // Add the error data to the act.
987
+ addError(
988
+ false,
989
+ false,
990
+ report,
991
+ actIndex,
992
+ `Non-JSON report file after message ${actResult.message}`
993
+ );
994
+ }
934
995
  }
935
- // Otherwise, i.e. if it is not an error code:
996
+ // Otherwise, i.e. if the child process closed abnormally:
936
997
  else {
937
- // Add the elapsed time of the tool to the report.
938
- const time = Math.round((Date.now() - startTime) / 1000);
939
- const {toolTimes} = report.jobData;
940
- toolTimes[act.which] ??= 0;
941
- toolTimes[act.which] += time;
942
- // If the act was not prevented:
943
- if (act.data && ! act.data.prevented) {
944
- // If the act has expectations:
945
- const expectations = act.expect;
946
- if (expectations) {
947
- // Initialize whether they were fulfilled.
948
- act.expectations = [];
949
- let failureCount = 0;
950
- // For each expectation:
951
- expectations.forEach(spec => {
952
- // Add its result to the act.
953
- const truth = isTrue(act, spec);
954
- act.expectations.push({
955
- property: spec[0],
956
- relation: spec[1],
957
- criterion: spec[2],
958
- actual: truth[0],
959
- passed: truth[1]
960
- });
961
- if (! truth[1]) {
962
- failureCount++;
963
- }
998
+ // Add the error data to the act.
999
+ const {code, error, kind, signal} = actResult;
1000
+ if (kind === 'close' && timedOut) {
1001
+ addError(
1002
+ false, false, report, actIndex, `Timed out at ${Math.round(limitMs / 1000)} seconds`
1003
+ );
1004
+ }
1005
+ else if (kind === 'close') {
1006
+ addError(
1007
+ true, false, report, actIndex, `Closed with code ${code} and signal ${signal})`
1008
+ );
1009
+ }
1010
+ else {
1011
+ addError(
1012
+ true, false, report, actIndex, `Terminated with error ${error}`
1013
+ );
1014
+ }
1015
+ }
1016
+ // Get the (usually revised) act.
1017
+ act = acts[actIndex];
1018
+ // Add the elapsed time of the tool to the report.
1019
+ const time = Math.round((Date.now() - startTime) / 1000);
1020
+ const {toolTimes} = report.jobData;
1021
+ toolTimes[act.which] ??= 0;
1022
+ toolTimes[act.which] += time;
1023
+ // If the act was not prevented:
1024
+ if (act.data && ! act.data.prevented) {
1025
+ const expectations = act.expect;
1026
+ // If the act has expectations:
1027
+ if (expectations) {
1028
+ // Initialize whether they were fulfilled.
1029
+ act.expectations = [];
1030
+ let failureCount = 0;
1031
+ // For each expectation:
1032
+ expectations.forEach(spec => {
1033
+ // Add its result to the act.
1034
+ const truth = isTrue(act, spec);
1035
+ act.expectations.push({
1036
+ property: spec[0],
1037
+ relation: spec[1],
1038
+ criterion: spec[2],
1039
+ actual: truth[0],
1040
+ passed: truth[1]
964
1041
  });
965
- act.expectationFailures = failureCount;
966
- }
1042
+ if (! truth[1]) {
1043
+ failureCount++;
1044
+ }
1045
+ });
1046
+ act.expectationFailures = failureCount;
967
1047
  }
968
1048
  }
969
1049
  }
@@ -1602,10 +1682,9 @@ const doActs = async (report, opts = {}) => {
1602
1682
  // Notify the observer of the act and log it.
1603
1683
  tellServer(report, messageParams, message);
1604
1684
  }
1605
-
1685
+ }
1606
1686
  // Notify the observer and log the start of standardization.
1607
1687
  tellServer(report, '', 'Starting result standardization');
1608
- }
1609
1688
  const launchSpecActs = {};
1610
1689
  // For each act:
1611
1690
  report.acts.forEach((act, index) => {
@@ -1645,20 +1724,36 @@ const doActs = async (report, opts = {}) => {
1645
1724
  };
1646
1725
  // Populate it.
1647
1726
  standardize(act);
1727
+ // If the original-format result is not to be included in the report:
1728
+ if (standard === 'only') {
1729
+ // Remove it.
1730
+ delete act.result;
1731
+ }
1732
+ // Notify the observer and log the start of identification.
1733
+ tellServer(report, '', 'Starting element identification');
1648
1734
  // For each of its standard instances:
1649
1735
  for (const instance of act.standardResult.instances) {
1650
- // If the instance does not have both a box ID and a path ID:
1651
- if (! (instance.boxID && instance.pathID)) {
1736
+ let {boxID, pathID} = instance;
1737
+ // If the instance does not have both a box ID and a valid path ID:
1738
+ if (! boxID && (! pathID || pathID.includes(' '))) {
1652
1739
  const elementID = await identify(instance, page);
1653
1740
  // If it has no box ID but the element has a bounding box:
1654
- if (elementID.boxID && ! instance.boxID) {
1655
- // Add a box ID.
1741
+ if (elementID.boxID && ! boxID) {
1742
+ // Add a box ID to the instance.
1656
1743
  instance.boxID = elementID.boxID;
1657
1744
  }
1658
- // If it has no path ID but the element has one:
1659
- if (elementID.pathID && ! instance.pathID) {
1660
- // Add a path ID.
1661
- instance.pathID = elementID.pathID;
1745
+ // If it has no valid path ID:
1746
+ if (! pathID || pathID.includes(' ')) {
1747
+ // If the element has a valid path ID:
1748
+ if (elementID.pathID && ! elementID.pathID.includes(' ')) {
1749
+ // Add or replace the path ID of the instance.
1750
+ instance.pathID = elementID.pathID;
1751
+ }
1752
+ // Otherwise, if the instance has an invalid but uncorrectable path ID:
1753
+ else if (pathID) {
1754
+ // Delete it.
1755
+ delete instance.pathID;
1756
+ }
1662
1757
  }
1663
1758
  }
1664
1759
  // If the instance excerpt contains a unique Testaro identifier attribute:
@@ -1669,12 +1764,22 @@ const doActs = async (report, opts = {}) => {
1669
1764
  .replace(/ data-testaro-id="[^" ]*("|$)/g, '')
1670
1765
  .replace(/ data-testaro-id="[^" ]* /g, ' ');
1671
1766
  }
1767
+ pathID = instance.pathID;
1768
+ // If the instance has a pathID and no text property:
1769
+ if (pathID && ! instance.text) {
1770
+ // Get the element.
1771
+ const elementLoc = page.locator(`xpath=${pathID}`, {hasText: /.+/});
1772
+ // If it exists and is unique:
1773
+ if (await elementLoc.count() === 1) {
1774
+ let text = await elementLoc.textContent();
1775
+ if (text.length > 200) {
1776
+ text = `${text.slice(0, 150)} … ${text.slice(-50)}`;
1777
+ }
1778
+ // Add the text content or its ends to the instance.
1779
+ instance.text = text.trim();
1780
+ }
1781
+ }
1672
1782
  };
1673
- // If the original-format result is not to be included in the report:
1674
- if (standard === 'only') {
1675
- // Remove it.
1676
- delete act.result;
1677
- }
1678
1783
  };
1679
1784
  }
1680
1785
  // Otherwise, i.e. if the launch or navigation failed:
@@ -1682,9 +1787,9 @@ const doActs = async (report, opts = {}) => {
1682
1787
  console.log(`ERROR: Launch or navigation to standardize ${specString} acts failed`);
1683
1788
  }
1684
1789
  };
1685
- // Close the last browser launched for standardization.
1790
+ // Close the last browser launched for standardization and element identification.
1686
1791
  await browserClose();
1687
- console.log('Standardization completed');
1792
+ console.log('Standardization and element identification completed');
1688
1793
  const {acts} = report;
1689
1794
  const idData = {};
1690
1795
  // For each act:
@@ -1697,15 +1802,17 @@ const doActs = async (report, opts = {}) => {
1697
1802
  instanceCount: 0,
1698
1803
  boxIDCount: 0,
1699
1804
  pathIDCount: 0,
1805
+ textCount: 0,
1700
1806
  boxIDPercent: null,
1701
- pathIDPercent: null
1807
+ pathIDPercent: null,
1808
+ textPercent: null
1702
1809
  };
1703
1810
  const actIDData = idData[which];
1704
1811
  const {standardResult} = act;
1705
1812
  const {instances} = standardResult;
1706
1813
  // For each standard instance in the act:
1707
1814
  for (const instance of instances) {
1708
- const {boxID, pathID} = instance;
1815
+ const {boxID, pathID, text} = instance;
1709
1816
  // Increment the instance count.
1710
1817
  actIDData.instanceCount++;
1711
1818
  // If the instance has a box ID:
@@ -1718,13 +1825,19 @@ const doActs = async (report, opts = {}) => {
1718
1825
  // Increment the path ID count.
1719
1826
  actIDData.pathIDCount++;
1720
1827
  }
1828
+ // If the instance has a text:
1829
+ if (text) {
1830
+ // Increment the text count.
1831
+ actIDData.textCount++;
1832
+ }
1721
1833
  }
1722
- const {instanceCount, boxIDCount, pathIDCount} = actIDData;
1834
+ const {instanceCount, boxIDCount, pathIDCount, textCount} = actIDData;
1723
1835
  // If there are any instances:
1724
1836
  if (instanceCount) {
1725
- // Add the box ID and path ID percentages to the iData property.
1837
+ // Add the box ID path ID, and text percentages to the iData property.
1726
1838
  actIDData.boxIDPercent = Math.round(100 * boxIDCount / instanceCount);
1727
1839
  actIDData.pathIDPercent = Math.round(100 * pathIDCount / instanceCount);
1840
+ actIDData.textPercent = Math.round(100 * textCount / instanceCount);
1728
1841
  }
1729
1842
  }
1730
1843
  }