@suffolklitlab/alkiln 5.2.1 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -44,6 +44,18 @@ Format:
44
44
  -->
45
45
  <!-- ## [Unreleased] -->
46
46
 
47
+ ## [5.3.0] - 2023-10-14
48
+
49
+ ### Added
50
+ - Screenshots now come with an HTML file of the same page. Pages can be examined for their full errors and for accessibility details. Note that styles are weird because we don't control the page with js like docassemble does. Otherwise hidden elements are visible, etc. See [#763](https://github.com/SuffolkLITLab/ALKiln/issues/763).
51
+
52
+ ### Fixed
53
+ - Fixed and improved error for accessibility failures. See [#744](https://github.com/SuffolkLITLab/ALKiln/issues/744).
54
+
55
+ ### Internal
56
+ - Refatored report functions into their own file. See https://github.com/SuffolkLITLab/ALKiln/issues/770.
57
+ - Changed the format of the aXe file name to shorten it by avoiding repeating the scenario name.
58
+
47
59
  ## [5.2.1] - 2023-09-29
48
60
 
49
61
  ### Added
package/lib/scope.js CHANGED
@@ -17,6 +17,7 @@ const { waitForTimeout } = require('./utils/time');
17
17
  const files = require('./utils/files' );
18
18
  const server = require('./docassemble/docassemble_api_interface');
19
19
  const wrapPromiseWithTimeout = require('./cucumber_8_shim');
20
+ const reports = require(`./utils/reports`);
20
21
 
21
22
 
22
23
  let ordinal_to_integer = {
@@ -82,152 +83,10 @@ let toBase64 = function ( utf8_str, exclude_padding=true) {
82
83
 
83
84
  let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
84
85
  let da_base64_regex = /^[A-Za-z0-9+/]+$/;
85
- let misc_artifacts_dir = `_alkiln-misc_artifacts`;
86
86
 
87
87
  module.exports = {
88
88
  trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
89
89
 
90
- addToReport: async function( scope, { type, value }) {
91
- /* Add an item to a specific list in a scenario's report.
92
- * Create report if necessary.
93
- *
94
- * The report has a key for each scenario and each of those
95
- * keys should correspond to a list containing the relevant data. */
96
- if ( session_vars.get_debug() ) { console.log(`Report:\ntype: ${ type }, value: ${ value }\n`) }
97
- let scenario = await scope.makeSureReportPartsExist( scope );
98
- scenario.lines.push({ type, value });
99
-
100
- // TODO: Not sure how to use .add_to_debug_log() here considering possibly dynamic filepath
101
- // log.add_to_debug_log(`Report:\ntype: ${ type }, value: ${ value }\n`);
102
-
103
- // Ensure filepath for saving local unit test logs
104
- if ( !scope.paths ) {
105
- // TODO: would it be useful to put in existing artifacts folder if possible.
106
- // Can this be usefully abstracted?
107
- scope.paths = { debug_log: `${ misc_artifacts_dir }/unit_tests_logs-${ files.readable_date() }.txt` };
108
- }
109
-
110
- try {
111
- fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
112
- } catch (err) {
113
- fs.mkdirSync( misc_artifacts_dir );
114
- fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
115
- }
116
- return scenario;
117
-
118
- }, // Ends scope.addToReport()
119
-
120
- makeSureReportPartsExist: async function( scope ) {
121
- /* If a part of the report needs to exist right now, but doesn't exist,
122
- * create it. */
123
- if ( !scope.report ) { scope.report = new Map(); }
124
- if ( !scope.scenario_id ) {
125
- scope.scenario_id = `_report_${ Date.now() }`;
126
- }
127
- if ( !scope.report.get( scope.scenario_id )) {
128
- scope.report.set( scope.scenario_id, {} )
129
- scope.report.get( scope.scenario_id ).status = `in progress`;
130
- }
131
-
132
- let scenario = scope.report.get( scope.scenario_id );
133
- if ( !scenario.lines ) { scenario.lines = []; }
134
-
135
- return scenario;
136
- }, // Ends scope.makeSureReportPartsExist()
137
-
138
- setReportScenarioStatus: async function( scope, { status=`PASSED` }) {
139
- /* Add final status of scenario to scenario report object - passed, failed, etc. */
140
- scope.report.get( scope.scenario_id ).status = status;
141
- }, // Ends scope.setReportScenarioStatus()
142
-
143
- convertToOriginalStoryTableRow: async function( scope, { row_data }) {
144
- /* Returns the original data of row into a string formatted as a story table row.
145
- * Should this show the actual row data that was used instead? That could
146
- * be pretty confusing to most devs. */
147
- let source_row = row_data.source || row_data;
148
- let original = source_row.original;
149
- let var_str = await scope.toTrimmedJSON( scope, { str: original.var });
150
- let value_str = await scope.toTrimmedJSON( scope, { str: original.value });
151
- let trigger_str = await scope.toTrimmedJSON( scope, { str: original.trigger });
152
- if ( trigger_str === scope.trigger_not_needed_flag ) {
153
- // This text was added by ALKiln itself. It's not part of the user's data
154
- trigger_str = ``;
155
- }
156
- return `${ ' '.repeat(6) }| ${ var_str } | ${ value_str } | ${ trigger_str } |`;
157
- }, // Ends scope.convertToOriginalStoryTableRows()
158
-
159
- toTrimmedJSON: async function( scope, { str='' }) {
160
- /* Turn into JSON string then trim quotes from the start and end. */
161
- let json = JSON.stringify( str );
162
- let trimmed = json.replace( /^"/, '' ).replace( /"$/, '' ).replace( /\\\\/g, `\\`);
163
- return trimmed;
164
- }, // Ends scope.toTrimmedJSON()
165
-
166
- addReportHeading: async function (scope, { scenario }) {
167
- /** Return printable heading based on Scenario name and tags.
168
- * Allow characters from other languages.
169
- */
170
- if ( session_vars.get_debug() ) { console.log( scenario.pickle.name ); }
171
-
172
- // Collect all tag names
173
- let tag_names = [];
174
- for ( let tag of scenario.pickle.tags ) {
175
- tag_names.push( tag.name );
176
- }
177
-
178
- let heading = `\n---------------\n`
179
- + `Scenario: ${ scenario.pickle.name }`;
180
-
181
- if ( tag_names.length > 0 ) {
182
- heading += `\nTags: ${ tag_names.join(' ') }`;
183
- }
184
-
185
- heading += `\n---------------`;
186
-
187
- await scope.addToReport( scope, {
188
- type: `heading`,
189
- value: heading,
190
- });
191
- }, // Ends scope.addReportHeading()
192
-
193
- getPrintableReport: async function( scope ) {
194
- /* Return a string generated from test report data. */
195
- let report = `Assembly Line Kiln Automated Testing Report - ${ (new Date()).toUTCString() }\n`;
196
-
197
- // First list failed scenarios, then non-passed scenarios, then passed scenarios
198
- let failed_reports = ``;
199
- let passed_reports = ``;
200
- let other_reports = ``;
201
- for ( let scenario of scope.report ) {
202
-
203
- let report = await scope.getPrintableScenario( scope, { scenario });
204
- let [ scenario_id, data ] = scenario;
205
- if ( data.status === `FAILED` ) { failed_reports += report; }
206
- else if ( data.status === `PASSED` ) { passed_reports += report; }
207
- else { other_reports += report; }
208
- } // ends for every scenario
209
-
210
- if ( failed_reports !== `` ) { report += `\n\n===============================\n===============================\nFailed scenarios:\n${ failed_reports }`; }
211
- if ( other_reports !== `` ) { report += `\n\n===============================\n===============================\nScenarios that did not pass:\n${ other_reports }`; }
212
- if ( passed_reports !== `` ) { report += `\n\n===============================\n===============================\nPassed scenarios:\n${ passed_reports }`; }
213
-
214
- return report;
215
- }, // Ends scope.getPrintableReport()
216
-
217
- getPrintableScenario: async function( scope, { scenario }) {
218
- /* Return a string generated from scenario list of report items. */
219
- let [ scenario_id, data ] = scenario;
220
- let report = ``;
221
- for ( let line of data.lines ) {
222
- if ( line.type === `page_id` ) { report += `screen id: `; }
223
- else if ( line.type === `outcome` ) { report += `\n`; }
224
- else if ( line.type === `warning` ) { report += `WARNING: `; }
225
- else if ( line.type === `error` ) { report += `\nERROR: ` }
226
- report += `${ line.value }\n`;
227
- }
228
- return report;
229
- }, // Ends scope.getPrintableScenario()
230
-
231
90
  getSafeScenarioBaseFilename: async function( scope, { scenario }) {
232
91
  /** Return a string that's safe to be a filename. Include the Scenario
233
92
  * description and tags.
@@ -275,6 +134,15 @@ module.exports = {
275
134
  return safe_name;
276
135
  }, // Ends scope.getSafeScenarioFilename()
277
136
 
137
+ getA11yPath: async function( scope ) {
138
+ // Get a unique path for a json file
139
+ let { id } = await scope.examinePageID( scope, 'none to match' );
140
+ let short_id = `${ id }`.substring(0, 20);
141
+ // Need timestamp in case of a loop where the same page gets assessed for a11y multiple times
142
+ let a11y_path = path.join( scope.paths.scenario, `aXe_failure-${ short_id }-${ Date.now() }.json` );
143
+ return a11y_path;
144
+ },
145
+
278
146
  getJSONFilename: async function( scope ) {
279
147
  // Get a unique path for a json file
280
148
  let { id } = await scope.examinePageID( scope, 'none to match' );
@@ -356,7 +224,7 @@ module.exports = {
356
224
  let error_text_prop = await error_elem.getProperty( 'textContent' );
357
225
  let system_error_text = await error_text_prop.jsonValue();
358
226
 
359
- await scope.addToReport(scope, { type: `error`, value: system_error_text });
227
+ reports.addToReport(scope, { type: `error`, value: system_error_text });
360
228
  throw system_error_text;
361
229
  };
362
230
 
@@ -386,7 +254,7 @@ module.exports = {
386
254
  // first in user-triggered cases - users used to be able to access this almost
387
255
  // directly, but they can't anymore. Our functions should know to only pass good things.
388
256
  let msg = `Cannot find the element to tap.`;
389
- if ( !elem ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
257
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
390
258
 
391
259
  expect( elem, msg ).to.exist;
392
260
 
@@ -425,7 +293,7 @@ module.exports = {
425
293
  } else {
426
294
  // Throw any non-timeout error
427
295
  let err_msg = `Error occurred when ALKiln tried to tap "${ tapperText }".`
428
- scope.addToReport( scope, { type: `error`, value: err_msg });
296
+ reports.addToReport( scope, { type: `error`, value: err_msg });
429
297
  throw error;
430
298
  } // ends if error is timeout error
431
299
 
@@ -517,7 +385,7 @@ module.exports = {
517
385
  await scope.get_server_reload_promise( scope );
518
386
  } else {
519
387
  // Otherwise just throw the error
520
- await scope.addToReport( scope, {
388
+ reports.addToReport( scope, {
521
389
  type: `error`,
522
390
  value: non_reload_report_msg });
523
391
  throw error;
@@ -579,7 +447,7 @@ module.exports = {
579
447
  + `${ Math.round( ms_till_server_timeout/1000 ) } seconds to respond. `
580
448
  + `If the server was just reloading and needed more time, try setting the `
581
449
  + `"SERVER_RELOAD_TIMEOUT_SECONDS" GitHub secret to a longer amount of time.`
582
- await scope.addToReport( scope, { type: `error`, value: timeout_message });
450
+ reports.addToReport( scope, { type: `error`, value: timeout_message });
583
451
  }
584
452
 
585
453
  // Once server has responded, throw an error to end the test. After the first
@@ -599,8 +467,8 @@ module.exports = {
599
467
  let reload_failure_msg = `The test was unable to continue because the `
600
468
  + `server was reloading. ALKiln will try this Scenario a total of 2 times. An `
601
469
  + `error will still be printed for this attempt.`;
602
- await scope.addToReport( scope, { type: `error`, value: reload_failure_msg });
603
- // await scope.addToReport( scope, { type: `error`, value: timeout_message });
470
+ reports.addToReport( scope, { type: `error`, value: reload_failure_msg });
471
+ // reports.addToReport( scope, { type: `error`, value: timeout_message });
604
472
  // clearTimeout( server_timeout_id );
605
473
  throw( reload_failure_msg );
606
474
  }, // Ends scope.throw_server_was_reloading_error()
@@ -673,7 +541,7 @@ module.exports = {
673
541
  let elem = await scope.page.$(`#${tab_id}`);
674
542
  let error_msg = `Couldn't find the tab with id "#${tab_id}" on the page, is there a typo?`;
675
543
  if ( !elem ) {
676
- await scope.addToReport(scope, { type: `error`, value: error_msg });
544
+ reports.addToReport(scope, { type: `error`, value: error_msg });
677
545
  }
678
546
  expect( elem, error_msg ).to.exist;
679
547
  await scope.tapElement( scope, elem, tab_id );
@@ -683,7 +551,7 @@ module.exports = {
683
551
  /** Return if there were any violations and the file containing aXe output with more details of the checks */
684
552
  let { id } = await scope.examinePageID( scope, 'none to match' );
685
553
  if ( scope.page_id !== id ) {
686
- await scope.addToReport(scope, { type: `page_id`, value: `${ id }`, });
554
+ reports.addToReport(scope, { type: `page_id`, value: `${ id }`, });
687
555
  scope.page_id = id;
688
556
  }
689
557
  const axe = new AxePuppeteer(scope.page);
@@ -701,16 +569,15 @@ module.exports = {
701
569
  let axe_path = null;
702
570
  const passed = axe_result.violations.length == 0;
703
571
  if (!passed) {
704
- let axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `aXe_failure`}) + '.json';
705
- axe_path = `${ scope.paths.scenario }/${ axe_filename }`;
572
+ axe_path = await scope.getA11yPath( scope );
706
573
  fs.writeFileSync( axe_path, JSON.stringify( axe_result, null, 2 ));
707
574
  let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_path }.`;
708
- await scope.addToReport( scope, {
575
+ reports.addToReport( scope, {
709
576
  type: `error`,
710
577
  value: msg
711
578
  });
712
579
  } else {
713
- await scope.addToReport(scope, { type: `info`, value: `Accessibility on ${ scope.page_id } passed!`})
580
+ reports.addToReport(scope, { type: `info`, value: `Accessibility on ${ scope.page_id } passed!`})
714
581
  }
715
582
  return {
716
583
  passed: axe_result.violations.length == 0,
@@ -722,7 +589,7 @@ module.exports = {
722
589
  let elem = await scope.page.$(`${selector}`);
723
590
  let error_msg = `Couldn't find "${selector}" on the page, is there a typo?`;
724
591
  if ( !elem ) {
725
- await scope.addToReport(scope, { type: `error`, value: error_msg });
592
+ reports.addToReport(scope, { type: `error`, value: error_msg });
726
593
  }
727
594
  expect( elem, error_msg ).to.exist;
728
595
  await scope.tapElement( scope, elem, '', waitForTimeout );
@@ -748,7 +615,7 @@ module.exports = {
748
615
  interview_url = `${ base_url }${ file_name }`;
749
616
  }
750
617
 
751
- await scope.addToReport(scope,{
618
+ reports.addToReport(scope,{
752
619
  type: `row`,
753
620
  value: `Trying to load the interview at "${interview_url}"`
754
621
  });
@@ -772,7 +639,7 @@ module.exports = {
772
639
  } else {
773
640
 
774
641
  // Throw any non-timeout error
775
- scope.addToReport( scope, {
642
+ reports.addToReport( scope, {
776
643
  type: `error`,
777
644
  value: `Error occurred when ALKiln tried to go to the interview url`
778
645
  });
@@ -784,7 +651,7 @@ module.exports = {
784
651
  let load_result = await scope.getLoadData( scope );
785
652
  // Errors, or reaching timeout, causes an exception and is shown to the developer at the end
786
653
  if ( load_result.error ) {
787
- await scope.addToReport(scope, { type: `error`, value: `On final attempt to load interview, got "${ load_result.error }"` });
654
+ reports.addToReport(scope, { type: `error`, value: `On final attempt to load interview, got "${ load_result.error }"` });
788
655
  expect( load_result.error ).to.equal( '' );
789
656
  }
790
657
 
@@ -854,7 +721,7 @@ module.exports = {
854
721
  if (!var_data && ! raw_var_data) {
855
722
  let no_data_msg = `ALKiln Internal error: normalizeTable called w/o var_data or raw_var_data:` +
856
723
  ` open an issue at https://github.com/SuffolkLITLab/ALKiln/issues/new if you see this!`;
857
- await scope.addToReport( scope, { type: `error`, value: no_data_msg });
724
+ reports.addToReport( scope, { type: `error`, value: no_data_msg });
858
725
  throw Error(no_data_msg);
859
726
  }
860
727
 
@@ -869,7 +736,7 @@ module.exports = {
869
736
  let bad_table_msg = `Your Story Table definition needs to be changed. The best practice`
870
737
  + ` is to make your table 3 columns wide with a first row in this format: "| var | value | trigger |".`
871
738
  + ` The first row of your table is actually "${ raw_array[0] }"`;
872
- await scope.addToReport( scope, { type: `error`, value: bad_table_msg})
739
+ reports.addToReport( scope, { type: `error`, value: bad_table_msg})
873
740
  throw Error(bad_table_msg);
874
741
  } else if (row_raw.length == 2) {
875
742
  var_data.push({var: row_raw[0], value: row_raw[1]});
@@ -889,7 +756,7 @@ module.exports = {
889
756
  actual_var_value = process.env[row.flags.secret_name];
890
757
  if ( actual_var_value === undefined ) {
891
758
  let not_in_env_msg = `The GitHub SECRET "${ row.flags.secret_name }" doesn't exist. Check for a typo and check your workflow file.`;
892
- await scope.addToReport( scope, { type: `error`, value: not_in_env_msg })
759
+ reports.addToReport( scope, { type: `error`, value: not_in_env_msg })
893
760
  throw ReferenceError(`Could not find "${row.flags.secret_name }": ${not_in_env_msg}.`)
894
761
  }
895
762
  }
@@ -941,7 +808,7 @@ module.exports = {
941
808
  let encoded_trigger_var = $( trigger_elem ).data( `variable` ); // This should come out to 'None' at the very least
942
809
  if ( !encoded_trigger_var ) {
943
810
  // TODO: Add documentation and then link to it in this message.
944
- await scope.addToReport(scope, { type: `warning`, value: `You are missing an element containing the data for the page's trigger variable. Some fields, like loops with index variables, may not work correctly in these tests. See https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#a-missing-trigger-variable.` });
811
+ reports.addToReport(scope, { type: `warning`, value: `You are missing an element containing the data for the page's trigger variable. Some fields, like loops with index variables, may not work correctly in these tests. See https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#a-missing-trigger-variable.` });
945
812
  encoded_trigger_var = toBase64( `None` );
946
813
  }
947
814
  let trigger = fromBase64( encoded_trigger_var );
@@ -1080,7 +947,7 @@ module.exports = {
1080
947
  // Exceptions: Some specific fields can be duplicates without being dangerous
1081
948
  if ( matching_nodes.length > 1 && !selector.includes(`[class="file-caption-name"]`) ) {
1082
949
  let msg = `${ matching_nodes.length } items on this page matched the base64 encoded name '${ selector }'. You may be setting the same variable in multiple places on this page.`;
1083
- await scope.addToReport(scope, { type: `warning`, value: msg });
950
+ reports.addToReport(scope, { type: `warning`, value: msg });
1084
951
  }
1085
952
 
1086
953
  return selector;
@@ -1359,7 +1226,7 @@ module.exports = {
1359
1226
  + `variable or a generic object, but the page does not `
1360
1227
  + `contain the trigger variable's name in its HTML. See `
1361
1228
  + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#a-missing-trigger-variable.`;
1362
- await scope.addToReport(scope, { type: `warning`, value: page_trigger_warning });
1229
+ reports.addToReport(scope, { type: `warning`, value: page_trigger_warning });
1363
1230
  }
1364
1231
 
1365
1232
  for ( let row_that_matches_this_field of rows_that_match_field_names ) {
@@ -1373,13 +1240,13 @@ module.exports = {
1373
1240
  // a useful message.
1374
1241
 
1375
1242
  // First, make the row data more readable to humans.
1376
- let row_as_story_table = await scope.convertToOriginalStoryTableRow( scope, { row_data: row_that_matches_this_field });
1243
+ let row_as_story_table = reports.convertToOriginalStoryTableRow( scope, { row_data: row_that_matches_this_field });
1377
1244
  let row_trigger_warning = `This story table row needs a value in the "trigger" `
1378
1245
  + `column, but it is empty. The test might get unexpected results. See `
1379
1246
  + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1380
1247
  + `\nThe row's data:\n${ row_as_story_table }`
1381
1248
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1382
- await scope.addToReport(scope, { type: `warning`, value: row_trigger_warning });
1249
+ reports.addToReport(scope, { type: `warning`, value: row_trigger_warning });
1383
1250
  rows_without_a_trigger_name.push( row_that_matches_this_field );
1384
1251
  } else {
1385
1252
  let triggers_match = await scope.matchNeutralizedQuotes( scope, { value1: page_trigger_name, value2: table_row_trigger_name });
@@ -1405,7 +1272,7 @@ module.exports = {
1405
1272
  // First, make the row data more readable to humans.
1406
1273
  let conflicts_as_story_table_rows = [];
1407
1274
  for ( let row of rows_with_conflicting_trigger_names ) {
1408
- let table_str = await scope.convertToOriginalStoryTableRow( scope, { row_data: row });
1275
+ let table_str = reports.convertToOriginalStoryTableRow( scope, { row_data: row });
1409
1276
  conflicts_as_story_table_rows.push( table_str );
1410
1277
  }
1411
1278
 
@@ -1416,7 +1283,7 @@ module.exports = {
1416
1283
  + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1417
1284
  + `\nThe data of row(s) with conflicting trigger name(s):\n${ conflicts_as_story_table_rows.join( `\n` )}`
1418
1285
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1419
- await scope.addToReport(scope, {
1286
+ reports.addToReport(scope, {
1420
1287
  type: `warning`,
1421
1288
  value: all_trigger_names_conflict_msg
1422
1289
  });
@@ -1433,7 +1300,7 @@ module.exports = {
1433
1300
  // First, make the row data more readable to humans.
1434
1301
  let rows_as_story_table = [];
1435
1302
  for ( let row of final_matching_rows ) {
1436
- let table_str = await scope.convertToOriginalStoryTableRow( scope, { row_data: row });
1303
+ let table_str = reports.convertToOriginalStoryTableRow( scope, { row_data: row });
1437
1304
  rows_as_story_table.push( table_str );
1438
1305
  }
1439
1306
 
@@ -1443,7 +1310,7 @@ module.exports = {
1443
1310
  + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1444
1311
  + `\nThe matching rows:\n${ rows_as_story_table.join(`\n`) }`
1445
1312
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1446
- await scope.addToReport(scope, { type: `warning`, value: multiple_matches_msg });
1313
+ reports.addToReport(scope, { type: `warning`, value: multiple_matches_msg });
1447
1314
  }
1448
1315
 
1449
1316
  if ( session_vars.get_debug() ) {
@@ -1617,7 +1484,7 @@ module.exports = {
1617
1484
  // If it doesn't have the right format
1618
1485
  if ( answer === null || !answer.includes(`/`) ) {
1619
1486
  // Return field unused.
1620
- await scope.addToReport( scope, {
1487
+ reports.addToReport( scope, {
1621
1488
  type: `warning`,
1622
1489
  value: `Invalid date. Value given: "${ answer }".`
1623
1490
  });
@@ -1823,7 +1690,7 @@ module.exports = {
1823
1690
  // allow a typo where someone forgot to include a space after a comma.
1824
1691
  let uploads_names = answer.split(`,`);
1825
1692
  if ( uploads_names.length === 0 ) {
1826
- await scope.addToReport( scope, {
1693
+ reports.addToReport( scope, {
1827
1694
  type: `error`,
1828
1695
  value: `Your list of files to upload is empty.`
1829
1696
  });
@@ -1855,7 +1722,7 @@ module.exports = {
1855
1722
 
1856
1723
  // Make sure sources directory exists and has files in it
1857
1724
  if ( dirs.length === 0 ) {
1858
- await scope.addToReport( scope, {
1725
+ reports.addToReport( scope, {
1859
1726
  type: `warning`,
1860
1727
  value: `The test files need to be in the "sources" directory, `
1861
1728
  + `but ALKiln couldn't find the directory or it was empty.`
@@ -1887,7 +1754,7 @@ module.exports = {
1887
1754
  } // ends for each file
1888
1755
 
1889
1756
  if ( missing_files.length > 0 ) {
1890
- await scope.addToReport( scope, {
1757
+ reports.addToReport( scope, {
1891
1758
  type: `warning`,
1892
1759
  value: `ALKiln could not find "${ missing_files.join('", "') }" in any of these folders: "${ dirs.join('", "') }".`
1893
1760
  });
@@ -1944,7 +1811,7 @@ module.exports = {
1944
1811
  * takes a while to load. */
1945
1812
  let canvas = await scope.page.waitForSelector( `#dasigcanvas` );
1946
1813
  let msg = `Could not find a signature field.`;
1947
- if ( !canvas ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
1814
+ if ( !canvas ) { reports.addToReport(scope, { type: `error`, value: msg }); }
1948
1815
  expect( canvas, msg ).to.exist;
1949
1816
 
1950
1817
  // Provides functionality to draw name arg on canvas
@@ -2017,7 +1884,7 @@ module.exports = {
2017
1884
  // Only record the page id once for each page to which we navigate
2018
1885
  let { id } = await scope.examinePageID( scope, 'none to match' );
2019
1886
  if ( scope.page_id !== id ) {
2020
- await scope.addToReport(scope, { type: `page_id`, value: `${ id }`, });
1887
+ reports.addToReport(scope, { type: `page_id`, value: `${ id }`, });
2021
1888
  scope.page_id = id;
2022
1889
  }
2023
1890
 
@@ -2047,9 +1914,9 @@ module.exports = {
2047
1914
 
2048
1915
  something_new_was_set = true;
2049
1916
  field.was_set = true;
2050
- await scope.addToReport(scope, {
1917
+ reports.addToReport(scope, {
2051
1918
  type: `row`,
2052
- value: `${ await scope.convertToOriginalStoryTableRow(scope, { row_data: row_used }) }`
1919
+ value: `${ reports.convertToOriginalStoryTableRow(scope, { row_data: row_used }) }`
2053
1920
  });
2054
1921
 
2055
1922
  // Accumulate number of times this row was used. Esp important for `.target_number`
@@ -2072,9 +1939,9 @@ module.exports = {
2072
1939
  let row_used = await scope.setVariable( scope, { field, var_data, });
2073
1940
  if ( row_used ) {
2074
1941
 
2075
- await scope.addToReport(scope, {
1942
+ reports.addToReport(scope, {
2076
1943
  type: `row`,
2077
- value: `${ await scope.convertToOriginalStoryTableRow(scope, { row_data: row_used }) }`
1944
+ value: `${ reports.convertToOriginalStoryTableRow(scope, { row_data: row_used }) }`
2078
1945
  });
2079
1946
 
2080
1947
  // Esp important for `.target_number`
@@ -2116,13 +1983,13 @@ module.exports = {
2116
1983
  if ( row.source ) { source_row = row.source; }
2117
1984
  let original_row = source_row.original;
2118
1985
  let msg = `Did not find a field on this page for the variable "${ original_row.var }" that could be set to "${ original_row.value }"`;
2119
- await scope.addToReport(scope, { type: `warning`, value: msg });
1986
+ reports.addToReport(scope, { type: `warning`, value: msg });
2120
1987
  }
2121
1988
  }
2122
1989
  if ( num_unused_rows > 0 ) {
2123
1990
  let { id } = await scope.examinePageID( scope, 'none to match' );
2124
1991
  let msg = `Missing variable or variables on page "${id}"`;
2125
- await scope.addToReport(scope, { type: `error`, value: msg });
1992
+ reports.addToReport(scope, { type: `error`, value: msg });
2126
1993
  expect( num_unused_rows, msg ).to.equal( 0 );
2127
1994
  }
2128
1995
  }
@@ -2312,7 +2179,7 @@ module.exports = {
2312
2179
  // probably add a confusing message to the report, so mutating seems, sadly, more useful.
2313
2180
  var_datum.value = `False`; // The original value is still preserved
2314
2181
  // Warn the developer not to use `.there_is_another`.
2315
- await scope.addToReport( scope, { type: `warning`,
2182
+ reports.addToReport( scope, { type: `warning`,
2316
2183
  value: 'The attribute `.there_is_another` is invalid in story table tests. Replace it with '
2317
2184
  + '`.target_number` in your `var` and `trigger` columns. Set the `value` to the number of items in '
2318
2185
  + `that list. This test will now set this row's \`value\` to \`False\`. See `
@@ -2346,10 +2213,10 @@ module.exports = {
2346
2213
  } else if ( value.match( /(true|yes|checked|check|selected|select)/i )) {
2347
2214
  if ( from_story_table && !row.artificial ) {
2348
2215
  if ( row.var.match( /\.there_is_another$/ ) ) {
2349
- await scope.addToReport( scope, { type: `warning`, value: `The attribute \`.there_is_another\` is invalid in story table tests. Replace it with \`.target_number\` in your \`var\` column. Set the \`value\` to the number of items in that list. This test will now set this row's \`value\` to \`False\`. See https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing#there_is_another-loop. The row data is\n${ JSON.stringify( row.original )}` });
2216
+ reports.addToReport( scope, { type: `warning`, value: `The attribute \`.there_is_another\` is invalid in story table tests. Replace it with \`.target_number\` in your \`var\` column. Set the \`value\` to the number of items in that list. This test will now set this row's \`value\` to \`False\`. See https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing#there_is_another-loop. The row data is\n${ JSON.stringify( row.original )}` });
2350
2217
  } else {
2351
2218
  // If `target_number` is set to True instead of a number
2352
- await scope.addToReport( scope, { type: `warning`, value: `${ val_to_print } value is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
2219
+ reports.addToReport( scope, { type: `warning`, value: `${ val_to_print } value is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
2353
2220
  }
2354
2221
  return `False`;
2355
2222
  } else { return `True`; }
@@ -2357,7 +2224,7 @@ module.exports = {
2357
2224
  } else if ( isNaN( desired_num )) { // Story table loop will end
2358
2225
  // I can't figure out how to trigger this warning, so maybe we're
2359
2226
  // already handling everything we need to
2360
- await scope.addToReport( scope, { type: `warning`, value: `${ val_to_print } value is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
2227
+ reports.addToReport( scope, { type: `warning`, value: `${ val_to_print } value is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
2361
2228
  return `False`;
2362
2229
 
2363
2230
  // Check that the number of elements in the list hits our desired number.
@@ -2408,7 +2275,7 @@ module.exports = {
2408
2275
  }
2409
2276
 
2410
2277
  let timeout_err_message = `The test tried to continue to the next page, but it took longer than ${ (scope.timeout/1000)/60 } min. Check the picture of this page. If a secret variable was used, there will be no picture.`
2411
- if ( timedOut ) { await scope.addToReport(scope, { type: `error`, value: timeout_err_message }); }
2278
+ if ( timedOut ) { reports.addToReport(scope, { type: `error`, value: timeout_err_message }); }
2412
2279
  expect( timedOut, timeout_err_message ).to.be.false;
2413
2280
 
2414
2281
  clearTimeout( cont_timeout );
@@ -2452,13 +2319,13 @@ module.exports = {
2452
2319
  /* Throw a cucumber error with the error that appeared on the page. */
2453
2320
  if ( error_handlers.system_error !== undefined ) {
2454
2321
  let sys_err_message = `The test tried to continue to the next page after ${ id }, but there was a system error. Check the picture of this page.`
2455
- await scope.addToReport(scope, { type: `error`, value: sys_err_message });
2322
+ reports.addToReport(scope, { type: `error`, value: sys_err_message });
2456
2323
  expect( was_system_error, sys_err_message ).to.be.false;
2457
2324
 
2458
2325
  } else {
2459
2326
  // If it wasn't a system error, it was a user error
2460
2327
  let user_err_message = `The test tried to continue to the next page after ${ id }, but the user got an error. Check the picture of this page.`
2461
- await scope.addToReport(scope, { type: `error`, value: user_err_message });
2328
+ reports.addToReport(scope, { type: `error`, value: user_err_message });
2462
2329
  expect( true, user_err_message ).to.be.false;
2463
2330
  }
2464
2331
 
@@ -2480,6 +2347,22 @@ module.exports = {
2480
2347
  fullPage: fullPage,
2481
2348
  });
2482
2349
 
2350
+ let html_path = path;
2351
+ if (path.endsWith(".jpg")) {
2352
+ html_path = html_path.substring(0, html_path.length - 4) + ".html";
2353
+ } else {
2354
+ html_path = html_path + ".html";
2355
+ }
2356
+
2357
+ // Also save the HTML of the page
2358
+ await scope.page.content().then(content => {
2359
+ let server_url = session_vars.get_da_server_url();
2360
+ content = content.replaceAll(/"(\/static\/.*\.css\?v=[^"]+")/g, server_url + "$1");
2361
+ content = content.replaceAll(/"(\/static\/.*\.js\?v=[^"]+")/g, server_url + "$1");
2362
+ content = content.replaceAll(/"(\/packagestatic\/.*\.css\?v=[^"]+")/g, server_url + "$1");
2363
+ content = content.replaceAll(/"(\/packagestatic\/.*\.js\?v=[^"]+")/g, server_url + "$1");
2364
+ fs.writeFileSync(html_path, content)
2365
+ });
2483
2366
  }, // Ends scope.take_a_screenshot()
2484
2367
 
2485
2368
 
@@ -2519,7 +2402,7 @@ module.exports = {
2519
2402
  });
2520
2403
 
2521
2404
  // In future, may be saved to artifact file
2522
- await scope.addToReport( scope, {
2405
+ reports.addToReport( scope, {
2523
2406
  type: `json`,
2524
2407
  value: JSON.stringify( data, null, 2 )
2525
2408
  });
@@ -2535,7 +2418,7 @@ module.exports = {
2535
2418
  let response = await linkPage.goto( actual_url, { waitUntil: 'domcontentloaded' });
2536
2419
 
2537
2420
  let msg = `The "${ linkText }" link is broken.`;
2538
- if ( !response.ok() ) { await scope.addToReport( scope, { type: `error`, value: msg }); }
2421
+ if ( !response.ok() ) { reports.addToReport( scope, { type: `error`, value: msg }); }
2539
2422
  expect( response.ok(), msg ).to.be.true;
2540
2423
 
2541
2424
  // This may cause problems if the linked page is the interview page
@@ -2584,7 +2467,7 @@ module.exports = {
2584
2467
  const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
2585
2468
 
2586
2469
  let msg = `The term "${ phrase }" seems to be missing.`;
2587
- if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
2470
+ if ( !link ) { reports.addToReport(scope, { type: `error`, value: msg }); }
2588
2471
  expect( link, msg ).to.exist;
2589
2472
 
2590
2473
  await scope.tapElement( scope, link );
@@ -2611,7 +2494,7 @@ module.exports = {
2611
2494
  let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
2612
2495
 
2613
2496
  let msg = `"${ filename }" seems to be missing. Cannot find a link to that document.`;
2614
- if ( !elem ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
2497
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
2615
2498
  expect( elem, msg ).to.exist;
2616
2499
 
2617
2500
  scope.toDownload = filename;
@@ -2647,7 +2530,7 @@ module.exports = {
2647
2530
  name_parts_slice.push(name_parts_last);
2648
2531
  name_parts = name_parts_slice;
2649
2532
 
2650
- await scope.addToReport(scope, {
2533
+ reports.addToReport(scope, {
2651
2534
  type: `warning`,
2652
2535
  value: `The name "${name_str}" has more than 4 parts, but 4 is the maximum allowed. The test will set the name to "${name_parts.join(" ")}".`
2653
2536
  });
@@ -2733,7 +2616,7 @@ module.exports = {
2733
2616
  await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
2734
2617
  } else {
2735
2618
  // Throw any non-timeout error
2736
- scope.addToReport( scope, { type: `error`, value: err_msg });
2619
+ reports.addToReport( scope, { type: `error`, value: err_msg });
2737
2620
  throw error;
2738
2621
  } // ends if error is timeout error
2739
2622
 
@@ -2747,11 +2630,11 @@ module.exports = {
2747
2630
  // this is complex
2748
2631
  let email_msg = `The email address GitHub SECRET "${ email_secret_name }" doesn't exist. Check for a typo and check your workflow file.`
2749
2632
  if ( email === undefined ) {
2750
- await scope.addToReport( scope, { type: `error`, value: email_msg })
2633
+ reports.addToReport( scope, { type: `error`, value: email_msg })
2751
2634
  }
2752
2635
  let password_msg = `The password GitHub SECRET "${ password_secret_name }" doesn't exist. Check for a typo and check your workflow file.`
2753
2636
  if ( password === undefined ) {
2754
- await scope.addToReport( scope, { type: `error`, value: password_msg })
2637
+ reports.addToReport( scope, { type: `error`, value: password_msg })
2755
2638
  }
2756
2639
 
2757
2640
  expect( email, email_msg ).to.not.equal( undefined );
@@ -2768,7 +2651,7 @@ module.exports = {
2768
2651
  // TODO: use a Promise.race to fail faster.
2769
2652
  await scope.page.waitForSelector( `.datopcenter .alert-success` );
2770
2653
 
2771
- await scope.addToReport( scope, {
2654
+ reports.addToReport( scope, {
2772
2655
  type: `row`,
2773
2656
  value: `Signed into ${ session_vars.get_da_server_url() }/user/sign-in successfully.`
2774
2657
  });
@@ -2910,7 +2793,7 @@ module.exports = {
2910
2793
  if ( type !== `` && type !== `hidden` ) {
2911
2794
  // Field types that we can't yet handle, but plan to in future
2912
2795
  if ( type === `file` ) {
2913
- await scope.addToReport( scope, {
2796
+ reports.addToReport( scope, {
2914
2797
  type: `warning`,
2915
2798
  value: `Sorry, ALKiln's random input tests cannot yet handle "${ type }" fields.`
2916
2799
  });
@@ -3016,7 +2899,7 @@ module.exports = {
3016
2899
  */
3017
2900
  let current_report_obj = scope.report.get( scope.scenario_id );
3018
2901
  let scenario = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
3019
- let report = await scope.getPrintableScenario( scope, { scenario });
2902
+ let report = reports.getPrintableScenario( scenario );
3020
2903
  let all_are_included = true;
3021
2904
  for ( let one_expectation of expected ) {
3022
2905
  if ( !report.includes( one_expectation )) {
@@ -3036,7 +2919,7 @@ module.exports = {
3036
2919
  */
3037
2920
  let current_report_obj = scope.report.get( scope.scenario_id );
3038
2921
  let scenario = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
3039
- let report = await scope.getPrintableScenario( scope, { scenario });
2922
+ let report = reports.getPrintableScenario( scenario );
3040
2923
  let none_included = true;
3041
2924
  for ( let one_expectation of not_expected ) {
3042
2925
  if ( report.includes( one_expectation )) {
package/lib/steps.js CHANGED
@@ -21,6 +21,7 @@ const scope = require('./scope');
21
21
  const log = require('./utils/log');
22
22
  const session_vars = require('./utils/session_vars');
23
23
  const files = require('./utils/files' );
24
+ const reports = require(`./utils/reports`);
24
25
 
25
26
  /* Of Note:
26
27
  - We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
@@ -125,7 +126,7 @@ Before(async (scenario) => {
125
126
  scope.expected_not_in_report = null;
126
127
  scope.server_reload_promise = null;
127
128
 
128
- await scope.addReportHeading(scope, {scenario});
129
+ reports.addReportHeading(scope, {scenario});
129
130
  // Make folder for this Scenario in the all-tests artifacts folder
130
131
  scope.base_filename = await scope.getSafeScenarioBaseFilename(scope, {scenario});
131
132
  // Add a date for uniqueness in case dev has accidentally copied a Scenario description
@@ -353,7 +354,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
353
354
  resolve();
354
355
  } catch ( err ) {
355
356
  let err_msg = `Error occurred when tried to answer fields on question "${ id }".`
356
- scope.addToReport( scope, { type: `error`, value: err_msg });
357
+ reports.addToReport( scope, { type: `error`, value: err_msg });
357
358
  // Throw error instead?
358
359
  reject( err );
359
360
  } finally {
@@ -372,7 +373,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
372
373
 
373
374
  // Add report strings from supported table, not an altered one
374
375
  for ( let row of supported_table ) {
375
- row.report_str = `${ await scope.convertToOriginalStoryTableRow(scope, { row_data: row }) }`;
376
+ row.report_str = `${ reports.convertToOriginalStoryTableRow(scope, { row_data: row }) }`;
376
377
  }
377
378
  // Sort into alphabetical order, as is recommended for writing story tables
378
379
  let sorted_rows = supported_table.sort( function ( row1, row2 ) {
@@ -383,13 +384,13 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
383
384
 
384
385
  // Add full used table to report so it can be copied if needed.
385
386
  // TODO: Should we/How do we get this in the final scenario report instead of here?
386
- await scope.addToReport( scope, { type: `note`, value: `\n${ ' '.repeat(2) }Rows that got set:` });
387
- await scope.addToReport( scope, { type: `step`, value: `${ ' '.repeat(4) }And I get to the question id "${ target_id }" with this data:` });
388
- await scope.addToReport(scope, { type: `row`, value: `${ ' '.repeat(6) }| var | value | trigger |`, });
387
+ reports.addToReport( scope, { type: `note`, value: `\n${ ' '.repeat(2) }Rows that got set:` });
388
+ reports.addToReport( scope, { type: `step`, value: `${ ' '.repeat(4) }And I get to the question id "${ target_id }" with this data:` });
389
+ reports.addToReport(scope, { type: `row`, value: `${ ' '.repeat(6) }| var | value | trigger |`, });
389
390
  let at_least_one_row_was_NOT_used = false;
390
391
  for ( let row of sorted_rows ) {
391
392
  if ( row.times_used > 0 ) {
392
- await scope.addToReport(scope, { type: `row`, value: row.report_str, });
393
+ reports.addToReport(scope, { type: `row`, value: row.report_str, });
393
394
  } else {
394
395
  at_least_one_row_was_NOT_used = true;
395
396
  }
@@ -397,17 +398,17 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
397
398
 
398
399
  // Add unused rows if needed
399
400
  if ( at_least_one_row_was_NOT_used ) {
400
- await scope.addToReport( scope, { type: `note`, value: `${ ' '.repeat(2) }Unused rows:` });
401
+ reports.addToReport( scope, { type: `note`, value: `${ ' '.repeat(2) }Unused rows:` });
401
402
  for ( let row of sorted_rows ) {
402
403
  if ( row.times_used === 0 ) {
403
- await scope.addToReport(scope, { type: `row`, value: row.report_str, });
404
+ reports.addToReport(scope, { type: `row`, value: row.report_str, });
404
405
  }
405
406
  }
406
407
  } else {
407
- await scope.addToReport(scope, { type: `note`, value: `${ ' '.repeat(2) }All rows were used` });
408
+ reports.addToReport(scope, { type: `note`, value: `${ ' '.repeat(2) }All rows were used` });
408
409
  }
409
410
 
410
- await scope.addToReport(scope, { type: `note`, value: `` });
411
+ reports.addToReport(scope, { type: `note`, value: `` });
411
412
  });
412
413
 
413
414
  //#####################################
@@ -457,7 +458,7 @@ Then(/the (?:question|page|screen) id should be "([^"]+)"/i, { timeout: -1 }, as
457
458
  // No question id exists
458
459
  let no_id_msg = `Did not find any question id.`;
459
460
  if ( !question_has_id ) {
460
- await scope.addToReport( scope, { type: `error`, value: no_id_msg });
461
+ reports.addToReport( scope, { type: `error`, value: no_id_msg });
461
462
  }
462
463
  expect( question_has_id , no_id_msg ).to.be.true;
463
464
 
@@ -465,7 +466,7 @@ Then(/the (?:question|page|screen) id should be "([^"]+)"/i, { timeout: -1 }, as
465
466
  let no_match_msg = `The question id was supposed to be "${ question_id }"`
466
467
  + `, but it's actually "${ id }".`;
467
468
  if ( !id_matches ) {
468
- await scope.addToReport( scope, { type: `error`, value: no_match_msg });
469
+ reports.addToReport( scope, { type: `error`, value: no_match_msg });
469
470
  }
470
471
  expect( id_matches, no_match_msg ).to.be.true;
471
472
 
@@ -478,7 +479,7 @@ Then(/I ?(?:should)? see the phrase "([^"]+)"/i, async ( phrase ) => { // √
478
479
  const bodyText = await scope.page.$eval('body', elem => elem.innerText);
479
480
 
480
481
  let msg = `The text "${ phrase }" SHOULD be on this page, but it's NOT.`;
481
- if ( !bodyText.includes( phrase )) { await scope.addToReport(scope, { type: `error`, value: msg }); }
482
+ if ( !bodyText.includes( phrase )) { reports.addToReport(scope, { type: `error`, value: msg }); }
482
483
  expect( bodyText, msg ).to.contain( phrase );
483
484
 
484
485
  await scope.afterStep(scope);
@@ -492,7 +493,7 @@ Then(/I should not see the phrase "([^"]+)"/i, async ( phrase ) => { // √
492
493
  const bodyText = await scope.page.$eval('body', elem => elem.innerText);
493
494
 
494
495
  let msg = `The text "${ phrase }" should NOT be on this page, but it IS here.`;
495
- if ( bodyText.includes( phrase )) { await scope.addToReport(scope, { type: `error`, value: msg }); }
496
+ if ( bodyText.includes( phrase )) { reports.addToReport(scope, { type: `error`, value: msg }); }
496
497
  expect( bodyText, msg ).not.to.contain( phrase );
497
498
 
498
499
  await scope.afterStep(scope);
@@ -502,7 +503,7 @@ Then('an element should have the id {string}', async ( id ) => { // √
502
503
  const element = await scope.page.$('#' + id);
503
504
 
504
505
  let msg = `No element on this page has the ID "${ id }".`;
505
- if ( !element ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
506
+ if ( !element ) { reports.addToReport(scope, { type: `error`, value: msg }); }
506
507
  expect( element, msg ).to.exist;
507
508
 
508
509
  await scope.afterStep(scope);
@@ -557,7 +558,7 @@ Then(/I (don't|can't|don’t|can’t|cannot|do not) continue/, async (unused) =>
557
558
  * Can other things trigger navigation? Re: other inputs things
558
559
  * like 'Enter' acting like a click is a test for docassemble */
559
560
  let msg = `The page should have stopped the user from continuing, but the user was able to continue.`;
560
- if ( scope.navigated ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
561
+ if ( scope.navigated ) { reports.addToReport(scope, { type: `error`, value: msg }); }
561
562
  expect( scope.navigated, msg ).to.be.false;
562
563
 
563
564
  await scope.afterStep(scope);
@@ -568,12 +569,12 @@ Then('I will be told an answer is invalid', async () => { // √
568
569
  let { was_found, error_handlers } = await scope.checkForError( scope );
569
570
 
570
571
  let no_error_msg = `No error message was found on the page`;
571
- if ( !was_found ) { await scope.addToReport(scope, { type: `error`, value: no_error_msg }); }
572
+ if ( !was_found ) { reports.addToReport(scope, { type: `error`, value: no_error_msg }); }
572
573
  expect( was_found, no_error_msg ).to.be.true;
573
574
 
574
575
  let was_user_error = error_handlers.complex_user_error !== undefined || error_handlers.simple_user_error !== undefined;
575
576
  let not_user_error_msg = `The error was a system error, not an invalid user answer. Check for the picture of the page's error if the step did not use any secret variables.`;
576
- if ( !was_user_error ) { await scope.addToReport(scope, { type: `error`, value: not_user_error_msg }); }
577
+ if ( !was_user_error ) { reports.addToReport(scope, { type: `error`, value: not_user_error_msg }); }
577
578
  expect( was_user_error, not_user_error_msg ).to.be.true;
578
579
 
579
580
  await scope.afterStep(scope);
@@ -584,7 +585,7 @@ Then('I arrive at the next page', async () => { // √
584
585
  * Can other things trigger navigation? Re: other inputs things
585
586
  * like 'Enter' acting like a click is a test for docassemble */
586
587
  let msg = `User did not arrive at the next page.`
587
- if ( !scope.navigated ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
588
+ if ( !scope.navigated ) { reports.addToReport(scope, { type: `error`, value: msg }); }
588
589
  expect( scope.navigated, msg ).to.be.true;
589
590
  await scope.afterStep(scope);
590
591
  });
@@ -595,7 +596,7 @@ Then('I should see the link {string}', async ( linkText ) => { // √
595
596
  let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
596
597
 
597
598
  let msg = `Cannot find a link with the text "${ linkText }".`;
598
- if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
599
+ if ( !link ) { reports.addToReport(scope, { type: `error`, value: msg }); }
599
600
  expect( link, msg ).to.exist;
600
601
 
601
602
  await scope.afterStep(scope);
@@ -606,7 +607,7 @@ Then('I should see the link to {string}', async ( url ) => { // √
606
607
  let link = await scope.page.$(`*[href="${ url }"]`);
607
608
 
608
609
  let msg = `Cannot find a link to "${ url }".`;
609
- if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
610
+ if ( !link ) { reports.addToReport(scope, { type: `error`, value: msg }); }
610
611
  expect(link, msg ).to.exist;
611
612
 
612
613
  await scope.afterStep(scope);
@@ -622,7 +623,7 @@ Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) =>
622
623
  let actual_url = await prop_obj.jsonValue();
623
624
 
624
625
  let msg = `Cannot find a link with the text "${ linkText }" leading to ${ expected_url }.`;
625
- if ( actual_url !== expected_url ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
626
+ if ( actual_url !== expected_url ) { reports.addToReport(scope, { type: `error`, value: msg }); }
626
627
  expect( actual_url ).to.equal( expected_url );
627
628
 
628
629
  await scope.afterStep(scope);
@@ -641,7 +642,7 @@ Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkTe
641
642
  || ( !should_open_a_new_window && !opens_a_new_window );
642
643
 
643
644
  let msg = `The link "${ linkText }" does NOT open in ${ which_window }.`;
644
- if ( !hasCorrectWindowTarget ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
645
+ if ( !hasCorrectWindowTarget ) { reports.addToReport(scope, { type: `error`, value: msg }); }
645
646
  expect( hasCorrectWindowTarget, msg ).to.be.true;
646
647
 
647
648
  await scope.afterStep(scope);
@@ -674,7 +675,7 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
674
675
 
675
676
  // Add warning to report if value is not as expected
676
677
  if ( actual_value !== expected_value ) {
677
- await scope.addToReport( scope, {
678
+ reports.addToReport( scope, {
678
679
  type: `error`,
679
680
  value: `The variable "${ var_name }" was not equal to the expected `
680
681
  + `value on the "${ scope.page_id }" screen. Check `
@@ -781,7 +782,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
781
782
  let random_input_path = `${ scope.paths.scenario }/random${ timestamp }`
782
783
  scope.paths.random_screenshots = random_input_path;
783
784
  fs.mkdirSync( scope.paths.random_screenshots );
784
- await scope.addToReport( scope, {
785
+ reports.addToReport( scope, {
785
786
  type: 'row',
786
787
  value: `Testing interview with random input (in ${ random_input_path })`
787
788
  });
@@ -816,7 +817,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
816
817
  let has_content = await scope.has_interview_content( scope );
817
818
 
818
819
  if ( !has_content ) {
819
- await scope.addToReport( scope, {
820
+ reports.addToReport( scope, {
820
821
  type: `error`,
821
822
  value: no_content_err
822
823
  });
@@ -825,7 +826,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
825
826
  } else {
826
827
  let timeout_message = `The page with an id of "${ id }" took too long`
827
828
  + ` to fill with random answers (over ${ scope.timeout/1000/60 } min).`
828
- await scope.addToReport( scope, { type: `error`, value: timeout_message });
829
+ reports.addToReport( scope, { type: `error`, value: timeout_message });
829
830
  reject( timeout_message );
830
831
  }
831
832
 
@@ -866,7 +867,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
866
867
  // Make sure to only throw once
867
868
  if ( !has_thrown ) {
868
869
  has_thrown = true;
869
- await scope.addToReport( scope, {
870
+ reports.addToReport( scope, {
870
871
  type: `error`,
871
872
  value: no_content_err
872
873
  });
@@ -890,7 +891,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
890
891
  // Throw a specific error if there's no interview content
891
892
  let has_content = await scope.has_interview_content( scope );
892
893
  if ( !has_content ) {
893
- await scope.addToReport( scope, {
894
+ reports.addToReport( scope, {
894
895
  type: `error`,
895
896
  value: no_content_err
896
897
  });
@@ -900,7 +901,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
900
901
  } else {
901
902
  let err_msg = `The page with the question id "${ id }" errored when trying to fill`
902
903
  + ` it with random answers: ${ error.name }`
903
- await scope.addToReport( scope, { type: `error`, value: err_msg });
904
+ reports.addToReport( scope, { type: `error`, value: err_msg });
904
905
  reject( error );
905
906
  } // ends if !has_content
906
907
 
@@ -1098,24 +1099,29 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
1098
1099
 
1099
1100
  After(async function(scenario) {
1100
1101
 
1102
+ let changeable_test_status = scenario.result.status;
1101
1103
  // The accessibility failures don't happen til the end, so the rest of the test can continue
1102
1104
  if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
1103
1105
  // The fact that the scenario failed has already been noted earlier in the report
1104
- scenario.result.status = `FAILED`;
1106
+ changeable_test_status = `FAILED`;
1107
+ scope.addToReport(scope, {
1108
+ type: `error`,
1109
+ value: `Accessibility standards weren't met. See information above.`
1110
+ });
1105
1111
  }
1106
1112
 
1107
1113
  // Record status before internal report tests can interfere with values
1108
- await scope.setReportScenarioStatus( scope, { status: scenario.result.status });
1114
+ reports.setReportScenarioStatus( scope, { status: changeable_test_status });
1109
1115
 
1110
- if (scenario.result.status !== `PASSED`) {
1116
+ if (changeable_test_status !== `PASSED`) {
1111
1117
  if ( session_vars.get_debug() ) {
1112
- console.log( `Non-passed status occurred while running test:`, scenario.result.status );
1118
+ console.log( `Non-passed status occurred while running test:`, changeable_test_status );
1113
1119
  }
1114
1120
 
1115
1121
  if ( scope.page ) {
1116
1122
 
1117
1123
  if ( !!scope.disable_error_screenshot ) {
1118
- scope.addToReport(scope, {
1124
+ reports.addToReport(scope, {
1119
1125
  type: `row`,
1120
1126
  value: `For security, ALKiln will avoid creating a picture of the page for this error. It's possible a secret is being used on this screen.`
1121
1127
  });
@@ -1139,21 +1145,19 @@ After(async function(scenario) {
1139
1145
  } // ends if scope.page exists
1140
1146
 
1141
1147
  // This has to come after the security message or security message doesn't print. Not sure why.
1142
- if (scenario.result.status === `FAILED`) {
1143
- await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
1148
+ if (changeable_test_status === `FAILED`) {
1149
+ reports.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
1144
1150
  }
1145
1151
 
1146
1152
  } // ends if not passed
1147
1153
 
1148
- let original_scenario_status = scenario.result.status;
1149
-
1150
1154
  // Internal tests of this framework
1151
1155
  if ( scope.expected_status ) {
1152
1156
  let expected = scope.expected_status;
1153
1157
  scope.expected_status = null; // reset for next Scenario
1154
- expect( scenario.result.status ).to.equal( expected );
1158
+ expect( changeable_test_status ).to.equal( expected );
1155
1159
  // If the status was as expected, pass this criteria
1156
- scenario.result.status = `PASSED`;
1160
+ changeable_test_status = `PASSED`;
1157
1161
  }
1158
1162
 
1159
1163
  if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
@@ -1162,8 +1166,8 @@ After(async function(scenario) {
1162
1166
  scope.expected_in_report = null;
1163
1167
  let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
1164
1168
 
1165
- if ( all_were_included ) { scenario.result.status = `PASSED`; }
1166
- else { scenario.result.status = `FAILED`; }
1169
+ if ( all_were_included ) { changeable_test_status = `PASSED`; }
1170
+ else { changeable_test_status = `FAILED`; }
1167
1171
  }
1168
1172
 
1169
1173
  if (scope.expected_not_in_report && scope.expected_not_in_report.length > 0) {
@@ -1173,9 +1177,9 @@ After(async function(scenario) {
1173
1177
  let all_were_not_there = await scope.reportDoesNotInclude( scope, { not_expected });
1174
1178
 
1175
1179
  if ( all_were_not_there ) {
1176
- scenario.result.status = `PASSED`;
1180
+ changeable_test_status = `PASSED`;
1177
1181
  } else {
1178
- scenario.result.status = `FAILED`;
1182
+ changeable_test_status = `FAILED`;
1179
1183
  }
1180
1184
 
1181
1185
  }
@@ -1199,7 +1203,7 @@ After(async function(scenario) {
1199
1203
  } else {
1200
1204
  // Throw any non-timeout error
1201
1205
  let err_msg = `Error occurred when ALKiln tried to sign out in preparation for the next test.`
1202
- scope.addToReport( scope, { type: `error`, value: err_msg });
1206
+ reports.addToReport( scope, { type: `error`, value: err_msg });
1203
1207
  throw error;
1204
1208
  } // ends if error is timeout error
1205
1209
 
@@ -1210,16 +1214,29 @@ After(async function(scenario) {
1210
1214
  // Add report text to Scenario folder after everything has been added to the report
1211
1215
  let current_report_obj = scope.report.get( scope.scenario_id );
1212
1216
  let scenario_report_obj = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
1213
- let report = await scope.getPrintableScenario( scope, { scenario: scenario_report_obj });
1217
+ let report = reports.getPrintableScenario( scenario_report_obj );
1214
1218
  // Save the report as in the Scenario folder
1215
1219
  fs.writeFileSync( scope.paths.scenario_report, report );
1220
+
1221
+ if ( changeable_test_status === `FAILED` ) {
1222
+ // If the result object status wasn't failure and we still want
1223
+ // the tests to fail, as with a11y tests, we can't mess with the
1224
+ // cucumber result.status object because it will cause unwanted
1225
+ // errors. We have to throw an error here instead.
1226
+ if ( scenario.result.status !== `FAILED` ) {
1227
+ throw new Error(`The test failed. See the report for more information.`);
1228
+ }
1229
+ } else {
1230
+ // Other types of errors should be ok
1231
+ scenario.result.status = changeable_test_status;
1232
+ }
1216
1233
  });
1217
1234
 
1218
1235
  AfterAll(async function() {
1219
1236
  // Stop collecting server response statuses
1220
1237
  scope.stop_tracking_server_status = true;
1221
1238
 
1222
- let report = await scope.getPrintableReport( scope );
1239
+ let report = reports.getPrintableReport( scope );
1223
1240
  // Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
1224
1241
  // May need newest version of cucumberjs
1225
1242
  console.log( `\n\n${ report }` );
@@ -0,0 +1,147 @@
1
+ const fs = require(`fs`);
2
+
3
+ // Ours
4
+ const session_vars = require(`./session_vars`);
5
+ const files = require(`./files` );
6
+
7
+
8
+ module.exports = reports = {};
9
+
10
+ reports.addReportHeading = function (scope, { scenario }) {
11
+ /** Return printable heading based on Scenario name and tags.
12
+ * Allow characters from other languages.
13
+ */
14
+ if ( session_vars.get_debug() ) { console.log(`reports.addReportHeading(): ${scenario.pickle.name} \n`); }
15
+
16
+ // Collect all tag names
17
+ let tag_names = [];
18
+ for ( let tag of scenario.pickle.tags ) {
19
+ tag_names.push( tag.name );
20
+ }
21
+
22
+ let heading = `\n---------------\n`
23
+ + `Scenario: ${ scenario.pickle.name }`;
24
+
25
+ if ( tag_names.length > 0 ) {
26
+ heading += `\nTags: ${ tag_names.join(' ') }`;
27
+ }
28
+
29
+ heading += `\n---------------`;
30
+
31
+ reports.addToReport( scope, {
32
+ type: `heading`,
33
+ value: heading,
34
+ });
35
+ }; // Ends reports.addReportHeading()
36
+
37
+ reports.addToReport = function( scope, { type, value }) {
38
+ /** Add an item to a specific list in a scenario's report.
39
+ * Create report if necessary.
40
+ *
41
+ * The report has a key for each scenario and each of those
42
+ * keys should correspond to a list containing the relevant data. */
43
+ if ( session_vars.get_debug() ) { console.log(`reports.addToReport():\ntype: ${ type }, value: ${ value }\n`) }
44
+ let scenario = makeSureReportPartsExist( scope );
45
+ scenario.lines.push({ type, value });
46
+
47
+ const MISC_ARTIFACTS_DIR = `_alkiln-misc_artifacts`;
48
+ // Ensure filepath for saving local unit test logs
49
+ if ( !scope.paths ) {
50
+ // TODO: would it be useful to put in existing artifacts folder if possible.
51
+ // Can this be usefully abstracted?
52
+ scope.paths = { debug_log: `${ MISC_ARTIFACTS_DIR }/unit_tests_logs-${ files.readable_date() }.txt` };
53
+ }
54
+
55
+ try {
56
+ fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
57
+ } catch (err) {
58
+ fs.mkdirSync( MISC_ARTIFACTS_DIR );
59
+ fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
60
+ }
61
+
62
+ return scenario;
63
+ }; // Ends reports.addToReport()
64
+
65
+ let makeSureReportPartsExist = function( scope ) {
66
+ /** If a part of the report needs to exist right now, but doesn't exist,
67
+ * create it. */
68
+ if ( !scope.report ) { scope.report = new Map(); }
69
+ if ( !scope.scenario_id ) {
70
+ scope.scenario_id = `_report_${ Date.now() }`;
71
+ }
72
+ if ( !scope.report.get( scope.scenario_id )) {
73
+ scope.report.set( scope.scenario_id, {} )
74
+ scope.report.get( scope.scenario_id ).status = `in progress`;
75
+ }
76
+
77
+ let scenario = scope.report.get( scope.scenario_id );
78
+ if ( !scenario.lines ) { scenario.lines = []; }
79
+
80
+ return scenario;
81
+ }; // Ends makeSureReportPartsExist()
82
+
83
+ reports.setReportScenarioStatus = function( scope, { status=`PASSED` }) {
84
+ /** Add final status of scenario to scenario report object - passed, failed, etc. */
85
+ scope.report.get( scope.scenario_id ).status = status;
86
+ }; // Ends reports.setReportScenarioStatus()
87
+
88
+ reports.convertToOriginalStoryTableRow = function( scope, { row_data }) {
89
+ /** Returns the original data of row into a string formatted as a story table row.
90
+ * Should this show the actual row data that was used instead? That could
91
+ * be pretty confusing to most devs. */
92
+ let source_row = row_data.source || row_data;
93
+ let original = source_row.original;
94
+ let var_str = trimJSON( original.var );
95
+ let value_str = trimJSON( original.value );
96
+ let trigger_str = trimJSON( original.trigger );
97
+ if ( trigger_str === scope.trigger_not_needed_flag ) {
98
+ // This text was added by ALKiln itself. It's not part of the user's data
99
+ trigger_str = ``;
100
+ }
101
+ return `${ ' '.repeat(6) }| ${ var_str } | ${ value_str } | ${ trigger_str } |`;
102
+ }; // Ends reports.convertToOriginalStoryTableRows()
103
+
104
+ let trimJSON = function( str=`` ) {
105
+ /** Turn into JSON string then trim quotes from the start and end. */
106
+ let json = JSON.stringify( str );
107
+ let trimmed = json.replace( /^"/, `` ).replace( /"$/, `` ).replace( /\\\\/g, `\\`);
108
+ return trimmed;
109
+ }; // Ends trimJSON()
110
+
111
+ reports.getPrintableReport = function( scope ) {
112
+ /** Return a string generated from test report data. */
113
+ let report = `Assembly Line Kiln Automated Testing Report - ${ (new Date()).toUTCString() }\n`;
114
+
115
+ // First list failed scenarios, then non-passed scenarios, then passed scenarios
116
+ let failed_reports = ``;
117
+ let passed_reports = ``;
118
+ let other_reports = ``;
119
+ for ( let scenario of scope.report ) {
120
+
121
+ let report = reports.getPrintableScenario( scenario );
122
+ let [ scenario_id, data ] = scenario;
123
+ if ( data.status === `FAILED` ) { failed_reports += report; }
124
+ else if ( data.status === `PASSED` ) { passed_reports += report; }
125
+ else { other_reports += report; }
126
+ }; // ends for every scenario
127
+
128
+ if ( failed_reports !== `` ) { report += `\n\n===============================\n===============================\nFailed scenarios:\n${ failed_reports }`; }
129
+ if ( other_reports !== `` ) { report += `\n\n===============================\n===============================\nScenarios that did not pass:\n${ other_reports }`; }
130
+ if ( passed_reports !== `` ) { report += `\n\n===============================\n===============================\nPassed scenarios:\n${ passed_reports }`; }
131
+
132
+ return report;
133
+ }; // Ends reports.getPrintableReport()
134
+
135
+ reports.getPrintableScenario = function( scenario ) {
136
+ /** Return a string generated from scenario list of report items. */
137
+ let [ scenario_id, data ] = scenario;
138
+ let report = ``;
139
+ for ( let line of data.lines ) {
140
+ if ( line.type === `page_id` ) { report += `screen id: `; }
141
+ else if ( line.type === `outcome` ) { report += `\n`; }
142
+ else if ( line.type === `warning` ) { report += `WARNING: `; }
143
+ else if ( line.type === `error` ) { report += `\nERROR: ` }
144
+ report += `${ line.value }\n`;
145
+ }
146
+ return report;
147
+ }; // Ends reports.getPrintableScenario()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {