@suffolklitlab/alkiln 4.6.1 → 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.
@@ -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,46 +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
- - name: upload error artifacts
61
+ # Artifacts
62
+ - name: upload unit tests artifacts folder
57
63
  uses: actions/upload-artifact@v2
58
- if: ${{ failure() }}
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
79
- if: ${{ always() }}
80
- with:
81
- name: report
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
64
+ # This will upload even if a previous step has failed
86
65
  if: ${{ always() }}
87
66
  with:
88
- name: a11y report
89
- path: ./**/aXe_failure_*
90
- - name: upload debug_log artifact
67
+ name: ${{ env.UNIT_TESTS_ARTIFACT_NAME }}
68
+ path: ./_alkiln_tests_misc-*
69
+ - name: upload integration tests artifacts folder
91
70
  uses: actions/upload-artifact@v2
92
71
  if: ${{ always() }}
93
72
  with:
94
- name: debug_log file
95
- path: ./debug_log.txt
73
+ name: ${{ env.ARTIFACT_NAME }}
74
+ path: ./alkiln_tests_output-*
75
+ # Delete Playground Project and project name file
96
76
  - name: Cleanup
97
77
  if: ${{ always() }}
98
78
  run: npm run takedown
package/CHANGELOG.md CHANGED
@@ -40,6 +40,12 @@ 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
+
43
49
  ## [4.6.1] - 2022-06-28
44
50
  ### Fixed
45
51
  - Single dropdown fields on pages are now recognized as fields 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,39 +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 errors to action summary artifacts"
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"
145
- if: ${{ always() }}
146
- uses: actions/upload-artifact@v2
147
- with:
148
- name: report
149
- path: ./**/*_report_*
150
- - name: "ALKiln: Upload Accessibility failures, if any, to action summary artifacts"
126
+ - name: "ALKiln: Upload artifacts folder"
151
127
  if: ${{ always() }}
152
128
  uses: actions/upload-artifact@v2
153
- # They will upload even if a previous step has failed
154
129
  with:
155
- name: a11y report
156
- path: ./**/aXe_failure*
130
+ name: ${{ env.ARTIFACT_NAME }}
131
+ path: ./alkiln_tests_output-*
157
132
 
158
133
  # ##################################
159
134
  # # A developer's workflow should look similar to the below
@@ -184,8 +159,4 @@ runs:
184
159
  # with:
185
160
  # SERVER_URL: "${{ secrets.SERVER_URL }}"
186
161
  # DOCASSEMBLE_DEVELOPER_API_KEY: "${{ secrets.DOCASSEMBLE_DEVELOPER_API_KEY }}"
187
- # PLAYGROUND_EMAIL: "${{ secrets.PLAYGROUND_EMAIL }}"
188
- # PLAYGROUND_PASSWORD: "${{ secrets.PLAYGROUND_PASSWORD }}"
189
- # PLAYGROUND_ID: "${{ secrets.PLAYGROUND_ID }}"
190
- # EXTRA_LANGUAGES: "${{ secrets.EXTRA_LANGUAGES }}"
191
162
  # - run: echo "Finished running ALKiln tests"
@@ -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
@@ -11,6 +11,7 @@ const get_base_interview_url = require(`./docassemble/get_base_interview_url`);
11
11
  const time = require('./utils/time');
12
12
  const log = require('./utils/log');
13
13
  const { waitForTimeout } = require('./utils/time');
14
+ const files = require('./utils/files' );
14
15
 
15
16
 
16
17
  let ordinal_to_integer = {
@@ -50,6 +51,7 @@ let is_secret_var = function(row) {
50
51
  }
51
52
 
52
53
  let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
54
+ let misc_artifacts_dir = `_alkiln_test-misc_artifacts`;
53
55
 
54
56
  module.exports = {
55
57
  trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
@@ -63,7 +65,22 @@ module.exports = {
63
65
  let scenario = await scope.makeSureReportPartsExist( scope );
64
66
  scenario.lines.push({ type, value });
65
67
 
66
- log.add_to_debug_log(`Report:\ntype: ${ type }, value: ${ value }\n`);
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
+ }
67
84
  return scenario;
68
85
 
69
86
  }, // Ends scope.addToReport()
@@ -72,7 +89,10 @@ module.exports = {
72
89
  /* If a part of the report needs to exist right now, but doesn't exist,
73
90
  * create it. */
74
91
  if ( !scope.report ) { scope.report = new Map(); }
75
- if ( !scope.scenario_id ) { scope.scenario_id = `_report_${ Date.now() }` }
92
+ if ( !scope.scenario_id ) {
93
+ let date = files.readable_date();
94
+ scope.scenario_id = `_report_${ date }`;
95
+ }
76
96
  if ( !scope.report.get( scope.scenario_id )) {
77
97
  scope.report.set( scope.scenario_id, {} )
78
98
  scope.report.get( scope.scenario_id ).status = `in progress`;
@@ -187,41 +207,49 @@ module.exports = {
187
207
  name_parts.push( scope.language );
188
208
  }
189
209
  name_parts.push( scenario.pickle.name );
190
- for ( let tag of scenario.pickle.tags ) {
191
- name_parts.push( tag.name );
192
- }
193
210
 
194
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));
195
215
 
196
- return safe_filename( filename, { replacement: `_` });
216
+ return safer_name;
197
217
  }, // Ends scope.getSafeScenarioBaseFilename()
