@suffolklitlab/alkiln 5.13.2 → 5.13.3-feat-random

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 (49) hide show
  1. package/.github/workflows/github_server.yml +7 -7
  2. package/.github/workflows/playground.yml +2 -1
  3. package/CHANGELOG.md +32 -1
  4. package/action.yml +89 -29
  5. package/action_for_github_server/action.yml +73 -29
  6. package/docs/4_environments.md +243 -0
  7. package/docs/artifacts_folder_name.md +135 -0
  8. package/docs/constrained_random_tests.md +254 -0
  9. package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +70 -0
  10. package/docs/decisions/007_getting_and_storing_logs.md +179 -0
  11. package/docs/decisions/move_runtime_config_to_sources_folder.md +5 -0
  12. package/docs/decisions/reports_and_logs_2024_08_11.md +161 -0
  13. package/docs/kinds_of_errors.md +26 -0
  14. package/docs/prioritize_constrained_random_tests.md +60 -0
  15. package/lib/docassemble/docassemble_api_interface.js +77 -51
  16. package/lib/docassemble/server_install.js +32 -9
  17. package/lib/docassemble/setup.js +41 -24
  18. package/lib/docassemble/takedown.js +33 -10
  19. package/lib/globals.js +10 -0
  20. package/lib/run_cucumber.js +155 -50
  21. package/lib/scope.js +101 -77
  22. package/lib/setup/TestGenerator.js +1745 -0
  23. package/lib/setup/artifacts.js +20 -0
  24. package/lib/setup/generate.js +42 -0
  25. package/lib/setup/get_Gherkin_AST.js +26 -0
  26. package/lib/setup/validate.js +89 -0
  27. package/lib/steps.js +176 -51
  28. package/lib/utils/Log.js +1132 -0
  29. package/lib/utils/_alkiln-constrained_random_tests_old.js +1727 -0
  30. package/lib/utils/files.js +18 -11
  31. package/lib/utils/get_error_stacks.js +16 -0
  32. package/lib/utils/reports.js +95 -28
  33. package/lib/utils/session_vars.js +102 -73
  34. package/lib/utils/set_sources_paths.js +98 -0
  35. package/lib/utils/time.js +44 -0
  36. package/package.json +16 -8
  37. package/tests/log_codes/check_codes.sh +19 -14
  38. package/tests/log_codes/log_code_expected_instances.txt +69 -9
  39. package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +155 -0
  40. package/tests/unit_tests/constrained_random_answers/invalid.test.js +233 -0
  41. package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +905 -0
  42. package/tests/unit_tests/constrained_random_answers/valid.test.js +521 -0
  43. package/tests/unit_tests/example_of_chai_test_lifecycle.js +78 -0
  44. package/tests/unit_tests/getAllFields.test.js +5 -0
  45. package/tests/unit_tests/getMatchingRows.test.js +5 -0
  46. package/tests/unit_tests/log.test.js +557 -0
  47. package/tests/unit_tests/normalizeTable.test.js +5 -0
  48. package/lib/utils/log.js +0 -96
  49. /package/docs/{expected_inernal_results.md → expected_internal_results.md} +0 -0
@@ -1,21 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const fast_glob = require(`fast-glob`);
5
+ const path = require(`path`);
4
6
  const cucumber_api = require('@cucumber/cucumber/api');
5
7
 
6
- const log = require('./utils/log'); // These always log in the console
8
+ const globals = require(`./globals`);
9
+ const Log = require('./utils/Log');
7
10
  const session_vars = require('./utils/session_vars');
11
+ const set_sources_paths = require(`./utils/set_sources_paths`);
12
+
13
+ // TODO: Remove after merging Log class
14
+ const files = require('./utils/files');
8
15
 
9
16
  const SUMMARY_FILENAME = `cucumber-report.txt`;
10
17
 
11
18
 
