@suffolklitlab/alkiln 5.12.0-delete6 → 5.13.0-logs1

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.
Files changed (35) hide show
  1. package/.env.example +29 -0
  2. package/.github/workflows/github_server.yml +11 -9
  3. package/.github/workflows/log_code_tests.yml +0 -2
  4. package/.github/workflows/playground.yml +7 -5
  5. package/CHANGELOG.md +30 -5
  6. package/CONTRIBUTING.md +98 -83
  7. package/action.yml +66 -28
  8. package/action_for_github_server/action.yml +59 -31
  9. package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +46 -0
  10. package/docs/decisions/adopt_architectural_decision_records.md +58 -0
  11. package/docs/decisions/detect_sign_in.md +124 -0
  12. package/docs/decisions/pdfjs_version_2024_08_08.md +18 -0
  13. package/docs/decisions/reports_and_logs_2024_08_11.md +141 -0
  14. package/docs/decisions/update_puppeteer_2024_07_28.md +62 -0
  15. package/lib/docassemble/docassemble_api_interface.js +63 -34
  16. package/lib/docassemble/server_install.js +23 -9
  17. package/lib/docassemble/setup.js +33 -20
  18. package/lib/docassemble/takedown.js +27 -7
  19. package/lib/run_cucumber.js +81 -33
  20. package/lib/scope.js +152 -79
  21. package/lib/steps.js +84 -51
  22. package/lib/utils/files.js +8 -6
  23. package/lib/utils/log.js +477 -84
  24. package/lib/utils/reports.js +14 -5
  25. package/lib/utils/session_vars.js +25 -18
  26. package/lib/utils/time.js +44 -0
  27. package/npm-shrinkwrap.json +635 -193
  28. package/package.json +6 -6
  29. package/tests/log_codes/check_codes.sh +163 -108
  30. package/tests/log_codes/log_code_expected_instances.txt +213 -0
  31. package/tests/unit_tests/getAllFields.test.js +5 -0
  32. package/tests/unit_tests/getMatchingRows.test.js +5 -0
  33. package/tests/unit_tests/log.test.js +20 -0
  34. package/tests/unit_tests/normalizeTable.test.js +5 -0
  35. package/tests/log_codes/deleted_codes.json +0 -8
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const path = require(`path`);
4
5
  const cucumber_api = require('@cucumber/cucumber/api');
5
6
 
6
- const log = require('./utils/log'); // These always log in the console
7
+ const Log = require('./utils/log');
7
8
  const session_vars = require('./utils/session_vars');
8
9
 
9
10
  const SUMMARY_FILENAME = `cucumber-report.txt`;
@@ -53,11 +54,14 @@ function build_config( command_args ) {
53
54
  }; // Ends build_config()
54
55
 
55
56
 