198
218
 
199
- getSafeScenarioFilename: async function( scope, options={} ) {
219
+ // TODO: Does this need a name change? getSafeScenarioBasedFilename?
220
+ getSafeScenarioFilename: async function( scope, options={ prefix: '' }) {
200
221
  /* Return a string that's safe to be a filename. Use a timestamp,
201
222
  * a prefix the caller wants, and the base filename. We hope it can
202
223
  * contain non-english characters too. We do not offer a suffix option
203
224
  * because it may be cut off if the filename is too long.
204
225
  */
205
- // Ensure prefix is a string
206
- let prefix = options.prefix || '';
226
+ let name = options.prefix || '';
227
+
207
228
  // As long as it's not an empty string, add a separator between it and the rest of the name
208
- // For now, it needs to be `_` for the action and .gitignore. In future,
209
- // aiming for `-`.
210
- if ( prefix ) {
211
- prefix = `${ prefix }_`;
229
+ if ( name.length > 0 ) {
230
+ name += `-`;
212
231
  }
213
232
 
214
- let safe_name = safe_filename(`${ prefix }${ Date.now() }-${ scope.base_filename }`, { replacement: `_` });
215
- let safer_name = safe_name.substring(0, (255 - 10)); // Allow room for extensions
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));
216
244
  return safer_name;
217
245
  }, // Ends scope.getSafeScenarioFilename()
218
246
 
219
247
  getJSONFilename: async function( scope ) {
220
- let filename = await scope.getSafeScenarioFilename( scope, {
221
- prefix: `downloads_${ scope.page_id }_JSON`
222
- });
223
- filename += `.json`;
224
- return filename;
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;
225
253
  },
226
254
 
