testaro 3.0.0 → 4.0.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/README.md CHANGED
@@ -10,16 +10,6 @@ The purpose of Testaro is to provide programmatic access to over 600 accessibili
10
10
 
11
11
  Running Testaro requires telling it which operations (including tests) to perform and which URLs to perform them on, and giving Testaro an object to put its output into.
12
12
 
13
- ## Origin
14
-
15
- Work on the custom tests in this package began in 2017, and work on the multi-package federation that Testaro implements began in early 2018. These two aspects were combined into the [Autotest](https://github.com/jrpool/autotest) package in early 2021 and into this more limited-purpose package, Testaro, in January 2022.
16
-
17
- Testaro omits some functionalities of Autotest, such as:
18
- - tests producing results intended to be human-inspected
19
- - previous versions of scoring algorithms
20
- - file operations for score aggregation, report revision, and HTML reports
21
- - a web user interface
22
-
23
13
  ## System requirements
24
14
 
25
15
  Version 14 or later of [Node.js](https://nodejs.org/en/).
@@ -35,6 +25,7 @@ Testaro includes some of its own accessibility tests. In addition, it performs t
35
25
  - [alfa](https://alfa.siteimprove.com/) (Siteimprove alfa)
36
26
  - [Automated Accessibility Testing Tool](https://www.npmjs.com/package/aatt) (Paypal AATT, running HTML CodeSniffer)
37
27
  - [axe-playwright](https://www.npmjs.com/package/axe-playwright) (Deque Axe-core)
28
+ - [Tenon](https://tenon.io/documentation/what-tenon-tests.php)
38
29
  - [WAVE API](https://wave.webaim.org/api/) (WebAIM WAVE)
39
30
 
40
31
  As of this version, the counts of tests in the packages referenced above were:
@@ -42,14 +33,11 @@ As of this version, the counts of tests in the packages referenced above were:
42
33
  - Alfa: 103
43
34
  - Axe-core: 138
44
35
  - Equal Access: 163
36
+ - Tenon: 180
45
37
  - WAVE: 110
46
38
  - subtotal: 612
47
39
  - Testaro tests: 16
48
- - grand total: 628
49
-
50
- ## Related packages
51
-
52
- [Testilo](https://github.com/jrpool/testilo) is an application that facilitates the use of Testaro.
40
+ - grand total: 808
53
41
 
54
42
  ## Code organization
55
43
 
@@ -266,6 +254,24 @@ An example of a **Testaro-defined** test is:
266
254
 
267
255
  In this case, Testaro runs the `motion` test with the specified parameters.
268
256
 
257
+ ###### Tenon
258
+
259
+ The `tenon` test requires two commands:
260
+ - A command of type `tenonRequest`.
261
+ - A command of type `test` with `tenon` as the value of `which`.
262
+
263
+ The reason for this is that the Tenon API operates asynchronously. You ask it to perform a test, and it puts your request into a queue. To learn whether Tenon has completed your test, you make a status request. You can continue making status requests until Tenon replies that your test has been completed. Then you submit a request for the test result, and Tenon replies with the result. (As of May 2022, status requests were observed to misreport still-running tests as completed. The `tenon` test works around that.)
264
+
265
+ Tenon says that tests are typically completed in 3 to 6 seconds but that the latency can be longer, depending on demand.
266
+
267
+ Therefore, you can include a `tenonRequest` command early in your script, and a `tenon` test late in your script. Tenon will move your request through its queue while Testaro is processing your script. When Testaro reaches your `tenon` test command, Tenon will most likely have completed your test. If not, the `tenon` test will wait and then make a second request before giving up.
268
+
269
+ Thus, a `tenon` test actually does not perform any test; it merely collects the result. The page that was active when the `tenonRequest` command was performed is the one that Tenon tests.
270
+
271
+ In case you want to perform more than one `tenon` test, you can do so. Just give each pair of commands a distinct `id` property, so each `tenon` test command will request the correct result.
272
+
273
+ Tenon recommends giving it a public URL rather than giving it the content of a page, if possible. So, it is best to give the `withNewContent` property of the `tenonRequest` command the value `true`, unless the page is not public.
274
+
269
275
  ##### Scoring
270
276
 
271
277
  An example of a **scoring** command is:
@@ -424,8 +430,14 @@ Another way to run Testaro is to use Testilo, which can handle batches and saves
424
430
 
425
431
  If a `wave` test is included in the script, an environment variable named `TESTARO_WAVE_KEY` must exist, with your WAVE API key as its value.
426
432
 
433
+ If a `tenon` test is included in the script, environment variables named `TESTARO_TENON_USER` and `TESTARO_TENON_PASSWORD` must exist, with your Tenon username and password, respectively, as their values.
434
+
435
+ The `text` command can interpolate the value of an environment variable into text that it enters on a page, as documented in the `commands.js` file.
436
+
427
437
  Before executing a Testaro script, you can optionally also set the environment variables `TESTARO_DEBUG` (to `'true'` or anything else) and/or `TESTARO_WAITS` (to a non-negative integer). The effects of these variables are described in the `index.js` file.
428
438
 
439
+ You may store these environment variables in an untracked `.env` file if you wish, and Testaro will recognize them.
440
+
429
441
  ## Validation
430
442
 
431
443
  _Executors_ for Testaro validation are located in the `validation` directory.
@@ -450,18 +462,6 @@ You can define additional Testaro commands and functionality. Contributions are
450
462
 
451
463
  The rationales motivating the Testaro-defined tests and scoring procs can be found in comments within the files of those tests and procs, in the `tests` and `procs/score` directories. Unavoidably, each test is opinionated. Testaro itself, however, can accommodate other tests representing different opinions. Testaro is intended to be neutral with respect to questions such as the criteria for accessibility, the severities of accessibility issues, whether accessibility is binary or graded, and the distinction between usability and accessibility.
452
464
 
453
- ### Future work
454
-
455
- Further development is contemplated, is taking place, or is welcomed, on:
456
- - addition of Tenon to the set of packages
457
- - links with href="#"
458
- - links and buttons styled non-distinguishably
459
- - first focused element not first focusable element in DOM
460
- - never-visible skip links
461
- - buttons with no text content
462
- - modal dialogs
463
- - autocomplete attributes
464
-
465
465
  ## Testing challenges
466
466
 
467
467
  ### Activation
@@ -478,9 +478,11 @@ Test packages sometimes do redundant testing, in that two or more packages test
478
478
 
479
479
  The files in the `temp` directory are presumed ephemeral and are not tracked by `git`. When tests require temporary files to be written, Testaro writes them there.
480
480
 
481
- ## Origin
481
+ ## Related packages
482
482
 
483
- Testaro is derived from [Autotest](https://github.com/jrpool/autotest), which in turn is derived from accessibility test investigations beginning in 2018.
483
+ [Testilo](https://www.npmjs.com/package/testilo) is an application that facilitates the use of Testaro.
484
+
485
+ Testaro is derived from [Autotest](https://github.com/jrpool/autotest).
484
486
 
485
487
  Testaro omits some functionalities of Autotest, such as:
486
488
  - tests producing results intended to be human-inspected
@@ -488,6 +490,44 @@ Testaro omits some functionalities of Autotest, such as:
488
490
  - file operations for score aggregation, report revision, and HTML reports
489
491
  - a web user interface
490
492
 
493
+ ## Origin
494
+
495
+ Work on the custom tests in this package began in 2017, and work on the multi-package federation that Testaro implements began in early 2018. These two aspects were combined into the [Autotest](https://github.com/jrpool/autotest) package in early 2021 and into this more single-purpose package, Testaro, in January 2022.
496
+
491
497
  ## Etymology
492
498
 
493
499
  “Testaro” means “collection of tests” in Esperanto.
500
+
501
+ ## Future work
502
+
503
+ ### Improvements
504
+
505
+ Further development is contemplated, is taking place, or is welcomed, on:
506
+ - addition of Tenon to the set of packages
507
+ - links with href="#"
508
+ - links and buttons styled non-distinguishably
509
+ - first focused element not first focusable element in DOM
510
+ - never-visible skip links
511
+ - buttons with no text content
512
+ - modal dialogs
513
+ - autocomplete attributes
514
+ - inclusion of other test packages, such as:
515
+ - FAE (https://github.com/opena11y/evaluation-library)
516
+ - Tenon
517
+
518
+ ## Corrections
519
+
520
+ Issues found or reported with the current version that need diagnosis and correction include:
521
+
522
+ ### hover
523
+
524
+ There seem to be a couple of problems with the hover test:
525
+ - The score for unhoverability is documented as 2 times the count of unhoverables, but is reported as 1 time that count.
526
+ - The list of unhoverables in the report is empty.
527
+ Observed after inquiry by Tobias Christian Jensen of Siteimprove on 2022-05-09.
528
+
529
+ ### axe
530
+
531
+ Configuration to include best practices and experimental tests.
532
+
533
+ Investigation of tags, including wcag2a, wcag2aa, wcag21a, wcag21aa, best-practice, wcag***, ACT, cat.*.
package/commands.js CHANGED
@@ -118,6 +118,14 @@ exports.commands = {
118
118
  what: [false, 'string', 'hasLength', 'comment']
119
119
  }
120
120
  ],
121
+ tenonRequest: [
122
+ 'Request a Tenon test',
123
+ {
124
+ id: [true, 'string', 'hasLength', 'ID for this test instance'],
125
+ withNewContent: [true, 'boolean', '', 'true: use a URL; false: use page content'],
126
+ what: [false, 'string', 'hasLength', 'comment']
127
+ }
128
+ ],
121
129
  text: [
122
130
  'Enter text into a text input, optionally with 1 placeholder for an all-caps literal environment variable',
123
131
  {
@@ -185,7 +193,7 @@ exports.commands = {
185
193
  {
186
194
  withItems: [true, 'boolean'],
187
195
  withNewContent: [
188
- false, 'boolean', '', 'true: use a URL; false: use page content; omitted: use both'
196
+ false, 'boolean', '', 'true: use a URL; false: use page content; omitted: both'
189
197
  ]
190
198
  }
191
199
  ],
@@ -233,6 +241,12 @@ exports.commands = {
233
241
  withItems: [true, 'boolean']
234
242
  }
235
243
  ],
244
+ tenon: [
245
+ 'Perform a Tenon test',
246
+ {
247
+ id: [true, 'string', 'hasLength', 'ID of the requested test instance']
248
+ }
249
+ ],
236
250
  wave: [
237
251
  'Perform a WebAIM WAVE test',
238
252
  {
package/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  testaro main script.
4
4
  */
5
5
  // ########## IMPORTS
6
+ // Module to keep secrets.
7
+ require('dotenv').config();
6
8
  // Requirements for commands.
7
9
  const {commands} = require('./commands');
8
10
  // ########## CONSTANTS
@@ -40,6 +42,7 @@ const tests = {
40
42
  role: 'roles',
41
43
  styleDiff: 'style inconsistencies',
42
44
  tabNav: 'keyboard navigation between tab elements',
45
+ tenon: 'Tenon',
43
46
  wave: 'WAVE',
44
47
  zIndex: 'z indexes'
45
48
  };
@@ -55,6 +58,11 @@ const browserTypeNames = {
55
58
  };
56
59
  // Items that may be waited for.
57
60
  const waitables = ['url', 'title', 'body'];
61
+ // Tenon data.
62
+ const tenonData = {
63
+ accessToken: '',
64
+ requestIDs: {}
65
+ };
58
66
  // ########## VARIABLES
59
67
  // Facts about the current session.
60
68
  let logCount = 0;
@@ -359,7 +367,7 @@ const textOf = async (page, element) => {
359
367
  const matchElement = async (page, selector, matchText, index = 0) => {
360
368
  // If the page still exists:
361
369
  if (page) {
362
- // Wait 3 seconds until the body contains any text to be matched.
370
+ // Wait 2 seconds until the body contains any text to be matched.
363
371
  const slimText = debloat(matchText);
364
372
  const bodyText = await page.textContent('body');
365
373
  const slimBody = debloat(bodyText);
@@ -706,157 +714,125 @@ const doActs = async (report, actIndex, page) => {
706
714
  await require('./procs/test/allVis').allVis(page);
707
715
  act.result = 'All elements visible.';
708
716
  }
709
- // Otherwise, if it is a repetitive keyboard navigation:
710
- else if (act.type === 'presses') {
711
- const {navKey, what, which, withItems} = act;
712
- const matchTexts = which ? which.map(text => debloat(text)) : [];
713
- // Initialize the loop variables.
714
- let status = 'more';
715
- let presses = 0;
716
- let amountRead = 0;
717
- let items = [];
718
- let matchedText;
719
- // As long as a matching element has not been reached:
720
- while (status === 'more') {
721
- // Press the Escape key to dismiss any modal dialog.
722
- await page.keyboard.press('Escape');
723
- // Press the specified navigation key.
724
- await page.keyboard.press(navKey);
725
- presses++;
726
- // Identify the newly current element or a failure.
727
- const currentJSHandle = await page.evaluateHandle(actCount => {
728
- // Initialize it as the focused element.
729
- let currentElement = document.activeElement;
730
- // If it exists in the page:
731
- if (currentElement && currentElement.tagName !== 'BODY') {
732
- // Change it, if necessary, to its active descendant.
733
- if (currentElement.hasAttribute('aria-activedescendant')) {
734
- currentElement = document.getElementById(
735
- currentElement.getAttribute('aria-activedescendant')
736
- );
737
- }
738
- // Or change it, if necessary, to its selected option.
739
- else if (currentElement.tagName === 'SELECT') {
740
- const currentIndex = Math.max(0, currentElement.selectedIndex);
741
- const options = currentElement.querySelectorAll('option');
742
- currentElement = options[currentIndex];
743
- }
744
- // Or change it, if necessary, to its active shadow-DOM element.
745
- else if (currentElement.shadowRoot) {
746
- currentElement = currentElement.shadowRoot.activeElement;
747
- }
748
- // If there is a current element:
749
- if (currentElement) {
750
- // If it was already reached within this command performance:
751
- if (currentElement.dataset.pressesReached === actCount.toString(10)) {
752
- // Report the error.
753
- console.log(`ERROR: ${currentElement.tagName} element reached again`);
754
- status = 'ERROR';
755
- return 'ERROR: locallyExhausted';
756
- }
757
- // Otherwise, i.e. if it is newly reached within this act:
758
- else {
759
- // Mark and return it.
760
- currentElement.dataset.pressesReached = actCount;
761
- return currentElement;
762
- }
763
- }
764
- // Otherwise, i.e. if there is no current element:
765
- else {
766
- // Report the error.
767
- status = 'ERROR';
768
- return 'noActiveElement';
769
- }
770
- }
771
- // Otherwise, i.e. if there is no focus in the page:
772
- else {
773
- // Report the error.
774
- status = 'ERROR';
775
- return 'ERROR: globallyExhausted';
776
- }
777
- }, actCount);
778
- // If the current element exists:
779
- const currentElement = currentJSHandle.asElement();
780
- if (currentElement) {
781
- // Update the data.
782
- const tagNameJSHandle = await currentElement.getProperty('tagName');
783
- const tagName = await tagNameJSHandle.jsonValue();
784
- const text = await textOf(page, currentElement);
785
- // If the text of the current element was found:
786
- if (text !== null) {
787
- const textLength = text.length;
788
- // If it is non-empty and there are texts to match:
789
- if (matchTexts.length && textLength) {
790
- // Identify the matching text.
791
- matchedText = matchTexts.find(matchText => text.includes(matchText));
792
- }
793
- // Update the item data if required.
794
- if (withItems) {
795
- const itemData = {
796
- tagName,
797
- text,
798
- textLength
799
- };
800
- if (matchedText) {
801
- itemData.matchedText = matchedText;
717
+ // Otherwise, if the act is a tenon request:
718
+ else if (act.type === 'tenonRequest') {
719
+ const {id, withNewContent} = act;
720
+ const https = require('https');
721
+ // If a Tenon access token has not yet been obtained:
722
+ if (! tenonData.accessToken) {
723
+ // Authenticate with the Tenon API.
724
+ const authData = await new Promise(resolve => {
725
+ const request = https.request(
726
+ {
727
+ host: 'tenon.io',
728
+ path: '/api/v2/auth',
729
+ port: 443,
730
+ protocol: 'https:',
731
+ method: 'POST',
732
+ headers: {
733
+ 'Content-Type': 'application/json',
734
+ 'Cache-Control': 'no-cache'
802
735
  }
803
- items.push(itemData);
804
- }
805
- amountRead += textLength;
806
- // If there is no text-match failure:
807
- if (matchedText || ! matchTexts.length) {
808
- // If the element has any specified tag name:
809
- if (! what || tagName === what) {
810
- // Change the status.
811
- status = 'done';
812
- // Perform the action.
813
- const inputText = act.text;
814
- if (inputText) {
815
- await page.keyboard.type(inputText);
816
- presses += inputText.length;
736
+ },
737
+ response => {
738
+ let responseData = '';
739
+ response.on('data', chunk => {
740
+ responseData += chunk;
741
+ });
742
+ response.on('end', () => {
743
+ try {
744
+ const responseJSON = JSON.parse(responseData);
745
+ return resolve(responseJSON);
817
746
  }
818
- if (act.action) {
819
- presses++;
820
- await page.keyboard.press(act.action);
821
- await page.waitForLoadState();
747
+ catch(error) {
748
+ return resolve({
749
+ error: 'Tenon did not return JSON authentication data.',
750
+ responseData
751
+ });
822
752
  }
823
- }
753
+ });
824
754
  }
825
- }
826
- else {
827
- status = 'ERROR';
828
- }
755
+ );
756
+ const tenonUser = process.env.TESTARO_TENON_USER;
757
+ const tenonPassword = process.env.TESTARO_TENON_PASSWORD;
758
+ const postData = JSON.stringify({
759
+ username: tenonUser,
760
+ password: tenonPassword
761
+ });
762
+ request.write(postData);
763
+ request.end();
764
+ });
765
+ // If the authentication succeeded:
766
+ if (authData.access_token) {
767
+ // Record the access token.
768
+ tenonData.accessToken = authData.access_token;
829
769
  }
830
- // Otherwise, i.e. if there was a failure:
770
+ // Otherwise, i.e. if the authentication failed:
831
771
  else {
832
- // Update the status.
833
- status = await currentJSHandle.jsonValue();
772
+ console.log('ERROR: tenon authentication failed');
834
773
  }
835
774
  }
836
- // Add the result to the act.
837
- act.result = {
838
- status,
839
- totals: {
840
- presses,
841
- amountRead
775
+ // If a Tenon access token exists:
776
+ if (tenonData.accessToken) {
777
+ // Request a Tenon test of the page and get a response ID.
778
+ const option = {};
779
+ // If Tenon is to be given the URL and not the content of the page:
780
+ if (withNewContent) {
781
+ // Specify this.
782
+ option.url = page.url();
842
783
  }
843
- };
844
- if (status === 'done' && matchedText) {
845
- act.result.matchedText = matchedText;
846
- }
847
- if (withItems) {
848
- act.result.items = items;
784
+ // Otherwise, i.e. if Tenon is to be given the page content:
785
+ else {
786
+ // Specify this.
787
+ option.src = await page.content();
788
+ }
789
+ // Request a Tenon test and get a response ID.
790
+ const responseID = await new Promise(resolve => {
791
+ const request = https.request(
792
+ {
793
+ host: 'tenon.io',
794
+ path: '/api/v2/',
795
+ port: 443,
796
+ protocol: 'https:',
797
+ method: 'POST',
798
+ headers: {
799
+ 'Content-Type': 'application/json',
800
+ 'Cache-Control': 'no-cache',
801
+ Authorization: `Bearer ${tenonData.accessToken}`
802
+ }
803
+ },
804
+ response => {
805
+ let resultJSON = '';
806
+ response.on('data', chunk => {
807
+ resultJSON += chunk;
808
+ });
809
+ // When the data arrive, return them as an object.
810
+ response.on('end', () => {
811
+ try {
812
+ const result = JSON.parse(resultJSON);
813
+ resolve(result.data.responseID || '');
814
+ }
815
+ catch (error) {
816
+ console.log('ERROR: Tenon did not return JSON.');
817
+ resolve('');
818
+ }
819
+ });
820
+ }
821
+ );
822
+ const postData = JSON.stringify(option);
823
+ request.write(postData);
824
+ request.end();
825
+ });
826
+ // Record the response ID.
827
+ tenonData.requestIDs[id] = responseID || '';
849
828
  }
850
- // Add the totals to the report.
851
- report.presses += presses;
852
- report.amountRead += amountRead;
853
829
  }
854
830
  // Otherwise, if the act is a test:
855
831
  else if (act.type === 'test') {
856
832
  // Add a description of the test to the act.
857
833
  act.what = tests[act.which];
858
834
  // Initialize the arguments.
859
- const args = [page];
835
+ const args = [act.which === 'tenon' ? tenonData : page];
860
836
  // Identify the additional validator of the test.
861
837
  const testValidator = commands.tests[act.which];
862
838
  // If it exists:
@@ -1025,6 +1001,151 @@ const doActs = async (report, actIndex, page) => {
1025
1001
  const qualifier = act.again ? `${1 + act.again} times` : 'once';
1026
1002
  act.result = `pressed ${qualifier}`;
1027
1003
  }
1004
+ // Otherwise, if it is a repetitive keyboard navigation:
1005
+ else if (act.type === 'presses') {
1006
+ const {navKey, what, which, withItems} = act;
1007
+ const matchTexts = which ? which.map(text => debloat(text)) : [];
1008
+ // Initialize the loop variables.
1009
+ let status = 'more';
1010
+ let presses = 0;
1011
+ let amountRead = 0;
1012
+ let items = [];
1013
+ let matchedText;
1014
+ // As long as a matching element has not been reached:
1015
+ while (status === 'more') {
1016
+ // Press the Escape key to dismiss any modal dialog.
1017
+ await page.keyboard.press('Escape');
1018
+ // Press the specified navigation key.
1019
+ await page.keyboard.press(navKey);
1020
+ presses++;
1021
+ // Identify the newly current element or a failure.
1022
+ const currentJSHandle = await page.evaluateHandle(actCount => {
1023
+ // Initialize it as the focused element.
1024
+ let currentElement = document.activeElement;
1025
+ // If it exists in the page:
1026
+ if (currentElement && currentElement.tagName !== 'BODY') {
1027
+ // Change it, if necessary, to its active descendant.
1028
+ if (currentElement.hasAttribute('aria-activedescendant')) {
1029
+ currentElement = document.getElementById(
1030
+ currentElement.getAttribute('aria-activedescendant')
1031
+ );
1032
+ }
1033
+ // Or change it, if necessary, to its selected option.
1034
+ else if (currentElement.tagName === 'SELECT') {
1035
+ const currentIndex = Math.max(0, currentElement.selectedIndex);
1036
+ const options = currentElement.querySelectorAll('option');
1037
+ currentElement = options[currentIndex];
1038
+ }
1039
+ // Or change it, if necessary, to its active shadow-DOM element.
1040
+ else if (currentElement.shadowRoot) {
1041
+ currentElement = currentElement.shadowRoot.activeElement;
1042
+ }
1043
+ // If there is a current element:
1044
+ if (currentElement) {
1045
+ // If it was already reached within this command performance:
1046
+ if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1047
+ // Report the error.
1048
+ console.log(`ERROR: ${currentElement.tagName} element reached again`);
1049
+ status = 'ERROR';
1050
+ return 'ERROR: locallyExhausted';
1051
+ }
1052
+ // Otherwise, i.e. if it is newly reached within this act:
1053
+ else {
1054
+ // Mark and return it.
1055
+ currentElement.dataset.pressesReached = actCount;
1056
+ return currentElement;
1057
+ }
1058
+ }
1059
+ // Otherwise, i.e. if there is no current element:
1060
+ else {
1061
+ // Report the error.
1062
+ status = 'ERROR';
1063
+ return 'noActiveElement';
1064
+ }
1065
+ }
1066
+ // Otherwise, i.e. if there is no focus in the page:
1067
+ else {
1068
+ // Report the error.
1069
+ status = 'ERROR';
1070
+ return 'ERROR: globallyExhausted';
1071
+ }
1072
+ }, actCount);
1073
+ // If the current element exists:
1074
+ const currentElement = currentJSHandle.asElement();
1075
+ if (currentElement) {
1076
+ // Update the data.
1077
+ const tagNameJSHandle = await currentElement.getProperty('tagName');
1078
+ const tagName = await tagNameJSHandle.jsonValue();
1079
+ const text = await textOf(page, currentElement);
1080
+ // If the text of the current element was found:
1081
+ if (text !== null) {
1082
+ const textLength = text.length;
1083
+ // If it is non-empty and there are texts to match:
1084
+ if (matchTexts.length && textLength) {
1085
+ // Identify the matching text.
1086
+ matchedText = matchTexts.find(matchText => text.includes(matchText));
1087
+ }
1088
+ // Update the item data if required.
1089
+ if (withItems) {
1090
+ const itemData = {
1091
+ tagName,
1092
+ text,
1093
+ textLength
1094
+ };
1095
+ if (matchedText) {
1096
+ itemData.matchedText = matchedText;
1097
+ }
1098
+ items.push(itemData);
1099
+ }
1100
+ amountRead += textLength;
1101
+ // If there is no text-match failure:
1102
+ if (matchedText || ! matchTexts.length) {
1103
+ // If the element has any specified tag name:
1104
+ if (! what || tagName === what) {
1105
+ // Change the status.
1106
+ status = 'done';
1107
+ // Perform the action.
1108
+ const inputText = act.text;
1109
+ if (inputText) {
1110
+ await page.keyboard.type(inputText);
1111
+ presses += inputText.length;
1112
+ }
1113
+ if (act.action) {
1114
+ presses++;
1115
+ await page.keyboard.press(act.action);
1116
+ await page.waitForLoadState();
1117
+ }
1118
+ }
1119
+ }
1120
+ }
1121
+ else {
1122
+ status = 'ERROR';
1123
+ }
1124
+ }
1125
+ // Otherwise, i.e. if there was a failure:
1126
+ else {
1127
+ // Update the status.
1128
+ status = await currentJSHandle.jsonValue();
1129
+ }
1130
+ }
1131
+ // Add the result to the act.
1132
+ act.result = {
1133
+ status,
1134
+ totals: {
1135
+ presses,
1136
+ amountRead
1137
+ }
1138
+ };
1139
+ if (status === 'done' && matchedText) {
1140
+ act.result.matchedText = matchedText;
1141
+ }
1142
+ if (withItems) {
1143
+ act.result.items = items;
1144
+ }
1145
+ // Add the totals to the report.
1146
+ report.presses += presses;
1147
+ report.amountRead += amountRead;
1148
+ }
1028
1149
  // Otherwise, i.e. if the act type is unknown:
1029
1150
  else {
1030
1151
  // Add the error result to the act.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,6 +30,7 @@
30
30
  "aatt": "*",
31
31
  "accessibility-checker": "*",
32
32
  "axe-playwright": "*",
33
+ "dotenv": "*",
33
34
  "pixelmatch": "*",
34
35
  "playwright": "*"
35
36
  },
@@ -1,5 +1,5 @@
1
1
  {
2
- "what": "Test example.com with alfa",
2
+ "what": "Test example.com with bulk",
3
3
  "strict": true,
4
4
  "commands": [
5
5
  {
@@ -14,8 +14,7 @@
14
14
  },
15
15
  {
16
16
  "type": "test",
17
- "which": "alfa",
18
- "what": "Siteimprove alfa package"
17
+ "which": "bulk"
19
18
  }
20
19
  ]
21
20
  }
@@ -0,0 +1,28 @@
1
+ {
2
+ "what": "Test Wikipedia with tenon",
3
+ "strict": true,
4
+ "commands": [
5
+ {
6
+ "type": "launch",
7
+ "which": "chromium",
8
+ "what": "Chromium browser"
9
+ },
10
+ {
11
+ "type": "url",
12
+ "which": "https://en.wikipedia.org/wiki/Main_Page",
13
+ "what": "Wikipedia English home page"
14
+ },
15
+ {
16
+ "type": "tenonRequest",
17
+ "withNewContent": true,
18
+ "id": "a",
19
+ "what": "tenon request"
20
+ },
21
+ {
22
+ "type": "test",
23
+ "which": "tenon",
24
+ "id": "a",
25
+ "what": "result of prior tenon request"
26
+ }
27
+ ]
28
+ }
package/tests/ibm.js CHANGED
@@ -50,28 +50,54 @@ const report = (result, withItems) => {
50
50
  }
51
51
  return data;
52
52
  };
53
- const all = {};
54
- // Returns results of an IBM test.
53
+ // Performs an IBM test.
54
+ const doTest = async (content, withItems, timeLimit) => {
55
+ // Start a timeout clock.
56
+ let timeoutID;
57
+ const wait = new Promise(resolve => {
58
+ timeoutID = setTimeout(() => {
59
+ resolve('');
60
+ }, 1000 * timeLimit);
61
+ });
62
+ // Conduct and report the test.
63
+ const result = run(content);
64
+ // Wait for the report until the time limit expires.
65
+ const resultIfFast = await Promise.race([result, wait]);
66
+ // Delete the report files.
67
+ try {
68
+ const reportNames = await fs.readdir('results');
69
+ for (const reportName of reportNames) {
70
+ await fs.rm(`results/${reportName}`);
71
+ }
72
+ }
73
+ catch(error) {
74
+ console.log('ibm test created no result files.');
75
+ }
76
+ // Return the result.
77
+ if (resultIfFast) {
78
+ clearTimeout(timeoutID);
79
+ const typeResult = report(result, withItems);
80
+ return typeResult;
81
+ }
82
+ else {
83
+ console.log('ERROR: getting ibm test report took too long');
84
+ return 'ERROR: getting ibm test report took too long';
85
+ }
86
+ };
87
+ // Returns results of one or two IBM tests.
55
88
  exports.reporter = async (page, withItems, withNewContent) => {
56
- // If the test is to be conducted with existing content:
89
+ // If a test with existing content is to be performed:
90
+ const result = {};
57
91
  if (! withNewContent) {
58
- // Conduct and report it.
59
- const content = await page.content();
60
- const result = await run(content);
61
- all.content = report(result, withItems);
92
+ const timeLimit = 15;
93
+ const typeContent = await page.content();
94
+ result.content = await doTest(typeContent, withItems, timeLimit);
62
95
  }
63
- // If the test is to be conducted with a URL:
96
+ // If a test with new content is to be performed:
64
97
  if ([true, undefined].includes(withNewContent)) {
65
- // Conduct and report it.
66
- const content = page.url();
67
- const result = await run(content);
68
- all.url = report(result, withItems);
98
+ const timeLimit = 20;
99
+ const typeContent = page.url();
100
+ result.url = await doTest(typeContent, withItems, timeLimit);
69
101
  }
70
- // Delete the report files.
71
- const reportNames = await fs.readdir('results');
72
- for (const reportName of reportNames) {
73
- await fs.rm(`results/${reportName}`);
74
- }
75
- // Return the result.
76
- return {result: all};
102
+ return {result};
77
103
  };
package/tests/tenon.js ADDED
@@ -0,0 +1,123 @@
1
+ /*
2
+ tenon
3
+ This test processes a previously requested test by the Tenon API.
4
+ */
5
+ const https = require('https');
6
+ // Wait until a time limit in seconds expires.
7
+ const wait = timeLimit => new Promise(resolve => setTimeout(resolve, 1000 * timeLimit));
8
+ exports.reporter = async (tenonData, id) => {
9
+ if (tenonData && tenonData.accessToken && tenonData.requestIDs && tenonData.requestIDs[id]) {
10
+ // Shared request options.
11
+ const requestOptions = {
12
+ host: 'tenon.io',
13
+ path: `/api/v2/${tenonData.requestIDs[id]}`,
14
+ port: 443,
15
+ protocol: 'https:',
16
+ headers: {
17
+ Authorization: `Bearer ${tenonData.accessToken}`,
18
+ 'Cache-Control': 'no-cache'
19
+ }
20
+ };
21
+ // Gets the test status.
22
+ const getStatus = async () => {
23
+ const testStatus = await new Promise((resolve, reject) => {
24
+ requestOptions.method = 'HEAD';
25
+ const statusRequest = https.request(requestOptions, statusResponse => {
26
+ const {statusCode} = statusResponse;
27
+ resolve(statusCode);
28
+ });
29
+ statusRequest.on('error', error => {
30
+ console.log(`ERROR getting Tenon test status (${error.message})`);
31
+ reject(`ERROR getting Tenon test status (${error.message})`);
32
+ });
33
+ statusRequest.end();
34
+ });
35
+ return testStatus;
36
+ };
37
+ // Gets the test result.
38
+ const getResult = async () => {
39
+ const testResult = await new Promise(resolve => {
40
+ requestOptions.method = 'GET';
41
+ const resultRequest = https.request(requestOptions, resultResponse => {
42
+ let resultJSON = '';
43
+ resultResponse.on('data', chunk => {
44
+ resultJSON += chunk;
45
+ });
46
+ resultResponse.on('end', () => {
47
+ try {
48
+ const result = JSON.parse(resultJSON);
49
+ resolve(result);
50
+ }
51
+ catch(error) {
52
+ console.log(`ERROR getting Tenon test result (${resultJSON.slice(0, 80)} …)`);
53
+ resolve({
54
+ error: 'ERROR getting Tenon test result',
55
+ resultStart: resultJSON.slice(0, 80)
56
+ });
57
+ }
58
+ });
59
+ });
60
+ resultRequest.end();
61
+ });
62
+ return testResult;
63
+ };
64
+ // Get the test status (not reliable: may say 200 instead of 202).
65
+ let testStatus = await getStatus();
66
+ // If the test is still in the Tenon queue:
67
+ if (testStatus === 202) {
68
+ // Wait 5 seconds.
69
+ await wait(5);
70
+ // Get the test status again.
71
+ testStatus = await getStatus();
72
+ }
73
+ // If the test has allegedly been completed:
74
+ if (testStatus === 200) {
75
+ // Get the test result.
76
+ let testResult = await getResult();
77
+ // If the test is still in the Tenon queue:
78
+ let {status} = testResult;
79
+ if (status === 202) {
80
+ // Wait 5 seconds.
81
+ await wait(5);
82
+ // Get the test result again.
83
+ testResult = await getResult();
84
+ // If the test is still in the Tenon queue:
85
+ status = testResult.status;
86
+ if (status === 202) {
87
+ // Wait 15 more seconds.
88
+ await wait(15);
89
+ // Get the test result again.
90
+ testResult = await getResult();
91
+ status = testResult.status;
92
+ }
93
+ }
94
+ // If the test has really been completed:
95
+ if (status === 200) {
96
+ // Return its result.
97
+ return {result: testResult};
98
+ }
99
+ // Otherwise, i.e. if the test is still running or failed:
100
+ else {
101
+ return {result: {
102
+ error: 'ERROR: Tenon result not retrieved',
103
+ status
104
+ }};
105
+ }
106
+ }
107
+ // Otherwise, if the test is still running after a wait for its status:
108
+ else {
109
+ // Report the test status.
110
+ return {result: {
111
+ error: 'ERROR: test status not completed',
112
+ testStatus
113
+ }};
114
+ }
115
+ }
116
+ else {
117
+ return {
118
+ result: {
119
+ error: 'ERROR: tenon authorization and test data incomplete'
120
+ }
121
+ };
122
+ }
123
+ };
@@ -0,0 +1,18 @@
1
+ // tenon.js
2
+ // Test executor for tenon sample script.
3
+
4
+ const fs = require('fs');
5
+ const {handleRequest} = require('../../index');
6
+ const scriptJSON = fs.readFileSync('samples/scripts/tenon.json', 'utf8');
7
+ const script = JSON.parse(scriptJSON);
8
+ const report = {
9
+ id: '',
10
+ script,
11
+ log: [],
12
+ acts: []
13
+ };
14
+ (async () => {
15
+ await handleRequest(report);
16
+ console.log(`Report log:\n${JSON.stringify(report.log, null, 2)}\n`);
17
+ console.log(`Report acts:\n${JSON.stringify(report.acts, null, 2)}`);
18
+ })();