@suffolklitlab/alkiln 4.5.2 → 4.6.2
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 +14 -27
- package/CHANGELOG.md +14 -0
- package/action.yml +6 -28
- package/lib/cucumber_8_shim.js +1 -1
- package/lib/docassemble/setup.js +6 -0
- package/lib/scope.js +125 -32
- package/lib/steps.js +83 -12
- package/lib/utils/clean_reports.js +10 -4
- package/lib/utils/files.js +32 -0
- package/lib/utils/log.js +7 -0
- package/lib/utils/session_vars.js +56 -13
- package/package.json +3 -1
- package/tests/features/interactive_steps.feature +5 -1
- package/tests/features/observation_steps.feature +25 -2
- package/tests/features/story_tables.feature +1 -0
- package/tests/unit_tests/getSafeScenarioBaseFilename.fixtures.js +0 -2
- package/tests/unit_tests/getSafeScenarioBaseFilename.test.js +10 -21
- package/tests/unit_tests/getSafeScenarioFilename.test.js +6 -3
- package/tests/unit_tests/names.fixtures.js +8 -8
|
@@ -39,6 +39,11 @@ jobs:
|
|
|
39
39
|
|
|
40
40
|
name: Run Puppeteer tests
|
|
41
41
|
steps:
|
|
42
|
+
- name: Set name of artifact folder with date and UTC time
|
|
43
|
+
# https://www.shell-tips.com/linux/how-to-format-date-and-time-in-linux-macos-and-bash/#how-to-format-a-date-in-bash
|
|
44
|
+
run: |
|
|
45
|
+
echo "ARTIFACT_NAME=alkiln_tests_output-$(date +'%Y-%m-%d at %Hhrs %Mmins %Ssecs' -u) UTC" >> $GITHUB_ENV
|
|
46
|
+
echo "UNIT_TESTS_ARTIFACT_NAME=_alkiln_tests_misc-$(date +'%Y-%m-%d at %Hhrs %Mmins %Ssecs' -u) UTC" >> $GITHUB_ENV
|
|
42
47
|
- name: Set languages
|
|
43
48
|
run: echo "EXTRA_LANGUAGES=${{ github.event.inputs.extra_languages }}" >> $GITHUB_ENV
|
|
44
49
|
if: ${{ github.event.inputs.extra_languages != '' }}
|
|
@@ -53,39 +58,21 @@ jobs:
|
|
|
53
58
|
- run: npm install
|
|
54
59
|
- run: npm run setup
|
|
55
60
|
- run: npm run test -- ${{ github.event.inputs.tags && format('"--tags" "{0}"', github.event.inputs.tags) }}
|
|
56
|
-
|
|
61
|
+
# Artifacts
|
|
62
|
+
- name: upload unit tests artifacts folder
|
|
57
63
|
uses: actions/upload-artifact@v2
|
|
58
|
-
if
|
|
59
|
-
with:
|
|
60
|
-
name: errors
|
|
61
|
-
path: ./**/error*.jpg
|
|
62
|
-
- name: upload screenshot steps
|
|
63
|
-
uses: actions/upload-artifact@v2
|
|
64
|
-
# They will upload even if a previosu step has failed
|
|
65
|
-
if: ${{ always() }}
|
|
66
|
-
with:
|
|
67
|
-
name: screenshots
|
|
68
|
-
path: ./**/screenshot*.jpg
|
|
69
|
-
- name: upload downloads
|
|
70
|
-
uses: actions/upload-artifact@v2
|
|
71
|
-
# They will upload even if a previous step has failed
|
|
72
|
-
if: ${{ always() }}
|
|
73
|
-
with:
|
|
74
|
-
name: downloads
|
|
75
|
-
path: ./**/downloads_*
|
|
76
|
-
- name: upload report
|
|
77
|
-
uses: actions/upload-artifact@v2
|
|
78
|
-
# They will upload even if a previous step has failed
|
|
64
|
+
# This will upload even if a previous step has failed
|
|
79
65
|
if: ${{ always() }}
|
|
80
66
|
with:
|
|
81
|
-
name:
|
|
82
|
-
path:
|
|
83
|
-
- name: upload
|
|
67
|
+
name: ${{ env.UNIT_TESTS_ARTIFACT_NAME }}
|
|
68
|
+
path: ./_alkiln_tests_misc-*
|
|
69
|
+
- name: upload integration tests artifacts folder
|
|
84
70
|
uses: actions/upload-artifact@v2
|
|
85
71
|
if: ${{ always() }}
|
|
86
72
|
with:
|
|
87
|
-
name:
|
|
88
|
-
path: ./
|
|
73
|
+
name: ${{ env.ARTIFACT_NAME }}
|
|
74
|
+
path: ./alkiln_tests_output-*
|
|
75
|
+
# Delete Playground Project and project name file
|
|
89
76
|
- name: Cleanup
|
|
90
77
|
if: ${{ always() }}
|
|
91
78
|
run: npm run takedown
|
package/CHANGELOG.md
CHANGED
|
@@ -40,6 +40,20 @@ Format:
|
|
|
40
40
|
-->
|
|
41
41
|
<!-- ## [Unreleased] -->
|
|
42
42
|
|
|
43
|
+
## [4.6.2] - 2022-06-28
|
|
44
|
+
### Changed
|
|
45
|
+
- Put cucumber tests' artifacts in one folder. See https://github.com/SuffolkLITLab/ALKiln/issues/552.
|
|
46
|
+
- Add separate artifacts dir for our own internal tests, like unit tests
|
|
47
|
+
- Abstract names of some of those artifact directories
|
|
48
|
+
|
|
49
|
+
## [4.6.1] - 2022-06-28
|
|
50
|
+
### Fixed
|
|
51
|
+
- Single dropdown fields on pages are now recognized as fields properly
|
|
52
|
+
|
|
53
|
+
## [4.6.0] - 2022-06-10
|
|
54
|
+
### Added
|
|
55
|
+
- Steps to test web accessibility either on individual page or for all subsequent pages.
|
|
56
|
+
|
|
43
57
|
## [4.5.2] - 2022-06-10
|
|
44
58
|
### Fixed
|
|
45
59
|
- (internal) Expected in report is reset properly
|
package/action.yml
CHANGED
|
@@ -36,12 +36,14 @@ inputs:
|
|
|
36
36
|
runs:
|
|
37
37
|
using: "composite"
|
|
38
38
|
steps:
|
|
39
|
-
|
|
40
39
|
# Set environment variables
|
|
41
40
|
- name: "ALKiln: Set environment variables"
|
|
42
41
|
# If extra languages were set manually, override the repository
|
|
43
42
|
# EXTRA_LANGUAGES secret with the manually set values
|
|
43
|
+
# Human-readable date/time:
|
|
44
|
+
# https://www.shell-tips.com/linux/how-to-format-date-and-time-in-linux-macos-and-bash/#how-to-format-a-date-in-bash
|
|
44
45
|
run: |
|
|
46
|
+
echo "ARTIFACT_NAME=alkiln_tests_output $(date +'%Y-%m-%d at %Hhrs %Mmins %Ssecs' -u) UTC" >> $GITHUB_ENV
|
|
45
47
|
if ${{ github.event.inputs.extra_languages != '' }}
|
|
46
48
|
then
|
|
47
49
|
echo "Manually set languages: ${{ github.event.inputs.extra_languages }}"
|
|
@@ -121,32 +123,12 @@ runs:
|
|
|
121
123
|
shell: bash
|
|
122
124
|
|
|
123
125
|
# Upload artifacts that subscribers can download on the Actions summary page
|
|
124
|
-
- name: "ALKiln: Upload
|
|
125
|
-
if: ${{ failure() }}
|
|
126
|
-
uses: actions/upload-artifact@v2
|
|
127
|
-
with:
|
|
128
|
-
name: errors
|
|
129
|
-
path: ./**/error*.jpg
|
|
130
|
-
- name: "ALKiln: Upload screenshots to action summary artifacts"
|
|
131
|
-
if: ${{ always() }}
|
|
132
|
-
uses: actions/upload-artifact@v2
|
|
133
|
-
#if-no-files-found: ignore
|
|
134
|
-
with:
|
|
135
|
-
name: screenshots
|
|
136
|
-
path: ./**/screenshot*.jpg
|
|
137
|
-
- name: "ALKiln: Upload downloaded files to action summary artifacts"
|
|
138
|
-
if: ${{ always() }}
|
|
139
|
-
uses: actions/upload-artifact@v2
|
|
140
|
-
#if-no-files-found: ignore
|
|
141
|
-
with:
|
|
142
|
-
name: downloads
|
|
143
|
-
path: ./**/downloads_*
|
|
144
|
-
- name: "ALKiln: Upload report to action summary artifacts"
|
|
126
|
+
- name: "ALKiln: Upload artifacts folder"
|
|
145
127
|
if: ${{ always() }}
|
|
146
128
|
uses: actions/upload-artifact@v2
|
|
147
129
|
with:
|
|
148
|
-
name:
|
|
149
|
-
path:
|
|
130
|
+
name: ${{ env.ARTIFACT_NAME }}
|
|
131
|
+
path: ./alkiln_tests_output-*
|
|
150
132
|
|
|
151
133
|
# ##################################
|
|
152
134
|
# # A developer's workflow should look similar to the below
|
|
@@ -177,8 +159,4 @@ runs:
|
|
|
177
159
|
# with:
|
|
178
160
|
# SERVER_URL: "${{ secrets.SERVER_URL }}"
|
|
179
161
|
# DOCASSEMBLE_DEVELOPER_API_KEY: "${{ secrets.DOCASSEMBLE_DEVELOPER_API_KEY }}"
|
|
180
|
-
# PLAYGROUND_EMAIL: "${{ secrets.PLAYGROUND_EMAIL }}"
|
|
181
|
-
# PLAYGROUND_PASSWORD: "${{ secrets.PLAYGROUND_PASSWORD }}"
|
|
182
|
-
# PLAYGROUND_ID: "${{ secrets.PLAYGROUND_ID }}"
|
|
183
|
-
# EXTRA_LANGUAGES: "${{ secrets.EXTRA_LANGUAGES }}"
|
|
184
162
|
# - run: echo "Finished running ALKiln tests"
|
package/lib/cucumber_8_shim.js
CHANGED
package/lib/docassemble/setup.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const da_i = require('./docassemble_api_interface');
|
|
4
4
|
const log = require('../utils/log');
|
|
5
5
|
const session_vars = require('../utils/session_vars' );
|
|
6
|
+
const files = require('../utils/files' );
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
const setup = async () => {
|
|
@@ -30,6 +31,11 @@ const setup = async () => {
|
|
|
30
31
|
await da_i.wait_for_server_to_restart( task_id );
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// Make the folder to store all the files for this run of the tests
|
|
35
|
+
const artifacts_path_name = files.make_artifacts_folder();
|
|
36
|
+
// Save its name in a file. `session_vars` can get it back later.
|
|
37
|
+
session_vars.save_artifacts_path_name( artifacts_path_name );
|
|
38
|
+
|
|
33
39
|
// Finish
|
|
34
40
|
log.info({ type: `setup`, pre: `Success! Test setup has finished successfully.` });
|
|
35
41
|
}; // Ends setup();
|
package/lib/scope.js
CHANGED
|
@@ -2,12 +2,16 @@ 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');
|
|
14
|
+
const files = require('./utils/files' );
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
let ordinal_to_integer = {
|
|
@@ -25,7 +29,7 @@ let escape_regex = function( to_escape ) {
|
|
|
25
29
|
let get_original_row = function (row) {
|
|
26
30
|
/* `row` might be a row, a source row, or an original row. */
|
|
27
31
|
|
|
28
|
-
// Until we flatten row data a
|
|
32
|
+
// Until we flatten row data a little, we need to find the `original` row, which has the flags
|
|
29
33
|
let source_row = row.source || row;
|
|
30
34
|
let original = source_row.original || source_row;
|
|
31
35
|
return original;
|
|
@@ -47,6 +51,7 @@ let is_secret_var = function(row) {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
|
|
54
|
+
let misc_artifacts_dir = `_alkiln_test-misc_artifacts`;
|
|
50
55
|
|
|
51
56
|
module.exports = {
|
|
52
57
|
trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
|
|
@@ -56,11 +61,26 @@ module.exports = {
|
|
|
56
61
|
*
|
|
57
62
|
* The report has a key for each scenario and each of those
|
|
58
63
|
* keys should correspond to a list containing the relevant data. */
|
|
59
|
-
if ( session_vars.get_debug() ) { console.log(`Report:\ntype: ${ type }, value: ${ value }`) }
|
|
64
|
+
if ( session_vars.get_debug() ) { console.log(`Report:\ntype: ${ type }, value: ${ value }\n`) }
|
|
60
65
|
let scenario = await scope.makeSureReportPartsExist( scope );
|
|
61
66
|
scenario.lines.push({ type, value });
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
// TODO: Not sure how to use .add_to_debug_log() here considering possibly dynamic filepath
|
|
69
|
+
// log.add_to_debug_log(`Report:\ntype: ${ type }, value: ${ value }\n`);
|
|
70
|
+
|
|
71
|
+
// Ensure filepath for saving local unit test logs
|
|
72
|
+
if ( !scope.paths ) {
|
|
73
|
+
// TODO: would it be useful to put in existing artifacts folder if possible.
|
|
74
|
+
// Can this be usefully abstracted?
|
|
75
|
+
scope.paths = { debug_log: `${ misc_artifacts_dir }/unit_tests_logs-${ files.readable_date() }.txt` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
fs.mkdirSync( misc_artifacts_dir );
|
|
82
|
+
fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
|
|
83
|
+
}
|
|
64
84
|
return scenario;
|
|
65
85
|
|
|
66
86
|
}, // Ends scope.addToReport()
|
|
@@ -69,7 +89,10 @@ module.exports = {
|
|
|
69
89
|
/* If a part of the report needs to exist right now, but doesn't exist,
|
|
70
90
|
* create it. */
|
|
71
91
|
if ( !scope.report ) { scope.report = new Map(); }
|
|
72
|
-
if ( !scope.scenario_id ) {
|
|
92
|
+
if ( !scope.scenario_id ) {
|
|
93
|
+
let date = files.readable_date();
|
|
94
|
+
scope.scenario_id = `_report_${ date }`;
|
|
95
|
+
}
|
|
73
96
|
if ( !scope.report.get( scope.scenario_id )) {
|
|
74
97
|
scope.report.set( scope.scenario_id, {} )
|
|
75
98
|
scope.report.get( scope.scenario_id ).status = `in progress`;
|
|
@@ -184,41 +207,49 @@ module.exports = {
|
|
|
184
207
|
name_parts.push( scope.language );
|
|
185
208
|
}
|
|
186
209
|
name_parts.push( scenario.pickle.name );
|
|
187
|
-
for ( let tag of scenario.pickle.tags ) {
|
|
188
|
-
name_parts.push( tag.name );
|
|
189
|
-
}
|
|
190
210
|
|
|
191
211
|
let filename = name_parts.join(`-`);
|
|
212
|
+
let safe_name = safe_filename( filename, { replacement: `_` });
|
|
213
|
+
// Allow room for extensions, date, etc.
|
|
214
|
+
let safer_name = safe_name.substring(0, (255 - 45));
|
|
192
215
|
|
|
193
|
-
return
|
|
216
|
+
return safer_name;
|
|
194
217
|
}, // Ends scope.getSafeScenarioBaseFilename()
|
|
195
218
|
|
|
196
|
-
|
|
219
|
+
// TODO: Does this need a name change? getSafeScenarioBasedFilename?
|
|
220
|
+
getSafeScenarioFilename: async function( scope, options={ prefix: '' }) {
|
|
197
221
|
/* Return a string that's safe to be a filename. Use a timestamp,
|
|
198
222
|
* a prefix the caller wants, and the base filename. We hope it can
|
|
199
223
|
* contain non-english characters too. We do not offer a suffix option
|
|
200
224
|
* because it may be cut off if the filename is too long.
|
|
201
225
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
226
|
+
let name = options.prefix || '';
|
|
227
|
+
|
|
204
228
|
// As long as it's not an empty string, add a separator between it and the rest of the name
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if ( prefix ) {
|
|
208
|
-
prefix = `${ prefix }_`;
|
|
229
|
+
if ( name.length > 0 ) {
|
|
230
|
+
name += `-`;
|
|
209
231
|
}
|
|
210
232
|
|
|
211
|
-
|
|
212
|
-
|
|
233
|
+
if ( scope.page ) {
|
|
234
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
235
|
+
name += `${ id }-`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
name += scope.base_filename;
|
|
239
|
+
name += `-${ files.readable_date() }`;
|
|
240
|
+
|
|
241
|
+
let safe_name = safe_filename( name, { replacement: `_` });
|
|
242
|
+
// Allow room for extensions, etc.
|
|
243
|
+
let safer_name = safe_name.substring(0, (255 - 10));
|
|
213
244
|
return safer_name;
|
|
214
245
|
}, // Ends scope.getSafeScenarioFilename()
|
|
215
246
|
|
|
216
247
|
getJSONFilename: async function( scope ) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return
|
|
248
|
+
// Get a unique path for a json file
|
|
249
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
250
|
+
let date = files.readable_date();
|
|
251
|
+
let json_path = path.join( scope.paths.scenario, `json_for-${ id }-${ date }.json` );
|
|
252
|
+
return json_path;
|
|
222
253
|
},
|
|
223
254
|
|
|
224
255
|
savePageJSONData: async function( scope ) {
|
|
@@ -228,13 +259,12 @@ module.exports = {
|
|
|
228
259
|
* @returns {string} info.filename The name of the file that was saved.
|
|
229
260
|
* @returns {Object} info.json The variables object.
|
|
230
261
|
*/
|
|
231
|
-
|
|
232
|
-
let filename = await scope.getJSONFilename( scope );
|
|
262
|
+
let json_path = await scope.getJSONFilename( scope );
|
|
233
263
|
// Get the data
|
|
234
264
|
let json = await scope.getPageJSON( scope );
|
|
235
265
|
// Save the data
|
|
236
|
-
fs.writeFileSync(
|
|
237
|
-
return {
|
|
266
|
+
fs.writeFileSync( json_path, JSON.stringify( json, null, 2 ));
|
|
267
|
+
return { json_path, json };
|
|
238
268
|
}, // Ends scope.savePageJSONData()
|
|
239
269
|
|
|
240
270
|
waitForShowIf: async function waitForShowIf(scope) {
|
|
@@ -383,7 +413,14 @@ module.exports = {
|
|
|
383
413
|
|
|
384
414
|
detectDownloadComplete: async function detectDownloadComplete(scope, endTime) {
|
|
385
415
|
/* Resolve if a download flag has changed. Timeout after a bit less than
|
|
386
|
-
* the global permitted timeout
|
|
416
|
+
* the global permitted timeout
|
|
417
|
+
*
|
|
418
|
+
* TODO: Properly wait for download to complete. See
|
|
419
|
+
* https://github.com/SuffolkLITLab/ALKiln/issues/492 and
|
|
420
|
+
* https://stackoverflow.com/a/56951024 and
|
|
421
|
+
* https://github.com/puppeteer/puppeteer/issues/4676 and
|
|
422
|
+
* https://github.com/puppeteer/puppeteer/issues/299 and
|
|
423
|
+
*/
|
|
387
424
|
|
|
388
425
|
// Stop/resolve if too much time has passed
|
|
389
426
|
let expirationTime = scope.timeout - 200; // ms
|
|
@@ -475,6 +512,45 @@ module.exports = {
|
|
|
475
512
|
await scope.tapElement( scope, elem, tab_id );
|
|
476
513
|
},
|
|
477
514
|
|
|
515
|
+
checkForA11y: async function (scope ) {
|
|
516
|
+
/** Return if there were any violations and the file containing aXe output with more details of the checks */
|
|
517
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
518
|
+
if ( scope.page_id !== id ) {
|
|
519
|
+
await scope.addToReport(scope, { type: `page_id`, value: `${ id }`, });
|
|
520
|
+
scope.page_id = id;
|
|
521
|
+
}
|
|
522
|
+
const axe = new AxePuppeteer(scope.page);
|
|
523
|
+
axe.options({
|
|
524
|
+
runOnly: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
|
|
525
|
+
resultTypes: ['violations', 'incomplete']
|
|
526
|
+
});
|
|
527
|
+
let axe_result = await axe.analyze();
|
|
528
|
+
axe_result.page_id = scope.page_id;
|
|
529
|
+
axe_result.passes_ids = axe_result.passes.map(pass_obj => pass_obj.id);
|
|
530
|
+
axe_result.inapplicable_ids = axe_result.inapplicable.map(inapp_obj => inapp_obj.id);
|
|
531
|
+
axe_result.passes = [];
|
|
532
|
+
axe_result.inapplicable = [];
|
|
533
|
+
|
|
534
|
+
let axe_path = null;
|
|
535
|
+
const passed = axe_result.violations.length == 0;
|
|
536
|
+
if (!passed) {
|
|
537
|
+
let axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `aXe_failure`}) + '.json';
|
|
538
|
+
axe_path = `${ scope.paths.scenario }/${ axe_filename }`;
|
|
539
|
+
fs.writeFileSync( axe_path, JSON.stringify( axe_result, null, 2 ));
|
|
540
|
+
let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_path }.`;
|
|
541
|
+
await scope.addToReport( scope, {
|
|
542
|
+
type: `error`,
|
|
543
|
+
value: msg
|
|
544
|
+
});
|
|
545
|
+
} else {
|
|
546
|
+
await scope.addToReport(scope, { type: `info`, value: `Accessibility on ${ scope.page_id } passed!`})
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
passed: axe_result.violations.length == 0,
|
|
550
|
+
axe_filepath: axe_path
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
|
|
478
554
|
tapElementBySelector: async function ( scope, selector, waitForTimeout ) {
|
|
479
555
|
let elem = await scope.page.$(`${selector}`);
|
|
480
556
|
let error_msg = `Couldn't find "${selector}" on the page, is there a typo?`;
|
|
@@ -625,7 +701,7 @@ module.exports = {
|
|
|
625
701
|
// All the different types of fields
|
|
626
702
|
// buttons, canvases, inputs of all kinds, selects (dropdowns), textareas. Are there more?
|
|
627
703
|
// Will deal with `option` once inside `select`
|
|
628
|
-
let all_nodes = $( `#dasigpage, fieldset button, .daquestionactionbutton, fieldset input, .da-form-group input, .da-form-group select, .da-form-group textarea` );
|
|
704
|
+
let all_nodes = $( `#dasigpage, fieldset button, .daquestionactionbutton, fieldset input, .da-form-group input, .da-form-group select, form select, .da-form-group textarea` );
|
|
629
705
|
for ( let node of all_nodes ) {
|
|
630
706
|
// Decision: Do not abstract the below. There's too much data to pass around for it to make sense
|
|
631
707
|
let $node = $( node );
|
|
@@ -1641,9 +1717,17 @@ module.exports = {
|
|
|
1641
1717
|
// await scope.throwPageError( scope, { id: id });
|
|
1642
1718
|
// }
|
|
1643
1719
|
|
|
1720
|
+
did_navigate = true;
|
|
1644
1721
|
process.stdout.write(`\x1b[36m${ '>' }\x1b[0m`); // continued successfully
|
|
1645
1722
|
} // ends if !did_navigate && ensure_navigation
|
|
1646
1723
|
|
|
1724
|
+
if ( did_navigate && scope.check_all_for_a11y ) {
|
|
1725
|
+
let { passed } = await scope.checkForA11y(scope);
|
|
1726
|
+
if (!passed) {
|
|
1727
|
+
scope.passed_all_for_a11y = false;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1647
1731
|
// TODO: Since we now continue only once no matter what, we can put
|
|
1648
1732
|
// the error in the caller of the function, though we may want
|
|
1649
1733
|
// to pass the symbol to print as well.
|
|
@@ -1836,12 +1920,18 @@ module.exports = {
|
|
|
1836
1920
|
/* Download and save a screenshot. `name` does not have to be
|
|
1837
1921
|
* unique as the filename will be made unique. */
|
|
1838
1922
|
name = name || '';
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
);
|
|
1923
|
+
|
|
1924
|
+
let date = files.readable_date();
|
|
1925
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
1926
|
+
// Include the custom name if it has one
|
|
1927
|
+
if ( name.length > 0 ) {
|
|
1928
|
+
name = `screenshot-${ name }-on-${ id }-${ date }`;
|
|
1929
|
+
} else {
|
|
1930
|
+
name = `screenshot_on-${ id }-${ date }`;
|
|
1931
|
+
}
|
|
1842
1932
|
|
|
1843
1933
|
await scope.page.screenshot({
|
|
1844
|
-
path: `./${
|
|
1934
|
+
path: `./${ scope.paths.scenario }/${ name }.jpg`,
|
|
1845
1935
|
type: 'jpeg',
|
|
1846
1936
|
fullPage: true
|
|
1847
1937
|
});
|
|
@@ -1941,6 +2031,9 @@ module.exports = {
|
|
|
1941
2031
|
* WARNING: Cannot download the same file twice in a single scenario.
|
|
1942
2032
|
* WARNING: Must not have any characters that are totally unique to that
|
|
1943
2033
|
* page load. It's usually just the name of the file.
|
|
2034
|
+
*
|
|
2035
|
+
* TODO: Properly wait for download to complete. See notes in
|
|
2036
|
+
* scope.js scope.detectDownloadComplete()
|
|
1944
2037
|
*/
|
|
1945
2038
|
let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
|
|
1946
2039
|
|
package/lib/steps.js
CHANGED
|
@@ -18,11 +18,15 @@ 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');
|
|
23
|
+
const files = require('./utils/files' );
|
|
24
|
+
|
|
22
25
|
/* Of Note:
|
|
23
|
-
- We're using `*=` because sometimes da text has funny characters in it that are hard to anticipate
|
|
26
|
+
- We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
|
|
24
27
|
*/
|
|
25
28
|
|
|
29
|
+
|
|
26
30
|
require("dotenv").config(); // Where did this go?
|
|
27
31
|
|
|
28
32
|
/* TODO:
|
|
@@ -68,7 +72,23 @@ BeforeAll(async () => {
|
|
|
68
72
|
scope.timeout = 30 * 1000;
|
|
69
73
|
scope.showif_timeout = 600;
|
|
70
74
|
scope.report = new Map();
|
|
71
|
-
|
|
75
|
+
|
|
76
|
+
// Store path names for files and docs created for the tests
|
|
77
|
+
scope.paths = {};
|
|
78
|
+
// Test setup should have created this filepath. Run setup if there's a problem with this.
|
|
79
|
+
scope.paths.artifacts = session_vars.get_artifacts_path_name();
|
|
80
|
+
// For GitHub tests, use the artifacts folder that was created during setup
|
|
81
|
+
// For local tests, make a new folder for each time scenarios start
|
|
82
|
+
if ( !session_vars.is_dev_env() ) {
|
|
83
|
+
// Make a new folder to store all the files for this run of the tests
|
|
84
|
+
scope.paths.artifacts = files.make_artifacts_folder();
|
|
85
|
+
// Save its name in a file. `session_vars` can get it back later.
|
|
86
|
+
session_vars.save_artifacts_path_name( scope.paths.artifacts );
|
|
87
|
+
}
|
|
88
|
+
scope.paths.report = `${ scope.paths.artifacts }/report.txt`;
|
|
89
|
+
|
|
90
|
+
scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_file }`;
|
|
91
|
+
fs.writeFileSync( scope.paths.debug_log, "" );
|
|
72
92
|
});
|
|
73
93
|
|
|
74
94
|
Before(async (scenario) => {
|
|
@@ -77,7 +97,14 @@ Before(async (scenario) => {
|
|
|
77
97
|
scope.expected_in_report = null;
|
|
78
98
|
|
|
79
99
|
await scope.addReportHeading(scope, {scenario});
|
|
100
|
+
// Make folder for this Scenario in the all-tests artifacts folder
|
|
80
101
|
scope.base_filename = await scope.getSafeScenarioBaseFilename(scope, {scenario});
|
|
102
|
+
// Add a date for uniqueness in case dev has accidentally copied a Scenario description
|
|
103
|
+
let date = files.readable_date();
|
|
104
|
+
scope.paths.scenario = `${ scope.paths.artifacts }/${ scope.base_filename }-${ date }`;
|
|
105
|
+
fs.mkdirSync( scope.paths.scenario );
|
|
106
|
+
// Store path name for this Scenario's individualized report
|
|
107
|
+
scope.paths.scenario_report = `${ scope.paths.scenario }/report.txt`;
|
|
81
108
|
|
|
82
109
|
// Downloads
|
|
83
110
|
scope.downloadComplete = false;
|
|
@@ -88,6 +115,8 @@ Before(async (scenario) => {
|
|
|
88
115
|
scope.disable_error_screenshot = false;
|
|
89
116
|
|
|
90
117
|
scope.page_id = null;
|
|
118
|
+
scope.check_all_for_a11y = false;
|
|
119
|
+
scope.passed_all_for_a11y = true;
|
|
91
120
|
|
|
92
121
|
// Reset default timeout
|
|
93
122
|
scope.timeout = 30 * 1000;
|
|
@@ -160,10 +189,9 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
160
189
|
// I've seen stuff take an extra moment or two. Shame to have it everywhere
|
|
161
190
|
try {
|
|
162
191
|
// Interview files should get downloaded to downloads folder
|
|
163
|
-
let docs_dir = await scope.getSafeScenarioFilename( scope, { prefix: `downloads` });
|
|
164
192
|
await scope.page._client.send('Page.setDownloadBehavior', {
|
|
165
193
|
behavior: 'allow',
|
|
166
|
-
downloadPath: path.resolve(
|
|
194
|
+
downloadPath: path.resolve( scope.paths.scenario ), // Save in root of this scenario
|
|
167
195
|
});
|
|
168
196
|
resolve();
|
|
169
197
|
} catch ( err ) { reject( err ); }
|
|
@@ -174,7 +202,11 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
174
202
|
await custom_download_behavior_timeout;
|
|
175
203
|
|
|
176
204
|
// ---- EVENT LISTENERS ----
|
|
177
|
-
|
|
205
|
+
|
|
206
|
+
// TODO: Wait for download to complete doesn't work. See notes in
|
|
207
|
+
// scope.js scope.detectDownloadComplete()
|
|
208
|
+
|
|
209
|
+
// ~Wait for possible download. From https://stackoverflow.com/a/51423688~
|
|
178
210
|
scope.page.on('response', async response => {
|
|
179
211
|
// Note: We can expect that only one file will be getting downloaded
|
|
180
212
|
// at any one time since each step completes synchronously
|
|
@@ -186,7 +218,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
186
218
|
// destroyed when the page closes anyway.
|
|
187
219
|
|
|
188
220
|
// Watch event on download folder or file
|
|
189
|
-
await fs.watchFile(
|
|
221
|
+
await fs.watchFile( scope.paths.scenario, function (curr, prev) {
|
|
190
222
|
// If current size eq to size from response then close
|
|
191
223
|
if (parseInt(curr.size) === parseInt(response._headers['content-length'])) {
|
|
192
224
|
// Lets the related function in `scope` know that the download
|
|
@@ -646,7 +678,7 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
|
|
|
646
678
|
*/
|
|
647
679
|
|
|
648
680
|
// Get JSON variable value
|
|
649
|
-
let {
|
|
681
|
+
let { json_path, json } = await scope.savePageJSONData( scope );
|
|
650
682
|
let actual_value = json.variables[ var_name ];
|
|
651
683
|
|
|
652
684
|
// Add warning to report if value is not as expected
|
|
@@ -654,8 +686,8 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
|
|
|
654
686
|
await scope.addToReport( scope, {
|
|
655
687
|
type: `error`,
|
|
656
688
|
value: `The variable "${ var_name }" was not equal to the expected `
|
|
657
|
-
+ `value on the "${ scope.page_id }" screen. Check
|
|
658
|
-
+
|
|
689
|
+
+ `value on the "${ scope.page_id }" screen. Check `
|
|
690
|
+
+ `${ json_path } to see the page's JSON variables.`
|
|
659
691
|
});
|
|
660
692
|
}
|
|
661
693
|
|
|
@@ -730,6 +762,19 @@ When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -
|
|
|
730
762
|
)
|
|
731
763
|
});
|
|
732
764
|
|
|
765
|
+
When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
|
|
766
|
+
let { passed, axe_filepath } = await wrapPromiseWithTimeout(
|
|
767
|
+
scope.checkForA11y(scope),
|
|
768
|
+
scope.timeout
|
|
769
|
+
)
|
|
770
|
+
let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filepath }.`;
|
|
771
|
+
expect( passed, msg ).to.be.true;
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
When(/I check all pages for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
|
|
775
|
+
scope.check_all_for_a11y = true;
|
|
776
|
+
scope.passed_all_for_a11y = true;
|
|
777
|
+
});
|
|
733
778
|
|
|
734
779
|
//#####################################
|
|
735
780
|
// UI element interaction
|
|
@@ -893,6 +938,11 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
|
|
|
893
938
|
//#####################################
|
|
894
939
|
|
|
895
940
|
After(async function(scenario) {
|
|
941
|
+
// The accessibility failures don't happen til the end, so the rest of the test can continue
|
|
942
|
+
if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
|
|
943
|
+
// The fact that the scenario failed has already been noted earlier in the report
|
|
944
|
+
scenario.result.status = `FAILED`;
|
|
945
|
+
}
|
|
896
946
|
|
|
897
947
|
// Record status before internal report tests can interfere with values
|
|
898
948
|
await scope.setReportScenarioStatus( scope, { status: scenario.result.status });
|
|
@@ -913,8 +963,22 @@ After(async function(scenario) {
|
|
|
913
963
|
});
|
|
914
964
|
} else {
|
|
915
965
|
// Save/download a picture of the screen that's showing during the unexpected status
|
|
916
|
-
|
|
917
|
-
await scope.
|
|
966
|
+
// Save one copy in the outer-most artifact folder
|
|
967
|
+
let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
|
|
968
|
+
await scope.page.screenshot({
|
|
969
|
+
path: `${ scope.paths.artifacts }/${ scenario_filename }.jpg`,
|
|
970
|
+
type: `jpeg`,
|
|
971
|
+
fullPage: true
|
|
972
|
+
});
|
|
973
|
+
// Save another copy in the artifact's Scenario folder
|
|
974
|
+
let screenshot_name = `error_on`;
|
|
975
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
976
|
+
screenshot_name += `-${ id }`;
|
|
977
|
+
await scope.page.screenshot({
|
|
978
|
+
path: `${ scope.paths.scenario }/${ screenshot_name }.jpg`,
|
|
979
|
+
type: `jpeg`,
|
|
980
|
+
fullPage: true
|
|
981
|
+
});
|
|
918
982
|
} // ends if scope.disable_error_screenshot
|
|
919
983
|
} // ends if scope.page exists
|
|
920
984
|
|
|
@@ -969,6 +1033,13 @@ After(async function(scenario) {
|
|
|
969
1033
|
await scope.page.close();
|
|
970
1034
|
scope.page = null;
|
|
971
1035
|
} // ends if scope.page
|
|
1036
|
+
|
|
1037
|
+
// Add report text to Scenario folder after everything has been added to the report
|
|
1038
|
+
let current_report_obj = scope.report.get( scope.scenario_id );
|
|
1039
|
+
let scenario_report_obj = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
|
|
1040
|
+
let report = await scope.getPrintableScenario( scope, { scenario: scenario_report_obj });
|
|
1041
|
+
// Save the report as in the Scenario folder
|
|
1042
|
+
fs.writeFileSync( scope.paths.scenario_report, report );
|
|
972
1043
|
});
|
|
973
1044
|
|
|
974
1045
|
AfterAll(async function() {
|
|
@@ -978,7 +1049,7 @@ AfterAll(async function() {
|
|
|
978
1049
|
console.log( `\n\n${ report }` );
|
|
979
1050
|
|
|
980
1051
|
// Save/download the report as an artifact
|
|
981
|
-
fs.writeFileSync(
|
|
1052
|
+
fs.writeFileSync( scope.paths.report, report );
|
|
982
1053
|
|
|
983
1054
|
// If there is a browser open, then close it
|
|
984
1055
|
if (scope.browser) {
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const log = require('./log');
|
|
5
|
+
const session_vars = require('./session_vars');
|
|
4
6
|
|
|
5
7
|
// This is hardcoded information from `cucumber.js`. Once we move to cucumber 8, we should
|
|
6
|
-
// change that the cucumber.json and actually parse the info from it.
|
|
8
|
+
// change that the cucumber.json and actually parse the info from it.
|
|
9
|
+
// The file won't be used for anything other than detecting and removing color codes
|
|
7
10
|
let summary_file_name = 'cucumber-report.txt';
|
|
8
11
|
fs.readFile(summary_file_name, 'utf8', (err, data) => {
|
|
9
12
|
if (err) {
|
|
10
13
|
console.error(err);
|
|
11
14
|
return;
|
|
12
15
|
}
|
|
16
|
+
|
|
13
17
|
// Replace terminal color codes, if present
|
|
14
18
|
// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#colors--graphics-mode
|
|
15
19
|
data = data.replace(/\x1b\[[0-9][0-9]?[0-9]?m/g, '');
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
|
|
21
|
+
// Put it in the correct file in the correct folder
|
|
22
|
+
let artifacts_path = session_vars.get_artifacts_path_name();
|
|
23
|
+
fs.appendFile( `${ artifacts_path }/${ log.debug_log_file }`, data, err => {
|
|
18
24
|
if (err) {
|
|
19
25
|
console.error(err);
|
|
20
26
|
return;
|
|
21
27
|
}
|
|
22
|
-
});
|
|
28
|
+
}); // Ends appendFileSync
|
|
23
29
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const safe_filename = require('sanitize-filename');
|
|
3
|
+
|
|
4
|
+
let files = {};
|
|
5
|
+
|
|
6
|
+
files.readable_date = function() {
|
|
7
|
+
/* Return the current timestamp in a human-readable filename-safe format to
|
|
8
|
+
* match the workflow artifact date format. */
|
|
9
|
+
let date = new Date(); // now
|
|
10
|
+
let str = date.toLocaleString('en-GB', { timeZone: 'UTC' });
|
|
11
|
+
let fancy_str = str.replace(/\//g, '-').replace(',', ' at').replace(':', 'hrs ').replace(':', 'mins ') + 'secs UTC';
|
|
12
|
+
return fancy_str;
|
|
13
|
+
}; // Ends files.readable_date()
|
|
14
|
+
|
|
15
|
+
files.make_artifacts_folder = function (folder_name) {
|
|
16
|
+
/** Makes a folder in the root of the project, either with the given name
|
|
17
|
+
* or with a default name.
|
|
18
|
+
*
|
|
19
|
+
* Works for local development as well, unlike action.yml or the workflow file.
|
|
20
|
+
*/
|
|
21
|
+
if (folder_name) {
|
|
22
|
+
folder_name = safe_filename( folder_name );
|
|
23
|
+
} else {
|
|
24
|
+
folder_name = `alkiln_tests_output-${ files.readable_date() }`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync( folder_name );
|
|
28
|
+
return folder_name;
|
|
29
|
+
}; // Ends files.make_artifacts_folder()
|
|
30
|
+
|
|
31
|
+
module.exports = files;
|
|
32
|
+
|
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'
|
|
@@ -21,9 +21,11 @@ const session_vars = {};
|
|
|
21
21
|
module.exports = session_vars;
|
|
22
22
|
|
|
23
23
|
// These default to null
|
|
24
|
+
// TODO: How to isolate debug elsewhere so this file can use the logger?
|
|
24
25
|
session_vars.get_debug = function () { return process.env.DEBUG || null; };
|
|
25
26
|
session_vars.get_dev_api_key = function () { return process.env.DOCASSEMBLE_DEVELOPER_API_KEY || null; };
|
|
26
27
|
session_vars.get_repo_url = function () { return process.env.REPO_URL || null; };
|
|
28
|
+
session_vars.is_dev_env = function () { return process.env.DEV || null; }
|
|
27
29
|
|
|
28
30
|
// More complex logic
|
|
29
31
|
session_vars.get_setup_timeout_ms = function () {
|
|
@@ -80,9 +82,10 @@ session_vars.get_languages = function () {
|
|
|
80
82
|
}; // Ends session_vars.get_languages()
|
|
81
83
|
|
|
82
84
|
|
|
83
|
-
const
|
|
85
|
+
const runtime_config_path = `runtime_config.json`;
|
|
86
|
+
const project_name_key = `da_project_name`;
|
|
84
87
|
session_vars.save_project_name = function ( project_name ) {
|
|
85
|
-
/* Saves the name of the project to a
|
|
88
|
+
/* Saves the name of the project to a json obj so deletion can use it later.
|
|
86
89
|
* Built for local as well as remote testing. Assumes a kosher string.
|
|
87
90
|
* We avoided a list because of concerns over getting out of date.
|
|
88
91
|
* Should we try to prevent someone overwriting a previous project name?
|
|
@@ -90,16 +93,23 @@ session_vars.save_project_name = function ( project_name ) {
|
|
|
90
93
|
* @param project_name { string } - purely alphanumeric string */
|
|
91
94
|
|
|
92
95
|
// Warn local developers if file already exists
|
|
93
|
-
if ( fs.existsSync(
|
|
96
|
+
if ( fs.existsSync( runtime_config_path ) ) {
|
|
94
97
|
let old_project_name = session_vars.get_project_name();
|
|
95
98
|
let session_project_already_exists_msg = `ALKiln WARNING: `
|
|
96
99
|
+ `This session had a project name already: ${ old_project_name }. `
|
|
97
100
|
+ `A new project has now been created called ${ project_name }. `
|
|
98
101
|
+ `Remember to delete ${ old_project_name }`;
|
|
99
102
|
console.warn( session_project_already_exists_msg );
|
|
103
|
+
} else {
|
|
104
|
+
// If the file doesn't exist, make one because next we need to read it
|
|
105
|
+
fs.writeFileSync( runtime_config_path, JSON.stringify( {}, null, 2 ) );
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
// Get the json, changing it, and re-write the file from scratch
|
|
109
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ) );
|
|
110
|
+
json[ project_name_key ] = project_name;
|
|
111
|
+
let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
|
|
112
|
+
|
|
103
113
|
if ( session_vars.get_debug() ) {
|
|
104
114
|
console.log( `ALKiln debug: Stored name of Project "${ project_name }" locally.` );
|
|
105
115
|
}
|
|
@@ -107,26 +117,59 @@ session_vars.save_project_name = function ( project_name ) {
|
|
|
107
117
|
}; // Ends save_project_name()
|
|
108
118
|
|
|
109
119
|
session_vars.get_project_name = function () {
|
|
110
|
-
/* Get the name of the current
|
|
111
|
-
let
|
|
112
|
-
|
|
120
|
+
/* Get the name of the current docassemble Project */
|
|
121
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ));
|
|
122
|
+
let project_name = json[ project_name_key ] || null;
|
|
123
|
+
if ( session_vars.get_debug() ) { console.log( `ALKiln debug: Project name from file is "${ project_name }".` ); }
|
|
113
124
|
return project_name;
|
|
114
125
|
}; // Ends get_project_name()
|
|
115
126
|
|
|
116
127
|
session_vars.delete_project_name = function () {
|
|
117
|
-
/*
|
|
118
|
-
// Avoid race conditions possibly caused by checking for existence first
|
|
119
|
-
// Delete file if possible
|
|
128
|
+
/* Empty the key storing the name of the docassemble Project */
|
|
120
129
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
// Get the json, changing it, and re-write the file from scratch
|
|
131
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ));
|
|
132
|
+
json[ project_name_key ] = ``;
|
|
133
|
+
console.log( `ALKiln: Deleted the data that stored the name of the Project.` );
|
|
134
|
+
fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
|
|
123
135
|
return true;
|
|
124
136
|
} catch ( error ) {
|
|
125
|
-
console.warn( `ALKiln WARNING: Could not delete the Project name
|
|
137
|
+
console.warn( `ALKiln WARNING: Could not delete the Project name data. Error: ${ error }.` );
|
|
126
138
|
return false;
|
|
127
139
|
}
|
|
128
140
|
};
|
|
129
141
|
|
|
142
|
+
// The file storing the artifacts path contains the name of the current artifacts folder.
|
|
143
|
+
// The directory name it contains will be created or changed with each run of all the tests.
|
|
144
|
+
// const file_storing_artifact_path = `_alkiln_test_artifacts_dir_name.json`;
|
|
145
|
+
const artifacts_path_key = `artifacts_path`;
|
|
146
|
+
session_vars.save_artifacts_path_name = function ( path_name ) {
|
|
147
|
+
/* Save a key with the name of the current artifacts folder in case it doesn't exist. */
|
|
148
|
+
|
|
149
|
+
// Get the json, changing it, and re-write the file from scratch
|
|
150
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ) );
|
|
151
|
+
json[ artifacts_path_key ] = path_name;
|
|
152
|
+
let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
|
|
153
|
+
|
|
154
|
+
if ( session_vars.get_debug() ) {
|
|
155
|
+
console.log( `ALKiln debug: Stored name of artifacts directory "${ path_name }" locally.` );
|
|
156
|
+
}
|
|
157
|
+
return file;
|
|
158
|
+
}; // Ends get_project_name()
|
|
159
|
+
|
|
160
|
+
session_vars.get_artifacts_path_name = function () {
|
|
161
|
+
/* Get the name of the current artifacts path. */
|
|
162
|
+
try {
|
|
163
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ));
|
|
164
|
+
let path_name = json[ artifacts_path_key ];
|
|
165
|
+
|
|
166
|
+
if ( session_vars.get_debug() ) { console.log( `ALKiln debug: Artifacts directory is "${ path_name }".` ); }
|
|
167
|
+
return path_name;
|
|
168
|
+
} catch ( error ) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}; // Ends get_project_name()
|
|
172
|
+
|
|
130
173
|
/**
|
|
131
174
|
* Ensure all required environment variables exist. Add each missing variable
|
|
132
175
|
* to an error that will be thrown at the end.
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suffolklitlab/alkiln",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.2",
|
|
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",
|
|
@@ -4,7 +4,7 @@ Feature: Interactive steps
|
|
|
4
4
|
# In tag names, 'i' is for 'interactive'
|
|
5
5
|
|
|
6
6
|
@slow @i1
|
|
7
|
-
Scenario: I set
|
|
7
|
+
Scenario: I set various values
|
|
8
8
|
Given I start the interview at "all_tests"
|
|
9
9
|
Then the question id should be "upload files"
|
|
10
10
|
When I set the var "upload_files_visible" to "some_png_1.png, some_png_2.png"
|
|
@@ -87,6 +87,10 @@ Scenario: I set text-type values
|
|
|
87
87
|
Then the question id should be "button event action"
|
|
88
88
|
And I tap to continue
|
|
89
89
|
# Next page
|
|
90
|
+
Then the question id should be "single-dropdown-field"
|
|
91
|
+
And I set the var "single_dropdown_field" to "email"
|
|
92
|
+
And I tap to continue
|
|
93
|
+
# Next page
|
|
90
94
|
Then the question id should be "the end"
|
|
91
95
|
|
|
92
96
|
@fast @i2 @secret
|
|
@@ -98,7 +98,7 @@ Scenario: Test "Then I don’t continue" with an apostrophe
|
|
|
98
98
|
Then I don’t continue
|
|
99
99
|
And I will be told an answer is invalid
|
|
100
100
|
|
|
101
|
-
@fast @o9 @table
|
|
101
|
+
@fast @o9 @table @json
|
|
102
102
|
Scenario: I can match JSON page var to str
|
|
103
103
|
Given I start the interview at "all_tests.yml"
|
|
104
104
|
And I get to "showifs" with this data:
|
|
@@ -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 @screenshot
|
|
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 | |
|
|
@@ -68,6 +68,7 @@ Covers story table tests for:
|
|
|
68
68
|
| signature_1 | | |
|
|
69
69
|
| signature_2 | | |
|
|
70
70
|
| single_quote_dict['single_quote_key']['sq_two'] | true | |
|
|
71
|
+
| single_dropdown_field | email | |
|
|
71
72
|
| x[i].name.first | Proxyname1 | proxy_list[0].name.first |
|
|
72
73
|
| x[i].name.first | Proxyname2 | proxy_list[1].name.first |
|
|
73
74
|
| x.target_number | 2 | proxy_list.target_number |
|
|
@@ -6,14 +6,11 @@ const scope = require(`../../lib/scope.js`);
|
|
|
6
6
|
const getSafeScenarioBaseFilename = scope.getSafeScenarioBaseFilename;
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
let mock_scenario = function ( description
|
|
9
|
+
let mock_scenario = function ( description ) {
|
|
10
10
|
/** Mock the complex structure of a Scenario object */
|
|
11
11
|
let scenario = {
|
|
12
|
-
pickle: { name: description
|
|
12
|
+
pickle: { name: description }
|
|
13
13
|
};
|
|
14
|
-
for ( let tag of tag_names ) {
|
|
15
|
-
scenario.pickle.tags.push({ name: tag });
|
|
16
|
-
}
|
|
17
14
|
return scenario;
|
|
18
15
|
}; // Ends mock_scenario();
|
|
19
16
|
|
|
@@ -23,39 +20,31 @@ describe(`When I use scope.getSafeScenarioBaseFilename()`, function() {
|
|
|
23
20
|
scope.language = undefined;
|
|
24
21
|
});
|
|
25
22
|
|
|
26
|
-
// No language
|
|
23
|
+
// No language
|
|
27
24
|
it(`preserves spaces`, async function() {
|
|
28
|
-
let scenario = mock_scenario( names.with_spaces
|
|
25
|
+
let scenario = mock_scenario( names.with_spaces );
|
|
29
26
|
let result = await getSafeScenarioBaseFilename( scope, { scenario });
|
|
30
27
|
expect( result ).to.equal( names.with_spaces );
|
|
31
28
|
});
|
|
32
29
|
|
|
33
30
|
it(`preserves non-english characters`, async function() {
|
|
34
|
-
let scenario = mock_scenario( names.non_english_chars
|
|
31
|
+
let scenario = mock_scenario( names.non_english_chars );
|
|
35
32
|
let result = await getSafeScenarioBaseFilename( scope, { scenario });
|
|
36
33
|
expect( result ).to.equal( names.non_english_chars );
|
|
37
34
|
});
|
|
38
35
|
|
|
39
36
|
it(`replaces invalid characters with "_"`, async function() {
|
|
40
|
-
let scenario = mock_scenario( names.colon_input
|
|
37
|
+
let scenario = mock_scenario( names.colon_input );
|
|
41
38
|
let result = await getSafeScenarioBaseFilename( scope, { scenario });
|
|
42
39
|
expect( result ).to.equal( names.colon_output );
|
|
43
40
|
});
|
|
44
41
|
|
|
45
|
-
// language
|
|
46
|
-
it(`adds a language
|
|
42
|
+
// language
|
|
43
|
+
it(`adds a language correctly to a description`, async function() {
|
|
47
44
|
scope.language = names.non_english_chars;
|
|
48
|
-
let scenario = mock_scenario( names.with_spaces
|
|
45
|
+
let scenario = mock_scenario( names.with_spaces );
|
|
49
46
|
let result = await getSafeScenarioBaseFilename( scope, { scenario });
|
|
50
|
-
let expected = `${ names.non_english_chars }-${ names.with_spaces }
|
|
51
|
-
expect( result ).to.equal( expected );
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// 2 tags
|
|
55
|
-
it(`adds 2 tags correctly, preserving numbers`, async function() {
|
|
56
|
-
let scenario = mock_scenario( names.with_spaces, [ names.tag1, names.tag2 ] );
|
|
57
|
-
let result = await getSafeScenarioBaseFilename( scope, { scenario });
|
|
58
|
-
let expected = `${ names.with_spaces }-${ names.tag1 }-${ names.tag2 }`;
|
|
47
|
+
let expected = `${ names.non_english_chars }-${ names.with_spaces }`;
|
|
59
48
|
expect( result ).to.equal( expected );
|
|
60
49
|
});
|
|
61
50
|
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let test_filename = function ( expected_prefix, result ) {
|
|
11
|
-
|
|
12
|
-
let expected_name_regex_str = `^${ expected_prefix }
|
|
13
|
-
|
|
11
|
+
/* Test a name to see if it's valid output with a language defined. */
|
|
12
|
+
let expected_name_regex_str = `^${ expected_prefix }-${ names.base_filename }-\.{37}$`;
|
|
13
|
+
if ( expected_prefix == `` ) {
|
|
14
|
+
expected_name_regex_str = `^${ names.base_filename }-\.{37}$`;
|
|
15
|
+
}
|
|
16
|
+
let regex = new RegExp( expected_name_regex_str );
|
|
14
17
|
|
|
15
18
|
// Log more info if it failed
|
|
16
19
|
let found_match = regex.test( result );
|
|
@@ -3,16 +3,16 @@ let names = {};
|
|
|
3
3
|
names.base_filename = `a_safe_id`;
|
|
4
4
|
|
|
5
5
|
names.chinese_input = `是汉字`;
|
|
6
|
-
names.chinese_output =
|
|
6
|
+
names.chinese_output = `是汉字`;
|
|
7
7
|
|
|
8
8
|
names.no_underscore_input = `ends_in_no_underscore`;
|
|
9
|
-
names.no_underscore_output = `
|
|
9
|
+
names.no_underscore_output = `ends_in_no_underscore`;
|
|
10
10
|
|
|
11
11
|
names.one_underscore_input = `ends_in_one_underscore_`;
|
|
12
|
-
names.one_underscore_output = `
|
|
12
|
+
names.one_underscore_output = `ends_in_one_underscore_`;
|
|
13
13
|
|
|
14
14
|
names.two_underscores_input = `ends_in_two_underscores__`;
|
|
15
|
-
names.two_underscores_output = `
|
|
15
|
+
names.two_underscores_output = `ends_in_two_underscores__`;
|
|
16
16
|
|
|
17
17
|
names.empty_string_input = ``;
|
|
18
18
|
names.empty_string_output = ``;
|
|
@@ -21,15 +21,15 @@ names.undefined_input;
|
|
|
21
21
|
names.undefined_output = ``;
|
|
22
22
|
|
|
23
23
|
names.colon_input = `invalid:colon`;
|
|
24
|
-
names.colon_output = `
|
|
24
|
+
names.colon_output = `invalid_colon`;
|
|
25
25
|
|
|
26
26
|
names.slash_input = `invalid/slash`;
|
|
27
|
-
names.slash_output = `
|
|
27
|
+
names.slash_output = `invalid_slash`;
|
|
28
28
|
|
|
29
29
|
names.two_consecutive_invalid_input = `two_consecutive//invalid`;
|
|
30
|
-
names.two_consecutive_invalid_output = `
|
|
30
|
+
names.two_consecutive_invalid_output = `two_consecutive__invalid`;
|
|
31
31
|
|
|
32
32
|
names.numerical_input = `scenario_w1th_number00`;
|
|
33
|
-
names.numerical_output = `
|
|
33
|
+
names.numerical_output = `scenario_w1th_number00`;
|
|
34
34
|
|
|
35
35
|
module.exports = names;
|