@suffolklitlab/alkiln 4.5.2 → 4.6.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.
@@ -80,6 +80,13 @@ jobs:
80
80
  with:
81
81
  name: report
82
82
  path: ./**/*_report_*
83
+ - name: upload a11y reports
84
+ uses: actions/upload-artifact@v2
85
+ # They will upload even if a previous step has failed
86
+ if: ${{ always() }}
87
+ with:
88
+ name: a11y report
89
+ path: ./**/aXe_failure_*
83
90
  - name: upload debug_log artifact
84
91
  uses: actions/upload-artifact@v2
85
92
  if: ${{ always() }}
package/CHANGELOG.md CHANGED
@@ -40,6 +40,10 @@ Format:
40
40
  -->
41
41
  <!-- ## [Unreleased] -->
42
42
 
43
+ ## [4.6.0] - 2022-06-10
44
+ ### Added
45
+ - Steps to test web accessibility either on individual page or for all subsequent pages.
46
+
43
47
  ## [4.5.2] - 2022-06-10
44
48
  ### Fixed
45
49
  - (internal) Expected in report is reset properly
package/action.yml CHANGED
@@ -147,6 +147,13 @@ runs:
147
147
  with:
148
148
  name: report
149
149
  path: ./**/*_report_*
150
+ - name: "ALKiln: Upload Accessibility failures, if any, to action summary artifacts"
151
+ if: ${{ always() }}
152
+ uses: actions/upload-artifact@v2
153
+ # They will upload even if a previous step has failed
154
+ with:
155
+ name: a11y report
156
+ path: ./**/aXe_failure*
150
157
 
151
158
  # ##################################
152
159
  # # A developer's workflow should look similar to the below
@@ -1,7 +1,7 @@
1
1
  module.exports = async function wrapPromiseWithTimeout (
2
2
  promise,
3
3
  timeoutInMilliseconds,
4
- timeoutMessage
4
+ timeoutMessage=''
5
5
  ) {
6
6
  /** Race between a timeout and another function and clean up
7
7
  * async afterwards.
package/lib/scope.js CHANGED
@@ -2,12 +2,15 @@ const { expect } = require('chai');
2
2
  const cheerio = require('cheerio');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
+ const { AxePuppeteer } = require('@axe-core/puppeteer');
5
6
  const safe_filename = require("sanitize-filename");
6
7
 
7
8
  // Ours
8
9
  const session_vars = require('./utils/session_vars');
9
10
  const get_base_interview_url = require(`./docassemble/get_base_interview_url`);
10
11
  const time = require('./utils/time');
12
+ const log = require('./utils/log');
13
+ const { waitForTimeout } = require('./utils/time');
11
14
 
12
15
 
13
16
  let ordinal_to_integer = {
@@ -25,7 +28,7 @@ let escape_regex = function( to_escape ) {
25
28
  let get_original_row = function (row) {
26
29
  /* `row` might be a row, a source row, or an original row. */
27
30
 
28
- // Until we flatten row data a litte, we need to find the `orignal` row, which has the flags
31
+ // Until we flatten row data a little, we need to find the `original` row, which has the flags
29
32
  let source_row = row.source || row;
30
33
  let original = source_row.original || source_row;
31
34
  return original;
@@ -56,11 +59,11 @@ module.exports = {
56
59
  *
57
60
  * The report has a key for each scenario and each of those
58
61
  * keys should correspond to a list containing the relevant data. */
59
- if ( session_vars.get_debug() ) { console.log(`Report:\ntype: ${ type }, value: ${ value }`) }
62
+ if ( session_vars.get_debug() ) { console.log(`Report:\ntype: ${ type }, value: ${ value }\n`) }
60
63
  let scenario = await scope.makeSureReportPartsExist( scope );
61
64
  scenario.lines.push({ type, value });
62
65
 
63
- fs.appendFileSync('debug_log.txt', `Report:\ntype: ${ type }, value: ${ value }`);
66
+ log.add_to_debug_log(`Report:\ntype: ${ type }, value: ${ value }\n`);
64
67
  return scenario;
65
68
 
66
69
  }, // Ends scope.addToReport()
@@ -475,6 +478,44 @@ module.exports = {
475
478
  await scope.tapElement( scope, elem, tab_id );
476
479
  },
477
480
 
481
+ checkForA11y: async function (scope ) {
482
+ /** Return if there were any violations and the file containing aXe output with more details of the checks */
483
+ let { id } = await scope.examinePageID( scope, 'none to match' );
484
+ if ( scope.page_id !== id ) {
485
+ await scope.addToReport(scope, { type: `page_id`, value: `${ id }`, });
486
+ scope.page_id = id;
487
+ }
488
+ const axe = new AxePuppeteer(scope.page);
489
+ axe.options({
490
+ runOnly: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
491
+ resultTypes: ['violations', 'incomplete']
492
+ });
493
+ let axe_result = await axe.analyze();
494
+ axe_result.page_id = scope.page_id;
495
+ axe_result.passes_ids = axe_result.passes.map(pass_obj => pass_obj.id);
496
+ axe_result.inapplicable_ids = axe_result.inapplicable.map(inapp_obj => inapp_obj.id);
497
+ axe_result.passes = [];
498
+ axe_result.inapplicable = [];
499
+
500
+ let axe_filename = null;
501
+ const passed = axe_result.violations.length == 0;
502
+ if (!passed) {
503
+ axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `aXe_failure_${ scope.page_id }`}) + '.json';
504
+ fs.writeFileSync( axe_filename, JSON.stringify( axe_result, null, 2 ));
505
+ let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filename }.`;
506
+ await scope.addToReport( scope, {
507
+ type: `error`,
508
+ value: msg
509
+ });
510
+ } else {
511
+ await scope.addToReport(scope, { type: `info`, value: `Accessibility on ${ scope.page_id } passed!`})
512
+ }
513
+ return {
514
+ passed: axe_result.violations.length == 0,
515
+ axe_filename: axe_filename
516
+ };
517
+ },
518
+
478
519
  tapElementBySelector: async function ( scope, selector, waitForTimeout ) {
479
520
  let elem = await scope.page.$(`${selector}`);
480
521
  let error_msg = `Couldn't find "${selector}" on the page, is there a typo?`;
