@suffolklitlab/alkiln 4.7.1 → 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/CHANGELOG.md CHANGED
@@ -40,6 +40,12 @@ Format:
40
40
  -->
41
41
  <!-- ## [Unreleased] -->
42
42
 
43
+ ## [4.7.2] - 2022-08-09
44
+ ### Changed
45
+ - Inform the developer when a test failure may have been caused by a server reload. See https://github.com/SuffolkLITLab/ALKiln/issues/392.
46
+ - Retry all failing tests once.
47
+
48
+
43
49
  ## [4.7.1] - 2022-08-07
44
50
  ### Changed
45
51
  - Ensure unique folder names for random input screenshots in case developer uses multiple random steps in one Scenario.
@@ -120,6 +120,24 @@ da_i.throw_an_error_if_server_is_not_responding = async function ( options ) {
120
120
  }
121
121
  }; // Ends da_i.throw_an_error_if_server_is_not_responding()
122
122
 
123
+ // node -e 'require("./lib/docassemble/docassemble_api_interface.js").is_server_responding()'
124
+ da_i.is_server_responding = async function ( dev_id_options ) {
125
+ /** Use _da_REST.get_dev_id() to test if server is up.
126
+ * See https://docassemble.org/docs/api.html#user. */
127
+
128
+ // Default timeouts
129
+ let { timeout } = dev_id_options || { timeout: DEFAULT_MAX_REQUEST_MS };
130
+ try {
131
+ let response = await _da_REST.get_dev_id( timeout );
132
+ if ( response && response.data && response.data.id ) {
133
+ return true;
134
+ } else {
135
+ return false;
136
+ }
137
+ } catch ( error ) {
138
+ return false;
139
+ }
140
+ }; // Ends da_i.is_server_responding();
123
141
 
