@suffolklitlab/alkiln 4.6.2 → 4.7.2

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
@@ -24,6 +24,13 @@ const files = require('./utils/files' );
24
24
 
25
25
  /* Of Note:
26
26
  - We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
27
+ - Can't use elem[ scope.activate ](). It works locally, but errors on GitHub
28
+ with "Node is not visible or not an HTMLElement". We're not using a custom Chromium version
29
+ Unfortunately, this won't catch an invisible element. Hopefully we'd catch it before click
30
+ attempts are made. "Tap" on mobile is more complex to implement ourselves.
31
+ See https://stackoverflow.com/a/56547605/14144258
32
+ and other convos around it. We haven't made it public that the device
33
+ can customized. Until we do that, we'll just use "click" all the time.
27
34
  */
28
35
 
29
36
 
@@ -61,7 +68,8 @@ regex thoughts: https://stackoverflow.com/questions/171480/regex-grabbing-values
61
68
 
62
69
  // Hoping that most load and download times will be under 30 seconds.
63
70
  // TODO: Check what average load and download times are in our current interviews.
64
- setDefaultTimeout( 30 * 1000 );
71
+ let default_timeout = 30 * 1000
72
+ setDefaultTimeout( default_timeout );
65
73
 
66
74
  let click_with = {
67
75
  mobile: 'tap',
@@ -69,10 +77,12 @@ let click_with = {
69
77
  };
70
78
 
71
79
  BeforeAll(async () => {
72
- scope.timeout = 30 * 1000;
73
80
  scope.showif_timeout = 600;
74
81
  scope.report = new Map();
75
82
 
83
+ // Start tracking whether server is up and running
84
+ scope.track_server_status( scope );
85
+
76
86
  // Store path names for files and docs created for the tests
77
87
  scope.paths = {};
78
88
  // Test setup should have created this filepath. Run setup if there's a problem with this.
@@ -92,9 +102,21 @@ BeforeAll(async () => {
92
102
  });
93
103
 
94
104
  Before(async (scenario) => {
105
+ // Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
106
+ if (!scope.browser) {
107
+ scope.browser = await scope.driver.launch({ headless: !session_vars.get_debug(), devtools: session_vars.get_debug() });
108
+ }
109
+ // Clean up all previously existing pages
110
+ for (const page of await scope.browser.pages()) {
111
+ await page.close();
112
+ }
113
+ // Make a new page
114
+ scope.page = await scope.browser.newPage()
115
+
95
116
  scope.scenario_id = uuidv4();
96
117
  scope.expected_status = null;
97
118
  scope.expected_in_report = null;
119
+ scope.server_reload_promise = null;
98
120
 
99
121
  await scope.addReportHeading(scope, {scenario});
100
122
  // Make folder for this Scenario in the all-tests artifacts folder
@@ -119,7 +141,7 @@ Before(async (scenario) => {
119
141
  scope.passed_all_for_a11y = true;
120
142
 
121
143
  // Reset default timeout
122
- scope.timeout = 30 * 1000;
144
+ scope.timeout = default_timeout;
123
145
  });
124
146
 
125
147
  // Add a check for an error page before each step? After each step?
@@ -132,47 +154,9 @@ Before(async (scenario) => {
132
154
  //#####################################
133
155
 
134
156
  Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}, async (file_name, language) => { // √
135
- // NO CUCUMBER TIMEOUT. Handling our own timeouts to deal with initial scenario load.
136
- // It involves wrapping everything in custom timeouts. See
137
- // https://github.com/SuffolkLITLab/ALKiln/issues/389#issuecomment-984844240
138
- let max_load_attempts = 3;
139
- let num_attempts = 0;
140
- // First test long load time discussion:
141
- // https://github.com/SuffolkLITLab/ALKiln/issues/389#issuecomment-984844240
142
- while ( num_attempts < max_load_attempts ) {
143
-
144
- num_attempts++;
145
- let timeout_message = `Failed to load "${ file_name }" after ${ num_attempts } tries. `
146
- + `Each try gave the page ${ scope.timeout/1000 } seconds to load.`;
147
-
148
- let loaded = new Promise(async ( resolve, reject ) => {
149
- // Exit if timeout is exceeded. Reject only if final attempt failed
150
- let page_timeout = setTimeout(function() {
151
- if ( num_attempts === max_load_attempts ) { reject( `${ timeout_message } Internal timeout.` ); }
152
- else { resolve( false ); } // Give it another shot
153
- }, scope.timeout);
154
- // self-invoke an anonymous async function so we don't have to
155
- // abstract the details of resolving and rejecting.
156
- (async function() {
157
- try {
158
- await scope.load( scope, file_name );
159
- resolve( true );
160
- } catch ( err ) {
161
- // Only fail on final attempt. Actual error should be logged by cucumber output.
162
- if ( num_attempts === max_load_attempts ) {
163
- // This may not be the most useful error for us framework developers, but
164
- // we'll have the rejection error I hope
165
- await scope.addToReport(scope, { type: `error`, value: timeout_message });
166
- reject( err );
167
- } else { resolve( false ); } // Give it another shot
168
- } finally {
169
- clearTimeout( page_timeout ); // prevent temporary hang at end of tests
170
- }
171
- })();
172
- }); // ends loaded promise
173
- await loaded;
174
- if ( loaded === true ) { break; }
175
- } // ends while attempting to load page
157
+
158
+ // Load the right interview page
159
+ await scope.load( scope, file_name );
176
160
 
177
161
  // The page timeout is set in here too in case someone set a custom timeout
178
162
  // before a page was loaded.
@@ -276,13 +260,13 @@ Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^
276
260
  * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
277
261
  * I log in with "USER1_EMAIL" and "USER1_PASSWORD"
278
262
  */
279
- return wrapPromiseWithTimeout(
280
- scope.steps.sign_in( scope, {
281
- email_secret_name: email,
282
- password_secret_name: password
283
- }),
284
- scope.timeout
285
- );
263
+ // Exiting the custom timeout happens in scope.tapElement() if needed
264
+ // Couldn't test the timeout for tapping the button because there's not
265
+ // enough of a pause between initial navigation and pressing button.
266
+ await scope.steps.sign_in( scope, {
267
+ email_secret_name: email,
268
+ password_secret_name: password
269
+ });
286
270
  });
287
271
 
288
272
  // I am using a mobile/pc
@@ -304,6 +288,7 @@ When(/I am using an? (.*)/, async ( device ) => {
304
288
 
305
289
  Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+)/i, async ( seconds ) => {
306
290
  /* At any time, set a custom timeout for the scenario. */
291
+ // TODO: Add 1 second to this to allow other steps to not fail, like "I wait n seconds"
307
292
  scope.timeout = parseInt( seconds ) * 1000;
308
293
 
309
294
  // Cucumber timeouts are made custom for each step that needs it
@@ -334,21 +319,30 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
334
319
  // It also seems harsh, but more reliable.
335
320
  // Ask developers what they'd like.
336
321
 
337
- let supported_table = await scope.normalizeTable( scope, { var_data: raw_var_data.hashes() });
322
+
323
+ let supported_table = await scope.normalizeTable( scope, { raw_var_data });
338
324
 
339
325
  let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
340
- // Until we reach the final page or hit an error
326
+ // Until we reach the target page, hit an error, or time out (take too long)
341
327
  while ( !id_matches ) {
342
328
  let custom_timeout = new Promise(function( resolve, reject ) {
343
- // Set up the timeout
344
- let page_timeout = setTimeout(function() {
345
- let timeout_message = `The target id was "${ target_id }", but it took too long` +
346
- ` to try to reach it (over ${ scope.timeout/1000/60 } min). The test got stuck on "${ id }".`
347
- reject( timeout_message );
348
- }, scope.timeout);
329
+
330
+ // Set up the timeout that avoids infinite loops
331
+ let on_page_timeout = async function() {
332
+
333
+ let non_reload_report_msg = `The target id was "${ target_id }", but it took `
334
+ + `too long to try to reach it (over ${ scope.timeout/1000/60 } min). The `
335
+ + `test got stuck on "${ id }".`
336
+ let error = non_reload_report_msg;
337
+ await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
338
+ }; // Ends on_page_timeout()
339
+ let page_timeout_id = setTimeout(on_page_timeout, scope.timeout);
349
340
 
350
341
  // self-invoke an anonymous async function so we don't have to
351
342
  // abstract the details of resolving and rejecting.
343
+ // If we need to call recursively, signature function might look like:
344
+ // func( scope, { resolve, reject, supported_table, page_timeout_id })
345
+ // Maybe list of timeout and/or interval ids in state that can all be cleared at once?
352
346
  (async function() {
353
347
  try {
354
348
  let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
@@ -364,9 +358,12 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
364
358
  if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
365
359
  resolve();
366
360
  } catch ( err ) {
361
+ let err_msg = `Error occurred when tried to answer fields on question "${ id }".`
362
+ scope.addToReport( scope, { type: `error`, value: err_msg });
363
+ // Throw error instead?
367
364
  reject( err );
368
365
  } finally {
369
- clearTimeout( page_timeout ); // prevent temporary hang at end of tests
366
+ clearTimeout( page_timeout_id ); // prevent temporary hang at end of tests
370
367
  }
371
368
  })();
372
369
  }); // ends custom_timeout for setFields
@@ -458,7 +455,7 @@ Then(/I take a screenshot ?(?:named "([^"]+)")?/, { timeout: -1 }, async ( name
458
455
  /* Here to make writing tests more comfortable. */
459
456
  When("I do nothing", async () => { await scope.afterStep(scope); }); // √
460
457
 
461
- Then('the question id should be {string}', { timeout: -1 }, async ( question_id ) => {
458
+ Then(/the (?:question|page|screen) id should be "([^"]+)"/i, { timeout: -1 }, async ( question_id ) => {
462
459
  /* Looks for a sanitized version of the question id as it's written
463
460
  * in the .yml. docassemble's way */
464
461
  let { question_has_id, id, id_matches } = await scope.examinePageID( scope, question_id );
@@ -641,7 +638,7 @@ Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkTe
641
638
  let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
642
639
 
643
640
  let prop_obj = await link.getProperty('target');
644
- let target = await await prop_obj.jsonValue();
641
+ let target = await prop_obj.jsonValue();
645
642
 
646
643
  let should_open_a_new_window = which_window === 'a new window';
647
644
  let opens_a_new_window = target === '_blank';
@@ -735,31 +732,25 @@ Then(/the "([^"]+)" link opens a working page/, { timeout: -1 }, async ( linkTex
735
732
  When('I tap to continue', { timeout: -1 }, async () => {
736
733
  // Any selectors for this seem somewhat precarious
737
734
  // No data will cause an automatic continue when `ensure_navigation` is true
738
- return wrapPromiseWithTimeout(
739
- scope.setFields( scope, { var_data: [], ensure_navigation: true }),
740
- scope.timeout
741
- );
735
+
736
+ // Exiting the custom timeout happens in scope.tapElement() where we can
737
+ // check if the server is reloading
738
+ await scope.setFields( scope, { var_data: [], ensure_navigation: true });
742
739
  });
743
740
 
744
741
  When(/I tap the "([^"]+)" tab/i, { timeout: -1 }, async ( tabId ) => {
745
- return wrapPromiseWithTimeout(
746
- scope.tapTab( scope, tabId ),
747
- scope.timeout
748
- )
742
+ // Exiting the custom timeout happens in scope.tapElement() if needed
743
+ await scope.tapTab( scope, tabId );
749
744
  });
750
745
 
751
746
  When(/I tap the "([^"]+)" element$/i, { timeout: -1 }, async ( elemSelector ) => {
752
- return wrapPromiseWithTimeout(
753
- scope.tapElementBySelector( scope, elemSelector, 2000 ),
754
- scope.timeout
755
- )
747
+ // Exiting the custom timeout happens in scope.tapElement() if needed
748
+ await scope.tapElementBySelector( scope, elemSelector, 2000 );
756
749
  });
757
750
 
758
751
  When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
759
- return wrapPromiseWithTimeout(
760
- scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 ),
761
- scope.timeout
762
- )
752
+ // Exiting the custom timeout happens in scope.tapElement() if needed
753
+ await scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 )
763
754
  });
764
755
 
765
756
  When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
@@ -780,10 +771,166 @@ When(/I check all pages for (?:accessibility|a11y) issues/i, {timeout: -1}, asyn
780
771
  // UI element interaction
781
772
  //#####################################
782
773
 
774
+ // -------------------------------------
775
+ // --- RANDOM INPUT ---
776
+ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { timeout: -1 }, async ( max_pages_str ) => {
777
+ /** Give random answers until the user can't continue any more.
778
+ *
779
+ * Variations:
780
+ * I answer randomly for at most 1 page
781
+ * I answer randomly for at most 10 pages
782
+ * I answer randomly for at most 101 screens
783
+ */
784
+ // Give each screenshots folder a time-stamped name in case the dev gets clever
785
+ // and use the same Scenario to run multiple randomized tests.
786
+ let timestamp = files.readable_date();
787
+ let random_input_path = `${ scope.paths.scenario }/random_answers_screenshots-${ timestamp }`
788
+ scope.paths.random_screenshots = random_input_path;
789
+ fs.mkdirSync( scope.paths.random_screenshots );
790
+ await scope.addToReport( scope, {
791
+ type: 'row',
792
+ value: `Testing interview with random input (in ${ random_input_path })`
793
+ });
794
+
795
+ // Force the dev to prevent infinite loops
796
+ let max_pages = parseInt( max_pages_str );
797
+ let curr_page_num = 0;
798
+
799
+ // While a continue-type button is on the current page, fill out
800
+ // the fields and then try to continue
801
+ let buttons_exist = true;
802
+ while ( buttons_exist && curr_page_num < max_pages ) {
803
+
804
+ curr_page_num = curr_page_num + 1;
805
+ let { id } = await scope.examinePageID( scope, `no target id` );
806
+
807
+ let no_content_err = `The interview seemed to run into an error page`
808
+ + ` after the page with the question id "${ id }".`
809
+ + ` See the screenshot in "${ random_input_path }".`;
810
+
811
+ let has_thrown = false;
812
+ // Ensure no infinite loop, but each page has a full timeout to run
813
+ let custom_timeout = new Promise(async function( resolve, reject ) {
814
+
815
+ // Set up timeout to stop infinite loops
816
+ let page_timeout_id = setTimeout(async function() {
817
+
818
+ // Make sure to only throw once
819
+ if ( has_thrown ) { return; }
820
+ else { has_thrown = true; }
821
+
822
+ let has_content = await scope.has_interview_content( scope );
823
+
824
+ if ( !has_content ) {
825
+ await scope.addToReport( scope, {
826
+ type: `error`,
827
+ value: no_content_err
828
+ });
829
+ reject( no_content_err );
830
+
831
+ } else {
832
+ let timeout_message = `The page with an id of "${ id }" took too long`
833
+ + ` to fill with random answers (over ${ scope.timeout/1000/60 } min).`
834
+ await scope.addToReport( scope, { type: `error`, value: timeout_message });
835
+ reject( timeout_message );
836
+ }
837
+
838
+ }, scope.timeout );
839
+
840
+ // self-invoke an anonymous async function so we don't have to
841
+ // abstract the details of resolving and rejecting.
842
+ (async function() {
843
+ try {
844
+
845
+ // Set all the fields on the page
846
+ let buttons = await scope.steps.set_random_page_vars( scope );
847
+
848
+ // Take a screenshot of the page before continuing or ending
849
+ await scope.page.screenshot({
850
+ path: `${ scope.paths.random_screenshots }/random-${ Date.now() }.jpg`,
851
+ type: `jpeg`,
852
+ fullPage: true
853
+ });
854
+
855
+ // Prep for the next loop to possibly stop. Signature page buttons
856
+ // don't count as buttons in the way our field detects them
857
+ buttons_exist = buttons.length > 0;
858
+ // If there were any buttons, pick a random one to try to press
859
+ if ( buttons_exist ) {
860
+ await scope.set_random_input_for.buttons( scope, { fields: buttons });
861
+ } else {
862
+ // If can theoretically continue, try to. Also prep to
863
+ // stop or continue for the next loop
864
+ buttons_exist = await scope.continue_exists( scope );
865
+ if ( buttons_exist ) {
866
+ await scope.continue( scope );
867
+ } // ends if continue exists
868
+ } // ends if buttons_exist
869
+
870
+ let has_content = await scope.has_interview_content( scope );
871
+ if ( !has_content ) {
872
+
873
+ // Throw an error if there's no interview content
874
+ // Make sure to only throw once
875
+ if ( !has_thrown ) {
876
+ has_thrown = true;
877
+ await scope.addToReport( scope, {
878
+ type: `error`,
879
+ value: no_content_err
880
+ });
881
+ reject( no_content_err );
882
+ }
883
+
884
+ } // ends if !has_content
885
+
886
+ if ( buttons_exist ) {
887
+ process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
888
+ }
889
+
890
+ resolve();
891
+
892
+ } catch ( error ) {
893
+
894
+ // Make sure to only throw once
895
+ if ( has_thrown ) { return; }
896
+ else { has_thrown = true; }
897
+
898
+ // Throw a specific error if there's no interview content
899
+ let has_content = await scope.has_interview_content( scope );
900
+ if ( !has_content ) {
901
+ await scope.addToReport( scope, {
902
+ type: `error`,
903
+ value: no_content_err
904
+ });
905
+ reject( no_content_err );
906
+
907
+ // Otherwise throw a generic error
908
+ } else {
909
+ let err_msg = `The page with the question id "${ id }" errored when trying to fill`
910
+ + ` it with random answers: ${ error.name }`
911
+ await scope.addToReport( scope, { type: `error`, value: err_msg });
912
+ reject( error );
913
+ } // ends if !has_content
914
+
915
+ } finally {
916
+ // prevent temporary hang at end of tests
917
+ clearTimeout( page_timeout_id );
918
+ }
919
+ })();
920
+
921
+ }); // ends custom_timeout for set_random_page_vars
922
+
923
+ await custom_timeout;
924
+
925
+ } // ends while continue_exists
926
+
927
+
928
+ }); // Ends random input
929
+
783
930
  // -------------------------------------