@@ -1641,9 +1682,17 @@ module.exports = {
1641
1682
  // await scope.throwPageError( scope, { id: id });
1642
1683
  // }
1643
1684
 
1685
+ did_navigate = true;
1644
1686
  process.stdout.write(`\x1b[36m${ '>' }\x1b[0m`); // continued successfully
1645
1687
  } // ends if !did_navigate && ensure_navigation
1646
1688
 
1689
+ if ( did_navigate && scope.check_all_for_a11y ) {
1690
+ let { passed } = await scope.checkForA11y(scope);
1691
+ if (!passed) {
1692
+ scope.passed_all_for_a11y = false;
1693
+ }
1694
+ }
1695
+
1647
1696
  // TODO: Since we now continue only once no matter what, we can put
1648
1697
  // the error in the caller of the function, though we may want
1649
1698
  // to pass the symbol to print as well.
package/lib/steps.js CHANGED
@@ -18,11 +18,13 @@ const { v4: uuidv4 } = require('uuid');
18
18
 
19
19
  // Ours
20
20
  const scope = require('./scope');
21
+ const log = require('./utils/log');
21
22
  const session_vars = require('./utils/session_vars');
22
23
  /* Of Note:
23
24
  - We're using `*=` because sometimes da text has funny characters in it that are hard to anticipate
24
25
  */
25
26
 
27
+
26
28
  require("dotenv").config(); // Where did this go?
27
29
 
