@suffolklitlab/alkiln 5.13.0 → 5.13.1

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/scope.js CHANGED
@@ -171,7 +171,7 @@ module.exports = {
171
171
  // If something might be hidden or shown, wait extra time to let it hide or show
172
172
  // I think `.$()` does not use a timeout
173
173
  let showif = await scope.page.$('.dashowif');
174
- if ( showif ) { await scope.page.waitForTimeout( scope.showif_timeout ); }
174
+ if ( showif ) { await time.waitForTimeout( scope.showif_timeout ); }
175
175
  },
176
176
 
177
177
  waitForTab: async function waitForTab(scope, { tab_id }) {
@@ -288,9 +288,7 @@ module.exports = {
288
288
  /** Tap a given element, detect navigation, and do appropriate related things.
289
289
  * `click_promise can be any kind of Promise, including a Promise.all()
290
290
  */
291
- let msg = `Cannot find the element to tap.`;
292
- if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0102`, value: msg }); }
293
- expect( elem, msg ).to.exist;
291
+ await scope.guard_against_missing_tap_element(scope, { elem });
294
292
 
295
293
  // Get the text on the button or link for a potential error message later
296
294
  let tapperText = await (await elem.getProperty('textContent')).jsonValue();
@@ -326,6 +324,14 @@ module.exports = {
326
324
  } // ends try/catch
327
325
  }, // Ends scope.tapElementAndStayOnPage()
328
326
 
327
+ guard_against_missing_tap_element: async function(scope, { elem }) {
328
+ /** Handle the error message for a missing tap element, if it's missing. */
329
+ let msg = `Cannot find the element to tap.`;
330
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0102`, value: msg }); }
331
+ expect( elem, msg ).to.exist;
332
+ return elem;
333
+ },
334
+
329
335
  // Handle server reload errors
330
336
  server_statuses: [],
331
337
  server_check_interval_ms: 1 * 1000,
@@ -501,7 +507,7 @@ module.exports = {
501
507
  if ( diff <= 0 ) { return false; }
502
508
 
503
509
  // If time hasn't expired, wait and then check again
504
- await scope.page.waitForTimeout( 100 );
510
+ await time.waitForTimeout( 100 );
505
511
  await scope.detectDownloadComplete(scope, endTime);
506
512
 
507
513
  return false;
@@ -1627,7 +1633,7 @@ module.exports = {
1627
1633
  catch(err){ elem.style.background = 'Khaki'; }
1628
1634
  return elem.getAttribute('for') || elem.getAttribute('name');
1629
1635
  });
1630
- await scope.page.waitForTimeout( 400 );
1636
+ await time.waitForTimeout( 400 );
1631
1637
  }
1632
1638
 
1633
1639
  // We use the last match. We wrote a warning for the user if there were
@@ -1850,7 +1856,7 @@ module.exports = {
1850
1856
  });