124
142
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_dev_id()'
125
143
  da_i.get_dev_id = async function ( dev_id_options ) {
package/lib/scope.js CHANGED
@@ -14,6 +14,8 @@ const time = require('./utils/time');
14
14
  const log = require('./utils/log');
15
15
  const { waitForTimeout } = require('./utils/time');
16
16
  const files = require('./utils/files' );
17
+ const server = require('./docassemble/docassemble_api_interface');
18
+ const wrapPromiseWithTimeout = require('./cucumber_8_shim');
17
19
 
18
20
 
19
21
  let ordinal_to_integer = {
@@ -57,6 +59,7 @@ let misc_artifacts_dir = `_alkiln_test-misc_artifacts`;
57
59
 
58
60
  module.exports = {
59
61
  trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
62
+
60
63
  addToReport: async function( scope, { type, value }) {
61
64
  /* Add an item to a specific list in a scenario's report.
62
65
  * Create report if necessary.
@@ -362,36 +365,49 @@ module.exports = {
362
365
 
363
366
  expect( elem, msg ).to.exist;
364
367
 
368
+ // Get the text on the button or link for a potential error message later
369
+ let tapperText = await (await elem.getProperty('textContent')).jsonValue();
370
+
365
371
  let start_url = await scope.page.url(); // so we can later check for navigation
366
372
 
367
- let clickWait;
368
- // Can't use elem[ scope.activate ](). It works locally, but errors on GitHub
369
- // with "Node is not visible or not an HTMLElement". We're not using a custom Chromium version
370
- // Unfortunately, this won't catch an invisible element. Hopefully we'd catch it before now...?
371
- // "Tap" on mobile is more complex to implement ourselves.
372
- // See https://stackoverflow.com/a/56547605/14144258
373
- // and other convos around it. We haven't made it public that the device
374
- // can customized. Until we do that, we'll just use "click" all the time.
375
- if (!tabToWaitFor) {
376
- // Click with no navigation will end immediately
377
- clickWait = Promise.all([
378
- elem.evaluate( (el) => { return el.click(); }),
379
- scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
380
- ]);
381
- } else {
382
- // Same thing as above, but the promise doesn't wait for navigation.
383
- clickWait = elem.evaluate( (el) => {return el.click(); });
373
+ // We'll wait for the click itself at the very least
374
+ let click_promises = [ elem.evaluate( (el) => { return el.click(); }) ];
375
+
376
+ // If we're possibly navigating, wait for navigation as well
377
+ if ( !tabToWaitFor ) {
378
+ click_promises.push( scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }) );
384
379
  }
385
- // Error loads page, so no need to detect to keep things short
386
- let winner = await Promise.race([
387
- clickWait,
388
- // This is meant to detect user-visible alerts/errors,
389
- // not breaking errors in the testing code.
390
- scope.page.waitForSelector('.alert-danger', { visible: true }),
391
- scope.page.waitForSelector('.da-has-error', { visible: true }),
392
- ])
393
-
394
- // TODO: Not sure how to detect navigation landing at same page.
380
+ let clickWait = Promise.all( click_promises );
381
+
382
+ let winner = null;
383
+
384
+ // TODO: implement and use scope.handle_possible_timeout()
385
+ try {
386
+ // Error loads page, so no need to detect to keep things short
387
+ // Also, puppeteer will ensure proper timeout.
388
+ winner = await Promise.race([
389
+ clickWait,
390
+ // This is meant to detect user-visible alerts/errors,
391
+ // not breaking errors in the testing code.
392
+ scope.page.waitForSelector('.alert-danger', { visible: true }),
393
+ scope.page.waitForSelector('.da-has-error', { visible: true }),
394
+ ]);
395
+ } catch ( error ) {
396
+
397
+ if ( error.name === `TimeoutError` ) {
398
+ let non_reload_report_msg = `ALKiln tapped "${ tapperText }", but the interview took to long to finish.`;
399
+ await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
400
+ } else {
401
+ // Throw any non-timeout error
402
+ let err_msg = `Error occurred when ALKiln tried to tap "${ tapperText }".`
403
+ scope.addToReport( scope, { type: `error`, value: err_msg });
404
+ throw error;
405
+ } // ends if error is timeout error
406
+
407
+ } // ends try/catch
408
+
409
+ // TODO: Not sure how to detect navigation landing at same page, such as
410
+ // with a user input validation error. Does that count as navigating?
395
411
  // Maybe url change + trigger var change? Maybe just trigger var?
396
412
  let end_url = await scope.page.url();
397
413
  let navigated = start_url !== end_url;
@@ -414,6 +430,156 @@ module.exports = {
414
430
  return winner;
415
431
  }, // Ends scope.tapElement()
416
432
 
433
+ // Handle server reload errors
434
+ server_statuses: [],
435
+ server_check_interval_ms: 1 * 1000,
436
+ stop_tracking_server_status: false,
437
+ track_server_status: async function( scope ) {
438
+ /** Updates server_statuses list by checking if server is responding until,
439
+ * eventually, it is told to stop. */
440
+ let server_check_start_time = Date.now();
441
+
442
+ while ( !scope.stop_tracking_server_status ) {
443
+ // This code assumes a reasonable response time would be n seconds if the
444
+ // server is up and that a server reload/restart won't take less than n seconds.
445
+ // Also, it's non-blocking, so it doesn't slow down tests
446
+ let is_up = await server.is_server_responding({ timeout: scope.server_check_interval_ms });
447
+ // Add status to the beginning of the list to make truncating easier
448
+ scope.server_statuses.unshift( is_up );
449
+
450
+ // Ensure we wait longer if a full second hasn't yet passed
451
+ let ms_passed = Date.now() - server_check_start_time;
452
+ // Use Math.max to ensure no negative numbers
453
+ let ms_remaining = Math.max( 0, scope.server_check_interval_ms - ms_passed );
454
+ await time.waitForTimeout( ms_remaining );
455
+ // Prepare for next check
456
+ server_check_start_time = Date.now()
457
+
458
+ // To make it easier to check past server statuses, remove defunct server statuses
459
+ let max_length = Math.round( scope.timeout/scope.server_check_interval_ms );
460
+ // Trying to extract more items than the actual length is ok
461
+ scope.server_statuses = scope.server_statuses.slice( 0, max_length );
462
+ } // ends while !scope.stop_tracking_server_status
463
+
464
+ }, // Ends scope.track_server_status()
465
+
466
+ did_server_reload: async function( scope ) {
467
+ return scope.server_statuses.includes( false );
468
+ }, // Ends scope.did_server_reload()
469
+
470
+ // handle_possible_timeout: async function( scope, {
471
+ // promise: ,
472
+ // non_timeout_msg: ,
473
+ // normal_timeout_msg: ,
474
+ // actual_error: ,
475
+ // }) {}, // Ends scope.handle_possible_timeout()
476
+
477
+ handle_page_timeout_error: async function( scope, { non_reload_report_msg, error } ) {
478
+ /* For a page timeout error, handles server reload vs. other timeout errors
479
+
480
+ @param options.non_reload_report_msg { str } Non-reload message to add to
481
+ the report as an error.
482
+ @param options.error { object | str } Error to throw. It can be the actual
483
+ error object or a string that will show up as a message in the error.
484
+ */
485
+ // If the server was reloading during that time, wait for the server to
486
+ // respond again so the next test has a better chance of passing. Then
487
+ // throw an error. The command-line script will re-try the Scenario after.
488
+ let server_reloaded_during_interval = await scope.did_server_reload( scope );
489
+ if ( server_reloaded_during_interval ) {
490
+ // Wait for the server to reload just to make sure the next test
491
+ // doesn't also fail because of this same server reload.
492
+ await scope.get_server_reload_promise( scope );
493
+ } else {
494
+ // Otherwise just throw the error
495
+ await scope.addToReport( scope, {
496
+ type: `error`,
497
+ value: non_reload_report_msg });
498
+ throw error;
499
+ }
500
+ }, // Ends scope.handle_page_timeout_error()
501
+
502
+ get_server_reload_promise: async function( scope ) {
503
+ /** If the promise already exists in state, return it because it can
504
+ * be used anywhere with `await`. Otherwise, make the promise, store
505
+ * it in state, and return that.
506
+ */
507
+ if ( scope.server_reload_promise !== null ) {
508
+ // Return the existing promise of the func that will check on reloading the server
509
+ return scope.server_reload_promise
510
+
511
+ } else {
512
+ // Create and return the promise of the func that will check on reloading the server
513
+ scope.server_reload_promise = scope.wait_for_server_response( scope );
514
+ return scope.server_reload_promise;
515
+ }
516
+ }, // Ends scope.get_server_reload_promise()
517
+
518
+ wait_for_server_response: async function( scope ) {
519
+ /** Wait for the server to respond using a multiple of scope.timeout
520
+ * (or a custom timeout?) so the tests don't fail just because the
521
+ * server isn't responding. Should this take an argument of an amount
522
+ * of time to check or just assume we're using scope.timeout?
523
+ *
524
+ * This should only be called if the server failed to respond at some point.
525
+ *
526
+ * This currently assumes we're either using scope.timeout as basis for
527
+ * the amount of time in which to detect a finished reload or that
528
+ * there's a test input that defines a different timeout.
529
+ */
530
+
531
+ let ms_till_server_timeout;
532
+ if ( session_vars.get_server_reload_timeout() !== null ) {
533
+ // Should we require the expected value to be seconds instead of ms?
534
+ ms_till_server_timeout = parseInt( session_vars.get_server_reload_timeout() );
535
+ } else {
536
+ // Default server timeout will always be based on scope.timeout even if it changes
537
+ ms_till_server_timeout = 5 * scope.timeout;
538
+ }
539
+
540
+ let throw_after_server_reloads = async function () {
541
+
542
+ // Continue waiting until server has responded or until it's taken too long
543
+ let time_accumulator = 0;
544
+ let max = ms_till_server_timeout/scope.server_check_interval_ms
545
+
546
+ while ( scope.server_statuses[0] !== true && time_accumulator < max ) {
547
+ await time.waitForTimeout( scope.server_check_interval_ms );
548
+ time_accumulator += 1;
549
+ }
550
+
551
+ // If the server hasn't responded yet even now, let the developer know
552
+ if ( time_accumulator >= max ) {
553
+ let timeout_message = `The server took longer than `
554
+ + `${ Math.round( ms_till_server_timeout/1000 ) } seconds to respond. `
555
+ + `If the server was just reloading and needed more time, try setting the `
556
+ + `"SERVER_RELOAD_TIMEOUT_SECONDS" GitHub secret to a longer amount of time.`
557
+ await scope.addToReport( scope, { type: `error`, value: timeout_message });
558
+ }
559
+
560
+ // Once server has responded, throw an error to end the test. After the first
561
+ // failure, the whole Scenario should retry because of command line flag. Hopefully
562
+ // it'll get through the next time. We considered trying to redo the input on the
563
+ // page instead of re-doing the whole test, but that wouldn't work - we don't
564
+ // know how many Steps to back to do that and we can't redo Steps anyway.
565
+ await scope.throw_server_was_reloading_error( scope )
566
+ };
567
+
568
+ await throw_after_server_reloads();
569
+
570
+ }, // Ends scope.wait_for_server_response()
571
+
572
+ throw_server_was_reloading_error: async function( scope ) {//, { server_timeout_id, timeout_message } ) {
573
+ /* Throw the server reload error with appropriate side effects. */
574
+ let reload_failure_msg = `The test was unable to continue because the `
575
+ + `server was reloading. ALKiln will try this Scenario a total of 2 times. An `
576
+ + `error will still be printed for this attempt.`;
577
+ await scope.addToReport( scope, { type: `error`, value: reload_failure_msg });
578
+ // await scope.addToReport( scope, { type: `error`, value: timeout_message });
579
+ // clearTimeout( server_timeout_id );
580
+ throw( reload_failure_msg );
581
+ }, // Ends scope.throw_server_was_reloading_error()
582
+
417
583
  detectDownloadComplete: async function detectDownloadComplete(scope, endTime) {
418
584
  /* Resolve if a download flag has changed. Timeout after a bit less than
419
585
  * the global permitted timeout
@@ -586,18 +752,36 @@ module.exports = {
586
752
  /* Try to load the page. Should we also pass a `timeout` arg or just
587
753
  * use scope.timeout? */
588
754
 
589
- // If there is no browser open, start a new one
590
- if (!scope.browser) {
591
- scope.browser = await scope.driver.launch({ headless: !session_vars.get_debug(), devtools: session_vars.get_debug() });
592
- }
593
- if ( !scope.page ) { scope.page = await scope.browser.newPage(); }
594
-
595
755
  if ( !file_name.includes('.yml') ) { file_name = `${ file_name }.yml` }
596
756
  let base_url = await get_base_interview_url();
597
757
  let interview_url = `${ base_url }${ file_name }`;
598
758
  if ( session_vars.get_debug() ) { console.log( interview_url ); }
599
759
 
600
- await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout })
760
+ if ( !scope.page ) { scope.page = await scope.browser.newPage(); }
761
+
762
+ // TODO: implement and use scope.handle_possible_timeout()
763
+ try {
764
+
765
+ await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout })
766
+
767
+ } catch ( error ) {
768
+
769
+ if ( error.name === `TimeoutError` ) {
770
+
771
+ let non_reload_report_msg = `Timeout error occurred when ALKiln tried to go to the interview url "${ interview_url }".`;
772
+ await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
773
+
774
+ } else {
775
+
776
+ // Throw any non-timeout error
777
+ scope.addToReport( scope, {
778
+ type: `error`,
779
+ value: `Error occurred when ALKiln tried to go to the interview url "${ interview_url }".`
780
+ });
781
+ throw( error );
782
+ } // ends if error is timeout error
783
+
784
+ } // ends try/catch
601
785
 