28
30
  /* TODO:
@@ -68,7 +70,7 @@ BeforeAll(async () => {
68
70
  scope.timeout = 30 * 1000;
69
71
  scope.showif_timeout = 600;
70
72
  scope.report = new Map();
71
- fs.writeFileSync('debug_log.txt', "");
73
+ fs.writeFileSync(log.debug_log_file, "");
72
74
  });
73
75
 
74
76
  Before(async (scenario) => {
@@ -88,6 +90,8 @@ Before(async (scenario) => {
88
90
  scope.disable_error_screenshot = false;
89
91
 
90
92
  scope.page_id = null;
93
+ scope.check_all_for_a11y = false;
94
+ scope.passed_all_for_a11y = true;
91
95
 
92
96
  // Reset default timeout
93
97
  scope.timeout = 30 * 1000;
@@ -730,6 +734,19 @@ When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -
730
734
  )
731
735
  });
732
736
 
737
+ When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
738
+ let { passed, axe_filename } = await wrapPromiseWithTimeout(
739
+ scope.checkForA11y(scope),
740
+ scope.timeout
741
+ )
742
+ let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filename }.`;
743
+ expect( passed, msg ).to.be.true;
744
+ });
745
+
746
+ When(/I check all pages for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
747
+ scope.check_all_for_a11y = true;
748
+ scope.passed_all_for_a11y = true;
749
+ });
733
750
 
734
751
  //#####################################
735
752
  // UI element interaction
@@ -893,6 +910,11 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
893
910
  //#####################################
894
911
 
895
912
  After(async function(scenario) {
913
+ // The accessibility failures don't happen til the end, so the rest of the test can continue
914
+ if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
915
+ // The fact that the scenario failed has already been noted earlier in the report
916
+ scenario.result.status = `FAILED`;
917
+ }
896
918
 
897
919
  // Record status before internal report tests can interfere with values
898
920
  await scope.setReportScenarioStatus( scope, { status: scenario.result.status });
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const log = require('./log');
4
5
 
5
6
  // This is hardcoded information from `cucumber.js`. Once we move to cucumber 8, we should
6
7
  // change that the cucumber.json and actually parse the info from it.
@@ -13,8 +14,7 @@ fs.readFile(summary_file_name, 'utf8', (err, data) => {
13
14
  // Replace terminal color codes, if present
14
15
  // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#colors--graphics-mode
15
16
  data = data.replace(/\x1b\[[0-9][0-9]?[0-9]?m/g, '');
16
- const debug_log_file = 'debug_log.txt';
17
- fs.appendFile(debug_log_file, data, err => {
17
+ fs.appendFile(log.debug_log_file, data, err => {
18
18
  if (err) {
19
19
  console.error(err);
20
20
  return;
package/lib/utils/log.js CHANGED
@@ -1,3 +1,4 @@
1
+ const fs = require('fs');
1
2
  const session_vars = require( './session_vars' );
2
3
 
3
4
  let log = {};
@@ -49,3 +50,9 @@ log.debug = function ({ type='', pre='', data='', post='' }) {
49
50
  if ( post ) { console.log( post ); }
50
51
  }
51
52
  };
53
+
54
+ log.add_to_debug_log = function(value) {
55
+ fs.appendFileSync(log.debug_log_file, value);
56
+ };
57
+
58
+ log.debug_log_file = 'debug_log.txt'
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "4.5.2",
3
+ "version": "4.6.0",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
7
7
  "test": "npm run unit && npm run langs_setup && npm run cucumber -- $@",
8
8
  "cucumber_base": "run_cucumber(){ ./node_modules/.bin/cucumber-js \"$@\"; status=$?; ./lib/utils/clean_reports.js; return $status; }; run_cucumber",
9
9
  "cucumber": "run_base(){ npm run cucumber_base -- \"$@\" tests/features/*.feature; }; run_base",
10
+ "cucumber_external": "run_cucumber(){ tmp_path=`pwd`; ( new_path=\"$1\"; shift; cd \"$new_path\"; npm run --prefix \"${tmp_path}\" cucumber_base -- \"$@\" \"$new_path\"/docassemble/*/data/sources/*.feature;) }; run_cucumber",
10
11
  "langs": "npm run langs_setup && ./node_modules/.bin/cucumber-js --require ./lib/index.js tests/features/*_lang*.feature",
11
12
  "langs_setup": "./lib/utils/langs.js 'tests/features/*.feature'",
12
13
  "langs_local": "npm run langs_setup && npm run local",
@@ -34,6 +35,7 @@
34
35
  },
35
36
  "homepage": "https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/",
36
37
  "dependencies": {
38
+ "@axe-core/puppeteer": "4.4.2",
37
39
  "@cucumber/cucumber": "7.3.2",
38
40
  "axios": "0.24.0",
39
41
  "chai": "4.2.0",
@@ -123,8 +123,31 @@ Scenario: I get the page's JSON
123
123
  Given I start the interview at "all_tests"
124
124
  Then I get the page's JSON variables and values
125
125
 
126
- @fast @o9
126
+ @fast @o11
127
127
  Scenario: I take a screenshot
128
128
    Given I start the interview at "all_tests"
129
129
  Then I take a screenshot
130
130
  Then I take a screenshot named "some-screenshot"
131
+
132
+ @slow @o12 @accessibility @a11y
133
+ Scenario: I check the pages for accessibility
134
+ Given I start the interview at "all_tests"
135
+ And I check all pages for accessibility issues
136
+ And I tap to continue
137
+   Then I get to "screen features" with this data:
138
+ | var | value | trigger |
139
+ | double_quote_dict["double_quote_key"]['dq_two'] | true | |
140
+ | checkboxes_other['checkbox_other_opt_1'] | true | |
141
+ | dropdown_test | dropdown_opt_2 | |
142
+ | radio_yesno | False | false |
143
+ | radio_other | radio_other_opt_3 | |
144
+ | object_checkboxes_test["obj_chkbx_opt_1"] | True | |
145
+ | object_dropdown | obj_opt_2 | |
146
+ | object_select_test | obj_chkbx_opt_2 | |
147
+ | single_quote_dict['single_quote_key']['sq_two'] | true | |
148
+ | text_input | Regular text input field value | |
149
+ | textarea | Multiline text\narea value | |
150
+ | date_input | today | |
151
+ | button_continue | True | |
152
+ | buttons_other | button_2 | |
153
+ | buttons_yesnomaybe | True | |