@suffolklitlab/alkiln 5.8.1 → 5.9.0-feat-url2

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/lib/steps.js CHANGED
@@ -106,6 +106,12 @@ BeforeAll(async () => {
106
106
  });
107
107
 
108
108
  Before(async (scenario) => {
109
+
110
+ // Useful for those with a lot of tests
111
+ // Is unable to print the first Scenario name for some reason
112
+ console.log('');
113
+ process.stdout.write(`Scenario: ${ scenario.pickle.name }: `);
114
+
109
115
  // Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
110
116
  if (!scope.browser) {
111
117
  if (session_vars.get_origin() === 'playground') {
@@ -171,12 +177,14 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
171
177
  // Load the right interview page
172
178
  await scope.load( scope, file_name );
173
179
 
180
+ // TODO: Move this above loading? Put it in `Before()` instead?
174
181
  // The page timeout is set in here too in case someone set a custom timeout
175
182
  // before a page was loaded.
176
183
  // The function itself won't really timeout here, so we don't need to wrap it in
177
184
  // a custom timeout
178
185
  await scope.page.setDefaultTimeout( scope.timeout ); // overrides outer default timeout
179
186
 
187
+ // TODO: Move this to the `download` Step and only do it once per scenario
180
188
  // Allow developer to save download files
181
189
  let custom_download_behavior_timeout = new Promise(async ( resolve, reject ) => {
182
190
  let page_timeout = setTimeout(function() {reject( `Took too long to set download behavior.` ); }, scope.timeout);
@@ -230,6 +238,8 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
230
238
  } // ends if this is the file the developer expected
231
239
  }); // ends page.on 'response'
232
240
 
241
+ // TODO: Wrap everything in this step in this timeout? Or is this Step's timeout different?
242
+ // Wait for navigation
233
243
  let custom_afterStep_timeout = new Promise(async ( resolve, reject ) => {
234
244
  let page_timeout = setTimeout(function() { reject(`\`afterStep()\` took too long.`); }, scope.timeout);
235
245
  // self-invoke an anonymous async function so we don't have to
@@ -245,6 +255,22 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
245
255
  await custom_afterStep_timeout;
246
256
  });
247
257
 
258
+ Given(/I go to "([^"]+)"/i, {timeout: -1}, async ( url ) => {
259
+ /** Navigate to an arbitrary url. What you do after that depends on you. There's
260
+ * a limited amount ALKiln can do on a non-docassemble page or a very customized
261
+ * docassemble page.
262
+ *
263
+ * @params {string} A valid url */
264
+ let result = await wrapPromiseWithTimeout(
265
+ scope.page.goto( url, { waitUntil: 'domcontentloaded', timeout: scope.timeout }),
266
+ scope.timeout,
267
+ `It took to long to get to "${ url }"`
268
+ );
269
+
270
+ reports.addToReport(scope, { type: `info`, value: `Successfully went to "${ url }"` });
271
+ return result;
272
+ });
273
+
248
274
  Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)" and(?: the password)? "([^"]+)"(?: SECRETs)?/i, {timeout: -1}, async ( email, password ) => {
249
275
  /** Uses the names of environment variables (most often GitHub SECRETs) to
250
276
  * log into an account on the user's server. Must be SECRETs to stay secure.
@@ -297,7 +323,9 @@ Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+
297
323
  // Story
298
324
  //#####################################
299
325
  //#####################################
300
-
326
+ // TODO: Consider more permissive sentences:
327
+ // with(?: this data| these answers)?:?
328
+ // with(?:.*):?
301
329
  When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)" with this data:?/, {timeout: -1}, async (target_id, raw_var_data) => {
302
330
  /* NO CUCUMBER TIMEOUT. Must handle timeout ourselves:
303
331
  * https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/timeouts.md#disable-timeouts
@@ -341,6 +369,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
341
369
  try {
342
370
  let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
343
371
 
372
+ // THIS FUNCTION ACTUALLY MATTERS
344
373
  // Add special rows. Basically, do weirder things.
345
374
  // TODO: Remove from_story_table - no longer needed if only called from in here
346
375
  let ensured_var_data = await scope.ensureSpecialRows( scope, {
@@ -352,8 +381,10 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
352
381
  console.log( `Rows, including special rows:`, JSON.stringify( ensured_var_data ));
353
382
  }
354
383
 
384
+ // THIS FUNCTION ACTUALLY MATTERS
355
385
  // Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
356
386
  let error = await scope.setFields(scope, { var_data: ensured_var_data, id, ensure_navigation: true });
387
+
357
388
  if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
358
389
  resolve();
359
390
  } catch ( err ) {
@@ -415,6 +446,123 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
415
446
  reports.addToReport(scope, { type: `note`, value: `` });
416
447
  });
417
448
 
449
+ // When(/^foo id "([^"]+)"/, {timeout: -1}, async (target_id, raw_var_data) => {
450
+ // /* NO CUCUMBER TIMEOUT. Must handle timeout ourselves:
451
+ // * https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/timeouts.md#disable-timeouts
452
+ // * Iterate in here to keep timeout reasonable */
453
+
454
+ // // Note: Why do we not just stop when we've used all the vars in the table or
455
+ // // reach the last var? Vars are not required to be given in sequence and pages that
456
+ // // have non-button fields don't usually have a var assigned to the 'continue' button.
457
+ // // Alternative: require vars even for 'continue' pages. Then in loop require at least one var
458
+ // // to be found on a page before hitting 'continue'. That requires detecting navigation because
459
+ // // pages with fields will not have a var for the continue button, so there will
460
+ // // still have to be some automatic continuing.
461
+ // // It also seems harsh, but more reliable.
462
+ // // Ask developers what they'd like.
463
+
464
+
465
+ // let supported_table = await scope.normalizeTable( scope, { raw_var_data });
466
+
467
+ // let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
468
+ // // Until we reach the target page, hit an error, or time out (take too long)
469
+ // while ( !id_matches ) {
470
+ // let custom_timeout = new Promise(function( resolve, reject ) {
471
+
472
+ // // Set up the timeout that avoids infinite loops
473
+ // let on_page_timeout = async function() {
474
+
475
+ // let non_reload_report_msg = `The target id was "${ target_id }", but it took `
476
+ // + `too long to try to reach it (over ${ scope.timeout/1000/60 } min). The `
477
+ // + `test got stuck on "${ id }".`
478
+ // let error = non_reload_report_msg;
479
+ // await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
480
+ // }; // Ends on_page_timeout()
481
+ // let page_timeout_id = setTimeout(on_page_timeout, scope.timeout);
482
+
483
+ // // self-invoke an anonymous async function so we don't have to
484
+ // // abstract the details of resolving and rejecting.
485
+ // // If we need to call recursively, signature function might look like:
486
+ // // func( scope, { resolve, reject, supported_table, page_timeout_id })
487
+ // // Maybe list of timeout and/or interval ids in state that can all be cleared at once?
488
+ // (async function() {
489
+ // try {
490
+ // let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
491
+
492
+ // // Add special rows. Basically, do weirder things.
493
+ // // TODO: Remove from_story_table - no longer needed if only called from in here
494
+ // let ensured_var_data = await scope.ensureSpecialRows( scope, {
495
+ // var_data: supported_table,
496
+ // from_story_table: true,
497
+ // });
498
+
499
+ // if ( session_vars.get_debug() ) {
500
+ // console.log( `Rows, including special rows:`, JSON.stringify( ensured_var_data ));
501
+ // }
502
+
503
+ // // Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
504
+ // let error = await scope.setFields(scope, { var_data: ensured_var_data, id, ensure_navigation: true });
505
+ // if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
506
+ // resolve();
507
+ // } catch ( err ) {
508
+ // let err_msg = `Error occurred when tried to answer fields on question "${ id }".`
509
+ // reports.addToReport( scope, { type: `error`, value: err_msg });
510
+ // // Throw error instead?
511
+ // reject( err );
512
+ // } finally {
513
+ // clearTimeout( page_timeout_id ); // prevent temporary hang at end of tests
514
+ // }
515
+ // })();
516
+ // }); // ends custom_timeout for setFields
517
+
518
+ // await custom_timeout;
519
+
520
+ // // Prep for next loop
521
+ // ({ question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id ));
522
+
523
+ // } // ends while !id_matches
524
+ // // Can anything unsatisfactory get through here?
525
+
526
+ // // Add report strings from supported table, not an altered one
527
+ // for ( let row of supported_table ) {
528
+ // row.report_str = `${ reports.convertToOriginalStoryTableRow(scope, { row_data: row }) }`;
529
+ // }
530
+ // // Sort into alphabetical order, as is recommended for writing story tables
531
+ // let sorted_rows = supported_table.sort( function ( row1, row2 ) {
532
+ // if( row1.report_str < row2.report_str ) { return -1; }
533
+ // if( row1.report_str > row2.report_str ) { return 1; }
534
+ // return 0;
535
+ // });
536
+
537
+ // // Add full used table to report so it can be copied if needed.
538
+ // // TODO: Should we/How do we get this in the final scenario report instead of here?
539
+ // reports.addToReport( scope, { type: `note`, value: `\n${ ' '.repeat(2) }Rows that got set:` });
540
+ // reports.addToReport( scope, { type: `step`, value: `${ ' '.repeat(4) }And I get to the question id "${ target_id }" with this data:` });
541
+ // reports.addToReport(scope, { type: `row`, value: `${ ' '.repeat(6) }| var | value | trigger |`, });
542
+ // let at_least_one_row_was_NOT_used = false;
543
+ // for ( let row of sorted_rows ) {
544
+ // if ( row.times_used > 0 ) {
545
+ // reports.addToReport(scope, { type: `row`, value: row.report_str, });
546
+ // } else {
547
+ // at_least_one_row_was_NOT_used = true;
548
+ // }
549
+ // }
550
+
551
+ // // Add unused rows if needed
552
+ // if ( at_least_one_row_was_NOT_used ) {
553
+ // reports.addToReport( scope, { type: `note`, value: `${ ' '.repeat(2) }Unused rows:` });
554
+ // for ( let row of sorted_rows ) {
555
+ // if ( row.times_used === 0 ) {
556
+ // reports.addToReport(scope, { type: `row`, value: row.report_str, });
557
+ // }
558
+ // }
559
+ // } else {
560
+ // reports.addToReport(scope, { type: `note`, value: `${ ' '.repeat(2) }All rows were used` });
561
+ // }
562
+
563
+ // reports.addToReport(scope, { type: `note`, value: `` });
564
+ // });
565
+
418
566
  //#####################################
419
567
  //#####################################
420
568
  // Passive/Observational
@@ -1122,8 +1270,8 @@ Given(/the Scenario report should not include/i, async ( report_text ) => {
1122
1270
 
1123
1271
  });
1124
1272
 
1125
- Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected_status ) => {
1126
- /* WARNING: If a Step will fail, this Step must come first.
1273
+ Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status ) => {
1274
+ /** WARNING: If a Step will fail, this Step must come first.
1127
1275
  *
1128
1276
  * Prepare to test that the Scenario completes with the correct status. */
1129
1277
  scope.expected_status = expected_status.toUpperCase();
@@ -1138,6 +1286,12 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
1138
1286
 
1139
1287
  After(async function(scenario) {
1140
1288
 
1289
+ // Log errors
1290
+ if ( scenario.result.message ) {
1291
+ log.debug({ type: `error`, pre: scenario.result.message });
1292
+ // log.debug({ type: `error`, pre: scenario.result.message, verbose: true });
1293
+ }
1294
+
1141
1295
  let changeable_test_status = scenario.result.status;
1142
1296
  // The accessibility failures don't happen til the end, so the rest of the test can continue
1143
1297
  if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
@@ -1199,39 +1353,46 @@ After(async function(scenario) {
1199
1353
 
1200
1354
  } // ends if not passed
1201
1355
 
1202
- // Internal tests of this framework
1203
- if ( scope.expected_status ) {
1204
- let expected = scope.expected_status;
1205
- scope.expected_status = null; // reset for next Scenario
1206
- expect( changeable_test_status ).to.equal( expected );
1207
- // If the status was as expected, pass this criteria
1208
- changeable_test_status = `PASSED`;
1209
- }
1210
-
1356
+ // Add report text to Scenario folder after everything has been added to the report
1357
+ let current_report_obj = scope.report.get( scope.scenario_id );
1358
+ let scenario_report_obj = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
1359
+ let report = reports.getPrintableScenario( scenario_report_obj );
1360
+ // Save the report as in the Scenario folder
1361
+ fs.writeFileSync( scope.paths.scenario_report, report );
1362
+
1363
+ // ---------------- Check internal test results of this framework ----------------
1364
+
1211
1365
  if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
1212
- let expected = scope.expected_in_report;
1366
+ let report_includes_all_expected_strings = await scope.reportIncludesAllExpected(
1367
+ scope,
1368
+ { expected: scope.expected_in_report }
1369
+ );
1370
+ if ( !report_includes_all_expected_strings ) { changeable_test_status = `FAILED`; }
1213
1371
  // Reset report values no matter what so they don't mess up future scenarios
1214
1372
  scope.expected_in_report = null;
1215
- let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
1216
-
1217
- if ( all_were_included ) { changeable_test_status = `PASSED`; }
1218
- else { changeable_test_status = `FAILED`; }
1219
1373
  }
1220
1374
 
1221
1375
  if (scope.expected_not_in_report && scope.expected_not_in_report.length > 0) {
1222
- let not_expected = scope.expected_not_in_report;
1376
+ let all_prohibited_strings_were_absent = await scope.reportDoesNotInclude(
1377
+ scope,
1378
+ { not_expected: scope.expected_not_in_report }
1379
+ );
1380
+ if ( !all_prohibited_strings_were_absent ) { changeable_test_status = `FAILED`;}
1223
1381
  // Reset report values no matter what so they don't mess up future scenarios
1224
1382
  scope.expected_not_in_report = null;
1225
- let all_were_not_there = await scope.reportDoesNotInclude( scope, { not_expected });
1226
-
1227
- if ( all_were_not_there ) {
1228
- changeable_test_status = `PASSED`;
1229
- } else {
1230
- changeable_test_status = `FAILED`;
1231
- }
1383
+ }
1232
1384
 
1385
+ // The default is 'passed', which won't need to get in here
1386
+ if ( scope.expected_status ) {
1387
+ // When internal tests should fail (to test our failure conditions), passing _is_ failing
1388
+ expect( changeable_test_status ).to.equal( scope.expected_status );
1389
+ // If the status was as expected, pass this Scenario
1390
+ changeable_test_status = `PASSED`;
1391
+ scope.expected_status = null; // reset for next Scenario
1233
1392
  }
1234
1393
 
1394
+ // ---------------- Ends check internal test results of this framework ----------------
1395
+
1235
1396
  // If there is a page open, then sign out and close it
1236
1397
  if ( scope.page ) {
1237
1398
  // Catch a possible reload error.
@@ -1259,23 +1420,19 @@ After(async function(scenario) {
1259
1420
 
1260
1421
  } // ends if scope.page
1261
1422
 
1262
- // Add report text to Scenario folder after everything has been added to the report
1263
- let current_report_obj = scope.report.get( scope.scenario_id );
1264
- let scenario_report_obj = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
1265
- let report = reports.getPrintableScenario( scenario_report_obj );
1266
- // Save the report as in the Scenario folder
1267
- fs.writeFileSync( scope.paths.scenario_report, report );
1268
-
1269
1423
  if ( changeable_test_status === `FAILED` ) {
1270
1424
  // If the result object status wasn't failure and we still want
1271
1425
  // the tests to fail, as with a11y tests, we can't mess with the
1272
1426
  // cucumber result.status object because it will cause unwanted
1273
1427
  // errors. We have to throw an error here instead.
1274
1428
  if ( scenario.result.status !== `FAILED` ) {
1275
- throw new Error(`The test failed. See the report for more information.`);
1429
+ throw new Error(`The test failed. See the report above for more information.`);
1276
1430
  }
1277
1431
  } else {
1278
- // Other types of errors should be ok
1432
+ // Some internal test failures should be ok
1433
+ // TODO: Look into setDefinitionFunctionWrapper to catch errors
1434
+ // of failing tests that should indeed be failing:
1435
+ // https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/api_reference.md#setdefinitionfunctionwrapperwrapper
1279
1436
  scenario.result.status = changeable_test_status;
1280
1437
  }
1281
1438
  });
package/lib/utils/log.js CHANGED
@@ -40,14 +40,25 @@ log.error = function ({ type='', pre='', data='', post='' }) {
40
40
  if ( post ) { console.error( post, `\n` ); }
41
41
  };
42
42
 
43
- log.debug = function ({ type='', pre='', data='', post='' }) {
43
+ log.debug = function ({ type='', pre='', data='', post='', verbose=false }) {
44
44
  /* If debugging is turned on, log a message that includes `ALKiln` and `debug` */
45
- if ( session_vars.get_debug() ) {
45
+ if ( session_vars.get_debug() || verbose ) {
46
46
  let start = create_msg_start([ type, 'debug' ], pre );
47
+ let full = `\n${ start }`;
47
48
  // Log them each on a separate line
48
- console.log( `ALKiln ${ type } debug: ${ pre }` );
49
- if ( data ) { console.log( data ); }
50
- if ( post ) { console.log( post ); }
49
+ console.log( `\n${ start }` );
50
+ if ( data ) {
51
+ full += `\n${ data }`;
52
+ console.log( data );
53
+ }
54
+ if ( post ) {
55
+ full += `\n${ post }`;
56
+ console.log( post );
57
+ }
58
+ // TODO: Make a verbose log instead of adding to the debug one and always add to it
59
+ if ( verbose ) {
60
+ log.add_to_debug_log( full );
61
+ }
51
62
  }
52
63
  };
53
64
 
@@ -294,7 +294,7 @@ session_vars.set_sources_paths = function ( paths ) {
294
294
  }
295
295
 
296
296
  console.info(
297
- `ALKiln info: Did find "sources" folder(s) at "${ existing_paths.join('", "') }".`
297
+ `ALKiln SUCCESS: Did find "sources" file(s) at "${ existing_paths.join('", "') }".`
298
298
  )
299
299
 
300
300
  // Otherwise, store valid paths
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.8.1",
3
+ "version": "5.9.0-feat-url2",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {