@suffolklitlab/alkiln 5.13.4 → 5.15.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.
Files changed (44) hide show
  1. package/.github/workflows/github_server.yml +1 -1
  2. package/.github/workflows/playground.yml +2 -1
  3. package/.github/workflows/unit_tests.yml +1 -1
  4. package/CHANGELOG.md +40 -0
  5. package/action.yml +26 -10
  6. package/action_for_github_server/action.yml +31 -27
  7. package/docs/4_environments.md +2 -2
  8. package/docs/artifacts_folder_name.md +121 -71
  9. package/docs/constrained_random_tests.md +220 -289
  10. package/docs/decisions/move_runtime_config_to_sources_folder.md +3 -1
  11. package/docs/prioritize_constrained_random_tests.md +35 -76
  12. package/lib/docassemble/server_install.js +0 -2
  13. package/lib/docassemble/setup.js +1 -3
  14. package/lib/docassemble/takedown.js +2 -2
  15. package/lib/globals.js +10 -0
  16. package/lib/run_cucumber.js +40 -53
  17. package/lib/scope.js +44 -1
  18. package/lib/setup/TestGenerator.js +1757 -0
  19. package/lib/setup/artifacts.js +20 -0
  20. package/lib/setup/generate.js +42 -0
  21. package/lib/setup/get_Gherkin_AST.js +26 -0
  22. package/lib/setup/validate.js +89 -0
  23. package/lib/steps.js +101 -42
  24. package/lib/utils/Log.js +317 -165
  25. package/lib/utils/_alkiln-constrained_random_tests_old.js +1776 -0
  26. package/lib/utils/get_error_stacks.js +16 -0
  27. package/lib/utils/reports.js +44 -22
  28. package/lib/utils/session_vars.js +77 -57
  29. package/lib/utils/set_sources_paths.js +98 -0
  30. package/package.json +18 -7
  31. package/tests/log_codes/check_codes.sh +17 -13
  32. package/tests/log_codes/log_code_expected_instances.txt +58 -0
  33. package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +178 -0
  34. package/tests/unit_tests/constrained_random_answers/invalid.test.js +251 -0
  35. package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +957 -0
  36. package/tests/unit_tests/constrained_random_answers/valid.test.js +563 -0
  37. package/tests/unit_tests/example_of_chai_test_lifecycle.js +78 -0
  38. package/tests/unit_tests/log.test.js +17 -41
  39. package/docs/seprate_file_generation_GitHub_action.md +0 -11
  40. package/lib/parse.js +0 -36
  41. package/lib/utils/get_sources_paths.js +0 -162
  42. package/lib/utils/parse.js +0 -1
  43. package/lib/utils/proccess_command_line_args.js +0 -267
  44. /package/docs/{expected_inernal_results.md → expected_internal_results.md} +0 -0
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ const session_vars = require(`../utils/session_vars`);
4
+ const files = require(`../utils/files`);
5
+ const Log = require(`../setup/artifacts.js`);
6
+
7
+ /** Reset and refresh artifacts path name */
8
+
9
+ session_vars.delete_artifacts_path_name();
10
+
11
+ // process.argv is a list of strings of commands and args.
12
+ // If using `npm run cucumber` (as opposed to the bin commands),
13
+ // use `--` before `--sources=./foo`
14
+ const argv = require(`minimist`)( process.argv.slice(2) );
15
+ const artifacts_path = files.make_artifacts_folder( argv.path );
16
+
17
+ session_vars.save_artifacts_path_name( artifacts_path );
18
+
19
+ // Leave the config Project name value as it is. Local developers find it
20
+ // useful to re-use that value.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Log = require(`../utils/Log`);
4
+ const set_sources_paths = require(`../utils/set_sources_paths`);
5
+ const TestGenerator = require(`./TestGenerator.js`);
6
+
7
+ // process.argv is a list of strings of commands and args.
8
+ // If using `npm run cucumber` (as opposed to the bin commands),
9
+ // use `--` before `--sources=./foo`
10
+ const argv = require(`minimist`)( process.argv.slice(2) );
11
+ const log = new Log({ path: argv.path, context: `generating files` });
12
+ // Run script
13
+ generate_constrained_random_test_files();
14
+
15
+
16
+ function generate_constrained_random_test_files() {
17
+ /** Executable that generates constrained random answers tests and throws
18
+ * errors.
19
+ *
20
+ * Takes no arguments - uses command line arguments instead.
21
+ * */
22
+ let all_errors = [];
23
+
24
+ // Every file that's executed separately needs to figure out the location of
25
+ // the sources folders.
26
+ let sources_paths = set_sources_paths({ log });
27
+ let generator = new TestGenerator();
28
+ for ( let sources_path of sources_paths ) {
29
+ let { errors: generating_errors } = generator.create({
30
+ sources_path,
31
+ logger: log
32
+ });
33
+ all_errors.push( ...generating_errors );
34
+ }
35
+
36
+ if ( all_errors.length > 0 ) {
37
+ log.throw({ code: `ALK0272`, context: `generating files`,
38
+ error: new Error( `ALKiln got error(s) when it tried to generate constrained random answers tests. See warnings and errors above.` )
39
+ });
40
+ };
41
+
42
+ };
@@ -0,0 +1,26 @@
1
+ const Gherkin = require(`@cucumber/gherkin`);
2
+ const Messages = require(`@cucumber/messages`);
3
+
4
+ module.exports = function get_Gherkin_AST({ file_text }) {
5
+ /** Return Gherkin parser's abstract syntax tree of the given generator file
6
+ *
7
+ * @param {object} obj - Named arguments
8
+ * @param {string} file_text - The generator file's text contents
9
+ *
10
+ * @returns {object} - Gherkin parser's AST of the given generator file
11
+ * */
12
+ let AST = null;
13
+ let parse_errors = [];
14
+ try {
15
+ let uuidFn = Messages.IdGenerator.uuid();
16
+ let builder = new Gherkin.AstBuilder(uuidFn);
17
+ let matcher = new Gherkin.GherkinClassicTokenMatcher(); // or Gherkin.GherkinInMarkdownTokenMatcher()
18
+
19
+ let parser = new Gherkin.Parser(builder, matcher);
20
+ AST = parser.parse( file_text );
21
+ } catch ( gherkin_error ) {
22
+ parse_errors.push( gherkin_error );
23
+ }
24
+
25
+ return { AST, errors: parse_errors };
26
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require(`fs`);
4
+ const fast_glob = require(`fast-glob`);
5
+
6
+ const globals = require(`../globals`);
7
+ const Log = require(`../utils/Log`);
8
+ const set_sources_paths = require(`../utils/set_sources_paths`);
9
+ const get_Gherkin_AST = require(`./get_Gherkin_AST`);
10
+ const get_error_stacks = require(`../utils/get_error_stacks`);
11
+
12
+
13
+ /**
14
+ * TODO: Document that generator files need to have valid Gherkin syntax
15
+ * TODO: Document how to read Gherkin syntax errors. E.g.
16
+ * (3:1): expected: #EOF, #Language, #TagLine, #FeatureLine, #Comment, #Empty, got 'Scenario: bar'
17
+ * (6:1): expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ExamplesLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got 'Feature: zoo'
18
+ * */
19
+
20
+
21
+ // process.argv is a list of strings of commands and args.
22
+ // If using `npm run cucumber` (as opposed to the bin commands),
23
+ // use `--` before `--sources=./foo`
24
+ const argv = require(`minimist`)( process.argv.slice(2) );
25
+ const log = new Log({ path: argv.path, context: `validating files` });
26
+ // Run script
27
+ validate_Gherkin_files();
28
+
29
+
30
+ function validate_Gherkin_files() {
31
+ /** Accumulates syntax errors for all `.feature`` test files, including
32
+ * generator files. If there are any, it logs those errors and then finally
33
+ * throws an error. Uses the values in command line arguments.
34
+ * */
35
+
36
+ let sources_paths = set_sources_paths({ log });
37
+
38
+ let all_errors = [];
39
+ for ( let sources_path of sources_paths ) {
40
+ let { errors: files_errors } = parse_all_feature_files_in({ sources_path });
41
+ all_errors.push( ...files_errors );
42
+ }
43
+
44
+ // Discuss: If an author has only some files with syntax errors, should we try
45
+ // to still run the tests for the files that had valid syntax?
46
+ if ( all_errors.length > 0 ) {
47
+ log.throw({ code: `ALK0230`, context: `validating files`,
48
+ error: new Error( `ALKiln ran into Gherkin syntax error(s) when parsing your test feature files. See warnings and errors above.` )
49
+ });
50
+ };
51
+ }
52
+
53
+
54
+ function parse_all_feature_files_in({ sources_path }) {
55
+ /** Returns accumulated list of Gherkin errors from all files in the
56
+ * docassemble "sources" folder (or an empty list).
57
+ *
58
+ * @param {object} obj - Named arguments
59
+ * @param {object} obj.sources_path - Path to the docassemble "sources" folder
60
+ *
61
+ * @returns {{errors: [Error]}} Obj with an `error` prop that is a list of
62
+ * Error instances. Follows the format of other functions that return
63
+ * accumulated errors.
64
+ * */
65
+ let all_AST_errors = [];
66
+
67
+ let feature_file_paths = fast_glob.sync(
68
+ `${ sources_path }/**/*.feature`,
69
+ // These files were generated last time and will get deleted before the run
70
+ { ignore: [`${ sources_path }/${ globals.generated_files_folder_name }/*`] }
71
+ );
72
+
73
+ for ( let feature_path of feature_file_paths ) {
74
+
75
+ let file_text = fs.readFileSync( feature_path, { encoding: `utf8` });
76
+ let { AST, errors: these_AST_errors } = get_Gherkin_AST({ file_text });
77
+
78
+ if ( these_AST_errors.length > 0 ) {
79
+ all_AST_errors.push( ...these_AST_errors );
80
+ log.warn({ code: `ALK0276`, context: `validating files`, },
81
+ `There is a Gherkin syntax error(s) in "${ feature_path }"`,
82
+ ...get_error_stacks({ errors: these_AST_errors })
83
+ );
84
+ }
85
+
86
+ } // ends for each file path
87
+
88
+ return { errors: all_AST_errors };
89
+ }
package/lib/steps.js CHANGED
@@ -5,6 +5,7 @@ const {
5
5
  Given,
6
6
  After,
7
7
  Before,
8
+ BeforeStep,
8
9
  AfterStep,
9
10
  BeforeAll,
10
11
  AfterAll,
@@ -98,17 +99,12 @@ BeforeAll(async function() {
98
99
  // Store path names for files and docs created for the tests
99
100
  scope.paths = {};
100
101
 
101
- // // Make the folder to store all the files for this run of the tests
102
- // scope.paths.artifacts = files.make_artifacts_folder();
103
- // // Save its name in a file. `session_vars` can get it back later.
104
- // // The value we are saving is used by at least run_cucumber.js.
105
-
106
102
  // A little indirect, but the artifacts path should always have already been
107
103
  // created by now, either by `run_cucumber.js` or the relevant test
108
- scope.paths.artifacts = session_vars.get_artifacts_path_name();
109
- log = new Log({ path: scope.paths.artifacts, context: `steps` });
104
+ log = new Log({ context: `steps` });
110
105
  scope.set_log({ log_obj: log });
111
106
 
107
+ scope.paths.artifacts = session_vars.get_artifacts_path_name();
112
108
  scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_filename }`;
113
109
  scope.paths.report_log = `${ scope.paths.artifacts }/${ log.report_log_filename }`;
114
110
  scope.paths.report = `${ scope.paths.artifacts }/${ log.report_filename }`;
@@ -179,6 +175,14 @@ Before(async (scenario) => {
179
175
 
180
176
  // Add a check for an error page before each step? After each step?
181
177
 
178
+ BeforeStep(async function ( step ) {
179
+ // Discuss: In the report, add a way for the reports to add "✔" or "✖" to the
180
+ // start of a Step after it's been completed, as cucumber already does. Add
181
+ // a 'step' type and additional success/failure information, though that
182
+ // may be challenging.
183
+ reports.addToReport(scope, { type: `row`, value: step.pickleStep.text });
184
+ reports.indent();
185
+ });
182
186
 
183
187
  //#####################################
184
188
  //#####################################
@@ -343,17 +347,17 @@ Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+
343
347
  // Story
344
348
  //#####################################
345
349
  //#####################################
346
- // TODO: Consider more permissive sentences:
347
- // with(?: this data| these answers)?:?
348
- // with(?:.*):?
349
- When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)" with this data:?/, {timeout: -1}, async (target_id, raw_var_data) => {
350
+ const INCLUDING_QUOTES_REGEX = /"([^"]*)"/g;
351
+ When(/I? ?gets? to ?.*?(".*").*?with.*?(?:this data:)?/i, {timeout: -1}, async (target_ids, raw_var_data) => {
350
352
  /* NO CUCUMBER TIMEOUT. Must handle timeout ourselves:
351
353
  * https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/timeouts.md#disable-timeouts
352
354
  * Iterate in here to keep timeout reasonable */
353
355
 
354
356
  // Note: Why do we not just stop when we've used all the vars in the table or
355
357
  // reach the last var? Vars are not required to be given in sequence and pages that
356
- // have non-button fields don't usually have a var assigned to the 'continue' button.
358
+ // have non-button fields often don't have a var assigned to the 'continue' button.
359
+ // It is optional for the table to use all the vars and some vars may be used
360
+ // multiple times.
357
361
  // Alternative: require vars even for 'continue' pages. Then in loop require at least one var
358
362
  // to be found on a page before hitting 'continue'. That requires detecting navigation because
359
363
  // pages with fields will not have a var for the continue button, so there will
@@ -361,10 +365,18 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
361
365
  // It also seems harsh, but more reliable.
362
366
  // Ask developers what they'd like.
363
367
 
368
+ let ids_as_list = Array.from(
369
+ target_ids.matchAll( INCLUDING_QUOTES_REGEX ),
370
+ ( match ) => { return match[ 1 ]; }
371
+ );
372
+ let sanitized_ids = ids_as_list.map(( risky_id ) => {
373
+ return scope.to_docassemble_sanitized_id( risky_id );
374
+ });
364
375
 
365
376
  let supported_table = await scope.normalizeTable( scope, { raw_var_data });
366
377
 
367
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
378
+ let id = await scope.get_page_id( scope );
379
+ let id_matches = id && sanitized_ids.includes( id );
368
380
  // Until we reach the target page, hit an error, or time out (take too long)
369
381
  while ( !id_matches ) {
370
382
  let custom_timeout = new Promise(function( resolve, reject ) {
@@ -372,8 +384,8 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
372
384
  // Set up the timeout that avoids infinite loops
373
385
  let on_page_timeout = async function() {
374
386
 
375
- let non_reload_report_msg = `The target id was "${ target_id }", but it took `
376
- + `too long to try to reach it (over ${ scope.timeout/1000/60 } min). The `
387
+ let non_reload_report_msg = `The target id or ids were ${ target_ids }, but it took `
388
+ + `too long to try to reach a valid id (over ${ scope.timeout/1000/60 } min). The `
377
389
  + `test got stuck on "${ id }".`
378
390
  let error = non_reload_report_msg;
379
391
  await scope.handle_page_timeout_error( scope, { non_reload_report_data: {
@@ -390,7 +402,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
390
402
  // Maybe list of timeout and/or interval ids in state that can all be cleared at once?
391
403
  (async function() {
392
404
  try {
393
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
405
+ let id = await scope.get_page_id( scope );
394
406
 
395
407
  // THIS FUNCTION ACTUALLY MATTERS
396
408
  // Add special rows. Basically, do weirder things.
@@ -408,10 +420,11 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
408
420
  // Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
409
421
  let error = await scope.setFields(scope, { var_data: ensured_var_data, id, ensure_navigation: true });
410
422
 
423
+ // Discuss: use Log class to throw this error?
411
424
  if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
412
425
  resolve();
413
426
  } catch ( err ) {
414
- let err_msg = `Error occurred when tried to answer fields on question "${ id }".`
427
+ let err_msg = `ALKiln ran into an error when it tried to answer fields on the page with the question id "${ id }". The text above might have more information.`
415
428
  reports.addToReport( scope, { type: `error`, code: `ALK0062`, value: err_msg });
416
429
  // Throw error instead?
417
430
  reject( err );
@@ -424,7 +437,8 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
424
437
  await custom_timeout;
425
438
 
426
439
  // Prep for next loop
427
- ({ question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id ));
440
+ id = await scope.get_page_id( scope );
441
+ id_matches = id && sanitized_ids.includes( id );
428
442
 
429
443
  } // ends while !id_matches
430
444
  // Can anything unsatisfactory get through here?
@@ -443,7 +457,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
443
457
  // Add full used table to report so it can be copied if needed.
444
458
  // TODO: Should we/How do we get this in the final scenario report instead of here?
445
459
  reports.addToReport( scope, { type: `note info`, code: `ALK0063`, value: `\n${ ' '.repeat(2) }Rows that got set:` });
446
- reports.addToReport( scope, { type: `step info`, code: `ALK0064`, value: `${ ' '.repeat(4) }And I get to the question id "${ target_id }" with this data:` });
460
+ reports.addToReport( scope, { type: `step info`, code: `ALK0064`, value: `${ ' '.repeat(4) }And I get to any of the question ids ${ target_ids } with this data:` });
447
461
  reports.addToReport(scope, { type: `row info`, code: `ALK0065`, value: `${ ' '.repeat(6) }| var | value | trigger |`, });
448
462
  let at_least_one_row_was_NOT_used = false;
449
463
  for ( let row of sorted_rows ) {
@@ -707,39 +721,48 @@ Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkTe
707
721
  await scope.afterStep(scope);
708
722
  });
709
723
 
710
- Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)" should be/i, async (var_name, expected_value) => {
711
- /* Check that the text string of the page's JSON var value matches
712
- * the given text string. Does not currently accept nested values
713
- * like "foo.bar". The word "text" is required.
714
- *
715
- * The format for this step is something like this:
716
- * ```
717
- * Then the text in the JSON variable "var_name" should be
718
- * """
719
- * A single or
720
- * multiline string
721
- * """
722
- * ```
723
- *
724
- * Sentence combinations that will work:
725
- * `the text in the JSON variable "variable_with_text_value" should be`
726
- * `the text in the JSON var "variable_with_text_value" should be`
727
- * `the text in "variable_with_text_value" should be`
728
- * `text var "variable_with_text_value" should be`
729
- */
724
+ Then(/(?:the )?text(?: in)?(?: for)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)" should be/i, async (var_name, expected_value) => {
725
+ /** Check that the text string of the page's JSON var value matches
726
+ * the given text string. Does not currently accept nested values
727
+ * like "foo.bar". The word "text" is required.
728
+ *
729
+ * The format for this step is something like this:
730
+ * ```
731
+ * Then the text in the JSON variable "var_name" should be
732
+ * """
733
+ * A single or
734
+ * multiline string
735
+ * """
736
+ * ```
737
+ *
738
+ * Note: ALKiln removes `\r` from both the expected and actual values.
739
+ *
740
+ * Sentence combinations that will work:
741
+ * the text in the JSON variable "variable_with_text_value" should be
742
+ * the text in the JSON var "variable_with_text_value" should be
743
+ * the text for "variable_with_text_value" should be
744
+ * text "variable_with_text_value" should be
745
+ * */
730
746
 
731
747
  // Get JSON variable value
732
748
  let { json_path, json } = await scope.savePageJSONData( scope );
749
+
750
+ // docassemble (at least sometimes) makes new lines with `\r\n`
733
751
  let actual_value = json.variables[ var_name ];
752
+ actual_value = actual_value.replace(/\r/, ``);
753
+ // Normalize `expected_value` as well for backwards compatibility in case any
754
+ // authors are already compensating for docassemble
755
+ expected_value = expected_value.replace(/\r/, ``);
734
756
 
735
757
  // Add warning to report if value is not as expected
736
758
  if ( actual_value !== expected_value ) {
737
759
  reports.addToReport( scope, {
738
760
  type: `error`,
739
761
  code: `ALK0083`,
740
- value: `The variable "${ var_name }" was not equal to the expected `
741
- + `value on the "${ scope.page_id }" screen. Check `
742
- + `${ json_path } to see the page's JSON variables.`
762
+ value: `The variable "${ var_name }" was different from the expected `
763
+ + `value on the "${ scope.page_id }" screen.`
764
+ + `The expected values was\n` + expected_value
765
+ + `\nwhile the actual value was\n` + actual_value
743
766
  });
744
767
  }
