@suffolklitlab/alkiln 5.12.0 → 5.13.0-logs2
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/.env.example +29 -0
- package/.github/workflows/github_server.yml +59 -12
- package/.github/workflows/playground.yml +11 -4
- package/CHANGELOG.md +41 -1
- package/CONTRIBUTING.md +98 -83
- package/README.md +1 -1
- package/action.yml +69 -29
- package/action_for_github_server/action.yml +65 -31
- package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +46 -0
- package/docs/decisions/adopt_architectural_decision_records.md +58 -0
- package/docs/decisions/detect_sign_in.md +124 -0
- package/docs/decisions/pdfjs_version_2024_08_08.md +18 -0
- package/docs/decisions/reports_and_logs_2024_08_11.md +141 -0
- package/docs/decisions/update_puppeteer_2024_07_28.md +62 -0
- package/docs/story_tables.md +1 -1
- package/lib/docassemble/docassemble_api_REST.js +67 -1
- package/lib/docassemble/docassemble_api_interface.js +225 -18
- package/lib/docassemble/server_install.js +23 -9
- package/lib/docassemble/setup.js +33 -20
- package/lib/docassemble/takedown.js +27 -7
- package/lib/run_cucumber.js +83 -33
- package/lib/scope.js +222 -82
- package/lib/steps.js +206 -62
- package/lib/utils/files.js +8 -6
- package/lib/utils/log.js +477 -84
- package/lib/utils/reports.js +14 -5
- package/lib/utils/session_vars.js +25 -18
- package/lib/utils/time.js +44 -0
- package/npm-shrinkwrap.json +635 -193
- package/package.json +8 -8
- package/setup.py +1 -1
- package/tests/log_codes/check_codes.sh +163 -108
- package/tests/log_codes/log_code_expected_instances.txt +213 -0
- package/tests/unit_tests/getAllFields.test.js +5 -0
- package/tests/unit_tests/getMatchingRows.test.js +5 -0
- package/tests/unit_tests/log.test.js +20 -0
- package/tests/unit_tests/normalizeTable.test.js +5 -0
- package/tests/log_codes/deleted_codes.json +0 -8
package/lib/steps.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
Given,
|
|
6
6
|
After,
|
|
7
7
|
Before,
|
|
8
|
+
AfterStep,
|
|
8
9
|
BeforeAll,
|
|
9
10
|
AfterAll,
|
|
10
11
|
setDefaultTimeout //,
|
|
@@ -18,10 +19,11 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
18
19
|
|
|
19
20
|
// Ours
|
|
20
21
|
const scope = require('./scope');
|
|
21
|
-
const
|
|
22
|
+
const Log = require(`./utils/log`);
|
|
22
23
|
const session_vars = require('./utils/session_vars');
|
|
23
24
|
const files = require('./utils/files' );
|
|
24
25
|
const reports = require(`./utils/reports`);
|
|
26
|
+
const da_api = require('./docassemble/docassemble_api_interface');
|
|
25
27
|
|
|
26
28
|
/* Of Note:
|
|
27
29
|
- We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
|
|
@@ -81,8 +83,13 @@ let click_with = {
|
|
|
81
83
|
pc: 'click',
|
|
82
84
|
};
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
let log = null;
|
|
87
|
+
|
|
88
|
+
BeforeAll(async function() {
|
|
85
89
|
scope.showif_timeout = 600;
|
|
90
|
+
// TODO: Do these really need to be maps? As sets, they may make the output
|
|
91
|
+
// more predictable to the user, but they add complexity.
|
|
92
|
+
scope.scenarios = new Map();
|
|
86
93
|
scope.report = new Map();
|
|
87
94
|
|
|
88
95
|
// Start tracking whether server is up and running
|
|
@@ -91,29 +98,29 @@ BeforeAll(async () => {
|
|
|
91
98
|
// Store path names for files and docs created for the tests
|
|
92
99
|
scope.paths = {};
|
|
93
100
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
//
|
|
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
|
+
// A little indirect, but the artifacts path should always have already been
|
|
107
|
+
// 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` });
|
|
110
|
+
scope.set_log({ log_obj: log });
|
|
102
111
|
|
|
103
|
-
scope.paths.
|
|
112
|
+
scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_filename }`;
|
|
113
|
+
scope.paths.report_log = `${ scope.paths.artifacts }/${ log.report_log_filename }`;
|
|
114
|
+
scope.paths.report = `${ scope.paths.artifacts }/${ log.report_filename }`;
|
|
104
115
|
scope.paths.failures = `${ scope.paths.artifacts }/${ log.unexpected_filename }`;
|
|
105
|
-
scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_file }`;
|
|
106
|
-
fs.writeFileSync( scope.paths.debug_log, "" );
|
|
107
116
|
|
|
108
117
|
reports.create( scope );
|
|
109
118
|
});
|
|
110
119
|
|
|
111
120
|
Before(async (scenario) => {
|
|
112
121
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
console.log('');
|
|
116
|
-
process.stdout.write(`Scenario: ${ scenario.pickle.name }: `);
|
|
122
|
+
// Start the running "progress bar" for the Scenario
|
|
123
|
+
log.stdout({}, `\nScenario: ${ scenario.pickle.name }: `);
|
|
117
124
|
|
|
118
125
|
// Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
|
|
119
126
|
if (!scope.browser) {
|
|
@@ -127,13 +134,16 @@ Before(async (scenario) => {
|
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
// Clean up all previously existing pages
|
|
130
|
-
for (const page of await scope.browser.pages()) {
|
|
137
|
+
for (const page of await scope.browser.pages()) {
|
|
131
138
|
await page.close();
|
|
132
139
|
}
|
|
133
140
|
// Make a new page
|
|
134
141
|
scope.page = await scope.browser.newPage()
|
|
135
142
|
|
|
136
143
|
scope.scenario_id = uuidv4();
|
|
144
|
+
scope.scenarios.set( scope.scenario_id, {} );
|
|
145
|
+
scope.scenarios.get( scope.scenario_id ).interviews = {};
|
|
146
|
+
scope.scenarios.get( scope.scenario_id ).api_keys = [];
|
|
137
147
|
scope.expected_status = null;
|
|
138
148
|
scope.expected_in_report = null;
|
|
139
149
|
scope.expected_not_in_report = null;
|
|
@@ -179,7 +189,7 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
|
|
|
179
189
|
|
|
180
190
|
// Load the right interview page
|
|
181
191
|
await scope.load( scope, file_name );
|
|
182
|
-
|
|
192
|
+
|
|
183
193
|
// The page timeout is set in here too in case someone set a custom timeout
|
|
184
194
|
// just before a page was loaded for the Scenario. Move this to just after page creation?
|
|
185
195
|
// The function itself won't really timeout here, so we don't need to wrap it in
|
|
@@ -274,21 +284,27 @@ Given(/I go to "([^"]+)"/i, {timeout: -1}, async ( url ) => {
|
|
|
274
284
|
return result;
|
|
275
285
|
});
|
|
276
286
|
|
|
277
|
-
Given(
|
|
287
|
+
Given(
|
|
288
|
+
/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)",?(?: and)?(?: the password)? "([^"]+)"(?: SECRETs)?(?:,?(?: and)?(?: the API key)? "([^"]+)")?/i,
|
|
289
|
+
{ timeout: -1 },
|
|
290
|
+
async ( email, password, api_key ) => {
|
|
278
291
|
/** Uses the names of environment variables (most often GitHub SECRETs) to
|
|
279
|
-
* log into an account on the user's server. Must be
|
|
292
|
+
* log into an account on the user's server. Must be secure.
|
|
280
293
|
*
|
|
281
294
|
* Variations:
|
|
282
|
-
* I sign into the server with the email "
|
|
283
|
-
* I sign on with the email "
|
|
284
|
-
* I log in with "
|
|
295
|
+
* I sign into the server with the email "USER2_EMAIL" and the password "USER2_PASSWORD"
|
|
296
|
+
* I sign on with the email "USER2_EMAIL" and the password "USER2_PASSWORD"
|
|
297
|
+
* I log in with "USER2_EMAIL" and "USER2_PASSWORD"
|
|
298
|
+
* I sign in with the email "USER2_EMAIL", the password "USER2_PASSWORD", and the API key "USER2_API_KEY"
|
|
299
|
+
* I sign in with "USER2_EMAIL", "USER2_PASSWORD", "USER2_API_KEY"
|
|
285
300
|
*/
|
|
286
301
|
// `page` timeout will deal with custom timeout
|
|
287
302
|
// Couldn't test the timeout for tapping the button because there's not
|
|
288
303
|
// enough of a pause between initial navigation and pressing button.
|
|
289
304
|
await scope.steps.sign_in( scope, {
|
|
290
305
|
email_secret_name: email,
|
|
291
|
-
password_secret_name: password
|
|
306
|
+
password_secret_name: password,
|
|
307
|
+
api_key_secret_name: api_key
|
|
292
308
|
});
|
|
293
309
|
});
|
|
294
310
|
|
|
@@ -313,7 +329,7 @@ Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+
|
|
|
313
329
|
/* At any time, set a custom timeout for the scenario. */
|
|
314
330
|
// TODO: Add 1 second to this to allow other steps to not fail, like "I wait n seconds"
|
|
315
331
|
scope.timeout = parseInt( seconds ) * 1000;
|
|
316
|
-
|
|
332
|
+
|
|
317
333
|
// Cucumber timeouts are made custom for each step that needs it
|
|
318
334
|
|
|
319
335
|
// If there is no page yet, we've ensured custom timeout will still be set
|
|
@@ -383,11 +399,9 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
|
|
|
383
399
|
from_story_table: true,
|
|
384
400
|
});
|
|
385
401
|
|
|
386
|
-
log.debug({
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
pre: `Rows, including special rows:, ${JSON.stringify( ensured_var_data )}`,
|
|
390
|
-
});
|
|
402
|
+
log.debug({ code: `ALK0061`, context: `table`, },
|
|
403
|
+
`Rows, including special rows:`, ensured_var_data
|
|
404
|
+
);
|
|
391
405
|
|
|
392
406
|
// THIS FUNCTION ACTUALLY MATTERS
|
|
393
407
|
// Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
|
|
@@ -637,7 +651,7 @@ Then('I arrive at the next page', async () => { // √
|
|
|
637
651
|
// Link observations have the 'Escape' button link in mind
|
|
638
652
|
Then('I should see the link {string}', async ( linkText ) => { // √
|
|
639
653
|
/* Loosely match link visible text. Should probably deprecate this. */
|
|
640
|
-
let [link] = await scope.page
|
|
654
|
+
let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
|
|
641
655
|
|
|
642
656
|
let msg = `Cannot find a link with the text "${ linkText }".`;
|
|
643
657
|
if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0079`, value: msg }); }
|
|
@@ -653,16 +667,16 @@ Then('I should see the link to {string}', async ( url ) => { // √
|
|
|
653
667
|
let msg = `Cannot find a link to "${ url }".`;
|
|
654
668
|
if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0080`, value: msg }); }
|
|
655
669
|
expect(link, msg ).to.exist;
|
|
656
|
-
|
|
670
|
+
|
|
657
671
|
await scope.afterStep(scope);
|
|
658
672
|
});
|
|
659
673
|
|
|
660
674
|
Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) => { // √
|
|
661
675
|
/* Find link by its visible text and where it leads. */
|
|
662
|
-
let [link] = await scope.page
|
|
676
|
+
let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
|
|
663
677
|
// TODO: Possibly add a check here for the link text, though note this
|
|
664
678
|
// Step is not currently supported.
|
|
665
|
-
|
|
679
|
+
|
|
666
680
|
let prop_obj = await link.getProperty('href');
|
|
667
681
|
let actual_url = await prop_obj.jsonValue();
|
|
668
682
|
|
|
@@ -674,7 +688,7 @@ Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) =>
|
|
|
674
688
|
});
|
|
675
689
|
|
|
676
690
|
Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkText, which_window) => { // √
|
|
677
|
-
let [link] = await scope.page
|
|
691
|
+
let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
|
|
678
692
|
|
|
679
693
|
let prop_obj = await link.getProperty('target');
|
|
680
694
|
let target = await prop_obj.jsonValue();
|
|
@@ -756,7 +770,7 @@ Then(/I expect the baseline PDF "([^"]+)" and the new PDF "([^"]+)" to be the sa
|
|
|
756
770
|
* Variations:
|
|
757
771
|
* Then I expect the baseline PDF "simple-doc-Baseline.pdf" and the new PDF "simple-doc.pdf" to be the same
|
|
758
772
|
*/
|
|
759
|
-
|
|
773
|
+
|
|
760
774
|
return await wrapPromiseWithTimeout(
|
|
761
775
|
scope.steps.compare_pdfs(scope, {existing_pdf_path, new_pdf_path}),
|
|
762
776
|
scope.timeout
|
|
@@ -880,7 +894,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
880
894
|
|
|
881
895
|
// Set up timeout to stop infinite loops
|
|
882
896
|
let page_timeout_id = setTimeout(async function() {
|
|
883
|
-
|
|
897
|
+
|
|
884
898
|
// Make sure to only throw once
|
|
885
899
|
if ( has_thrown ) { return; }
|
|
886
900
|
else { has_thrown = true; }
|
|
@@ -959,7 +973,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
959
973
|
} // ends if !has_content
|
|
960
974
|
|
|
961
975
|
if ( buttons_exist ) {
|
|
962
|
-
|
|
976
|
+
log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button
|
|
963
977
|
}
|
|
964
978
|
|
|
965
979
|
resolve();
|
|
@@ -1178,6 +1192,71 @@ Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status
|
|
|
1178
1192
|
scope.expected_status = expected_status.toUpperCase();
|
|
1179
1193
|
});
|
|
1180
1194
|
|
|
1195
|
+
Then(/of two interviews, I delete one __internal__/i, async () => {
|
|
1196
|
+
/** Test that an interview does get successfully deleted. This tests whether
|
|
1197
|
+
* the API code is working correctly.
|
|
1198
|
+
*/
|
|
1199
|
+
// Make sure we have two interviews in `scope.scenario.interviews` (two session ids)
|
|
1200
|
+
let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
|
|
1201
|
+
let interview_keys = Object.keys( interviews );
|
|
1202
|
+
expect(
|
|
1203
|
+
interview_keys.length,
|
|
1204
|
+
`Of 2 interview sessions, ALKiln only saved ${ interview_keys.length }`
|
|
1205
|
+
).to.equal( 2 );
|
|
1206
|
+
|
|
1207
|
+
// Check that both interviews exist on the server. 1 √ 2 √
|
|
1208
|
+
let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
|
|
1209
|
+
let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
|
|
1210
|
+
let visible_keys = Object.keys( visible_interviews );
|
|
1211
|
+
expect(
|
|
1212
|
+
visible_keys.length,
|
|
1213
|
+
`2 interviews should be on the server. Instead: "${ visible_keys.join(', ') }"`
|
|
1214
|
+
).to.equal( 2 );
|
|
1215
|
+
|
|
1216
|
+
// Delete one. 1 x 2 √
|
|
1217
|
+
let to_delete = interviews[ interview_keys[ 0 ]];
|
|
1218
|
+
await da_api.quietly_try_to_delete_interviews({
|
|
1219
|
+
interviews: { [interview_keys[ 0 ]]: to_delete },
|
|
1220
|
+
api_keys
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// Check that only one session exists on the server. This tests that
|
|
1224
|
+
// 1. An interview has been deleted and
|
|
1225
|
+
// 2. We are fetching interviews correctly because we are getting one
|
|
1226
|
+
// back (e.g. not passing null into the api function)
|
|
1227
|
+
// 1 x 2 √
|
|
1228
|
+
let after_deletion = await da_api.get_interviews({ interviews, api_keys });
|
|
1229
|
+
let keys_after_deletion = Object.keys( after_deletion );
|
|
1230
|
+
expect(
|
|
1231
|
+
keys_after_deletion.length,
|
|
1232
|
+
`1 interviews should be on the server: ${ keys_after_deletion.join(', ') }`
|
|
1233
|
+
).to.equal( 1 )
|
|
1234
|
+
// In After(), the other session should get deleted
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
Then(/I fail to delete (\d) detected interview(?:s)? and get no error __internal__/i, async ( num ) => {
|
|
1238
|
+
/** Test that there is no error when ALKiln is unable to delete an interview. */
|
|
1239
|
+
num = parseInt( num );
|
|
1240
|
+
// Make sure we started an interview
|
|
1241
|
+
let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
|
|
1242
|
+
expect(
|
|
1243
|
+
Object.keys( interviews ).length,
|
|
1244
|
+
`The test should have saved ${ num } interview session(s)`
|
|
1245
|
+
).to.equal( num );
|
|
1246
|
+
|
|
1247
|
+
// There should be no error about an unauthorized user
|
|
1248
|
+
let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
|
|
1249
|
+
let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
|
|
1250
|
+
let visible_keys = Object.keys( visible_interviews );
|
|
1251
|
+
expect(
|
|
1252
|
+
visible_keys.length,
|
|
1253
|
+
`User should detect 0 interviews on the server. Instead: "${ visible_keys.join(', ') }"`
|
|
1254
|
+
).to.equal( 0 );
|
|
1255
|
+
|
|
1256
|
+
// Deleting with an anonymous user should fail silently
|
|
1257
|
+
await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1181
1260
|
|
|
1182
1261
|
//#####################################
|
|
1183
1262
|
//#####################################
|
|
@@ -1185,11 +1264,22 @@ Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status
|
|
|
1185
1264
|
//#####################################
|
|
1186
1265
|
//#####################################
|
|
1187
1266
|
|
|
1267
|
+
AfterStep(async function({ result }) {
|
|
1268
|
+
// TODO: Abstract this
|
|
1269
|
+
if ( result.status === `PASSED` ) { log.stdout({ records_only: true }, `.`); }
|
|
1270
|
+
else if ( result.status === `FAILED` ) { log.stdout({ records_only: true }, `F`); }
|
|
1271
|
+
else if ( result.status === `UNDEFINED` ) { log.stdout({ records_only: true }, `U`); }
|
|
1272
|
+
else if ( result.status === `SKIPPED` ) { log.stdout({ records_only: true }, `-`); }
|
|
1273
|
+
else if ( result.status === `PENDING` ) { log.stdout({ records_only: true }, `P`); }
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1188
1276
|
After(async function(scenario) {
|
|
1189
1277
|
|
|
1190
1278
|
// Log errors
|
|
1191
1279
|
if ( scenario.result.message ) {
|
|
1192
|
-
log.debug({
|
|
1280
|
+
log.debug({ code: `ALK0091`, context: `scenario`, },
|
|
1281
|
+
scenario.result.message
|
|
1282
|
+
);
|
|
1193
1283
|
}
|
|
1194
1284
|
|
|
1195
1285
|
let changeable_test_status = scenario.result.status;
|
|
@@ -1218,11 +1308,9 @@ After(async function(scenario) {
|
|
|
1218
1308
|
reports.saveFinalScenarioData( scope, { status: changeable_test_status });
|
|
1219
1309
|
|
|
1220
1310
|
if (changeable_test_status !== `PASSED`) {
|
|
1221
|
-
log.debug({
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
pre: `Non-passed status occurred while running test: ${changeable_test_status}`,
|
|
1225
|
-
});
|
|
1311
|
+
log.debug({ code: `ALK0094`, level: `warn`, },
|
|
1312
|
+
`Non-passed status occurred while running test: ${ changeable_test_status }`
|
|
1313
|
+
);
|
|
1226
1314
|
|
|
1227
1315
|
if ( scope.page ) {
|
|
1228
1316
|
|
|
@@ -1314,12 +1402,15 @@ After(async function(scenario) {
|
|
|
1314
1402
|
// it as failed too.
|
|
1315
1403
|
expected_status_was_met = false;
|
|
1316
1404
|
// Keep it general. Users may see this message, e.g. for a11y tests.
|
|
1317
|
-
let msg = `Test
|
|
1318
|
-
+ `
|
|
1405
|
+
let msg = `Test ${ changeable_test_status }. `
|
|
1406
|
+
+ `Expected status: ${scope.expected_status}. `
|
|
1319
1407
|
+ `See the report for details.`;
|
|
1320
1408
|
let code = `ALK0099`;
|
|
1321
|
-
log.debug({
|
|
1322
|
-
|
|
1409
|
+
expected_status_err_msg = log.debug({ code: code, level: `error`, },
|
|
1410
|
+
msg
|
|
1411
|
+
);
|
|
1412
|
+
// Temp for debugging new log output
|
|
1413
|
+
expect( expected_status_err_msg ).to.equal(`🤕 ${ code } ERROR: ${ msg }`);
|
|
1323
1414
|
}
|
|
1324
1415
|
}
|
|
1325
1416
|
scope.expected_status = null; // reset for next Scenario
|
|
@@ -1339,17 +1430,25 @@ After(async function(scenario) {
|
|
|
1339
1430
|
// Make sure that tests which ALKiln has calculated as failed
|
|
1340
1431
|
// cause a cucumber error later. Just changing the status to `FAILED`
|
|
1341
1432
|
// causes unhelpful cucumber errors. TODO: Add to internal docs.
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
});
|
|
1433
|
+
// TODO: Should we move this to where the error is thrown?
|
|
1434
|
+
log.debug({ code: force_failure_code, level: `error`, },
|
|
1435
|
+
force_failure_msg
|
|
1436
|
+
);
|
|
1347
1437
|
// TODO: Look into setDefinitionFunctionWrapper to catch errors
|
|
1348
1438
|
// of failing tests that should indeed be failing:
|
|
1349
1439
|
// https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/api_reference.md#setdefinitionfunctionwrapperwrapper
|
|
1350
1440
|
need_to_force_failed_status = true;
|
|
1351
1441
|
}
|
|
1352
1442
|
|
|
1443
|
+
// Not sure if this is necessary
|
|
1444
|
+
if ( changeable_test_status === 'PASSED' ) {
|
|
1445
|
+
log.stdout({ records_only: true }, `.` );
|
|
1446
|
+
} else if ( changeable_test_status === `SKIPPED` ) {
|
|
1447
|
+
log.stdout({ records_only: true }, `-` );
|
|
1448
|
+
} else {
|
|
1449
|
+
log.stdout({ records_only: true }, changeable_test_status[0].toUpperCase() );
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1353
1452
|
let signout_succeeded = true;
|
|
1354
1453
|
let signout_error_msg = ``;
|
|
1355
1454
|
// If there is a page open, then sign out and close it
|
|
@@ -1372,7 +1471,7 @@ After(async function(scenario) {
|
|
|
1372
1471
|
code: code,
|
|
1373
1472
|
message: non_reload_report_msg,
|
|
1374
1473
|
}, error });
|
|
1375
|
-
} catch {
|
|
1474
|
+
} catch ( error ) {
|
|
1376
1475
|
signout_error_msg = `🤕 ${ code } ERROR: ${ non_reload_report_msg }`;
|
|
1377
1476
|
}
|
|
1378
1477
|
} else {
|
|
@@ -1387,21 +1486,66 @@ After(async function(scenario) {
|
|
|
1387
1486
|
|
|
1388
1487
|
} // ends if scope.page
|
|
1389
1488
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1489
|
+
// Now that we're no longer on the interview page, one way or
|
|
1490
|
+
// another, try to delete the interviews created during the test
|
|
1491
|
+
// Authors running this manually get to choose what to do with
|
|
1492
|
+
// the interviews they're creating unless we hear otherwise.
|
|
1493
|
+
if ( session_vars.get_origin() !== `playground` ) {
|
|
1494
|
+
try {
|
|
1495
|
+
let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
|
|
1496
|
+
let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
|
|
1497
|
+
await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
|
|
1498
|
+
let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
|
|
1499
|
+
let visible_keys = Object.keys( visible_interviews );
|
|
1500
|
+
// Deleting interviews is nice, but not non-critical. Don't bother the
|
|
1501
|
+
// user with it. Don't want to cover up a more meaningful failure (like
|
|
1502
|
+
// going to the wrong file). If it becomes a problem we can track it down later.
|
|
1503
|
+
if ( visible_keys.length > 0 ) {
|
|
1504
|
+
// Can an API key ever see an interview that it can't delete?
|
|
1505
|
+
log.debug({ code: `ALK0197`, context: `internal`, level: `warn`, }
|
|
1506
|
+
`The API key can see ${ visible_keys.length } interviews that were not deleted:`,
|
|
1507
|
+
visible_keys
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
} catch ( error ) {
|
|
1511
|
+
// Deleting interviews is nice, but not non-critical. Don't bother the
|
|
1512
|
+
// user with it. Don't want to cover up a more meaningful failure (like
|
|
1513
|
+
// going to the wrong file). Example of OK failure: the user owns the
|
|
1514
|
+
// interview is different than the API key that's trying to delete the
|
|
1515
|
+
// interview. If it becomes a problem we can track it down later.
|
|
1516
|
+
// Consider add "internal" to docs: "If you see this, make an
|
|
1517
|
+
// issue in ALKiln with your debug/verbose log"
|
|
1518
|
+
log.debug({ code: `ALK0198`, context: `internal`, level: `warn`, error },
|
|
1519
|
+
`Cleaning up interviews incomplete, but unimportant.`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1395
1522
|
}
|
|
1396
1523
|
|
|
1524
|
+
// Collect all the relevant special errors
|
|
1525
|
+
let internal_errors = []
|
|
1526
|
+
if ( !expected_status_was_met ) { internal_errors.push( expected_status_err_msg ); }
|
|
1527
|
+
if ( !signout_succeeded ) { internal_errors.push( signout_error_msg ); }
|
|
1528
|
+
|
|
1397
1529
|
// Theoretically, we should never get here:
|
|
1398
1530
|
// If everything else worked, if we need to force an internal test failure
|
|
1399
1531
|
if ( need_to_force_failed_status ) {
|
|
1400
|
-
|
|
1532
|
+
if ( internal_errors.length > 0 ) {
|
|
1533
|
+
// Handle all the accumulated errors
|
|
1534
|
+
throw new Error( `${internal_errors.join( internal_errors, '\n')}` );
|
|
1535
|
+
}
|
|
1536
|
+
throw new Error( `🤕 ${ force_failure_code } unreachable ERROR: ${ force_failure_msg }` );
|
|
1537
|
+
|
|
1538
|
+
} else if ( internal_errors.length > 0 ) {
|
|
1539
|
+
log.debug({ code: `ALK0203`, context: `internal`, level: `warn`, },
|
|
1540
|
+
`There was no need to force failed status and yet there are error messages.`,
|
|
1541
|
+
internal_errors
|
|
1542
|
+
);
|
|
1401
1543
|
}
|
|
1402
1544
|
|
|
1403
1545
|
// Find the race condition.
|
|
1404
|
-
log.debug({
|
|
1546
|
+
log.debug({ code: `ALK0100`, context: `scenario`, },
|
|
1547
|
+
`Scenario After() message:`, scenario.result.message
|
|
1548
|
+
);
|
|
1405
1549
|
|
|
1406
1550
|
});
|
|
1407
1551
|
|
package/lib/utils/files.js
CHANGED
|
@@ -22,18 +22,20 @@ files.readable_date = function() {
|
|
|
22
22
|
}; // Ends files.readable_date()
|
|
23
23
|
|
|
24
24
|
files.make_artifacts_folder = function (folder_name) {
|
|
25
|
-
/** Makes a folder in the root of the project
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
/** Makes a folder in the root of the project if it doesn't
|
|
26
|
+
* already exist, either with the given name or with a default name.
|
|
27
|
+
*
|
|
28
|
+
* Works for local development as well, without action.yml or workflow files.
|
|
29
|
+
* */
|
|
30
30
|
if (folder_name) {
|
|
31
31
|
folder_name = safe_filename( folder_name );
|
|
32
32
|
} else {
|
|
33
33
|
folder_name = `alkiln-${ files.readable_date() }`
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// `recursive` avoids errors if folder already exists
|
|
37
|
+
// https://nodejs.org/docs/latest-v18.x/api/fs.html#fspromisesmkdirpath-options
|
|
38
|
+
fs.mkdirSync( folder_name, { recursive: true });
|
|
37
39
|
return folder_name;
|
|
38
40
|
}; // Ends files.make_artifacts_folder()
|
|
39
41
|
|