784
931
  // --- VARIABLES-BASED STEPS ---
785
932
 
786
- When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( var_name, set_to ) => {
933
+ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( var_name, answer ) => {
787
934
  /* Set a non-story table variable (with or without a choice associated with it) to a value. Set:
788
935
  * - Buttons associated with variables
789
936
  * - Dropdowns
@@ -794,20 +941,18 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
794
941
  *
795
942
  * Variations:
796
943
  * 1. I set the var "benefits['SSI']" to "false"
797
- * 1. I set the var "rent_interval" to "weekly"
944
+ * 1. I set the variable "rent_interval" to "weekly"
798
945
  */
799
- return wrapPromiseWithTimeout(
800
- scope.steps.set_regular_var( scope, var_name, set_to ),
801
- scope.timeout
802
- );
946
+ // Exiting the custom timeout happens in scope.tapElement() if needed
947
+ await scope.steps.set_regular_var( scope, var_name, answer )
948
+
803
949
  });
804
950
 
805
951
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
806
952
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
807
- return wrapPromiseWithTimeout(
808
- scope.steps.set_secret_var( scope, var_name, set_to_env_name ),
809
- scope.timeout
810
- );
953
+ // This could theoretically tap an element, so
954
+ // exiting the custom timeout happens in scope.tapElement() if needed
955
+ await scope.steps.set_secret_var( scope, var_name, set_to_env_name )
811
956
  });