56
- (async function main() {
57
+ async function main() {
57
58
  /** Create the cucumber config, run the tests, create and
58
59
  * manipluate the output. */
60
+
59
61
  const environment = { cwd: process.cwd() };
60
- log.info({ type: `run`, code: `ALK0049`, pre: `environment`, data: environment });
62
+ log.info({ code: `ALK0049`, context: `pre_run environment` },
63
+ environment
64
+ );
61
65
 
62
66
  // We'll use the user info to construct the possible paths to /sources
63
67
  let id = session_vars.get_user_id();
@@ -65,8 +69,9 @@ function build_config( command_args ) {
65
69
  let project = session_vars.get_user_project_name();
66
70
 
67
71
  // process.argv is a list of strings of commands and args.
68
- // If using `npm run cucumber`, use `--` before `--sources=./foo`
69
- let argv = require(`minimist`)( process.argv.slice(2), {
72
+ // If using `npm run cucumber` (as opposed to the bin commands),
73
+ // use `--` before `--sources=./foo`
74
+ const argv = require(`minimist`)( process.argv.slice(2), {
70
75
  default: {
71
76
  sources: [
72
77
  `./docassemble/*/data/sources`, // In GitHub folder
@@ -85,35 +90,45 @@ function build_config( command_args ) {
85
90
  let one_dir_exists = session_vars.set_sources_paths( sources_list );
86
91
  if ( !one_dir_exists ) {
87
92
  // If we don't throw, it'll be more confusing when no tests run
88
- let msg = `ALKiln can't find your "sources" folder(s). See above for details.`;
89
- let code = `ALK0050`
90
- log.error({ type: `run`, code: code, pre: msg });
91
- throw new ReferenceError(`🤕 ${ code } run ERROR: ${ msg }`);
93
+ log.throw({ code: `ALK0050`, context: `pre_run`,
94
+ error: new ReferenceError(
95
+ `ALKiln can't find your "sources" folder(s). See above for details.`
96
+ ),
97
+ });
92
98
  }
93
99
 
94
100
  const { runConfiguration } = await cucumber_api.loadConfiguration({
95
101
  provided: build_config( argv ),
96
102
  }, environment);
97
-
98
- log.info({ type: `run`, code: `ALK0051`, pre: `runConfiguration`, data: runConfiguration });
99
-
100
- var path = require(`path`);
101
- var codeDir = path.dirname(require.main.filename);
102
- log.info({ type: `run`, code: `ALK0052`, pre: `working directory`, data: codeDir });
103
+ log.info({ code: `ALK0051`, context: `pre_run config`, },
104
+ runConfiguration
105
+ );
106
+
107
+ const codeDir = path.dirname(require.main.filename);
108
+ log.info({ code: `ALK0052`, context: `pre_run directory`, },
109
+ codeDir, `\n\n`
110
+ );
103
111
  const support = await cucumber_api.loadSupport({ support: {
104
112
  requireModules: [ `${ codeDir }/index.js` ],
105
113
  requirePaths: [ `${ codeDir }/index.js` ],
106
114
  importPaths: [],
107
115
  }, sources: {paths: []}}, environment);
108
116
 
109
- log.info({ type: `run`, code: `ALK0053`, pre: `working directory`, data: codeDir });
110
- log.info({ type: `plain`, pre: `\n\n` });
111
-
117
+ // ====================
118
+ // Run
119
+ // ====================
112
120
  const { success } = await cucumber_api.runCucumber({...runConfiguration, support});
121
+ // ====================
122
+
113
123
  if ( success ) {
114
- log.success({ type: `run`, code: `ALK0054`, pre: `Tests succeeded!` });
124
+ log.success({ code: `ALK0054`, context: `post_run`, },
125
+ `Tests succeeded!`
126
+ );
115
127
  } else {
116
- log.error({ type: `run`, code: `ALK0055`, pre: `The test run ran into something unexpected. Look above for more details.` });
128
+ // Don't want to throw error here, so don't pass error object
129
+ log.warn({ code: `ALK0055`, context: `post_run`, },
130
+ `The tests ran into something unexpected. Look above for more details.`
131
+ );
117
132
  }
118
133
  // Custom exit code. May overlap with other systems.
119
134
  // Best info I've found via stack overflow: https://tldp.org/LDP/abs/html/exitcodes.html
@@ -123,31 +138,64 @@ function build_config( command_args ) {
123
138
  // This is hardcoded information from `provide_config` above.
124
139
  // This code isn't used for anything other than detecting and removing color codes.
125
140
  let summary_file_name = SUMMARY_FILENAME;
126
- fs.readFile(summary_file_name, 'utf8', (err, text) => {
127
- if (err) {
141
+ fs.readFile(summary_file_name, 'utf8', (read_error, text) => {
142
+ if ( read_error ) {
128
143
  // Something else will probably show an error that's more relevant
129
- log.info({ type: `run`, code: `ALK0056`, pre: `Failed to read cucumber report`, data: err });
144
+ log.warn({ code: `ALK0056`, context: `post_run error`, },
145
+ `Failed to read cucumber report`, read_error
146
+ );
130
147
  return;
131
148
  }
132
149
 
133
150
  // Replace terminal color codes, if present
134
151
  // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#colors--graphics-mode
135
152
  text = text.replace(/\x1b\[[0-9][0-9]?[0-9]?m/g, '');
136
-
137
- // Put it in the correct file in the correct folder
138
- let artifacts_path = session_vars.get_artifacts_path_name();
153
+ // Add to the debug file
139
154
  try {
140
- fs.appendFileSync( `${ artifacts_path }/${ log.debug_log_file }`, `\n\n${text}`);
155
+ // fs.appendFileSync( `${ artifacts_path }/${ log.debug_log_file }`, `\n\n${text}`);
156
+ log.debug({ code: `ALK0216`, context: `post_run`, },
157
+ `\n\n`, text
158
+ );
141
159
  } catch ( error ) {
142
- log.info({ type: `run`, code: `ALK0182`, pre: `Something probably went wrong earlier. Look above for more details, but here is more information just in case: ALKiln was unable to write to ${ artifacts_path }/${ log.debug_log_file }.`, data: error });
160
+ log.warn({ code: `ALK0182`, context: `post_run error`, },
161
+ `Something probably went wrong earlier. Look above for more details. Problem when writing to ${ log.path }/${ log.debug_log_filename }.`,
162
+ error
163
+ );
143
164
  }
165
+
166
+ // Add to the non-passed test output file
144
167
  try {
145
- // Add to a unexpected results file when tests themselves fail
168
+ // Save only non-passing test output to cut out cruft.
169
+ // cucumber only outputs non-success details anyway.
170
+ // Our reports only non-passing tests to this file.
146
171
  if ( exitCode === 50 ) {
147
- fs.appendFileSync( `${ artifacts_path }/${ log.unexpected_filename }`, `\n\n${ text }` );
172
+ // fs.appendFileSync( `${ log.path }/${ Log.unexpected_filename }`, `\n\n${ text }` );
173
+ log.unexpected({ text });
148
174
  }
149
- } catch ( error ) {
150
- log.info({ type: `run`, code: `ALK0181`, pre: `Something probably went wrong earlier. Look above for more details, but here is more information just in case: ALKiln was unable to write to ${ artifacts_path }/${ log.unexpected_filename }.`, data: error });
175
+ } catch ( unexpected_file_error ) {
176
+ console.warn(
177
+ `🔎 ALK0181 internal WARNING: Something probably went wrong earlier. Problem when writing to ${ log.path }/${ log.unexpected_filename }`,
178
+ unexpected_file_error
179
+ );
151
180
  }
181
+
182
+ return;
152
183
  });
153
- })();
184
+ }
185
+
186
+
187
+ // 1. session_vars - make sure all the required env vars exist
188
+ session_vars.validateEnvironment();
189
+
190
+ // 2. Prepare logs
191
+ // process.argv is a list of strings of commands and args.
192
+ // If using `npm run cucumber` (as opposed to the bin commands),
193
+ // use `--` before `--sources=./foo`
194
+ const argv = require(`minimist`)( process.argv.slice(2) );
195
+ // The artifacts folder might be getting created for the first
196
+ // time right here. The tests themselves should avoid creating
197
+ // their own file.
198
+ const log = new Log({ path: argv.path, context: `run` });
199
+
200
+ // 3. run
201
+ main();
package/lib/scope.js CHANGED
@@ -13,14 +13,13 @@ const pdf_reader = require('./utils/pdf_reader');
13
13
  const session_vars = require('./utils/session_vars');
14
14
  const get_base_interview_url = require(`./docassemble/get_base_interview_url`);
15
15
  const time = require('./utils/time');
16
- const log = require('./utils/log');
16
+ // const log = require('./utils/log');
17
17
  const { waitForTimeout } = require('./utils/time');
18
18
  const files = require('./utils/files' );
19
19
  const da_api = require('./docassemble/docassemble_api_interface');
20
20
  const wrapPromiseWithTimeout = require('./cucumber_8_shim');
21
21
  const reports = require(`./utils/reports`);
22
22
 
23
-
24
23
  let ordinal_to_integer = {
25
24
  only: 0, first: 0, second: 1, third: 2, fourth: 3, fifth: 4,
26
25
  sixth: 5, seventh: 6, eighth: 7, ninth: 8, tenth: 9,
@@ -84,10 +83,16 @@ let toBase64 = function ( utf8_str, exclude_padding=true) {
84
83
 
85
84
  let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
86
85
  let da_base64_regex = /^[A-Za-z0-9+/]+$/;
86
+ let log = null;
87
87
 
88
88
  module.exports = {
89
89
  trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
90
90
 
91
+
92
+ set_log: function ({ log_obj }) {
93
+ log = log_obj;
94
+ },
95
+
91
96
  getSafeScenarioBaseFilename: async function( scope, { scenario }) {
92
97
  /** Return a string that's safe to be a filename. Include the Scenario
93
98
  * description and tags.
@@ -171,7 +176,7 @@ module.exports = {
171
176
  // If something might be hidden or shown, wait extra time to let it hide or show
172
177
  // I think `.$()` does not use a timeout
173
178
  let showif = await scope.page.$('.dashowif');
174
- if ( showif ) { await scope.page.waitForTimeout( scope.showif_timeout ); }
179
+ if ( showif ) { await time.waitForTimeout( scope.showif_timeout ); }
175
180
  },
176
181
 
177
182
  waitForTab: async function waitForTab(scope, { tab_id }) {
@@ -288,9 +293,7 @@ module.exports = {
288
293
  /** Tap a given element, detect navigation, and do appropriate related things.
289
294
  * `click_promise can be any kind of Promise, including a Promise.all()
290
295
  */
291
- let msg = `Cannot find the element to tap.`;
292
- if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0102`, value: msg }); }
293
- expect( elem, msg ).to.exist;
296
+ await scope.guard_against_missing_tap_element(scope, { elem });
294
297
 
295
298
  // Get the text on the button or link for a potential error message later
296
299
  let tapperText = await (await elem.getProperty('textContent')).jsonValue();
@@ -326,6 +329,14 @@ module.exports = {
326
329
  } // ends try/catch
327
330
  }, // Ends scope.tapElementAndStayOnPage()
328
331
 
332
+ guard_against_missing_tap_element: async function(scope, { elem }) {
333
+ /** Handle the error message for a missing tap element, if it's missing. */
334
+ let msg = `Cannot find the element to tap.`;
335
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0102`, value: msg }); }
336
+ expect( elem, msg ).to.exist;
337
+ return elem;
338
+ },
339
+
329
340
  // Handle server reload errors
330
341
  server_statuses: [],
331
342
  server_check_interval_ms: 1 * 1000,
@@ -501,7 +512,7 @@ module.exports = {
501
512
  if ( diff <= 0 ) { return false; }
502
513
 
503
514
  // If time hasn't expired, wait and then check again
504
- await scope.page.waitForTimeout( 100 );
515
+ await time.waitForTimeout( 100 );
505
516
  await scope.detectDownloadComplete(scope, endTime);
506
517
 
507
518
  return false;
@@ -686,7 +697,7 @@ module.exports = {
686
697
  });
687
698
  expect( load_result.error ).to.equal( '' );
688
699
  } else {
689
- reports.addToReport(scope, { type: `row info`, code: `ALK0117`, value: `Successfully found the interview at "${ interview_url }"` });
700
+ reports.addToReport(scope, { type: `row info`, code: `ALK0117`, value: `Successfully found the interview` });
690
701
  }
691
702
 
692
703
  return scope.page;
@@ -705,19 +716,19 @@ module.exports = {
705
716
  if ( !scenario.interviews ) { scenario.interviews = {}; }
706
717
  if ( !scenario.interviews[ vars.uid ] ) {
707
718
  scenario.interviews[ vars.uid ] = vars;
708
- log.debug({
709
- code: `ALK0185`, type: `info`,
710
- pre: `Saved new interview data. File path: ${ vars.i }. Session id: ${ vars.uid }.`
711
- });
719
+ log.debug({ code: `ALK0185`, context: `session_id_etc` },
720
+ `Saved new interview data. File path: ${ vars.i }. Session id: ${ vars.uid }.`
721
+ );
712
722
  } else {
713
- log.debug({
714
- code: `ALK0186`, type: `info`,
715
- pre: `Interview data already existed. File path: ${ vars.i }. Session id: ${ vars.uid }.`
716
- });
723
+ log.debug({ code: `ALK0186`, context: `session_id_etc` },
724
+ `Interview data already existed. File path: ${ vars.i }. Session id: ${ vars.uid }.`
725
+ );
717
726
  }
718
727
  return vars;
719
728
  } else {
720
- log.debug({ code: `ALK0187`, type: `info`, pre: `Found no interview data.` });
729
+ log.debug({ code: `ALK0187`, context: `session_id_etc` },
730
+ `Found no interview data.`
731
+ );
721
732
  return null;
722
733
  }
723
734
  }, // Ends scope.set_interview_session_data()
@@ -864,11 +875,9 @@ module.exports = {
864
875
  supported_table.push( result );
865
876
  } // ends for every row
866
877
 
867
- log.debug({
868
- code: `ALK0122`,
869
- type: `info`,
870
- pre: `scope.normalizedTable():\n ${JSON.stringify( supported_table )}`
871
- });
878
+ log.debug({ code: `ALK0122`, context: `table`, },
879
+ `scope.normalizedTable():`, supported_table
880
+ );
872
881
 
873
882
  return supported_table;
874
883
  }, // Ends scope.normalizeTable()
@@ -1018,11 +1027,9 @@ module.exports = {
1018
1027
  fields.push( field_data );
1019
1028
  } // ends for every node
1020
1029
 
1021
- log.debug({
1022
- code: `ALK0124`,
1023
- type: `info`,
1024
- pre: `page fields:\n${ JSON.stringify( fields )}`
1025
- });
1030
+ log.debug({ code: `ALK0124`, context: `page fields`, },
1031
+ fields
1032
+ );
1026
1033
  return fields;
1027
1034
  }, // Ends scope.getAllFields()
1028
1035
 
@@ -1072,11 +1079,9 @@ module.exports = {
1072
1079
  /* Return variable names and values combined in ways that can
1073
1080
  * try to match table rows. Tables can have two properties:
1074
1081
  * variable name and value. */
1075
- log.debug({
1076
- code: `ALK0126`,
1077
- type: `info`,
1078
- pre: `scope.getPossibleTableRowMatchers() var_names: ${JSON.stringify(var_names)} values: ${JSON.stringify(values)}`
1079
- });
1082
+ log.debug({ code: `ALK0126`, context: `matching`, },
1083
+ `scope.getPossibleTableRowMatchers() var_names:`, var_names, `values:`, values
1084
+ );
1080
1085
  let possible_table_rows = [];
1081
1086
  for ( let var_name of var_names ) {
1082
1087
  // Inputs like textareas don't have values
@@ -1551,17 +1556,13 @@ module.exports = {
1551
1556
  reports.addToReport(scope, { type: `warning`, code: `ALK0130`, value: multiple_matches_msg });
1552
1557
  }
1553
1558
 
1554
- log.debug({
1555
- code: `ALK0131`,
1556
- type: `info`,
1557
- pre: `final_matching_rows:\n${JSON.stringify( final_matching_rows )}`
1558
- });
1559
+ log.debug({ code: `ALK0131`, context: `matching`, },
1560
+ `final_matching_rows:`, final_matching_rows
1561
+ );
1559
1562
  if ( final_matching_rows.length == 0) {
1560
- log.debug({
1561
- code: `ALK0132`,
1562
- type: `info`,
1563
- pre: `field:\n${JSON.stringify( field )}`
1564
- });
1563
+ log.debug({ code: `ALK0132`, context: `matching`, },
1564
+ `0 matching rows. Field:`, field
1565
+ );
1565
1566
  }
1566
1567
 
1567
1568
  return final_matching_rows;
@@ -1627,7 +1628,7 @@ module.exports = {
1627
1628
  catch(err){ elem.style.background = 'Khaki'; }
1628
1629
  return elem.getAttribute('for') || elem.getAttribute('name');
1629
1630
  });
1630
- await scope.page.waitForTimeout( 400 );
1631
+ await time.waitForTimeout( 400 );
1631
1632
  }
1632
1633
 
1633
1634
  // We use the last match. We wrote a warning for the user if there were
@@ -1647,11 +1648,9 @@ module.exports = {
1647
1648
 
1648
1649
  get_handle_from_field: async function ( scope, { field }) {
1649
1650
  /** Get the element's puppeteer handle using the field's selector. */
1650
- log.debug({
1651
- code: `ALK0133`,
1652
- type: `info`,
1653
- pre: `scope.get_handle_from_field() field.selector: ${JSON.stringify(field.selector)}`
1654
- });
1651
+ log.debug({ code: `ALK0133`, },
1652
+ `scope.get_handle_from_field() field.selector:`, field.selector
1653
+ );
1655
1654
  let handle = await scope.page.evaluateHandle(( selector ) => {
1656
1655
  let elem = document.querySelector( selector );
1657
1656
  // If the element has a sibling that's a label, we need to
@@ -1850,7 +1849,7 @@ module.exports = {
1850
1849
  });
1851
1850
  if ( type === `radio` || type === `checkbox` ) {
1852
1851
 
1853
- let [label] = await handle.$x(`following-sibling::label`);
1852
+ let [label] = await handle.$$(`xpath/.//following-sibling::label`);
1854
1853
 
1855
1854
  if ( session_vars.get_debug() ) {
1856
1855
  if ( answer.toLowerCase() === 'false' ) { await label.evaluate( elem => { elem.style.background = 'tomato'; }); }
@@ -2163,7 +2162,9 @@ module.exports = {
2163
2162
  * @param ensure_all_vars_used {bool} [false] = Default false. If true, errors if any
2164
2163
  * object in var_data is not used during running of this function.
2165
2164
  */
2166
- log.debug({ type: `plain`, pre: `~~~~~~~~~~~~~~ scope.setFields() ~~~~~~~~~~~~~~` });
2165
+ log.debug({ code: `ALK0221`, before: `~~~~~~~~~~~~~~\n`, },
2166
+ `scope.setFields()`
2167
+ );
2167
2168
 
2168
2169
  // Only record the page id once for each page to which we navigate
2169
2170
  let { id } = await scope.examinePageID( scope, 'none to match' );
@@ -2207,13 +2208,12 @@ module.exports = {
2207
2208
  // Accumulate number of times this row was used. Esp important for `.target_number`
2208
2209
  await scope.increment_row_use( scope, { row: row_used });
2209
2210
 
2210
- process.stdout.write(`\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
2211
-
2211
+ log.stdout({}, `\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
2212
2212
  }
2213
2213
  } // ends for every non-submit-button field on the page
2214
2214
  } // ends while fields are still being set
2215
2215
 
2216
- if ( session_vars.get_debug() ) { await scope.page.waitForTimeout(2000); }
2216
+ if ( session_vars.get_debug() ) { await time.waitForTimeout(2000); }
2217
2217
 
2218
2218
  // Press a continue button with a story table variable name if it exists
2219
2219
  let did_navigate = false;
@@ -2253,7 +2253,7 @@ module.exports = {
2253
2253
  // }
2254
2254
 
2255
2255
  did_navigate = true;
2256
- process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
2256
+ log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
2257
2257
  break;
2258
2258
 
2259
2259
  } // ends if row_used
@@ -2300,7 +2300,7 @@ module.exports = {
2300
2300
  // }
2301
2301
 
2302
2302
  did_navigate = true;
2303
- process.stdout.write(`\x1b[36m${ '>' }\x1b[0m`); // continued successfully
2303
+ log.stdout({}, `\x1b[36m${ '>' }\x1b[0m`); // continued successfully
2304
2304
  } // ends if !did_navigate && ensure_navigation
2305
2305
 
2306
2306
  if ( did_navigate && scope.check_all_for_a11y ) {
@@ -2561,7 +2561,7 @@ module.exports = {
2561
2561
 
2562
2562
  // Attempt to solve weird race condition issues where page has not properly continued
2563
2563
  while ( curr_url === prev_url && !error.was_found && !timedOut ) {
2564
- await scope.page.waitForTimeout( 50 );
2564
+ await time.waitForTimeout( 50 );
2565
2565
  curr_url = await scope.page.url();
2566
2566
  error = await scope.checkForError( scope );
2567
2567
  }
@@ -2582,10 +2582,10 @@ module.exports = {
2582
2582
 
2583
2583
  // All results are lists to match `$x`
2584
2584
  let possible_errors = {
2585
- complex_user_error: await scope.page.$$('.alert-danger'),
2585
+ complex_user_error: await scope.page.$$(`.alert-danger`),
2586
2586
  // The below does not work if there's not a space between ':' and 'none'
2587
- simple_user_error: await scope.page.$x(`//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`),
2588
- system_error: await scope.page.$x( '//meta[@content="docassemble: Error"]' ),
2587
+ simple_user_error: await scope.page.$$(`xpath/.//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`),
2588
+ system_error: await scope.page.$$(`xpath/.//meta[@content="docassemble: Error"]` ),
2589
2589
  };
2590
2590
 
2591
2591
  let error_handlers = {};
@@ -2597,7 +2597,7 @@ module.exports = {
2597
2597
  // Also, if you're debugging and you close the window early, the process will still
2598
2598
  // take this long to exit after the tests finish. Not sure how to account for that.
2599
2599
  if ( session_vars.get_debug() ) {
2600
- await scope.page.waitForTimeout( ( scope.timeout - (1 * 1000)) );
2600
+ await time.waitForTimeout( ( scope.timeout - (1 * 1000)) );
2601
2601
  }
2602
2602
  was_found = true;
2603
2603
  error_handlers[ error_key ] = possible_errors[ error_key ][0];
@@ -2702,7 +2702,7 @@ module.exports = {
2702
2702
  }, // Ends scope.steps.add_json_vars_to_report()
2703
2703
 
2704
2704
  link_works: async ( scope, linkText ) => {
2705
- let [link] = await scope.page.$x(`//a[contains(text(), "${ linkText }")]`);
2705
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${ linkText }")]`);
2706
2706
  let prop_obj = await link.getProperty( 'href' );
2707
2707
  let actual_url = await prop_obj.jsonValue();
2708
2708
 
@@ -2760,13 +2760,13 @@ module.exports = {
2760
2760
  /** Tap a link that doesn't navigate. Depends on the language of the text.
2761
2761
  * TODO: This may be broken at this point.
2762
2762
  * */
2763
- const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
2763
+ const [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${phrase}")]`);
2764
2764
 
2765
2765
  let msg = `The term "${ phrase }" seems to be missing.`;
2766
2766
  if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0151`, value: msg }); }
2767
2767
  expect( link, msg ).to.exist;
2768
2768
  await scope.tapElementAndStayOnPage(scope, { elem: link });
2769
- await scope.page.waitForTimeout(10000);
2769
+ await time.waitForTimeout(10000);
2770
2770
  }, // Ends scope.steps.tap_term()
2771
2771
 
2772
2772
  sign: async ( scope, name ) => {
@@ -2785,7 +2785,7 @@ module.exports = {
2785
2785
  * TODO: Properly wait for download to complete. See notes in
2786
2786
  * scope.js scope.detectDownloadComplete()
2787
2787
  */
2788
- let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
2788
+ let [elem] = await scope.page.$$(`xpath/.//a[contains(@href, "${ filename }")]`);
2789
2789
 
2790
2790
  let msg = `"${ filename }" seems to be missing. Cannot find a link to that document.`;
2791
2791
  if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0152`, value: msg }); }
@@ -3017,20 +3017,94 @@ module.exports = {
3017
3017
  expect( email, email_msg ).to.not.equal( undefined );
3018
3018
  expect( password, password_msg ).to.not.equal( undefined );
3019
3019
 
3020
- // Try to log in
3020
+ // Try to enter the information
3021
3021
  await scope.page.type( `#email`, email );
3022
3022
  await scope.page.type( `#password`, password );
3023
- let elem = await scope.page.$( `button[name="submit"]` );
3024
- await scope.tapElementAndNavigate( scope, { elem });
3023
+ let elem = await scope.page.$( `button[type="submit"]` );
3024
+ await scope.guard_against_missing_tap_element(scope, { elem });
3025
3025
 
3026
- reports.addToReport( scope, {
3027
- type: `row info`,
3028
- code: `ALK0163`,
3029
- value: `Signed into ${ session_vars.get_da_server_url() }/user/sign-in successfully.`
3026
+ // Submit and see what happens
3027
+ let winner = await scope.steps.race_sign_in_navigation( scope, { elem });
3028
+ // Add the result to the report and possibly throw errors
3029
+ if ( winner[0] === `success` ) {
3030
+ reports.addToReport( scope, {
3031
+ type: `row info`, code: `ALK0163`,
3032
+ value: `Successfully signed into ${ login_url }`
3033
+ });
3034
+ await scope.afterStep(scope, {waitForShowIf: false});
3035
+ } else if ( winner[0] === `failure` ) {
3036
+ let error_msg = reports.addToReport( scope, {
3037
+ type: `error`, code: `ALK0209`,
3038
+ value: `Failed to sign into ${ login_url }. Make sure you followed the instructions at https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#sign-in.`
3039
+ });
3040
+ throw new Error( error_msg );
3041
+ } else if ( winner[0] === `error` ) {
3042
+ let error_msg = reports.addToReport( scope, {
3043
+ type: `error`, code: `ALK0210`,
3044
+ value: `Error on sign in page at ${ login_url }. Try this by hand and then run the test again.`
3045
+ });
3046
+ throw new Error( error_msg );
3047
+ }
3048
+ }, // Ends scope.steps.sign_in()
3049
+
3050
+ race_sign_in_navigation: async function ( scope, { elem }) {
3051
+ /** Wait for sign in navigation success or failure, or system error. */
3052
+
3053
+ // After everything, clean up incomplete promises
3054
+ const controller = new AbortController;
3055
+
3056
+ // Redirect
3057
+ let redirect_promise = scope.page.waitForResponse(function ( response ) {
3058
+ return response.status() === 302;
3059
+ }, { signal: controller.signal })
3060
+ .catch(( catch_info ) => {
3061
+ log.debug({ code: `ALK0204`, context: `nav`, },
3062
+ `302 sign-in wait catch error:`, catch_info.name, catch_info,
3063
+ );
3030
3064
  });
3031
3065
 
3032
- await scope.afterStep(scope, {waitForShowIf: false});
3033
- }, // Ends scope.steps.sign_in()
3066
+ // Invalid credentials, no navigation, no sign in
3067
+ let wrong_sign_in_promise = scope.page.waitForResponse(function ( response ) {
3068
+ return response.status() >= 200 && response.status() < 300 && response.url().includes(`/sign-in`);
3069
+ }, { signal: controller.signal })
3070
+ .catch(( catch_info ) => {
3071
+ log.debug({ code: `ALK0205`, context: `nav`, },
3072
+ `200s sign-in wait catch error:`, catch_info.name, catch_info,
3073
+ );
3074
+ });
3075
+
3076
+ // System error
3077
+ let error_promise = scope.page.waitForResponse(function ( response ) {
3078
+ return response.status() >= 500 && response.status() < 600;
3079
+ }, { signal: controller.signal })
3080
+ .catch(( catch_info ) => {
3081
+ log.debug({ code: `ALK0206`, context: `nav`, },
3082
+ `500s sign-in wait catch error:`, catch_info.name, catch_info,
3083
+ );
3084
+ });
3085
+
3086
+ let click_promise = elem.click(); // MUST complete
3087
+ const winner = await Promise.race([
3088
+ Promise.all([ `success`, click_promise, redirect_promise ]),
3089
+ Promise.all([ `failure`, click_promise, wrong_sign_in_promise ]),
3090
+ Promise.all([ `error`, click_promise, error_promise ]),
3091
+ ]).catch(function ( error ) {
3092
+ let error_msg = reports.addToReport( scope, {
3093
+ type: `error`, code: `ALK0207`,
3094
+ value: `Unknown error waiting for results during sign in at ${ login_url }.`
3095
+ });
3096
+ throw new Error( error );
3097
+ });
3098
+
3099
+ // Clean up unresolved promises
3100
+ controller.abort();
3101
+
3102
+ log.debug({ code: `ALK0208`, context: `nav` },
3103
+ `Sign-in winner:`, winner
3104
+ );
3105
+
3106
+ return winner;
3107
+ },
3034
3108
 
3035
3109
  set_random_page_vars: async ( scope ) => {
3036
3110
  /** Answer inputs randomly. */
@@ -3050,7 +3124,7 @@ module.exports = {
3050
3124
  } else {
3051
3125
  // Try to give a random answer for that element tag
3052
3126
  await scope.set_random_input_for[ field.tag ]( scope, { field: field });
3053
- process.stdout.write(`\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
3127
+ log.stdout({}, `\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
3054
3128
  }
3055
3129
 
3056
3130
  } // End for all page fields
@@ -3086,11 +3160,10 @@ module.exports = {
3086
3160
  }
3087
3161
  if (field == null) {
3088
3162
  // Just skip it? Shouldn't happen but has before. Dig in more
3089
- log.debug({
3090
- code: `ALK0164`,
3091
- type: `info`,
3092
- pre: `A random button field choice was null. These were the fields to choose from:\n${ JSON.stringify(fields, null, 2) }`,
3093
- });
3163
+ log.debug({ code: `ALK0164`, context: `random`, },
3164
+ `A random button field choice was null. These were the fields to choose from:`,
3165
+ fields
3166
+ );
3094
3167
  return;
3095
3168
  }
3096
3169
  let handle = await scope.get_handle_from_field( scope, { field: field });