602
786
  let load_result = await scope.getLoadData( scope );
603
787
  // Errors, or reaching timeout, causes an exception and is shown to the developer at the end
@@ -1557,7 +1741,9 @@ module.exports = {
1557
1741
  }, // ends scope.enter_answer
1558
1742
 
1559
1743
  uploadFiles: async function ( scope, { handle, answer }) {
1560
- /* Try to upload one or more files to a file-type input field. */
1744
+ /* Try to upload one or more files to a file-type input field.
1745
+ * Does not submit, so no danger of timeout with server reload.
1746
+ */
1561
1747
 
1562
1748
  // Tests could be in one of two places. Figure out which directory they're in
1563
1749
  // Makes an absolute path from the directory in which `npm run` was used plus our internal folders
@@ -2110,12 +2296,14 @@ module.exports = {
2110
2296
  let actual_url = await prop_obj.jsonValue();
2111
2297
 
2112
2298
  let linkPage = await scope.browser.newPage();
2299
+ // This should not check for a server reload timeout error - it could be going anywhere.
2113
2300
  let response = await linkPage.goto( actual_url, { waitUntil: 'domcontentloaded' });
2114
2301
 
2115
2302
  let msg = `The "${ linkText }" link is broken.`;
2116
2303
  if ( !response.ok() ) { await scope.addToReport( scope, { type: `error`, value: msg }); }
2117
2304
  expect( response.ok(), msg ).to.be.true;
2118
2305
 
2306
+ // This may cause problems if the linked page is the interview page
2119
2307
  linkPage.close()
2120
2308
 
2121
2309
  await scope.afterStep(scope);
@@ -2164,7 +2352,7 @@ module.exports = {
2164
2352
  if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
2165
2353
  expect( link, msg ).to.exist;
2166
2354
 
2167
- await link.evaluate( elem => { return elem.click(); });
2355
+ await scope.tapElement( scope, link );
2168
2356
 
2169
2357
  await scope.afterStep(scope, {waitForShowIf: true});
2170
2358
  }, // Ends tap_term()
@@ -2192,6 +2380,7 @@ module.exports = {
2192
2380
  expect( elem, msg ).to.exist;
2193
2381
 
2194
2382
  scope.toDownload = filename;
2383
+ // Should this be using `scope.tapElement()`?
2195
2384
  await elem.evaluate( elem => { return elem.click(); });
2196
2385
  await scope.detectDownloadComplete( scope );
2197
2386
  }, // Ends download()
@@ -2284,8 +2473,26 @@ module.exports = {
2284
2473
 
2285
2474
  // Go to the sign in page
2286
2475
  let login_url = `${ session_vars.get_da_server_url() }/user/sign-in`;
2287
- await scope.page.goto( login_url, { waitUntil: `domcontentloaded`, timeout: scope.timeout })
2288
- await scope.page.waitForSelector( `.dabody` );
2476
+
2477
+ // TODO: implement and use scope.handle_possible_timeout()
2478
+ try {
2479
+ // puppeteer will ensure proper timeout.
2480
+ await scope.page.goto( login_url, { waitUntil: `domcontentloaded`, timeout: scope.timeout });
2481
+ await scope.page.waitForSelector( `.dabody` );
2482
+
2483
+ } catch ( error ) {
2484
+
2485
+ let err_msg = `Error occurred when ALKiln tried to go to "${ login_url }".`
2486
+ if ( error.name === `TimeoutError` ) {
2487
+ let non_reload_report_msg = `It took too long to load "${ login_url }"`;
2488
+ await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
2489
+ } else {
2490
+ // Throw any non-timeout error
2491
+ scope.addToReport( scope, { type: `error`, value: err_msg });
2492
+ throw error;
2493
+ } // ends if error is timeout error
2494
+
2495
+ } // ends try/catch
2289
2496
 
2290
2497
  let email = process.env[ email_secret_name ];
2291
2498
  let password = process.env[ password_secret_name ];
@@ -2301,13 +2508,15 @@ module.exports = {
2301
2508
  if ( password === undefined ) {
2302
2509
  await scope.addToReport( scope, { type: `error`, value: password_msg })
2303
2510
  }
2511
+
2304
2512
  expect( email, email_msg ).to.not.equal( undefined );
2305
2513
  expect( password, password_msg ).to.not.equal( undefined );
2306
2514
 
2307
2515
  // Try to log in
2308
2516
  await scope.page.type( `#email`, email );
2309
2517
  await scope.page.type( `#password`, password );
2310
- await scope.page.$eval( `button[name="submit"]`, (elem) => { return elem.click(); });
2518
+ let elem = await scope.page.$( `button[name="submit"]` );
2519
+ await scope.tapElement( scope, elem );
2311
2520
 
2312
2521
  // Wait to see the success message.
2313
2522
  // This is a bit of an assumption and needs success and failure testing
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
@@ -338,18 +323,26 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
338
323
  let supported_table = await scope.normalizeTable( scope, { raw_var_data });
339
324
 
340
325
  let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
341
- // 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)
342
327
  while ( !id_matches ) {
343
328
  let custom_timeout = new Promise(function( resolve, reject ) {
344
- // Set up the timeout
345
- let page_timeout = setTimeout(function() {
346
- let timeout_message = `The target id was "${ target_id }", but it took too long` +
347
- ` to try to reach it (over ${ scope.timeout/1000/60 } min). The test got stuck on "${ id }".`
348
- reject( timeout_message );
349
- }, 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);
350
340
 
351
341
  // self-invoke an anonymous async function so we don't have to
352
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?
353
346
  (async function() {
354
347
  try {
355
348
  let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
@@ -365,9 +358,12 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
365
358
  if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
366
359
  resolve();
367
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?
368
364
  reject( err );
369
365
  } finally {
370
- clearTimeout( page_timeout ); // prevent temporary hang at end of tests
366
+ clearTimeout( page_timeout_id ); // prevent temporary hang at end of tests
371
367
  }
372
368
  })();
373
369
  }); // ends custom_timeout for setFields
@@ -736,31 +732,25 @@ Then(/the "([^"]+)" link opens a working page/, { timeout: -1 }, async ( linkTex
736
732
  When('I tap to continue', { timeout: -1 }, async () => {
737
733
  // Any selectors for this seem somewhat precarious
738
734
  // No data will cause an automatic continue when `ensure_navigation` is true
739
- return wrapPromiseWithTimeout(
740
- scope.setFields( scope, { var_data: [], ensure_navigation: true }),
741
- scope.timeout
742
- );
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 });
743
739
  });
744
740
 
745
741
  When(/I tap the "([^"]+)" tab/i, { timeout: -1 }, async ( tabId ) => {
746
- return wrapPromiseWithTimeout(
747
- scope.tapTab( scope, tabId ),
748
- scope.timeout
749
- )
742
+ // Exiting the custom timeout happens in scope.tapElement() if needed
743
+ await scope.tapTab( scope, tabId );
750
744
  });