12
- function build_config( command_args ) {
19
+ function build_config( cli_args ) {
20
+ /** Use the command line arguments to build the cucumberjs config object.
21
+ *
22
+ * @param {string?} - Arguments passed to the command line.
23
+ *
24
+ * @returns {object} - cucumberjs config object
25
+ * */
26
+ // ========== TODO: Abstract after merging Log class PR
13
27
  // Turn all the paths to the sources folders into paths to the feature files
14
28
  let sources_paths = session_vars.get_sources_paths();
15
29
  let feature_paths = [];
16
- for ( let source of sources_paths ) {
17
- feature_paths.push( `${ source }/*.feature` );
30
+
31
+ // Get all the paths for the right feature files (for cucumber to use)
32
+ for ( let sources_path of sources_paths ) {
33
+ // Ignore feature files that we parsed ourselves
34
+ let glob = fast_glob.sync(
35
+ `${ sources_path }/**/*.feature`,
36
+ { ignore: [`${ sources_path }/${ globals.generators_prefix }*.feature`] }
37
+ );
38
+ feature_paths.push( ...glob );
18
39
  }
40
+ // ========== Ends TODO: Abstract
19
41
 
20
42
  const config = {
21
43
  publishQuiet: true,
@@ -27,7 +49,7 @@ function build_config( command_args ) {
27
49
 
28
50
  // Normalize tag expressions, if there are any
29
51
  // Joining and splitting handles both tags surrounded with `""` or those without `""`
30
- let tags = command_args[`_`].join(` `).split(` `);
52
+ let tags = cli_args[`_`].join(` `).split(` `);
31
53
  // If there was no tag expression, `tags` is ['']. Remove that item.
32
54
  tags = tags.filter(function(entry) { return entry !== ""; })
33
55
  if ( tags.length > 0 ) {
@@ -53,11 +75,18 @@ function build_config( command_args ) {
53
75
  }; // Ends build_config()
54
76
 
55
77
 
56
- (async function main() {
78
+ async function main() {
57
79
  /** Create the cucumber config, run the tests, create and
58
- * manipluate the output. */
80
+ * manipulate the output. */
81
+
59
82
  const environment = { cwd: process.cwd() };
60
- log.info({ type: `run`, code: `ALK0049`, pre: `environment`, data: environment });
83
+ log.info({ code: `ALK0049`, context: `pre_run environment` },
84
+ environment
85
+ );
86
+
87
+ // ========== TODO: Abstract after merging Log class PR
88
+ let artifacts_path = files.make_artifacts_folder();
89
+ session_vars.save_artifacts_path_name( artifacts_path );
61
90
 
62
91
  // We'll use the user info to construct the possible paths to /sources
63
92
  let id = session_vars.get_user_id();
@@ -65,8 +94,9 @@ function build_config( command_args ) {
65
94
  let project = session_vars.get_user_project_name();
66
95
 
67
96
  // 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), {
97
+ // If using `npm run cucumber` (as opposed to the bin commands),
98
+ // use `--` before `--sources=./foo`
99
+ const argv = require(`minimist`)( process.argv.slice(2), {
70
100
  default: {
71
101
  sources: [
72
102
  `./docassemble/*/data/sources`, // In GitHub folder
@@ -75,58 +105,78 @@ function build_config( command_args ) {
75
105
  ],
76
106
  }
77
107
  });
78
-
79
- // Make sure `sources` is always a list
80
- let sources_list = argv.sources;
81
- if ( typeof argv.sources === `string` ) {
82
- sources_list = [ argv.sources ];
83
- }
84
108
 
85
- let one_dir_exists = session_vars.set_sources_paths( sources_list );
86
- if ( !one_dir_exists ) {
87
- // 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 }`);
92
- }
109
+ // ========== Ends TODO: Abstract
93
110
 
94
111
  const { runConfiguration } = await cucumber_api.loadConfiguration({
95
112
  provided: build_config( argv ),
96
113
  }, environment);
114
+ log.info({ code: `ALK0051`, context: `pre_run config`, },
115
+ runConfiguration
116
+ );
97
117
 
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 });
118
+ const codeDir = path.dirname(require.main.filename);
119
+ log.info({ code: `ALK0052`, context: `pre_run directory`, },
120
+ codeDir, `\n\n`
121
+ );
103
122
  const support = await cucumber_api.loadSupport({ support: {
104
123
  requireModules: [ `${ codeDir }/index.js` ],
105
124
  requirePaths: [ `${ codeDir }/index.js` ],
106
125
  importPaths: [],
107
126
  }, sources: {paths: []}}, environment);
108
127
 
109
- log.info({ type: `run`, code: `ALK0053`, pre: `working directory`, data: codeDir });
110
- log.info({ type: `plain`, pre: `\n\n` });
111
-
128
+ // ====================
129
+ /** Run */
130
+ // ====================
112
131
  const { success } = await cucumber_api.runCucumber({...runConfiguration, support});
132
+ // ====================
133
+
134
+ /** Write to files.
135
+ *
136
+ * Note: Don't add to the purely report-focused files. Those are files where
137
+ * authors can focus on the info we give them, which they often miss in
138
+ * the GitHub action.
139
+ */
140
+
113
141
  if ( success ) {
114
- log.success({ type: `run`, code: `ALK0054`, pre: `Tests succeeded!` });
142
+ log.success({ code: `ALK0054`, context: `post_run`, },
143
+ `Tests succeeded!`
144
+ );
115
145
  } else {
116
- log.error({ type: `run`, code: `ALK0055`, pre: `The test run ran into something unexpected. Look above for more details.` });
146
+ // Don't want to throw error here, so don't pass error object
147
+ log.warn({ code: `ALK0055`, context: `post_run`, },
148
+ `The tests ran into something unexpected. See warnings and errors above.`
149
+ );
117
150
  }
118
151
  // Custom exit code. May overlap with other systems.
119
152
  // Best info I've found via stack overflow: https://tldp.org/LDP/abs/html/exitcodes.html
120
153
  const exitCode = success ? 0 : 50;
121
154
  process.exitCode = exitCode;
122
155
 
156
+ /** TODO:
157
+ * randomly generated test file cleanup
158
+ * 1. Generate files into the sources folder
159
+ * 2. copy the randomly generated files into the artifacts folder
160
+ * 3. MVP: delete the randomly generated files from the sources folder
161
+ * 4. post MVP: let the author keep the randomly generated tests in the
162
+ * sources folder and make a PR with them
163
+ * */
164
+
165
+ /**
166
+ * Loop through sources folders
167
+ * If there are generated files in the generated files folder(s)
168
+ * delete that folder from the sources folders where we found them
169
+ * */
170
+
123
171
  // This is hardcoded information from `provide_config` above.
124
172
  // This code isn't used for anything other than detecting and removing color codes.
125
173
  let summary_file_name = SUMMARY_FILENAME;
126
- fs.readFile(summary_file_name, 'utf8', (err, text) => {
127
- if (err) {
174
+ fs.readFile(summary_file_name, 'utf8', (read_error, text) => {
175
+ if ( read_error ) {
128
176
  // 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 });
177
+ log.warn({ code: `ALK0056`, context: `post_run error`, },
178
+ `Failed to read cucumber report`, read_error
179
+ );
130
180
  return;
131
181
  }
132
182
 
@@ -134,20 +184,75 @@ function build_config( command_args ) {
134
184
  // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#colors--graphics-mode
135
185
  text = text.replace(/\x1b\[[0-9][0-9]?[0-9]?m/g, '');
136
186
 
137
- // Put it in the correct file in the correct folder
138
- let artifacts_path = session_vars.get_artifacts_path_name();
139
- try {
140
- fs.appendFileSync( `${ artifacts_path }/${ log.debug_log_file }`, `\n\n${text}`);
141
- } 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 });
143
- }
144
187
  try {
145
- // Add to a unexpected results file when tests themselves fail
188
+ // Add to the debug and non-passed test output files
146
189
  if ( exitCode === 50 ) {
147
- fs.appendFileSync( `${ artifacts_path }/${ log.unexpected_filename }`, `\n\n${ text }` );
190
+ // Save only non-passing test output to cut out cruft.
191
+ // Our reports save only non-passing tests to this file.
192
+ log.debug({ icon: `🔎`, code: `ALK0216`, context: `post_run`, level: `warn` },
193
+ `\n\n`, text
194
+ );
195
+ } else {
196
+ log.debug({ code: `ALK0225`, context: `post_run`, },
197
+ `\n\n`, text
198
+ );
148
199
  }
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 });
151
- }
200
+
201
+ /** Include debug info at the bottom of the unexpected results file if
202
+ * needed. Avoid creating the file if its not necessary. */
203
+ let temp_debug_unexpected_content = ``;
204
+ try {
205
+ temp_debug_unexpected_content = fs.readFileSync(`${ log.path }/${ log.temp_unexpected_debug_log_filename }`);
206
+ } catch ( ignore_err ) { /* Missing debug info is ok */ }
207
+
208
+ if ( temp_debug_unexpected_content !== `` ) {
209
+ // Add spacing before the debug content if file already has content
210
+ try {
211
+ fs.readFileSync(`${ log.path }/${ log.unexpected_filename }`); // Does it have contents?
212
+ fs.appendFileSync(`${ log.path }/${ log.unexpected_filename }`, `\n\n\n\n\n` );
213
+ } catch ( ignore_err ) { /* Missing unexpected file is ok */ }
214
+
215
+ // Add the heading for debug content
216
+ fs.appendFileSync( `${ log.path }/${ log.unexpected_filename }`,
217
+ `≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
218
+ 🐛 Full DEBUG info (some repeated content) 🐛
219
+ ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡\n\n`
220
+ );
221
+ // Add the debug content itself
222
+ fs.appendFileSync(`${ log.path }/${ log.unexpected_filename }`, temp_debug_unexpected_content );
223
+ }
224
+
225
+ } catch ( writing_cucumber_error ) {
226
+ let code = `ALK0182`;
227
+ let context = `post_run error`
228
+ let msg = `Something probably went wrong earlier. See warnings and errors above. Skipped writing to ${ log.path }/${ log.debug_log_filename }.`;
229
+ try {
230
+ log.warn({ code, context, }, msg, writing_cucumber_error );
231
+ } catch ( all_writing_error ) {
232
+ console.warn( `🔎 ${ code } ${ context } WARNING ${ Date.now() }: ${ msg }` );
233
+ console.warn( writing_cucumber_error );
234
+ console.warn( all_writing_error );
235
+ }
236
+
237
+ } // ends try writing to debug files
238
+
239
+ return;
152
240
  });
153
- })();
241
+ }
242
+
243
+
244
+ // 1. session_vars - make sure all the required env vars exist
245
+ session_vars.validateEnvironment();
246
+
247
+ // 2. Prepare logs
248
+ // process.argv is a list of strings of commands and args.
249
+ // If using `npm run cucumber` (as opposed to the bin commands),
250
+ // use `--` before `--sources=./foo`
251
+ const argv = require(`minimist`)( process.argv.slice(2) );
252
+ const log = new Log({ path: argv.path, context: `run` });
253
+
254
+ // 3. Prep the sources paths to find .feature files
255
+ let sources_paths = set_sources_paths({ log });
256
+
257
+ // 4. run
258
+ main();
package/lib/scope.js CHANGED
@@ -13,14 +13,12 @@ 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');
17
16
  const { waitForTimeout } = require('./utils/time');
18
17
  const files = require('./utils/files' );
19
18
  const da_api = require('./docassemble/docassemble_api_interface');
20
19
  const wrapPromiseWithTimeout = require('./cucumber_8_shim');
21
20
  const reports = require(`./utils/reports`);
22
21
 
23
-
24
22
  let ordinal_to_integer = {
25
23
  only: 0, first: 0, second: 1, third: 2, fourth: 3, fifth: 4,
26
24
  sixth: 5, seventh: 6, eighth: 7, ninth: 8, tenth: 9,
@@ -84,10 +82,16 @@ let toBase64 = function ( utf8_str, exclude_padding=true) {
84
82
 
85
83
  let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
86
84
  let da_base64_regex = /^[A-Za-z0-9+/]+$/;
85
+ let log = null;
87
86
 
88
87
  module.exports = {
89
88
  trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
90
89
 
90
+
91
+ set_log: function ({ log_obj }) {
92
+ log = log_obj;
93
+ },
94
+
91
95
  getSafeScenarioBaseFilename: async function( scope, { scenario }) {
92
96
  /** Return a string that's safe to be a filename. Include the Scenario
93
97
  * description and tags.
@@ -514,13 +518,17 @@ module.exports = {
514
518
  },
515
519
 
516
520
  continue_button_selector: `fieldset.da-field-buttons button[type="submit"].btn-primary`,
521
+ continue_button_selector_backup: `.da-fieldset.da-field-buttons button[type="submit"].btn-primary`,
517
522
  signature_selector: `fieldset .dasigsave`,
523
+ signature_selector_backup: `.da-fieldset .dasigsave`,
518
524
  continue: async function ( scope ) {
519
525
  /* Presses whatever button it finds that might lead to the next page. */
520
526
  // Any selectors I find seem somewhat precarious.
521
527
  let elem = await Promise.race([
522
528
  scope.page.waitForSelector(scope.continue_button_selector), // other pages (this is the most consistent way)
529
+ scope.page.waitForSelector(scope.continue_button_selector_backup),
523
530
  scope.page.waitForSelector(scope.signature_selector), // signature page
531
+ scope.page.waitForSelector(scope.signature_selector_backup),
524
532
  ]);
525
533
  await elem.evaluate( el => { return el.className });
526
534
  // Waits for navigation or user error
@@ -540,9 +548,11 @@ module.exports = {
540
548
  // <button type="submit" class="btn btn-da btn-danger" name="X211bHRpcGxlX2Nob2ljZQ" value="0">exit</button>
541
549
  // `buttons:` can be used in question blocks as choices
542
550
  let regular = await scope.page.$(scope.continue_button_selector);
551
+ let regular_backup = await scope.page.$(scope.continue_button_selector_backup);
543
552
  let signature = await scope.page.$(scope.signature_selector);
553
+ let signature_backup = await scope.page.$(scope.signature_selector_backup);
544
554
 
545
- return regular !== null || signature !== null;
555
+ return regular !== null || regular_backup !== null || signature !== null || signature_backup !== null;
546
556
 
547
557
  }, // Ends scope.continue_exists()
548
558
 
@@ -692,7 +702,7 @@ module.exports = {
692
702
  });
693
703
  expect( load_result.error ).to.equal( '' );
694
704
  } else {
695
- reports.addToReport(scope, { type: `row info`, code: `ALK0117`, value: `Successfully found the interview at "${ interview_url }"` });
705
+ reports.addToReport(scope, { type: `row info`, code: `ALK0117`, value: `Successfully found the interview` });
696
706
  }
697
707
 
698
708
  return scope.page;
@@ -711,19 +721,19 @@ module.exports = {
711
721
  if ( !scenario.interviews ) { scenario.interviews = {}; }
712
722
  if ( !scenario.interviews[ vars.uid ] ) {
713
723
  scenario.interviews[ vars.uid ] = vars;
714
- log.debug({
715
- code: `ALK0185`, type: `info`,
716
- pre: `Saved new interview data. File path: ${ vars.i }. Session id: ${ vars.uid }.`
717
- });
724
+ log.debug({ code: `ALK0185`, context: `session_id_etc` },
725
+ `Saved new interview data. File path: ${ vars.i }. Session id: ${ vars.uid }.`
726
+ );
718
727
  } else {
719
- log.debug({
720
- code: `ALK0186`, type: `info`,
721
- pre: `Interview data already existed. File path: ${ vars.i }. Session id: ${ vars.uid }.`
722
- });
728
+ log.debug({ code: `ALK0186`, context: `session_id_etc` },
729
+ `Interview data already existed. File path: ${ vars.i }. Session id: ${ vars.uid }.`
730
+ );
723
731
  }
724
732
  return vars;
725
733
  } else {
726
- log.debug({ code: `ALK0187`, type: `info`, pre: `Found no interview data.` });
734
+ log.debug({ code: `ALK0187`, context: `session_id_etc` },
735
+ `Found no interview data.`
736
+ );
727
737
  return null;
728
738
  }
729
739
  }, // Ends scope.set_interview_session_data()
@@ -870,11 +880,9 @@ module.exports = {
870
880
  supported_table.push( result );
871
881
  } // ends for every row
872
882
 
873
- log.debug({
874
- code: `ALK0122`,
875
- type: `info`,
876
- pre: `scope.normalizedTable():\n ${JSON.stringify( supported_table )}`
877
- });
883
+ log.debug({ code: `ALK0122`, context: `table`, },
884
+ `scope.normalizedTable():`, supported_table
885
+ );
878
886
 
879
887
  return supported_table;
880
888
  }, // Ends scope.normalizeTable()
@@ -916,7 +924,7 @@ module.exports = {
916
924
  // All the different types of fields
917
925
  // buttons, canvases, inputs of all kinds, selects (dropdowns), textareas. Are there more?
918
926
  // Will deal with `option` once inside `select`
919
- let all_nodes = $( `#dasigpage, fieldset button, .daquestionactionbutton, fieldset input, .da-form-group input, .da-form-group select, form select, .da-form-group textarea` );
927
+ let all_nodes = $( `#dasigpage, fieldset button, .da-fieldset button, .daquestionactionbutton, fieldset input, da-fieldset input, .da-form-group input, .da-form-group select, form select, .da-form-group textarea` );
920
928
  for ( let node of all_nodes ) {
921
929
  // Decision: Do not abstract the below. There's too much data to pass around for it to make sense
922
930
  let $node = $( node );
@@ -1024,11 +1032,9 @@ module.exports = {
1024
1032
  fields.push( field_data );
1025
1033
  } // ends for every node
1026
1034
 
1027
- log.debug({
1028
- code: `ALK0124`,
1029
- type: `info`,
1030
- pre: `page fields:\n${ JSON.stringify( fields )}`
1031
- });
1035
+ log.debug({ code: `ALK0124`, context: `page fields`, },
1036
+ fields
1037
+ );
1032
1038
  return fields;
1033
1039
  }, // Ends scope.getAllFields()
1034
1040
 
@@ -1078,11 +1084,9 @@ module.exports = {
1078
1084
  /* Return variable names and values combined in ways that can
1079
1085
  * try to match table rows. Tables can have two properties:
1080
1086
  * variable name and value. */
1081
- log.debug({
1082
- code: `ALK0126`,
1083
- type: `info`,
1084
- pre: `scope.getPossibleTableRowMatchers() var_names: ${JSON.stringify(var_names)} values: ${JSON.stringify(values)}`
1085
- });
1087
+ log.debug({ code: `ALK0126`, context: `matching`, },
1088
+ `scope.getPossibleTableRowMatchers() var_names:`, var_names, `values:`, values
1089
+ );
1086
1090
  let possible_table_rows = [];
1087
1091
  for ( let var_name of var_names ) {
1088
1092
  // Inputs like textareas don't have values
@@ -1557,17 +1561,13 @@ module.exports = {
1557
1561
  reports.addToReport(scope, { type: `warning`, code: `ALK0130`, value: multiple_matches_msg });
1558
1562
  }
1559
1563
 
1560
- log.debug({
1561
- code: `ALK0131`,
1562
- type: `info`,
1563
- pre: `final_matching_rows:\n${JSON.stringify( final_matching_rows )}`
1564
- });
1564
+ log.debug({ code: `ALK0131`, context: `matching`, },
1565
+ `final_matching_rows:`, final_matching_rows
1566
+ );
1565
1567
  if ( final_matching_rows.length == 0) {
1566
- log.debug({
1567
- code: `ALK0132`,
1568
- type: `info`,
1569
- pre: `field:\n${JSON.stringify( field )}`
1570
- });
1568
+ log.debug({ code: `ALK0132`, context: `matching`, },
1569
+ `0 matching rows. Field:`, field
1570
+ );
1571
1571
  }
1572
1572
 
1573
1573
  return final_matching_rows;
@@ -1653,11 +1653,9 @@ module.exports = {
1653
1653
 
1654
1654
  get_handle_from_field: async function ( scope, { field }) {
1655
1655
  /** Get the element's puppeteer handle using the field's selector. */
1656
- log.debug({
1657
- code: `ALK0133`,
1658
- type: `info`,
1659
- pre: `scope.get_handle_from_field() field.selector: ${JSON.stringify(field.selector)}`
1660
- });
1656
+ log.debug({ code: `ALK0133`, },
1657
+ `scope.get_handle_from_field() field.selector:`, field.selector
1658
+ );
1661
1659
  let handle = await scope.page.evaluateHandle(( selector ) => {
1662
1660
  let elem = document.querySelector( selector );
1663
1661
  // If the element has a sibling that's a label, we need to
@@ -2120,6 +2118,38 @@ module.exports = {
2120
2118
  // await scope.page.mouse.move(1, 1); // Too big? Wait a while before drawing?
2121
2119
  await scope.page.mouse.up();
2122
2120
  },
2121
+
2122
+ get_page_id: async function ( scope ) {
2123
+ /** Return the current page's docassemble `question` block ID or `null`
2124
+ *
2125
+ * Note: The ID is already sanitized by docassemble.
2126
+ *
2127
+ * @param {object} scope - Tests state object
2128
+ * @param {object} scope.page - puppeteerjs page object
2129
+ *
2130
+ * @returns {null | string}
2131
+ * */
2132
+ let question_id_match = await scope.page.$eval(`body`, function (elem) {
2133
+ return elem.className.match(/question-([^ ]+)/);
2134
+ });
2135
+ if ( question_id_match !== null ) {
2136
+ return question_id_match[ 1 ];
2137
+ }
2138
+ return null;
2139
+ },
2140
+
2141
+ to_docassemble_sanitized_id: function ( risky_id ) {
2142
+ /** Return a version of `risky_id` that's sanitized the same way docassemble
2143
+ * sanitizes its IDs to turn them into HTML class names in python:
2144
+ * re.sub(r'[^A-Za-z0-9]+', '-', interview_status.question.id.lower())
2145
+ * (reference needed)
2146
+ *
2147
+ * @param {string} risky_id - The un-sanitized id
2148
+ *
2149
+ * @returns {string} - Sanitized id
2150
+ * */
2151
+ return risky_id.toLowerCase().replace(/[^A-Za-z0-9]+/g, '-');
2152
+ },
2123
2153
 
2124
2154
  examinePageID: async function ( scope, question_id = '' ) {
2125
2155
  /* Looks for a sanitized version of the question id as it's written
@@ -2169,7 +2199,9 @@ module.exports = {
2169
2199
  * @param ensure_all_vars_used {bool} [false] = Default false. If true, errors if any
2170
2200
  * object in var_data is not used during running of this function.
2171
2201
  */
2172
- log.debug({ type: `plain`, pre: `~~~~~~~~~~~~~~ scope.setFields() ~~~~~~~~~~~~~~` });
2202
+ log.debug({ code: `ALK0221`, before: `~~~~~~~~~~~~~~\n`, },
2203
+ `scope.setFields()`
2204
+ );
2173
2205
 
2174
2206
  // Only record the page id once for each page to which we navigate
2175
2207
  let { id } = await scope.examinePageID( scope, 'none to match' );
@@ -2213,8 +2245,7 @@ module.exports = {
2213
2245
  // Accumulate number of times this row was used. Esp important for `.target_number`
2214
2246
  await scope.increment_row_use( scope, { row: row_used });
2215
2247
 
2216
- process.stdout.write(`\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
2217
-
2248
+ log.stdout({}, `\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
2218
2249
  }
2219
2250
  } // ends for every non-submit-button field on the page
2220
2251
  } // ends while fields are still being set
@@ -2259,7 +2290,7 @@ module.exports = {
2259
2290
  // }
2260
2291
 
2261
2292
  did_navigate = true;
2262
- process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
2293
+ log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
2263
2294
  break;
2264
2295
 
2265
2296
  } // ends if row_used
@@ -2306,7 +2337,7 @@ module.exports = {
2306
2337
  // }
2307
2338
 
2308
2339
  did_navigate = true;
2309
- process.stdout.write(`\x1b[36m${ '>' }\x1b[0m`); // continued successfully
2340
+ log.stdout({}, `\x1b[36m${ '>' }\x1b[0m`); // continued successfully
2310
2341
  } // ends if !did_navigate && ensure_navigation
2311
2342
 
2312
2343
  if ( did_navigate && scope.check_all_for_a11y ) {
@@ -2635,6 +2666,7 @@ module.exports = {
2635
2666
 
2636
2667
  let fullPage = true;
2637
2668
  let signature_elem = await scope.page.$(scope.signature_selector);
2669
+ signature_elem = signature_elem || await scope.page.$(scope.signature_selector_backup);
2638
2670
  if ( signature_elem !== null ) {
2639
2671
  fullPage = false;
2640
2672
  }
@@ -3102,36 +3134,30 @@ module.exports = {
3102
3134
  let redirect_promise = scope.page.waitForResponse(function ( response ) {
3103
3135
  return response.status() === 302;
3104
3136
  }, { signal: controller.signal })
3105
- .catch(( error ) => {
3106
- log.debug({
3107
- code: `ALK0204`, type: `info`,
3108
- pre: `302 sign-in wait error: ${ error.name }`,
3109
- data: error,
3110
- });
3137
+ .catch(( catch_info ) => {
3138
+ log.debug({ code: `ALK0204`, context: `nav`, },
3139
+ `302 sign-in wait catch error:`, catch_info.name, catch_info,
3140
+ );
3111
3141
  });
3112
3142
 
3113
3143
  // Invalid credentials, no navigation, no sign in
3114
3144
  let wrong_sign_in_promise = scope.page.waitForResponse(function ( response ) {
3115
3145
  return response.status() >= 200 && response.status() < 300 && response.url().includes(`/sign-in`);
3116
3146
  }, { signal: controller.signal })
3117
- .catch(( error ) => {
3118
- log.debug({
3119
- code: `ALK0205`, type: `info`,
3120
- pre: `200s sign-in wait error: ${ error.name }`,
3121
- data: error,
3122
- });
3147
+ .catch(( catch_info ) => {
3148
+ log.debug({ code: `ALK0205`, context: `nav`, },
3149
+ `200s sign-in wait catch error:`, catch_info.name, catch_info,
3150
+ );
3123
3151
  });
3124
3152
 
3125
3153
  // System error
3126
3154
  let error_promise = scope.page.waitForResponse(function ( response ) {
3127
3155
  return response.status() >= 500 && response.status() < 600;
3128
3156
  }, { signal: controller.signal })
3129
- .catch(( error ) => {
3130
- log.debug({
3131
- code: `ALK0206`, type: `info`,
3132
- pre: `500s sign-in wait error: ${ error.name }`,
3133
- data: error,
3134
- });
3157
+ .catch(( catch_info ) => {
3158
+ log.debug({ code: `ALK0206`, context: `nav`, },
3159
+ `500s sign-in wait catch error:`, catch_info.name, catch_info,
3160
+ );
3135
3161
  });
3136
3162
 
3137
3163
  let click_promise = elem.click(); // MUST complete
@@ -3150,10 +3176,9 @@ module.exports = {
3150
3176
  // Clean up unresolved promises
3151
3177
  controller.abort();
3152
3178
 
3153
- log.debug({
3154
- code: `ALK0208`, type: `info`,
3155
- pre: `Sign-in winner:`, data: winner,
3156
- });
3179
+ log.debug({ code: `ALK0208`, context: `nav` },
3180
+ `Sign-in winner:`, winner
3181
+ );
3157
3182
 
3158
3183
  return winner;
3159
3184
  },
@@ -3176,7 +3201,7 @@ module.exports = {
3176
3201
  } else {
3177
3202
  // Try to give a random answer for that element tag
3178
3203
  await scope.set_random_input_for[ field.tag ]( scope, { field: field });
3179
- process.stdout.write(`\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
3204
+ log.stdout({}, `\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
3180
3205
  }
3181
3206
 
3182
3207
  } // End for all page fields
@@ -3212,11 +3237,10 @@ module.exports = {
3212
3237
  }
3213
3238
  if (field == null) {
3214
3239
  // Just skip it? Shouldn't happen but has before. Dig in more
3215
- log.debug({
3216
- code: `ALK0164`,
3217
- type: `info`,
3218
- pre: `A random button field choice was null. These were the fields to choose from:\n${ JSON.stringify(fields, null, 2) }`,
3219
- });
3240
+ log.debug({ code: `ALK0164`, context: `random`, },
3241
+ `A random button field choice was null. These were the fields to choose from:`,
3242
+ fields
3243
+ );
3220
3244
  return;
3221
3245
  }
3222
3246
  let handle = await scope.get_handle_from_field( scope, { field: field });