1851
1857
  if ( type === `radio` || type === `checkbox` ) {
1852
1858
 
1853
- let [label] = await handle.$x(`following-sibling::label`);
1859
+ let [label] = await handle.$$(`xpath/.//following-sibling::label`);
1854
1860
 
1855
1861
  if ( session_vars.get_debug() ) {
1856
1862
  if ( answer.toLowerCase() === 'false' ) { await label.evaluate( elem => { elem.style.background = 'tomato'; }); }
@@ -2213,7 +2219,7 @@ module.exports = {
2213
2219
  } // ends for every non-submit-button field on the page
2214
2220
  } // ends while fields are still being set
2215
2221
 
2216
- if ( session_vars.get_debug() ) { await scope.page.waitForTimeout(2000); }
2222
+ if ( session_vars.get_debug() ) { await time.waitForTimeout(2000); }
2217
2223
 
2218
2224
  // Press a continue button with a story table variable name if it exists
2219
2225
  let did_navigate = false;
@@ -2561,7 +2567,7 @@ module.exports = {
2561
2567
 
2562
2568
  // Attempt to solve weird race condition issues where page has not properly continued
2563
2569
  while ( curr_url === prev_url && !error.was_found && !timedOut ) {
2564
- await scope.page.waitForTimeout( 50 );
2570
+ await time.waitForTimeout( 50 );
2565
2571
  curr_url = await scope.page.url();
2566
2572
  error = await scope.checkForError( scope );
2567
2573
  }
@@ -2582,10 +2588,10 @@ module.exports = {
2582
2588
 
2583
2589
  // All results are lists to match `$x`
2584
2590
  let possible_errors = {
2585
- complex_user_error: await scope.page.$$('.alert-danger'),
2591
+ complex_user_error: await scope.page.$$(`.alert-danger`),
2586
2592
  // The below does not work if there's not a space between ':' and 'none'
2587
- simple_user_error: await scope.page.$x(`//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`),
2588
- system_error: await scope.page.$x( '//meta[@content="docassemble: Error"]' ),
2593
+ simple_user_error: await scope.page.$$(`xpath/.//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`),
2594
+ system_error: await scope.page.$$(`xpath/.//meta[@content="docassemble: Error"]` ),
2589
2595
  };
2590
2596
 
2591
2597
  let error_handlers = {};
@@ -2597,7 +2603,7 @@ module.exports = {
2597
2603
  // Also, if you're debugging and you close the window early, the process will still
2598
2604
  // take this long to exit after the tests finish. Not sure how to account for that.
2599
2605
  if ( session_vars.get_debug() ) {
2600
- await scope.page.waitForTimeout( ( scope.timeout - (1 * 1000)) );
2606
+ await time.waitForTimeout( ( scope.timeout - (1 * 1000)) );
2601
2607
  }
2602
2608
  was_found = true;
2603
2609
  error_handlers[ error_key ] = possible_errors[ error_key ][0];
@@ -2702,7 +2708,7 @@ module.exports = {
2702
2708
  }, // Ends scope.steps.add_json_vars_to_report()
2703
2709
 
2704
2710
  link_works: async ( scope, linkText ) => {
2705
- let [link] = await scope.page.$x(`//a[contains(text(), "${ linkText }")]`);
2711
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${ linkText }")]`);
2706
2712
  let prop_obj = await link.getProperty( 'href' );
2707
2713
  let actual_url = await prop_obj.jsonValue();
2708
2714
 
@@ -2760,13 +2766,13 @@ module.exports = {
2760
2766
  /** Tap a link that doesn't navigate. Depends on the language of the text.
2761
2767
  * TODO: This may be broken at this point.
2762
2768
  * */
2763
- const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
2769
+ const [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${phrase}")]`);
2764
2770
 
2765
2771
  let msg = `The term "${ phrase }" seems to be missing.`;
2766
2772
  if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0151`, value: msg }); }
2767
2773
  expect( link, msg ).to.exist;
2768
2774
  await scope.tapElementAndStayOnPage(scope, { elem: link });
2769
- await scope.page.waitForTimeout(10000);
2775
+ await time.waitForTimeout(10000);
2770
2776
  }, // Ends scope.steps.tap_term()
2771
2777
 
2772
2778
  sign: async ( scope, name ) => {
@@ -2785,7 +2791,7 @@ module.exports = {
2785
2791
  * TODO: Properly wait for download to complete. See notes in
2786
2792
  * scope.js scope.detectDownloadComplete()
2787
2793
  */
2788
- let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
2794
+ let [elem] = await scope.page.$$(`xpath/.//a[contains(@href, "${ filename }")]`);
2789
2795
 
2790
2796
  let msg = `"${ filename }" seems to be missing. Cannot find a link to that document.`;
2791
2797
  if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0152`, value: msg }); }
@@ -3017,20 +3023,101 @@ module.exports = {
3017
3023
  expect( email, email_msg ).to.not.equal( undefined );
3018
3024
  expect( password, password_msg ).to.not.equal( undefined );
3019
3025
 
3020
- // Try to log in
3026
+ // Try to enter the information
3021
3027
  await scope.page.type( `#email`, email );
3022
3028
  await scope.page.type( `#password`, password );
3023
- let elem = await scope.page.$( `button[name="submit"]` );
3024
- await scope.tapElementAndNavigate( scope, { elem });
3029
+ let elem = await scope.page.$( `button[type="submit"]` );
3030
+ await scope.guard_against_missing_tap_element(scope, { elem });
3025
3031
 
3026
- reports.addToReport( scope, {
3027
- type: `row info`,
3028
- code: `ALK0163`,
3029
- value: `Signed into ${ session_vars.get_da_server_url() }/user/sign-in successfully.`
3032
+ // Submit and see what happens
3033
+ let winner = await scope.steps.race_sign_in_navigation( scope, { elem });
3034
+ // Add the result to the report and possibly throw errors
3035
+ if ( winner[0] === `success` ) {
3036
+ reports.addToReport( scope, {
3037
+ type: `row info`, code: `ALK0163`,
3038
+ value: `Successfully signed into ${ login_url }`
3039
+ });
3040
+ await scope.afterStep(scope, {waitForShowIf: false});
3041
+ } else if ( winner[0] === `failure` ) {
3042
+ let error_msg = reports.addToReport( scope, {
3043
+ type: `error`, code: `ALK0209`,
3044
+ value: `Failed to sign into ${ login_url }. Make sure you followed the instructions at https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#sign-in.`
3045
+ });
3046
+ throw new Error( error_msg );
3047
+ } else if ( winner[0] === `error` ) {
3048
+ let error_msg = reports.addToReport( scope, {
3049
+ type: `error`, code: `ALK0210`,
3050
+ value: `Error on sign in page at ${ login_url }. Try this by hand and then run the test again.`
3051
+ });
3052
+ throw new Error( error_msg );
3053
+ }
3054
+ }, // Ends scope.steps.sign_in()
3055
+
3056
+ race_sign_in_navigation: async function ( scope, { elem }) {
3057
+ /** Wait for sign in navigation success or failure, or system error. */
3058
+
3059
+ // After everything, clean up incomplete promises
3060
+ const controller = new AbortController;
3061
+
3062
+ // Redirect
3063
+ let redirect_promise = scope.page.waitForResponse(function ( response ) {
3064
+ return response.status() === 302;
3065
+ }, { signal: controller.signal })
3066
+ .catch(( error ) => {
3067
+ log.debug({
3068
+ code: `ALK0204`, type: `info`,
3069
+ pre: `302 sign-in wait error: ${ error.name }`,
3070
+ data: error,
3071
+ });
3030
3072
  });
3031
3073
 
3032
- await scope.afterStep(scope, {waitForShowIf: false});
3033
- }, // Ends scope.steps.sign_in()
3074
+ // Invalid credentials, no navigation, no sign in
3075
+ let wrong_sign_in_promise = scope.page.waitForResponse(function ( response ) {
3076
+ return response.status() >= 200 && response.status() < 300 && response.url().includes(`/sign-in`);
3077
+ }, { signal: controller.signal })
3078
+ .catch(( error ) => {
3079
+ log.debug({
3080
+ code: `ALK0205`, type: `info`,
3081
+ pre: `200s sign-in wait error: ${ error.name }`,
3082
+ data: error,
3083
+ });
3084
+ });
3085
+
3086
+ // System error
3087
+ let error_promise = scope.page.waitForResponse(function ( response ) {
3088
+ return response.status() >= 500 && response.status() < 600;
3089
+ }, { signal: controller.signal })
3090
+ .catch(( error ) => {
3091
+ log.debug({
3092
+ code: `ALK0206`, type: `info`,
3093
+ pre: `500s sign-in wait error: ${ error.name }`,
3094
+ data: error,
3095
+ });
3096
+ });
3097
+
3098
+ let click_promise = elem.click(); // MUST complete
3099
+ const winner = await Promise.race([
3100
+ Promise.all([ `success`, click_promise, redirect_promise ]),
3101
+ Promise.all([ `failure`, click_promise, wrong_sign_in_promise ]),
3102
+ Promise.all([ `error`, click_promise, error_promise ]),
3103
+ ]).catch(function ( error ) {
3104
+ let error_msg = reports.addToReport( scope, {
3105
+ type: `error`, code: `ALK0207`,
3106
+ value: `Unknown error waiting for results during sign in at ${ login_url }.`
3107
+ });
3108
+ throw new Error( error );
3109
+ });
3110
+
3111
+ // Clean up unresolved promises
3112
+ controller.abort();
3113
+
3114
+ log.debug({
3115
+ code: `ALK0208`, type: `info`,
3116
+ pre: `Sign-in winner:`, data: winner,
3117
+ });
3118
+
3119
+ return winner;
3120
+ },
3034
3121
 
3035
3122
  set_random_page_vars: async ( scope ) => {
3036
3123
  /** Answer inputs randomly. */
package/lib/steps.js CHANGED
@@ -650,7 +650,7 @@ Then('I arrive at the next page', async () => { // √
650
650
  // Link observations have the 'Escape' button link in mind
651
651
  Then('I should see the link {string}', async ( linkText ) => { // √
652
652
  /* Loosely match link visible text. Should probably deprecate this. */
653
- let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
653
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
654
654
 
655
655
  let msg = `Cannot find a link with the text "${ linkText }".`;
656
656
  if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0079`, value: msg }); }
@@ -672,7 +672,7 @@ Then('I should see the link to {string}', async ( url ) => { // √
672
672
 
673
673
  Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) => { // √
674
674
  /* Find link by its visible text and where it leads. */
675
- let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
675
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
676
676
  // TODO: Possibly add a check here for the link text, though note this
677
677
  // Step is not currently supported.
678
678
 
@@ -687,7 +687,7 @@ Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) =>
687
687
  });
688
688
 
689
689
  Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkText, which_window) => { // √
690
- let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
690
+ let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${linkText}")]`);
691
691
 
692
692
  let prop_obj = await link.getProperty('target');
693
693
  let target = await prop_obj.jsonValue();
@@ -1497,18 +1497,22 @@ After(async function(scenario) {
1497
1497
  }
1498
1498
  }
1499
1499
 
1500
- // Handle all the accumulated errors
1501
- let errors = []
1502
- if ( !expected_status_was_met ) { errors.push( expected_status_err_msg ); }
1503
- if ( !signout_succeeded ) { errors.push( signout_error_msg ); }
1504
- if ( errors.length > 0 ) {
1505
- throw new Error( `${errors.join( errors, '\n')}` );
1506
- }
1500
+ // Collect all the relevant special errors
1501
+ let internal_errors = []
1502
+ if ( !expected_status_was_met ) { internal_errors.push( expected_status_err_msg ); }
1503
+ if ( !signout_succeeded ) { internal_errors.push( signout_error_msg ); }
1507
1504
 
1508
1505
  // Theoretically, we should never get here:
1509
1506
  // If everything else worked, if we need to force an internal test failure
1510
1507
  if ( need_to_force_failed_status ) {
1511
- throw new Error( `🤕 ${force_failure_code} unreachable ERROR: ${force_failure_msg}` );
1508
+ if ( internal_errors.length > 0 ) {
1509
+ // Handle all the accumulated errors
1510
+ throw new Error( `${internal_errors.join( internal_errors, '\n')}` );
1511
+ }
1512
+ throw new Error( `🤕 ${ force_failure_code } unreachable ERROR: ${ force_failure_msg }` );
1513
+
1514
+ } else if ( internal_errors.length > 0 ) {
1515
+ log.info({ type: `internal error`, code: `ALK0203`, pre: `No need to force failed status and yet there are error messages. ${ errors.join( errors, '\n') }` });
1512
1516
  }
1513
1517
 
1514
1518
  // Find the race condition.
@@ -81,7 +81,7 @@ reports.addToReport = function( scope, { type, code, value }) {
81
81
  let scenario = reports.makeSureScenarioExists( scope );
82
82
  scenario.lines.push({ type, code, value });
83
83
 
84
- return scenario;
84
+ return getLineText({ type, code, value });
85
85
  }; // Ends reports.addToReport()
86
86
 
87
87
  reports.create = function( scope ) {
@@ -242,7 +242,7 @@ session_vars.get_runtime_config_var = function ( key ) {
242
242
  let json = JSON.parse( fs.readFileSync( runtime_config_path ));
243
243
  let value = json[ key ];
244
244
  if ( session_vars.get_debug() ) {
245
- console.log( `🐞 ALK0041 DEBUG: Runtime config var "${key}"" is "${value}".` );
245
+ console.log( `🐞 ALK0041 DEBUG: Runtime config var "${key}" is "${value}".` );
246
246
  }
247
247
 
248
248
  return value;