227
255
  savePageJSONData: async function( scope ) {
@@ -231,13 +259,12 @@ module.exports = {
231
259
  * @returns {string} info.filename The name of the file that was saved.
232
260
  * @returns {Object} info.json The variables object.
233
261
  */
234
- // Get a unique json file filename
235
- let filename = await scope.getJSONFilename( scope );
262
+ let json_path = await scope.getJSONFilename( scope );
236
263
  // Get the data
237
264
  let json = await scope.getPageJSON( scope );
238
265
  // Save the data
239
- fs.writeFileSync( filename, JSON.stringify( json, null, 2 ));
240
- return { filename, json };
266
+ fs.writeFileSync( json_path, JSON.stringify( json, null, 2 ));
267
+ return { json_path, json };
241
268
  }, // Ends scope.savePageJSONData()
242
269
 
243
270
  waitForShowIf: async function waitForShowIf(scope) {
@@ -386,7 +413,14 @@ module.exports = {
386
413
 
387
414
  detectDownloadComplete: async function detectDownloadComplete(scope, endTime) {
388
415
  /* Resolve if a download flag has changed. Timeout after a bit less than
389
- * 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
+ */
390
424
 
391
425
  // Stop/resolve if too much time has passed
392
426
  let expirationTime = scope.timeout - 200; // ms
@@ -497,12 +531,13 @@ module.exports = {
497
531
  axe_result.passes = [];
498
532
  axe_result.inapplicable = [];
499
533
 
500
- let axe_filename = null;
534
+ let axe_path = null;
501
535
  const passed = axe_result.violations.length == 0;
502
536
  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 }.`;
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 }.`;
506
541
  await scope.addToReport( scope, {
507
542
  type: `error`,
508
543
  value: msg
@@ -512,7 +547,7 @@ module.exports = {
512
547
  }
513
548
  return {
514
549
  passed: axe_result.violations.length == 0,
515
- axe_filename: axe_filename
550
+ axe_filepath: axe_path
516
551
  };
517
552
  },
518
553
 
@@ -1885,12 +1920,18 @@ module.exports = {
1885
1920
  /* Download and save a screenshot. `name` does not have to be
1886
1921
  * unique as the filename will be made unique. */
1887
1922
  name = name || '';
1888
- let safe_path = await scope.getSafeScenarioFilename(
1889
- scope, { prefix: `screenshot_${ name }` }
1890
- );
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
+ }
1891
1932
 
1892
1933
  await scope.page.screenshot({
1893
- path: `./${ safe_path }.jpg`,
1934
+ path: `./${ scope.paths.scenario }/${ name }.jpg`,
1894
1935
  type: 'jpeg',
1895
1936
  fullPage: true
1896
1937
  });
@@ -1990,6 +2031,9 @@ module.exports = {
1990
2031
  * WARNING: Cannot download the same file twice in a single scenario.
1991
2032
  * WARNING: Must not have any characters that are totally unique to that
1992
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()
1993
2037
  */
1994
2038
  let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
1995
2039
 
package/lib/steps.js CHANGED
@@ -20,8 +20,10 @@ const { v4: uuidv4 } = require('uuid');
20
20
  const scope = require('./scope');
21
21
  const log = require('./utils/log');
22
22
  const session_vars = require('./utils/session_vars');
23
+ const files = require('./utils/files' );
24
+
23
25
  /* Of Note:
24
- - 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
25
27
  */
26
28
 
27
29
 
@@ -70,7 +72,23 @@ BeforeAll(async () => {
70
72
  scope.timeout = 30 * 1000;
71
73
  scope.showif_timeout = 600;
72
74
  scope.report = new Map();
73
- fs.writeFileSync(log.debug_log_file, "");
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, "" );
74
92
  });
75
93
 
76
94
  Before(async (scenario) => {
@@ -79,7 +97,14 @@ Before(async (scenario) => {
79
97
  scope.expected_in_report = null;
80
98
 
81
99
  await scope.addReportHeading(scope, {scenario});
100
+ // Make folder for this Scenario in the all-tests artifacts folder
82
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`;
83
108
 
84
109
  // Downloads
85
110
  scope.downloadComplete = false;
@@ -164,10 +189,9 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
164
189
  // I've seen stuff take an extra moment or two. Shame to have it everywhere
165
190
  try {
166
191
  // Interview files should get downloaded to downloads folder
167
- let docs_dir = await scope.getSafeScenarioFilename( scope, { prefix: `downloads` });
168
192
  await scope.page._client.send('Page.setDownloadBehavior', {
169
193
  behavior: 'allow',
170
- downloadPath: path.resolve( docs_dir ), // Save in root of this project
194
+ downloadPath: path.resolve( scope.paths.scenario ), // Save in root of this scenario
171
195
  });
172
196
  resolve();
173
197
  } catch ( err ) { reject( err ); }
@@ -178,7 +202,11 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
178
202
  await custom_download_behavior_timeout;
179
203
 
180
204
  // ---- EVENT LISTENERS ----
181
- // Wait for possible download. From https://stackoverflow.com/a/51423688
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~
182
210
  scope.page.on('response', async response => {
183
211
  // Note: We can expect that only one file will be getting downloaded
184
212
  // at any one time since each step completes synchronously
@@ -190,7 +218,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
190
218
  // destroyed when the page closes anyway.
191
219
 
192
220
  // Watch event on download folder or file
193
- await fs.watchFile( docs_dir, function (curr, prev) {
221
+ await fs.watchFile( scope.paths.scenario, function (curr, prev) {
194
222
  // If current size eq to size from response then close
195
223
  if (parseInt(curr.size) === parseInt(response._headers['content-length'])) {
196
224
  // Lets the related function in `scope` know that the download
@@ -650,7 +678,7 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
650
678
  */
651
679
 
652
680
  // Get JSON variable value
653
- let { filename, json } = await scope.savePageJSONData( scope );
681
+ let { json_path, json } = await scope.savePageJSONData( scope );
654
682
  let actual_value = json.variables[ var_name ];
655
683
 
656
684
  // Add warning to report if value is not as expected
@@ -658,8 +686,8 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
658
686
  await scope.addToReport( scope, {
659
687
  type: `error`,
660
688
  value: `The variable "${ var_name }" was not equal to the expected `
661
- + `value on the "${ scope.page_id }" screen. Check the downloads `
662
- + `file ${ filename } to see the page's JSON variables.`
689
+ + `value on the "${ scope.page_id }" screen. Check `
690
+ + `${ json_path } to see the page's JSON variables.`
663
691
  });
664
692
  }
665
693
 
@@ -735,11 +763,11 @@ When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -
735
763
  });
736
764
 
737
765
  When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
738
- let { passed, axe_filename } = await wrapPromiseWithTimeout(
766
+ let { passed, axe_filepath } = await wrapPromiseWithTimeout(
739
767
  scope.checkForA11y(scope),
740
768
  scope.timeout
741
769
  )
742
- let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filename }.`;
770
+ let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filepath }.`;
743
771
  expect( passed, msg ).to.be.true;
744
772
  });
745
773
 
@@ -935,8 +963,22 @@ After(async function(scenario) {
935
963
  });
936
964
  } else {
937
965
  // Save/download a picture of the screen that's showing during the unexpected status
938
- let safe_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error` });
939
- await scope.page.screenshot({ path: `${ safe_filename }.jpg`, type: `jpeg`, fullPage: true });
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
+ });
940
982
  } // ends if scope.disable_error_screenshot
941
983
  } // ends if scope.page exists
942
984
 
@@ -991,6 +1033,13 @@ After(async function(scenario) {
991
1033
  await scope.page.close();
992
1034
  scope.page = null;
993
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 );
994
1043
  });
995
1044
 
996
1045
  AfterAll(async function() {
@@ -1000,7 +1049,7 @@ AfterAll(async function() {
1000
1049
  console.log( `\n\n${ report }` );
1001
1050
 
1002
1051
  // Save/download the report as an artifact
1003
- fs.writeFileSync( `alkiln_report_${ Date.now() }.txt`, report );
1052
+ fs.writeFileSync( scope.paths.report, report );
1004
1053
 
1005
1054
  // If there is a browser open, then close it
1006
1055
  if (scope.browser) {
@@ -2,22 +2,28 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const log = require('./log');
5
+ const session_vars = require('./session_vars');
5
6
 
6
7
  // This is hardcoded information from `cucumber.js`. Once we move to cucumber 8, we should
7
- // 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
8
10
  let summary_file_name = 'cucumber-report.txt';
9
11
  fs.readFile(summary_file_name, 'utf8', (err, data) => {
10
12
  if (err) {
11
13
  console.error(err);
12
14
  return;
13
15
  }
16
+
14
17
  // Replace terminal color codes, if present
15
18
  // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#colors--graphics-mode
16
19
  data = data.replace(/\x1b\[[0-9][0-9]?[0-9]?m/g, '');
17
- fs.appendFile(log.debug_log_file, data, err => {
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
+
@@ -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 project_name_file_path = `_al_test_project_name.json`;
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 file so deletion can use it later.
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( project_name_file_path ) ) {
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
- let file = fs.writeFileSync( project_name_file_path, JSON.stringify( project_name ) );
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 project */
111
- let project_name = JSON.parse( fs.readFileSync( project_name_file_path ));
112
- if ( session_vars.get_debug() ) { console.log( `Project name from file is "${ project_name }".` ); }
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
- /* Delete the file storing the name of the docassemble project */
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
- fs.unlinkSync( project_name_file_path );
122
- console.log( `ALKiln: Deleted the file that stored the name of the Project.` );
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 file. Error: ${ error }.` );
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "4.6.1",
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": {
@@ -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 text-type values
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"
@@ -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,7 +123,7 @@ 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 @o11
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
@@ -1,8 +1,6 @@
1
1
  let names = {};
2
2
 
3
3
  names.with_spaces = `a tests description`;
4
- names.tag1 = `tag1`;
5
- names.tag2 = `tag2`;
6
4
 
7
5
  names.non_english_chars = `是汉字`;
8
6
 
@@ -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, tag_names=[] ) {
9
+ let mock_scenario = function ( description ) {
10
10
  /** Mock the complex structure of a Scenario object */
11
11
  let scenario = {
12
- pickle: { name: description, tags: [], }
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, no tags
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, 1 tag
46
- it(`adds a language and 1 tag correctly, preserving numbers`, async function() {
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, [ names.tag1 ] );
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 }-${ names.tag1 }`;
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
- /* Test a name to see if it's valid output with a language defined. */
12
- let expected_name_regex_str = `^${ expected_prefix }\\d{13}-${ names.base_filename }$`;
13
- let regex = new RegExp( expected_name_regex_str );
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 = `ends_in_no_underscore_`;
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 = `ends_in_one_underscore__`;
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 = `ends_in_two_underscores___`;
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 = `invalid_colon_`;
24
+ names.colon_output = `invalid_colon`;
25
25
 
26
26
  names.slash_input = `invalid/slash`;
27
- names.slash_output = `invalid_slash_`;
27
+ names.slash_output = `invalid_slash`;
28
28
 
29
29
  names.two_consecutive_invalid_input = `two_consecutive//invalid`;
30
- names.two_consecutive_invalid_output = `two_consecutive__invalid_`;
30
+ names.two_consecutive_invalid_output = `two_consecutive__invalid`;
31
31
 
32
32
  names.numerical_input = `scenario_w1th_number00`;
33
- names.numerical_output = `scenario_w1th_number00_`;
33
+ names.numerical_output = `scenario_w1th_number00`;
34
34
 
35
35
  module.exports = names;