@suffolklitlab/alkiln 4.5.0 → 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.
@@ -5,9 +5,11 @@ on:
5
5
  workflow_dispatch:
6
6
  inputs:
7
7
  extra_languages:
8
+ required: False
8
9
  description: 'Optional. A list of comma separated language names visible on buttons or links that change the language of the interview. Overrides the EXTRA_LANGUAGES repo secret.'
9
10
  default: ''
10
11
  tags:
12
+ required: False
11
13
  description: 'Optional. Use a "tag expression" specify which tagged tests to run (https://cucumber.io/docs/cucumber/api/#tag-expressions)'
12
14
  default: ''
13
15
 
@@ -78,6 +80,13 @@ jobs:
78
80
  with:
79
81
  name: report
80
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_*
81
90
  - name: upload debug_log artifact
82
91
  uses: actions/upload-artifact@v2
83
92
  if: ${{ always() }}
package/CHANGELOG.md CHANGED
@@ -40,7 +40,23 @@ 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
+
47
+ ## [4.5.2] - 2022-06-10
48
+ ### Fixed
49
+ - (internal) Expected in report is reset properly
50
+ - if a test used "the Scenario report should include ...", then all tests that were
51
+ run after that would pass even if the test itself failed but wasn't expected to fail
52
+
53
+ ## [4.5.1] - 2022-06-10
54
+ ### Fixed
55
+ - Corrected some schema errors in the `action.yml` (the `run` key was specified twice, should only have been once, and `required: False` is needed
56
+ for optional inputs)
57
+
43
58
  ## [4.5.0] - 2022-05-23
59
+ ### Added
44
60
  - Steps that tap tabs (made using the `tabbed_templates_html` function from ALToolbox) and other arbitrary elements on the page.
45
61
 
46
62
  ## [4.4.0] - 2022-05-23
package/action.yml CHANGED
@@ -61,8 +61,9 @@ runs:
61
61
  echo "MAX_SECONDS_FOR_SETUP=${{ inputs.MAX_SECONDS_FOR_SETUP }}" >> $GITHUB_ENV
62
62
  shell: bash
63
63
  - name: "ALKiln: confirm info"
64
- run: echo -e "\nRepo is $REPO_URL\nBranch ref is $BRANCH_PATH\nManually added languages are $EXTRA_LANGUAGES\nAll other data is secret\n"
65
- run: echo -e "\nALKiln version is $ALKILN_VERSION\nRepo is $REPO_URL\nBranch ref is $BRANCH_PATH\nMAX_SECONDS_FOR_SETUP is $MAX_SECONDS_FOR_SETUP\nManually added languages are $EXTRA_LANGUAGES\n"
64
+ run: |
65
+ echo -e "\nRepo is $REPO_URL\nBranch ref is $BRANCH_PATH\nManually added languages are $EXTRA_LANGUAGES\nAll other data is secret\n"
66
+ echo -e "\nALKiln version is $ALKILN_VERSION\nRepo is $REPO_URL\nBranch ref is $BRANCH_PATH\nMAX_SECONDS_FOR_SETUP is $MAX_SECONDS_FOR_SETUP\nManually added languages are $EXTRA_LANGUAGES\n"
66
67
  shell: bash
67
68
 
68
69
  # This doesn't use an already established package-lock.json and that
@@ -146,6 +147,13 @@ runs:
146
147
  with:
147
148
  name: report
148
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*
149
157
 
150
158
  # ##################################
151
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,11 +70,13 @@ 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) => {
75
77
  scope.scenario_id = uuidv4();
78
+ scope.expected_status = null;
79
+ scope.expected_in_report = null;
76
80
 
77
81
  await scope.addReportHeading(scope, {scenario});
78
82
  scope.base_filename = await scope.getSafeScenarioBaseFilename(scope, {scenario});
@@ -86,6 +90,8 @@ Before(async (scenario) => {
86
90
  scope.disable_error_screenshot = false;
87
91
 
88
92
  scope.page_id = null;
93
+ scope.check_all_for_a11y = false;
94
+ scope.passed_all_for_a11y = true;
89
95
 
90
96
  // Reset default timeout
91
97
  scope.timeout = 30 * 1000;
@@ -728,6 +734,19 @@ When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -
728
734
  )
729
735
  });
730
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
+ });
731
750
 
732
751
  //#####################################
733
752
  // UI element interaction
@@ -891,6 +910,11 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
891
910
  //#####################################
892
911
 
893
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
+ }
894
918
 
895
919
  // Record status before internal report tests can interfere with values
896
920
  await scope.setReportScenarioStatus( scope, { status: scenario.result.status });
