@suffolklitlab/alkiln 5.14.0 → 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.
- package/.github/workflows/github_server.yml +1 -1
- package/.github/workflows/playground.yml +2 -1
- package/CHANGELOG.md +29 -0
- package/action.yml +26 -10
- package/action_for_github_server/action.yml +31 -27
- package/docs/4_environments.md +2 -2
- package/docs/artifacts_folder_name.md +121 -71
- package/lib/docassemble/server_install.js +0 -2
- package/lib/docassemble/setup.js +1 -3
- package/lib/docassemble/takedown.js +2 -2
- package/lib/globals.js +10 -0
- package/lib/run_cucumber.js +40 -53
- package/lib/scope.js +32 -0
- package/lib/setup/TestGenerator.js +1757 -0
- package/lib/setup/artifacts.js +20 -0
- package/lib/setup/generate.js +42 -0
- package/lib/setup/get_Gherkin_AST.js +26 -0
- package/lib/setup/validate.js +89 -0
- package/lib/steps.js +53 -19
- package/lib/utils/Log.js +302 -149
- package/lib/utils/get_error_stacks.js +16 -0
- package/lib/utils/reports.js +43 -23
- package/lib/utils/session_vars.js +77 -57
- package/lib/utils/set_sources_paths.js +98 -0
- package/package.json +18 -7
- package/tests/log_codes/check_codes.sh +17 -13
- package/tests/log_codes/log_code_expected_instances.txt +58 -0
- package/tests/unit_tests/constrained_random_answers/invalid.fixtures.js +178 -0
- package/tests/unit_tests/constrained_random_answers/invalid.test.js +251 -0
- package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +957 -0
- package/tests/unit_tests/constrained_random_answers/valid.test.js +563 -0
- package/tests/unit_tests/log.test.js +17 -41
- package/_alkiln_meta/gherkin_parsed_example.json +0 -1546
- package/_alkiln_meta/shift_logs.js +0 -75
- /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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
|
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
|
|
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
|
|
376
|
-
+ `too long to try to reach
|
|
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
|
|
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 = `
|
|
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
|
-
|
|
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
|
|
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 ) {
|
|
@@ -1177,6 +1191,22 @@ When(/I set the address of ?(?:the var(?:iable)?)? "([^"]+)" to "([^"]+)"/, { ti
|
|
|
1177
1191
|
// Next macro - download sequence?
|
|
1178
1192
|
|
|
1179
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
|
+
|
|
1180
1210
|
|
|
1181
1211
|
//#####################################
|
|
1182
1212
|
//#####################################
|
|
@@ -1214,6 +1244,8 @@ Given(/the Scenario report should not include/i, async ( report_text ) => {
|
|
|
1214
1244
|
Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status ) => {
|
|
1215
1245
|
/** WARNING: If a Step will fail, this Step must come first.
|
|
1216
1246
|
*
|
|
1247
|
+
* TODO: Status "A" ambiguous (internal)
|
|
1248
|
+
*
|
|
1217
1249
|
* Prepare to test that the Scenario completes with the correct status. */
|
|
1218
1250
|
scope.expected_status = expected_status.toUpperCase();
|
|
1219
1251
|
});
|
|
@@ -1297,6 +1329,8 @@ AfterStep(async function({ result }) {
|
|
|
1297
1329
|
else if ( result.status === `UNDEFINED` ) { log.stdout({ records_only: true }, `U`); }
|
|
1298
1330
|
else if ( result.status === `SKIPPED` ) { log.stdout({ records_only: true }, `-`); }
|
|
1299
1331
|
else if ( result.status === `PENDING` ) { log.stdout({ records_only: true }, `P`); }
|
|
1332
|
+
|
|
1333
|
+
reports.outdent();
|
|
1300
1334
|
});
|
|
1301
1335
|
|
|
1302
1336
|
After(async function(scenario) {
|