745
768
 
@@ -1044,6 +1067,22 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
1044
1067
 
1045
1068
  });
1046
1069
 
1070
+ When(/I set the var(?:iable)? "([^"]+)" to\s*$/, { timeout: -1 }, async ( var_name, answer ) => {
1071
+ /** Set a non-story table variable (with or without a choice associated with
1072
+ * it) to a value that can have new lines in it. Most useful for
1073
+ * `textarea`s.
1074
+ *
1075
+ * Example:
1076
+ * I set the var "benefits['SSI']" to
1077
+ * """
1078
+ * Some multline
1079
+ * value
1080
+ * """
1081
+ * */
1082
+ // `page` timeout will deal with custom timeout
1083
+ await scope.steps.set_regular_var( scope, var_name, answer )
1084
+ });
1085
+
1047
1086
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
1048
1087
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
1049
1088
  // This could theoretically tap an element, so
@@ -1152,6 +1191,22 @@ When(/I set the address of ?(?:the var(?:iable)?)? "([^"]+)" to "([^"]+)"/, { ti
1152
1191
  // Next macro - download sequence?
1153
1192
 
1154
1193
 
1194
+ //#####################################
1195
+ //#####################################
1196
+ // Messages from ALKiln to author
1197
+ //#####################################
1198
+ //#####################################
1199
+
1200
+ const LOG_CODE_REGEX = /ALKiln\d\d\d\d/;
1201
+ Given(/ALKiln warns the author/i, async ( warnings_text ) => {
1202
+ /** Create a warning message for the author files they wrote to generate
1203
+ * constrained random answers tests.
1204
+ *
1205
+ * @param {str} warnings_text - Warning text generated by ALKiln
1206
+ * */
1207
+ reports.addToReport( scope, { type: `as_is`, value: warnings_text });
1208
+ })
1209
+
1155
1210
 
1156
1211
  //#####################################
1157
1212
  //#####################################
@@ -1189,6 +1244,8 @@ Given(/the Scenario report should not include/i, async ( report_text ) => {
1189
1244
  Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status ) => {
1190
1245
  /** WARNING: If a Step will fail, this Step must come first.
1191
1246
  *
1247
+ * TODO: Status "A" ambiguous (internal)
1248
+ *
1192
1249
  * Prepare to test that the Scenario completes with the correct status. */
1193
1250
  scope.expected_status = expected_status.toUpperCase();
1194
1251
  });
@@ -1272,6 +1329,8 @@ AfterStep(async function({ result }) {
1272
1329
  else if ( result.status === `UNDEFINED` ) { log.stdout({ records_only: true }, `U`); }
1273
1330
  else if ( result.status === `SKIPPED` ) { log.stdout({ records_only: true }, `-`); }
1274
1331
  else if ( result.status === `PENDING` ) { log.stdout({ records_only: true }, `P`); }
1332
+
1333
+ reports.outdent();
1275
1334
  });
1276
1335
 
1277
1336
  After(async function(scenario) {