@suffolklitlab/alkiln 4.7.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -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 () => {
@@ -790,35 +781,61 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
790
781
  * I answer randomly for at most 10 pages
791
782
  * I answer randomly for at most 101 screens
792
783
  */
793
- scope.paths.random_screenshots = `${ scope.paths.scenario }/random_answers_screenshots`;
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;
794
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
+ });
795
794
 
796
795
  // Force the dev to prevent infinite loops
797
796
  let max_pages = parseInt( max_pages_str );
798
797
  let curr_page_num = 0;
799
798
 
800
- // While a continue button is on the current page, fill out
799
+ // While a continue-type button is on the current page, fill out
801
800
  // the fields and then try to continue
802
- // let continue_exists = true;
803
- // while ( continue_exists && curr_page_num < max_pages ) {
804
801
  let buttons_exist = true;
805
802
  while ( buttons_exist && curr_page_num < max_pages ) {
806
803
 
807
804
  curr_page_num = curr_page_num + 1;
808
805
  let { id } = await scope.examinePageID( scope, `no target id` );
809
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;
810
812
  // Ensure no infinite loop, but each page has a full timeout to run
811
813
  let custom_timeout = new Promise(async function( resolve, reject ) {
812
814
 
813
815
  // Set up timeout to stop infinite loops
814
816
  let page_timeout_id = setTimeout(async function() {
815
817
 
816
- let timeout_message = `The page with an id of "${ id }" took too long `
817
- + `to fill it with random answers (over ${ scope.timeout/1000/60 } min).`
818
- await scope.addToReport( scope, { type: `error`, value: timeout_message });
819
- throw( timeout_message );
818
+ // Make sure to only throw once
819
+ if ( has_thrown ) { return; }
820
+ else { has_thrown = true; }
820
821
 
821
- }, scope.timeout);
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 );
822
839
 
823
840
  // self-invoke an anonymous async function so we don't have to
824
841
  // abstract the details of resolving and rejecting.
@@ -830,7 +847,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
830
847
 
831
848
  // Take a screenshot of the page before continuing or ending
832
849
  await scope.page.screenshot({
833
- path: `${ scope.paths.random_screenshots }/${ Date.now() }.jpg`,
850
+ path: `${ scope.paths.random_screenshots }/random-${ Date.now() }.jpg`,
834
851
  type: `jpeg`,
835
852
  fullPage: true
836
853
  });
@@ -848,7 +865,23 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
848
865
  if ( buttons_exist ) {
849
866
  await scope.continue( scope );
850
867
  } // ends if continue exists
851
- }
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
852
885
 
853
886
  if ( buttons_exist ) {
854
887
  process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
@@ -856,11 +889,28 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
856
889
 
857
890
  resolve();
858
891
 
859
- } catch ( err ) {
860
- let err_msg = `The page with the "${ id }" errored when trying to fill `
861
- + `it with random answers: ${ err.name }`
862
- await scope.addToReport( scope, { type: `error`, value: err_msg });
863
- throw( err );
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
864
914
 
865
915
  } finally {
866
916
  // prevent temporary hang at end of tests
@@ -891,20 +941,18 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
891
941
  *
892
942
  * Variations:
893
943
  * 1. I set the var "benefits['SSI']" to "false"
894
- * 1. I set the var "rent_interval" to "weekly"
944
+ * 1. I set the variable "rent_interval" to "weekly"
895
945
  */
896
- return wrapPromiseWithTimeout(
897
- scope.steps.set_regular_var( scope, var_name, answer ),
898
- scope.timeout
899
- );
946
+ // Exiting the custom timeout happens in scope.tapElement() if needed
947
+ await scope.steps.set_regular_var( scope, var_name, answer )
948
+
900
949
  });
901
950
 
902
951
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
903
952
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
904
- return wrapPromiseWithTimeout(
905
- scope.steps.set_secret_var( scope, var_name, set_to_env_name ),
906
- scope.timeout
907
- );
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 )
908
956
  });
909
957
 
910
958
 
@@ -923,10 +971,8 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
923
971
 
924
972
  When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
925
973
  /** Tap a link that doesn't navigate. Depends on the language of the text. */