812
957
 
813
958
 
@@ -826,10 +971,8 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
826
971
 
827
972
  When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
828
973
  /** Tap a link that doesn't navigate. Depends on the language of the text. */
829
- return wrapPromiseWithTimeout(
830
- scope.steps.tap_term( scope, phrase ),
831
- scope.timeout
832
- );
974
+ // exiting the custom timeout happens in scope.tapElement() if needed
975
+ await scope.steps.tap_term( scope, phrase );
833
976
  });
834
977
 
835
978
  Then('I sign', { timeout: -1 }, async () => {
@@ -846,6 +989,9 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
846
989
  * WARNING: Must not contain any characters that are totally unique to that
847
990
  * page load. Just stick to the name of the file.
848
991
  */
992
+ // TODO:
993
+ // This should be using `scope.tapElement()`? When someone clicks on a link to
994
+ // download a document, it sends a request to the server, which could be reloading
849
995
  return wrapPromiseWithTimeout(
850
996
  scope.steps.download( scope, filename ),
851
997
  scope.timeout
@@ -938,6 +1084,7 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
938
1084
  //#####################################
939
1085
 
940
1086
  After(async function(scenario) {
1087
+
941
1088
  // The accessibility failures don't happen til the end, so the rest of the test can continue
942
1089
  if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
943
1090
  // The fact that the scenario failed has already been noted earlier in the report
@@ -954,14 +1101,15 @@ After(async function(scenario) {
954
1101
  if ( scope.page ) { await scope.page.waitForTimeout( 60 * 1000 ); }
955
1102
  }
956
1103
 
957
- await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
958
1104
  if ( scope.page ) {
959
- if ( scope.disable_error_screenshot ) {
1105
+
1106
+ if ( !!scope.disable_error_screenshot ) {
960
1107
  scope.addToReport(scope, {
961
1108
  type: `row`,
962
1109
  value: `For security, ALKiln will not create a screenshot for this error. It's possible a secret is being used on this screen.`
963
1110
  });
964
1111
  } else {
1112
+
965
1113
  // Save/download a picture of the screen that's showing during the unexpected status
966
1114
  // Save one copy in the outer-most artifact folder
967
1115
  let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
@@ -982,11 +1130,12 @@ After(async function(scenario) {
982
1130
  } // ends if scope.disable_error_screenshot
983
1131
  } // ends if scope.page exists
984
1132
 
985
- } // ends if not passed
1133
+ // This has to come after the security message or security message doesn't print. Not sure why.
1134
+ if (scenario.result.status === `FAILED`) {
1135
+ await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
1136
+ }
986
1137
 
987
- if (scenario.result.status === `FAILED`) {
988
- await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
989
- }
1138
+ } // ends if not passed
990
1139
 
991
1140
  let original_scenario_status = scenario.result.status;
992
1141
 
@@ -995,43 +1144,44 @@ After(async function(scenario) {
995
1144
  let expected = scope.expected_status;
996
1145
  scope.expected_status = null; // reset for next Scenario
997
1146
  expect( scenario.result.status ).to.equal( expected );
1147
+ // If the status was as expected, pass this criteria
1148
+ scenario.result.status = `PASSED`;
998
1149
  }
999
1150
  if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
1000
1151
  let expected = scope.expected_in_report;
1001
1152
  // Reset report values no matter what so they don't mess up future scenarios
1002
1153
  scope.expected_in_report = null;
1003
1154
  let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
1155
+
1004
1156
  if ( all_were_included ) { scenario.result.status = `PASSED`; }
1157
+ else { scenario.result.status = `FAILED`; }
1005
1158
  }
1006
1159
 
1007
1160
  // If there is a page open, then sign out and close it
1008
1161
  if ( scope.page ) {
1009
- // Make sure the new interview really starts fresh. Sometimes insufficient.
1010
- // Also, often is a failure point.
1011
- // Consider, instead, catching the reload error for a bad restart and just
1012
- // re-trying at that point.
1162
+ // Catch a possible reload error.
1163
+
1164
+ // TODO: implement and use scope.handle_possible_timeout()
1013
1165
  try {
1166
+ // puppeteer will ensure proper timeout.
1014
1167
  await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
1015
- } catch ( err ) {
1016
- if ( session_vars.DEBUG ) {
1017
- console.log( `Trying to reset for the next test a second time because the first time failed.` );
1018
- }
1019
1168
 
1020
- try {
1021
- await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
1022
-
1023
- } catch ( err ) {
1024
- await scope.addToReport(scope, {
1025
- type: `warning`,
1026
- value: `This test ${ original_scenario_status }, but also there `
1027
- + `was an error while trying to reset for the next test. You can try `
1028
- + `re-running the test or checking your server.`
1029
- });
1030
- }
1031
- }
1032
-
1033
- await scope.page.close();
1034
- scope.page = null;
1169
+ } catch ( error ) {
1170
+
1171
+ if ( error.name === `TimeoutError` ) {
1172
+ // TODO: Should a more specific message about signing out get into the report no matter what?
1173
+ // If so, what should the message be in order to not get confused with these other messages?
1174
+ let non_reload_report_msg = `It took too long to sign out in preparation for the next test.`;
1175
+ await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
1176
+ } else {
1177
+ // Throw any non-timeout error
1178
+ let err_msg = `Error occurred when ALKiln tried to sign out in preparation for the next test.`
1179
+ scope.addToReport( scope, { type: `error`, value: err_msg });
1180
+ throw error;
1181
+ } // ends if error is timeout error
1182
+
1183
+ } // ends try/catch
1184
+
1035
1185
  } // ends if scope.page
1036
1186
 
1037
1187
  // Add report text to Scenario folder after everything has been added to the report
@@ -1043,6 +1193,9 @@ After(async function(scenario) {
1043
1193
  });
1044
1194
 
1045
1195
  AfterAll(async function() {
1196
+ // Stop collecting server response statuses
1197
+ scope.stop_tracking_server_status = true;
1198
+
1046
1199
  let report = await scope.getPrintableReport( scope );
1047
1200
  // Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
1048
1201
  // May need newest version of cucumberjs
@@ -8,6 +8,7 @@ files.readable_date = function() {
8
8
  * match the workflow artifact date format. */
9
9
  let date = new Date(); // now
10
10
  let str = date.toLocaleString('en-GB', { timeZone: 'UTC' });
11
+ // To properly get the year/month/day format, look at https://stackoverflow.com/a/48729396/14144258
11
12
  let fancy_str = str.replace(/\//g, '-').replace(',', ' at').replace(':', 'hrs ').replace(':', 'mins ') + 'secs UTC';
12
13
  return fancy_str;
13
14
  }; // Ends files.readable_date()
@@ -25,6 +25,7 @@ module.exports = session_vars;
25
25
  session_vars.get_debug = function () { return process.env.DEBUG || null; };
26
26
  session_vars.get_dev_api_key = function () { return process.env.DOCASSEMBLE_DEVELOPER_API_KEY || null; };
27
27
  session_vars.get_repo_url = function () { return process.env.REPO_URL || null; };
28
+ session_vars.get_server_reload_timeout = function () { return process.env.SERVER_RELOAD_TIMEOUT_SECONDS || null; };
28
29
  session_vars.is_dev_env = function () { return process.env.DEV || null; }
29
30
 
30
31
  // More complex logic
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "4.6.2",
3
+ "version": "4.7.2",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
7
7
  "test": "npm run unit && npm run langs_setup && npm run cucumber -- $@",
8
8
  "cucumber_base": "run_cucumber(){ ./node_modules/.bin/cucumber-js \"$@\"; status=$?; ./lib/utils/clean_reports.js; return $status; }; run_cucumber",
9
- "cucumber": "run_base(){ npm run cucumber_base -- \"$@\" tests/features/*.feature; }; run_base",
9
+ "cucumber": "run_base(){ npm run cucumber_base -- \"$@\" tests/features/*.feature --retry 1; }; run_base",
10
10
  "cucumber_external": "run_cucumber(){ tmp_path=`pwd`; ( new_path=\"$1\"; shift; cd \"$new_path\"; npm run --prefix \"${tmp_path}\" cucumber_base -- \"$@\" \"$new_path\"/docassemble/*/data/sources/*.feature;) }; run_cucumber",
11
11
  "langs": "npm run langs_setup && ./node_modules/.bin/cucumber-js --require ./lib/index.js tests/features/*_lang*.feature",
12
12
  "langs_setup": "./lib/utils/langs.js 'tests/features/*.feature'",
@@ -37,6 +37,7 @@
37
37
  "dependencies": {
38
38
  "@axe-core/puppeteer": "4.4.2",
39
39
  "@cucumber/cucumber": "7.3.2",
40
+ "@faker-js/faker": "^7.3.0",
40
41
  "axios": "0.24.0",
41
42
  "chai": "4.2.0",
42
43
  "cheerio": "1.0.0-rc.10",
@@ -131,4 +131,4 @@ Scenario: tap element with an error
131
131
  is there a typo?
132
132
  """
133
133
  Given I start the interview at "test_taps"
134
- And I tap the "Tests-not_there-tab" tab
134
+ And I tap the "Tests-not_there-tab" tab