751
745
 
752
746
  When(/I tap the "([^"]+)" element$/i, { timeout: -1 }, async ( elemSelector ) => {
753
- return wrapPromiseWithTimeout(
754
- scope.tapElementBySelector( scope, elemSelector, 2000 ),
755
- scope.timeout
756
- )
747
+ // Exiting the custom timeout happens in scope.tapElement() if needed
748
+ await scope.tapElementBySelector( scope, elemSelector, 2000 );
757
749
  });
758
750
 
759
751
  When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
760
- return wrapPromiseWithTimeout(
761
- scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 ),
762
- scope.timeout
763
- )
752
+ // Exiting the custom timeout happens in scope.tapElement() if needed
753
+ await scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 )
764
754
  });
765
755
 
766
756
  When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
@@ -951,20 +941,18 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
951
941
  *
952
942
  * Variations:
953
943
  * 1. I set the var "benefits['SSI']" to "false"
954
- * 1. I set the var "rent_interval" to "weekly"
944
+ * 1. I set the variable "rent_interval" to "weekly"
955
945
  */
956
- return wrapPromiseWithTimeout(
957
- scope.steps.set_regular_var( scope, var_name, answer ),
958
- scope.timeout
959
- );
946
+ // Exiting the custom timeout happens in scope.tapElement() if needed
947
+ await scope.steps.set_regular_var( scope, var_name, answer )
948
+
960
949
  });