@@ -930,11 +954,10 @@ After(async function(scenario) {
930
954
  scope.expected_status = null; // reset for next Scenario
931
955
  expect( scenario.result.status ).to.equal( expected );
932
956
  }
933
- if ( scope.expected_in_report ) {
957
+ if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
934
958
  let expected = scope.expected_in_report;
935
959
  // Reset report values no matter what so they don't mess up future scenarios
936
- // Maybe this should be in `Before`, but it would be so out of context there...
937
- scope.expected_in_report = [];
960
+ scope.expected_in_report = null;
938
961
  let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
939
962
  if ( all_were_included ) { scenario.result.status = `PASSED`; }
940
963
  }
@@ -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.0",
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",
@@ -41,6 +41,11 @@ Scenario: I set text-type values
41
41
  And I set the var "showif_dropdown" to "showif_dropdown_1"
42
42
  When I tap to continue
43
43
  # Next page
44
+ Then the question id should be "object checkboxes"
45
+ And I set the var "object_checkboxes_test['obj_chkbx_opt_1']" to "true"
46
+ And I set the var "object_dropdown" to "obj_opt_2"
47
+ When I tap to continue
48
+ # Next page
44
49
  Then the question id should be "buttons yesnomaybe"
45
50
  When I set the var "buttons_yesnomaybe" to "None"
46
51
  # Next page
@@ -114,7 +119,7 @@ Scenario: tap elements
114
119
  And I tap the "#special_event" element and wait 1 second
115
120
  Then I see the phrase "Portishead"
116
121
 
117
- @i4 @tap-elements @tabs
122
+ @i4 @tap-elements @tabs @error
118
123
  Scenario: tap element with an error
119
124
  Given the final Scenario status should be "failed"
120
125
  And the Scenario report should include:
@@ -46,6 +46,8 @@ Scenario: I check navigation
46
46
  Then I arrive at the next page
47
47
    Then I get to "screen features" with this data:
48
48
  | var | value | trigger |
49
+ | object_checkboxes_test["obj_chkbx_opt_1"] | True | |
50
+ | object_dropdown | obj_opt_2 | |
49
51
  | button_continue | True | |
50
52
  | buttons_other | button_2 | |
51
53
  | buttons_yesnomaybe | True | |
@@ -121,8 +123,31 @@ Scenario: I get the page's JSON
121
123
  Given I start the interview at "all_tests"
122
124
  Then I get the page's JSON variables and values
123
125
 
124
- @fast @o9
126
+ @fast @o11
125
127
  Scenario: I take a screenshot
126
128
    Given I start the interview at "all_tests"
127
129
  Then I take a screenshot
128
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 | |
@@ -160,6 +160,8 @@ Scenario: Fail with link with given text does not lead to correct url
160
160
  Then I arrive at the next page
161
161
    Then I get to "screen features" with this data:
162
162
  | var | value | trigger |
163
+ | object_checkboxes_test["obj_chkbx_opt_1"] | true | |
164
+ | object_dropdown | obj_opt_2 | |
163
165
  | button_continue | True | |
164
166
  | buttons_other | button_2 | |
165
167
  | buttons_yesnomaybe | True | |
@@ -188,6 +190,8 @@ Scenario: Fail with link unexpectedly opens in same window
188
190
  Then I arrive at the next page
189
191
    Then I get to "screen features" with this data:
190
192
  | var | value | trigger |
193
+ | object_checkboxes_test["obj_chkbx_opt_1"] | true | |
194
+ | object_dropdown | obj_opt_2 | |
191
195
  | button_continue | True | |
192
196
  | buttons_other | button_2 | |
193
197
  | buttons_yesnomaybe | True | |
@@ -216,6 +220,8 @@ Scenario: Fail with link unexpectedly opens in a new window
216
220
  Then I arrive at the next page
217
221
    Then I get to "screen features" with this data:
218
222
  | var | value | trigger |
223
+ | object_checkboxes_test["obj_chkbx_opt_1"] | true | |
224
+ | object_dropdown | obj_opt_2 | |
219
225
  | button_continue | True | |
220
226
  | buttons_other | button_2 | |
221
227
  | buttons_yesnomaybe | True | |
@@ -472,6 +478,11 @@ Scenario: Report still shows page id when I tap to continue without setting any
472
478
  # Next page
473
479
  Then the question id should be "showifs"
474
480
  When I tap to continue
481
+ # Next page
482
+ Then the question id should be "object checkboxes"
483
+ And I set the var "object_checkboxes_test['obj_chkbx_opt_1']" to "true"
484
+ And I set the var "object_dropdown" to "obj_opt_2"
485
+ When I tap to continue
475
486
  # Next page (showifs ID SHOULD BE SHOWN IN REPORT)
476
487
  Then the question id should be "buttons yesnomaybe"
477
488
 
@@ -517,6 +528,8 @@ Scenario: Report lists unused table rows
517
528
  | extra_5 | extra 5 | |
518
529
    Then I get to "screen features" with this data:
519
530
  | var | value | trigger |
531
+ | object_checkboxes_test["obj_chkbx_opt_1"] | true | |
532
+ | object_dropdown | obj_opt_2 | |
520
533
  | extra_6 | extra 6 | |
521
534
  | extra_7 | extra 7 | |
522
535
  | button_continue | True | |
@@ -47,6 +47,8 @@ Covers story table tests for:
47
47
  | checkboxes_yesno | True | |
48
48
  | direct_standard_fields | True | |
49
49
  | dropdown_test | dropdown_opt_2 | |
50
+ | object_checkboxes_test["obj_chkbx_opt_1"] | true | |
51
+ | object_dropdown | obj_opt_2 | |
50
52
  | radio_other | radio_other_opt_3 | |
51
53
  | radio_yesno | False | |
52
54
  | screen_features | True | |