@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.
- package/.github/workflows/test_tests.yml +7 -0
- package/CHANGELOG.md +4 -0
- package/action.yml +7 -0
- package/lib/cucumber_8_shim.js +1 -1
- package/lib/scope.js +52 -3
- package/lib/steps.js +23 -1
- package/lib/utils/clean_reports.js +2 -2
- package/lib/utils/log.js +7 -0
- package/package.json +3 -1
- package/tests/features/observation_steps.feature +24 -1
|
@@ -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
|
package/lib/cucumber_8_shim.js
CHANGED
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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 @
|
|
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 | |
|