926
- return wrapPromiseWithTimeout(
927
- scope.steps.tap_term( scope, phrase ),
928
- scope.timeout
929
- );
974
+ // exiting the custom timeout happens in scope.tapElement() if needed
975
+ await scope.steps.tap_term( scope, phrase );
930
976
  });
931
977
 
932
978
  Then('I sign', { timeout: -1 }, async () => {
@@ -943,6 +989,9 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
943
989
  * WARNING: Must not contain any characters that are totally unique to that
944
990
  * page load. Just stick to the name of the file.
945
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
946
995
  return wrapPromiseWithTimeout(
947
996
  scope.steps.download( scope, filename ),
948
997
  scope.timeout
@@ -1035,6 +1084,7 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
1035
1084
  //#####################################
1036
1085
 
1037
1086
  After(async function(scenario) {
1087
+
1038
1088
  // The accessibility failures don't happen til the end, so the rest of the test can continue
1039
1089
  if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
1040
1090
  // The fact that the scenario failed has already been noted earlier in the report
@@ -1051,14 +1101,15 @@ After(async function(scenario) {
1051
1101
  if ( scope.page ) { await scope.page.waitForTimeout( 60 * 1000 ); }
1052
1102
  }
1053
1103
 
1054
- await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
1055
1104
  if ( scope.page ) {
1056
- if ( scope.disable_error_screenshot ) {
1105
+
1106
+ if ( !!scope.disable_error_screenshot ) {
1057
1107
  scope.addToReport(scope, {
1058
1108
  type: `row`,
1059
1109
  value: `For security, ALKiln will not create a screenshot for this error. It's possible a secret is being used on this screen.`
1060
1110
  });
1061
1111
  } else {
1112
+
1062
1113
  // Save/download a picture of the screen that's showing during the unexpected status
1063
1114
  // Save one copy in the outer-most artifact folder
1064
1115
  let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
@@ -1079,11 +1130,12 @@ After(async function(scenario) {
1079
1130
  } // ends if scope.disable_error_screenshot
1080
1131
  } // ends if scope.page exists
1081
1132
 
1082
- } // 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
+ }
1083
1137
 
1084
- if (scenario.result.status === `FAILED`) {
1085
- await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
1086
- }
1138
+ } // ends if not passed
1087
1139
 
1088
1140
  let original_scenario_status = scenario.result.status;
1089
1141
 
@@ -1092,43 +1144,44 @@ After(async function(scenario) {
1092
1144
  let expected = scope.expected_status;
1093
1145
  scope.expected_status = null; // reset for next Scenario
1094
1146
  expect( scenario.result.status ).to.equal( expected );
1147
+ // If the status was as expected, pass this criteria
1148
+ scenario.result.status = `PASSED`;
1095
1149
  }
1096
1150
  if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
1097
1151
  let expected = scope.expected_in_report;
1098
1152
  // Reset report values no matter what so they don't mess up future scenarios
1099
1153
  scope.expected_in_report = null;
1100
1154
  let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
1155
+
1101
1156
  if ( all_were_included ) { scenario.result.status = `PASSED`; }
1157
+ else { scenario.result.status = `FAILED`; }
1102
1158
  }
1103
1159
 
1104
1160
  // If there is a page open, then sign out and close it
1105
1161
  if ( scope.page ) {
1106
- // Make sure the new interview really starts fresh. Sometimes insufficient.
1107
- // Also, often is a failure point.
1108
- // Consider, instead, catching the reload error for a bad restart and just
1109
- // re-trying at that point.
1162
+ // Catch a possible reload error.
1163
+
1164
+ // TODO: implement and use scope.handle_possible_timeout()
1110
1165
  try {
1166
+ // puppeteer will ensure proper timeout.
1111
1167
  await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
1112
- } catch ( err ) {
1113
- if ( session_vars.DEBUG ) {
1114
- console.log( `Trying to reset for the next test a second time because the first time failed.` );
1115
- }
1116
1168
 
1117
- try {
1118
- await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
1119
-
1120
- } catch ( err ) {
1121
- await scope.addToReport(scope, {
1122
- type: `warning`,
1123
- value: `This test ${ original_scenario_status }, but also there `
1124
- + `was an error while trying to reset for the next test. You can try `
1125
- + `re-running the test or checking your server.`
1126
- });
1127
- }
1128
- }
1129
-
1130
- await scope.page.close();
1131
- 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
+
1132
1185
  } // ends if scope.page
1133
1186
 
1134
1187
  // Add report text to Scenario folder after everything has been added to the report
@@ -1140,6 +1193,9 @@ After(async function(scenario) {
1140
1193
  });
1141
1194
 
1142
1195
  AfterAll(async function() {
1196
+ // Stop collecting server response statuses
1197
+ scope.stop_tracking_server_status = true;
1198
+
1143
1199
  let report = await scope.getPrintableReport( scope );
1144
1200
  // Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
1145
1201
  // May need newest version of cucumberjs
@@ -5,10 +5,20 @@ let files = {};
5
5
 
6
6
  files.readable_date = function() {
7
7
  /* Return the current timestamp in a human-readable filename-safe format to
8
- * match the workflow artifact date format. */
8
+ * match the workflow artifact date format.
9
+ *
10
+ * See https://stackoverflow.com/a/48729396/14144258 and https://stackoverflow.com/a/6040556/14144258
11
+ */
9
12
  let date = new Date(); // now
10
- let str = date.toLocaleString('en-GB', { timeZone: 'UTC' });
11
- let fancy_str = str.replace(/\//g, '-').replace(',', ' at').replace(':', 'hrs ').replace(':', 'mins ') + 'secs UTC';
13
+ let day = ("0" + date.getUTCDate()).slice(-2);
14
+ let month = ("0" + date.getUTCMonth() + 1).slice(-2);
15
+ let year = date.getUTCFullYear();
16
+ let hours = ("0" + date.getUTCHours()).slice(-2);
17
+ let mins = ("0" + date.getUTCMinutes()).slice(-2);
18
+ let secs = ("0" + date.getUTCSeconds()).slice(-2);
19
+ let msecs = ("00" + date.getUTCMilliseconds()).slice(-3);
20
+
21
+ let fancy_str = `${ year }-${ month }-${ day } at ${ hours }hr ${ mins }min ${ secs }sec ${ msecs }ms UTC`;
12
22
  return fancy_str;
13
23
  }; // Ends files.readable_date()
14
24
 
@@ -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.7.0",
3
+ "version": "4.8.0",
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'",
@@ -132,8 +132,3 @@ Scenario: tap element with an error
132
132
  """
133
133
  Given I start the interview at "test_taps"
134
134
  And I tap the "Tests-not_there-tab" tab
135
-
136
- @i5 @random
137
- Scenario: I answer randomly till the end
138
- Given I start the interview at "all_tests"
139
- And I answer randomly for at most 50 pages
@@ -3,13 +3,29 @@ Feature: Assembly Line package-specific Steps
3
3
 
4
4
  # In tag names, 'ri' is for 'random input'
5
5
 
6
- @slow @ri1 @random
6
+ @fast @ri1 @random
7
7
  Scenario: I fill in random input till the end
8
8
    Given I start the interview at "test_random_input"
9
9
  And I answer randomly for at most 20 pages
10
10
  Then the question id should be "end"
11
11
 
12
- @slow @ri2 @random
12
+ @fast @ri2 @random
13
13
  Scenario: I fill in random input for 1 page
14
14
    Given I start the interview at "test_random_input"
15
15
  And I answer randomly for at most 1 page
16
+
17
+ # Should create two unique folders for random step screenshots which
18
+ # must be checked manually
19
+ @fast @ri3 @random
20
+ Scenario: I answer randomly till the end twice
21
+ Given I start the interview at "test_random_input"
22
+ And I answer randomly for at most 50 pages
23
+ Given I start the interview at "test_random_input"
24
+ And I answer randomly for at most 50 pages
25
+
26
+ @fast @ri4 @random @error
27
+ Scenario: Fail with error page from random input
28
+ Given the final Scenario status should be "failed"
29
+ Given I start the interview at "test_missing_var_error_screen"
30
+ And the max seconds for each step in this scenario is 10
31
+ And I answer randomly for at most 3 pages