961
950
 
962
951
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
963
952
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
964
- return wrapPromiseWithTimeout(
965
- scope.steps.set_secret_var( scope, var_name, set_to_env_name ),
966
- scope.timeout
967
- );
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 )
968
956
  });
969
957
 
970
958
 
@@ -983,10 +971,8 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
983
971
 
984
972
  When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
985
973
  /** Tap a link that doesn't navigate. Depends on the language of the text. */
986
- return wrapPromiseWithTimeout(
987
- scope.steps.tap_term( scope, phrase ),
988
- scope.timeout
989
- );
974
+ // exiting the custom timeout happens in scope.tapElement() if needed
975
+ await scope.steps.tap_term( scope, phrase );
990
976
  });
991
977
 
992
978
  Then('I sign', { timeout: -1 }, async () => {
@@ -1003,6 +989,9 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
1003
989
  * WARNING: Must not contain any characters that are totally unique to that
1004
990
  * page load. Just stick to the name of the file.
1005
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
1006
995
  return wrapPromiseWithTimeout(
1007
996
  scope.steps.download( scope, filename ),
1008
997
  scope.timeout
@@ -1095,6 +1084,7 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
1095
1084
  //#####################################
1096
1085
 
1097
1086
  After(async function(scenario) {
1087
+
1098
1088
  // The accessibility failures don't happen til the end, so the rest of the test can continue
1099
1089
  if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
1100
1090
  // The fact that the scenario failed has already been noted earlier in the report
@@ -1119,6 +1109,7 @@ After(async function(scenario) {
1119
1109
  value: `For security, ALKiln will not create a screenshot for this error. It's possible a secret is being used on this screen.`
1120
1110
  });
1121
1111
  } else {
1112
+
1122
1113
  // Save/download a picture of the screen that's showing during the unexpected status
1123
1114
  // Save one copy in the outer-most artifact folder
1124
1115
  let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
@@ -1168,32 +1159,29 @@ After(async function(scenario) {
1168
1159
 
1169
1160
  // If there is a page open, then sign out and close it
1170
1161
  if ( scope.page ) {
1171
- // Make sure the new interview really starts fresh. Sometimes insufficient.
1172
- // Also, often is a failure point.
1173
- // Consider, instead, catching the reload error for a bad restart and just
1174
- // re-trying at that point.
1162
+ // Catch a possible reload error.
1163
+
1164
+ // TODO: implement and use scope.handle_possible_timeout()
1175
1165
  try {
1166
+ // puppeteer will ensure proper timeout.
1176
1167
  await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
1177
- } catch ( err ) {
1178
- if ( session_vars.DEBUG ) {
1179
- console.log( `Trying to reset for the next test a second time because the first time failed.` );
1180
- }
1181
1168
 
1182
- try {
1183
- await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
1184
-
1185
- } catch ( err ) {
1186
- await scope.addToReport(scope, {
1187
- type: `warning`,
1188
- value: `This test ${ original_scenario_status }, but also there `
1189
- + `was an error while trying to reset for the next test. You can try `
1190
- + `re-running the test or checking your server.`
1191
- });
1192
- }
1193
- }
1194
-
1195
- await scope.page.close();
1196
- 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
+
1197
1185
  } // ends if scope.page
1198
1186
 
1199
1187
  // Add report text to Scenario folder after everything has been added to the report
@@ -1205,6 +1193,9 @@ After(async function(scenario) {
1205
1193
  });
1206
1194
 
1207
1195
  AfterAll(async function() {
1196
+ // Stop collecting server response statuses
1197
+ scope.stop_tracking_server_status = true;
1198
+
1208
1199
  let report = await scope.getPrintableReport( scope );
1209
1200
  // Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
1210
1201
  // May need newest version of cucumberjs
@@ -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.1",
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'",
@@ -0,0 +1,155 @@
1
+ @reload
2
+ Feature: Errors caused by server reload
3
+
4
+ # These can only be run locally one at a time once you uncomment them:
5
+ # 1. Make sure you have two versions of the repo locally and open a terminal window for each of them
6
+ # 2. With one repo, start one specific test
7
+ # 3. Be ready to run the setup step for the 2nd repo
8
+ # 4. Prepatory explanation: With some noted exceptions, when the 1st test is _starting_ its long wait time (which they should each have), start the setup step in the other terminal.
9
+ # 4a. Start the test in the 1st repo. Ex: npm run cucumber -- "--tags" "@reload1"
10
+ # 4b. Wait until the long wait time is just starting (count the number of steps to match the progress dots)
11
+ # 4c. Start the setup in the 2nd repo: npm run setup
12
+ # 5. Make sure the test waits longer than the scope.timeout
13
+ # 6. Make sure the reload error is triggered for the first attempt of the test
14
+ # 7. Make sure the reload error is not present for the second attempt
15
+ # 8. For the second repo: npm run takedown
16
+
17
+ # # Tests that need to be written and run:
18
+ # # Theoretically implemented
19
+ # - [ ] 1. sign in to server with `.sign_in` on two levels:
20
+ # - [x] (throw_after_server_reloads() solved this I think) when navigating to server
21
+ # - [ ] when submitting to log in (impossible to time? they both happen at once)
22
+ # - [x] 2. (waits full time for sign-out page, but I think it's working otherwise) I tap the ... tab (should not cause timeout at all, so should be fine)
23
+ # - [x] 3. tap elements
24
+ # - [x] I tap the ... element$
25
+ # - [ ] (same as above?) I tap the ... element and wait
26
+ # - [x] 4. Set the var of a continue button
27
+ # - [x] I set the var ... (using a button press to continue)
28
+ # - [x] (same as above?) I set the var ... GitHub)? secret (for a button press)
29
+ # - [ ] (same as above? We don't have an interview ready for this yet. Seen notes in 4c) I set the var for a button in a story table
30
+ # - [x] 5. Signing out at end of step
31
+ # - [x] 6. Tap to continue
32
+ # - [x] I tap to continue
33
+ # - [x] In the middle of a story table (basically same as "tap to continue")
34
+ # - [x] 7. Fails both tests
35
+ # - [x] 8. The server does actually finish reloading during the first attempt, so there's only one error printed in the report.
36
+ #
37
+ # # Not yet implemented
38
+ # - [ ] I download
39
+ #
40
+ # Skipping for now because not a documented step yet
41
+ # - [ ] I tap the defined text link
42
+
43
+ ## For this scenario, start the server setup before you start running the test and wait till 4th attempt to pull in the code.
44
+ ## No screenshots are taken because these are secrets
45
+ #@reload1a
46
+ #Scenario: The server reloads while I navigate to the server sign in page
47
+ # Given the max seconds for each step in this scenario is 5
48
+ #  Given I sign in with the email "USER1_EMAIL" and the password "USER1_PASSWORD"
49
+ #
50
+ #@reload1b
51
+ ##Scenario: The server reloads while I'm submitting my signin (impossible)
52
+ #
53
+ ## Should error after finishing test, but only because can't finish the test by going to the sign out page during server #reload. Sign out page is a necessary evil at the moment to make sure the next test starts fresh.
54
+ #@reload2
55
+ #Scenario: The server reloads while I tap a tab
56
+ # Given I start the interview at "test_taps"
57
+ # And I wait 20 seconds
58
+ # And I tap the "Tests-second_template-tab" tab
59
+ # Then I see the phrase "villify"
60
+ #
61
+ #@reload3a
62
+ #Scenario: The server reloads while I tap an element selector
63
+ # Given I start the interview at "all_tests"
64
+ # And I wait 18 seconds
65
+ # Given the max seconds for each step in this scenario is 5
66
+ # Then I tap the "button.btn.btn-da.btn-primary[type='submit']" element
67
+ #
68
+ ##@reload3b
69
+ ## Given I start the interview at "all_tests"
70
+ ## And I wait 18 seconds
71
+ ## Given the max seconds for each step in this scenario is 5
72
+ ## Then I tap the "button.btn.btn-da.btn-primary[type='submit']" element and wait 1 seconds
73
+ #
74
+ ## Start setup on the 12th dot
75
+ #@reload4a
76
+ #Scenario: The server reloads while I set the var of a continue button
77
+ # Given I start the interview at "all_tests"
78
+ # And I tap to continue
79
+ # And I tap to continue
80
+ # When I set the var "checkboxes_yesno" to "True"
81
+ # And I set the var "checkboxes_other['checkbox_other_opt_1']" to "true"
82
+ # And I set the var "dropdown_test" to "dropdown_opt_2"
83
+ # And I set the var "radio_yesno" to "False"
84
+ # And I set the var "radio_other" to "radio_other_opt_3"
85
+ # And I set the var "text_input" to "Regular text input field value"
86
+ # And I set the var "textarea" to "Multiline text\narea value"
87
+ # And I wait 25 seconds
88
+ # Given the max seconds for each step in this scenario is 5
89
+ # Then I set the var "direct_standard_fields" to "True"
90
+ #
91
+ ## You must add the env var `STANDARD_FIELDS`. Otherwise, it's just like @reload4a
92
+ #@reload4b
93
+ #Scenario: The server reloads while I set the var of a continue button
94
+ # Given I start the interview at "all_tests"
95
+ # And I tap to continue
96
+ # And I tap to continue
97
+ # When I set the var "checkboxes_yesno" to "True"
98
+ # And I set the var "checkboxes_other['checkbox_other_opt_1']" to "true"
99
+ # And I set the var "dropdown_test" to "dropdown_opt_2"
100
+ # And I set the var "radio_yesno" to "False"
101
+ # And I set the var "radio_other" to "radio_other_opt_3"
102
+ # And I set the var "text_input" to "Regular text input field value"
103
+ # And I set the var "textarea" to "Multiline text\narea value"
104
+ # And I wait 25 seconds
105
+ # Given the max seconds for each step in this scenario is 5
106
+ # Then I set the var "direct_standard_fields" to the github secret "STANDARD_FIELDS"
107
+ #
108
+ ### We don't have an interview set up for this yet. We need one where the _first_ page
109
+ ### has a continue button field that sets a variable.
110
+ ##@reload4c
111
+ ##Scenario: The server reloads while I run a story table
112
+ ##  Given I start the interview at "all_tests"
113
+ ## And I wait 18 seconds
114
+ ## Given the max seconds for each step in this scenario is 3
115
+ ##  And I get to "id of second page" with this data:
116
+ ## | var | value | trigger |
117
+ ## | some_continue_button_var_on_first_page | true | |
118
+ #
119
+ #@reload5
120
+ #Scenario: The server reloads while signing out
121
+ # Given I start the interview at "all_tests"
122
+ # And I wait 16 seconds
123
+ # Given the max seconds for each step in this scenario is 1
124
+ #
125
+ #@reload6a
126
+ #Scenario: The server reloads while I tap to continue
127
+ # Given I start the interview at "all_tests"
128
+ # And I wait 18 seconds
129
+ # Given the max seconds for each step in this scenario is 3
130
+ # And I tap to continue
131
+ #
132
+ #@reload6b
133
+ #Scenario: The server reloads while I run a story table
134
+ #  Given I start the interview at "all_tests"
135
+ # And I wait 18 seconds
136
+ # Given the max seconds for each step in this scenario is 3
137
+ #  And I get to "direct standard fields" with this data:
138
+ # | var | value | trigger |
139
+ # | double_quote_dict['double_quote_key']["dq_two"] | true | |
140
+ # | single_quote_dict["single_quote_key"]['sq_two'] | true | |
141
+ #
142
+ ## This test might not be necessary. The test run should end in failure and the message should appear in both reports.
143
+ ## Run `npm run setup` both times. This will complicate takedown - it creates two projects, so at the end you have to takedown #the current project, which removes the project name in the config, then you have to put the name of the first project into the #config and then run takedown again.
144
+ #@reload7
145
+ #Scenario: The server is reloading in both attempts
146
+ # Given I start the interview at "all_tests"
147
+ # And I wait 22 seconds
148
+ # Given the max seconds for each step in this scenario is 1
149
+ #
150
+ #@reload8
151
+ #Scenario: The server finishes reloading before the first timeout
152
+ # Given I start the interview at "all_tests"
153
+ # And I wait 18 seconds
154
+ # Given the max seconds for each step in this scenario is 30
155
+ # And I tap to continue