@suffolklitlab/alkiln 4.7.1 → 4.9.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/CHANGELOG.md +15 -0
- package/lib/docassemble/docassemble_api_interface.js +18 -0
- package/lib/docassemble/setup.js +0 -5
- package/lib/scope.js +294 -46
- package/lib/steps.js +112 -122
- package/lib/utils/files.js +13 -4
- package/lib/utils/session_vars.js +1 -0
- package/package.json +2 -2
- package/tests/features/establishing_steps.feature +25 -0
- package/tests/features/reload.feature +155 -0
- package/tests/features/reports.feature +1 -0
- package/tests/unit_tests/getSafeScenarioFilename.test.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -40,6 +40,21 @@ Format:
|
|
|
40
40
|
-->
|
|
41
41
|
<!-- ## [Unreleased] -->
|
|
42
42
|
|
|
43
|
+
## [4.9.0] - 2022-09-01
|
|
44
|
+
### Changed
|
|
45
|
+
- Allow devs to test an arbitrary interview. See https://github.com/SuffolkLITLab/ALKiln/issues/600.
|
|
46
|
+
- Create artifacts folder even when setup has been skipped. See https://github.com/SuffolkLITLab/ALKiln/issues/600.
|
|
47
|
+
|
|
48
|
+
## [4.8.0] - 2022-08-30
|
|
49
|
+
### Changed
|
|
50
|
+
- Date for filenames. Format date in y-m-d format instead of d-m-y that it had by default. Include ms.
|
|
51
|
+
|
|
52
|
+
## [4.7.2] - 2022-08-09
|
|
53
|
+
### Changed
|
|
54
|
+
- Inform the developer when a test failure may have been caused by a server reload. See https://github.com/SuffolkLITLab/ALKiln/issues/392.
|
|
55
|
+
- Retry all failing tests once.
|
|
56
|
+
|
|
57
|
+
|
|
43
58
|
## [4.7.1] - 2022-08-07
|
|
44
59
|
### Changed
|
|
45
60
|
- 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/docassemble/setup.js
CHANGED
|
@@ -31,11 +31,6 @@ const setup = async () => {
|
|
|
31
31
|
await da_i.wait_for_server_to_restart( task_id );
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Make the folder to store all the files for this run of the tests
|
|
35
|
-
const artifacts_path_name = files.make_artifacts_folder();
|
|
36
|
-
// Save its name in a file. `session_vars` can get it back later.
|
|
37
|
-
session_vars.save_artifacts_path_name( artifacts_path_name );
|
|
38
|
-
|
|
39
34
|
// Finish
|
|
40
35
|
log.info({ type: `setup`, pre: `Success! Test setup has finished successfully.` });
|
|
41
36
|
}; // Ends setup();
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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,55 @@ 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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
755
|
+
await scope.addToReport(scope,{
|
|
756
|
+
type: `row`,
|
|
757
|
+
value: `Trying to load the interview`
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
let interview_url = null;
|
|
761
|
+
// If the file is already a full url, just use that arbitrary url
|
|
762
|
+
if ( /^http/.test( file_name ) ) {
|
|
763
|
+
interview_url = file_name;
|
|
764
|
+
|
|
765
|
+
// If the filename is not a full url
|
|
766
|
+
} else {
|
|
767
|
+
// If the filename is just the name of the interview without `.yml`, add `.yml` to the end
|
|
768
|
+
if ( !file_name.includes('.yml') ) { file_name = `${ file_name }.yml` }
|
|
769
|
+
// Otherwise, the filename might have url args, so leave it alone
|
|
770
|
+
|
|
771
|
+
// Add the server address to the filename
|
|
772
|
+
let base_url = await get_base_interview_url();
|
|
773
|
+
interview_url = `${ base_url }${ file_name }`;
|
|
592
774
|
}
|
|
775
|
+
if ( session_vars.get_debug() ) { console.log( `interview_url:`, interview_url ); }
|
|
776
|
+
|
|
593
777
|
if ( !scope.page ) { scope.page = await scope.browser.newPage(); }
|
|
594
778
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
779
|
+
// TODO: implement and use scope.handle_possible_timeout()
|
|
780
|
+
try {
|
|
781
|
+
|
|
782
|
+
await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout })
|
|
599
783
|
|
|
600
|
-
|
|
784
|
+
} catch ( error ) {
|
|
785
|
+
|
|
786
|
+
// If the page didn't load because it took to long to load
|
|
787
|
+
if ( error.name === `TimeoutError` ) {
|
|
788
|
+
|
|
789
|
+
let non_reload_report_msg = `Timeout error occurred when ALKiln tried to go to the interview url`;
|
|
790
|
+
await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
|
|
791
|
+
|
|
792
|
+
// If the page didn't load because of any other reason
|
|
793
|
+
} else {
|
|
794
|
+
|
|
795
|
+
// Throw any non-timeout error
|
|
796
|
+
scope.addToReport( scope, {
|
|
797
|
+
type: `error`,
|
|
798
|
+
value: `Error occurred when ALKiln tried to go to the interview url`
|
|
799
|
+
});
|
|
800
|
+
throw( error );
|
|
801
|
+
} // ends if error is timeout error
|
|
802
|
+
|
|
803
|
+
} // ends try/catch
|
|
601
804
|
|
|
602
805
|
let load_result = await scope.getLoadData( scope );
|
|
603
806
|
// Errors, or reaching timeout, causes an exception and is shown to the developer at the end
|
|
@@ -610,35 +813,55 @@ module.exports = {
|
|
|
610
813
|
}, // Ends scope.load()
|
|
611
814
|
|
|
612
815
|
getLoadData: async function ( scope ) {
|
|
613
|
-
/* Return
|
|
614
|
-
*
|
|
816
|
+
/* Return elements that could load on the page, like #daMainQuestion
|
|
817
|
+
* and #da-retry. If there's a system error on the page, return its main text.
|
|
615
818
|
*
|
|
616
819
|
* If server isn't loaded yet, it should get the browser timeout error,
|
|
617
|
-
* which won't match
|
|
820
|
+
* which won't match those selectors and this will timeout and error. */
|
|
618
821
|
let result = { error: ``, elem: null };
|
|
619
822
|
|
|
823
|
+
// Wait for at least one of these to appear
|
|
620
824
|
let winner = await Promise.race([
|
|
621
825
|
// Signature page
|
|
622
826
|
scope.page.waitForSelector( `#dasigpage`, { timeout: scope.timeout }),
|
|
623
827
|
// Any other valid page
|
|
624
828
|
scope.page.waitForSelector( `#daMainQuestion`, { timeout: scope.timeout }),
|
|
625
829
|
scope.page.waitForSelector( `#da-retry`, { timeout: scope.timeout }),
|
|
626
|
-
])
|
|
830
|
+
]).catch( function ( error ) {
|
|
831
|
+
// With any error in here, stop execution of the test
|
|
832
|
+
|
|
833
|
+
// If none of them appear before this times out
|
|
834
|
+
if ( error.name.includes(`TimeoutError`) ) {
|
|
835
|
+
|
|
836
|
+
result.error = `ALKiln could not find any interview question page with the given information`;
|
|
837
|
+
return result;
|
|
838
|
+
|
|
839
|
+
// or if they cause some other kind of error
|
|
840
|
+
} else {
|
|
841
|
+
throw error;
|
|
842
|
+
} // ends if error.name.includes(`TimeoutError`)
|
|
843
|
+
}); // ends Promise.race()
|
|
627
844
|
|
|
628
845
|
result.elem = winner;
|
|
629
846
|
|
|
847
|
+
// If a page was loaded, but there's a specific type of error on the page
|
|
848
|
+
// throw an error
|
|
630
849
|
let error_id_elem = await scope.page.$( `#da-retry` );
|
|
631
850
|
if ( error_id_elem ) {
|
|
851
|
+
|
|
852
|
+
// If it's a specific type of error, we can show the developer its text
|
|
632
853
|
let error_elem = await scope.page.$( `blockquote` );
|
|
633
854
|
if (error_elem !== null) {
|
|
634
855
|
let error_handle = await error_elem.getProperty( `textContent` );
|
|
635
856
|
let system_error_text = await error_handle.jsonValue();
|
|
636
857
|
|
|
637
858
|
result.error = system_error_text;
|
|
859
|
+
|
|
860
|
+
// Error types we haven't accounted for
|
|
638
861
|
} else {
|
|
639
862
|
result.error = "Docassemble ran into an error on the page, but ALKiln does not know what the error is. If a screenshot was taken, check the screenshot";
|
|
640
|
-
}
|
|
641
|
-
}
|
|
863
|
+
} // ends if error_elem !== null
|
|
864
|
+
} // ends if error_id_elem
|
|
642
865
|
|
|
643
866
|
return result;
|
|
644
867
|
}, // Ends scope.getLoadData()
|
|
@@ -1557,7 +1780,9 @@ module.exports = {
|
|
|
1557
1780
|
}, // ends scope.enter_answer
|
|
1558
1781
|
|
|
1559
1782
|
uploadFiles: async function ( scope, { handle, answer }) {
|
|
1560
|
-
/* Try to upload one or more files to a file-type input field.
|
|
1783
|
+
/* Try to upload one or more files to a file-type input field.
|
|
1784
|
+
* Does not submit, so no danger of timeout with server reload.
|
|
1785
|
+
*/
|
|
1561
1786
|
|
|
1562
1787
|
// Tests could be in one of two places. Figure out which directory they're in
|
|
1563
1788
|
// Makes an absolute path from the directory in which `npm run` was used plus our internal folders
|
|
@@ -2110,12 +2335,14 @@ module.exports = {
|
|
|
2110
2335
|
let actual_url = await prop_obj.jsonValue();
|
|
2111
2336
|
|
|
2112
2337
|
let linkPage = await scope.browser.newPage();
|
|
2338
|
+
// This should not check for a server reload timeout error - it could be going anywhere.
|
|
2113
2339
|
let response = await linkPage.goto( actual_url, { waitUntil: 'domcontentloaded' });
|
|
2114
2340
|
|
|
2115
2341
|
let msg = `The "${ linkText }" link is broken.`;
|
|
2116
2342
|
if ( !response.ok() ) { await scope.addToReport( scope, { type: `error`, value: msg }); }
|
|
2117
2343
|
expect( response.ok(), msg ).to.be.true;
|
|
2118
2344
|
|
|
2345
|
+
// This may cause problems if the linked page is the interview page
|
|
2119
2346
|
linkPage.close()
|
|
2120
2347
|
|
|
2121
2348
|
await scope.afterStep(scope);
|
|
@@ -2164,7 +2391,7 @@ module.exports = {
|
|
|
2164
2391
|
if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
2165
2392
|
expect( link, msg ).to.exist;
|
|
2166
2393
|
|
|
2167
|
-
await
|
|
2394
|
+
await scope.tapElement( scope, link );
|
|
2168
2395
|
|
|
2169
2396
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2170
2397
|
}, // Ends tap_term()
|
|
@@ -2192,6 +2419,7 @@ module.exports = {
|
|
|
2192
2419
|
expect( elem, msg ).to.exist;
|
|
2193
2420
|
|
|
2194
2421
|
scope.toDownload = filename;
|
|
2422
|
+
// Should this be using `scope.tapElement()`?
|
|
2195
2423
|
await elem.evaluate( elem => { return elem.click(); });
|
|
2196
2424
|
await scope.detectDownloadComplete( scope );
|
|
2197
2425
|
}, // Ends download()
|
|
@@ -2284,8 +2512,26 @@ module.exports = {
|
|
|
2284
2512
|
|
|
2285
2513
|
// Go to the sign in page
|
|
2286
2514
|
let login_url = `${ session_vars.get_da_server_url() }/user/sign-in`;
|
|
2287
|
-
|
|
2288
|
-
|
|
2515
|
+
|
|
2516
|
+
// TODO: implement and use scope.handle_possible_timeout()
|
|
2517
|
+
try {
|
|
2518
|
+
// puppeteer will ensure proper timeout.
|
|
2519
|
+
await scope.page.goto( login_url, { waitUntil: `domcontentloaded`, timeout: scope.timeout });
|
|
2520
|
+
await scope.page.waitForSelector( `.dabody` );
|
|
2521
|
+
|
|
2522
|
+
} catch ( error ) {
|
|
2523
|
+
|
|
2524
|
+
let err_msg = `Error occurred when ALKiln tried to go to "${ login_url }".`
|
|
2525
|
+
if ( error.name === `TimeoutError` ) {
|
|
2526
|
+
let non_reload_report_msg = `It took too long to load "${ login_url }"`;
|
|
2527
|
+
await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
|
|
2528
|
+
} else {
|
|
2529
|
+
// Throw any non-timeout error
|
|
2530
|
+
scope.addToReport( scope, { type: `error`, value: err_msg });
|
|
2531
|
+
throw error;
|
|
2532
|
+
} // ends if error is timeout error
|
|
2533
|
+
|
|
2534
|
+
} // ends try/catch
|
|
2289
2535
|
|
|
2290
2536
|
let email = process.env[ email_secret_name ];
|
|
2291
2537
|
let password = process.env[ password_secret_name ];
|
|
@@ -2301,13 +2547,15 @@ module.exports = {
|
|
|
2301
2547
|
if ( password === undefined ) {
|
|
2302
2548
|
await scope.addToReport( scope, { type: `error`, value: password_msg })
|
|
2303
2549
|
}
|
|
2550
|
+
|
|
2304
2551
|
expect( email, email_msg ).to.not.equal( undefined );
|
|
2305
2552
|
expect( password, password_msg ).to.not.equal( undefined );
|
|
2306
2553
|
|
|
2307
2554
|
// Try to log in
|
|
2308
2555
|
await scope.page.type( `#email`, email );
|
|
2309
2556
|
await scope.page.type( `#password`, password );
|
|
2310
|
-
await scope.page.$
|
|
2557
|
+
let elem = await scope.page.$( `button[name="submit"]` );
|
|
2558
|
+
await scope.tapElement( scope, elem );
|
|
2311
2559
|
|
|
2312
2560
|
// Wait to see the success message.
|
|
2313
2561
|
// 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
|
-
|
|
71
|
+
let default_timeout = 30 * 1000
|
|
72
|
+
setDefaultTimeout( default_timeout );
|
|
65
73
|
|
|
66
74
|
let click_with = {
|
|
67
75
|
mobile: 'tap',
|
|
@@ -69,32 +77,45 @@ 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
|
-
// Test setup should have created this filepath. Run setup if there's a problem with this.
|
|
79
|
-
scope.paths.artifacts = session_vars.get_artifacts_path_name();
|
|
80
|
-
// For GitHub tests, use the artifacts folder that was created during setup
|
|
81
|
-
// For local tests, make a new folder for each time scenarios start
|
|
82
|
-
if ( !session_vars.is_dev_env() ) {
|
|
83
|
-
// Make a new folder to store all the files for this run of the tests
|
|
84
|
-
scope.paths.artifacts = files.make_artifacts_folder();
|
|
85
|
-
// Save its name in a file. `session_vars` can get it back later.
|
|
86
|
-
session_vars.save_artifacts_path_name( scope.paths.artifacts );
|
|
87
|
-
}
|
|
88
|
-
scope.paths.report = `${ scope.paths.artifacts }/report.txt`;
|
|
89
88
|
|
|
89
|
+
// TODO: figure out how to test file and folder creation stuff,
|
|
90
|
+
// especially when setup is excluded from the flow.
|
|
91
|
+
|
|
92
|
+
// Make the folder to store all the files for this run of the tests
|
|
93
|
+
scope.paths.artifacts = files.make_artifacts_folder();
|
|
94
|
+
// Save its name in a file. `session_vars` can get it back later.
|
|
95
|
+
// This is used by at least clean_reports.js.
|
|
96
|
+
session_vars.save_artifacts_path_name( scope.paths.artifacts );
|
|
97
|
+
|
|
98
|
+
scope.paths.report = `${ scope.paths.artifacts }/report.txt`;
|
|
90
99
|
scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_file }`;
|
|
91
100
|
fs.writeFileSync( scope.paths.debug_log, "" );
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
Before(async (scenario) => {
|
|
104
|
+
// Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason
|
|
105
|
+
if (!scope.browser) {
|
|
106
|
+
scope.browser = await scope.driver.launch({ headless: !session_vars.get_debug(), devtools: session_vars.get_debug() });
|
|
107
|
+
}
|
|
108
|
+
// Clean up all previously existing pages
|
|
109
|
+
for (const page of await scope.browser.pages()) {
|
|
110
|
+
await page.close();
|
|
111
|
+
}
|
|
112
|
+
// Make a new page
|
|
113
|
+
scope.page = await scope.browser.newPage()
|
|
114
|
+
|
|
95
115
|
scope.scenario_id = uuidv4();
|
|
96
116
|
scope.expected_status = null;
|
|
97
117
|
scope.expected_in_report = null;
|
|
118
|
+
scope.server_reload_promise = null;
|
|
98
119
|
|
|
99
120
|
await scope.addReportHeading(scope, {scenario});
|
|
100
121
|
// Make folder for this Scenario in the all-tests artifacts folder
|
|
@@ -119,7 +140,7 @@ Before(async (scenario) => {
|
|
|
119
140
|
scope.passed_all_for_a11y = true;
|
|
120
141
|
|
|
121
142
|
// Reset default timeout
|
|
122
|
-
scope.timeout =
|
|
143
|
+
scope.timeout = default_timeout;
|
|
123
144
|
});
|
|
124
145
|
|
|
125
146
|
// Add a check for an error page before each step? After each step?
|
|
@@ -132,47 +153,9 @@ Before(async (scenario) => {
|
|
|
132
153
|
//#####################################
|
|
133
154
|
|
|
134
155
|
Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}, async (file_name, language) => { // √
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
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
|
|
156
|
+
|
|
157
|
+
// Load the right interview page
|
|
158
|
+
await scope.load( scope, file_name );
|
|
176
159
|
|
|
177
160
|
// The page timeout is set in here too in case someone set a custom timeout
|
|
178
161
|
// before a page was loaded.
|
|
@@ -276,13 +259,13 @@ Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^
|
|
|
276
259
|
* I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
|
|
277
260
|
* I log in with "USER1_EMAIL" and "USER1_PASSWORD"
|
|
278
261
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
262
|
+
// Exiting the custom timeout happens in scope.tapElement() if needed
|
|
263
|
+
// Couldn't test the timeout for tapping the button because there's not
|
|
264
|
+
// enough of a pause between initial navigation and pressing button.
|
|
265
|
+
await scope.steps.sign_in( scope, {
|
|
266
|
+
email_secret_name: email,
|
|
267
|
+
password_secret_name: password
|
|
268
|
+
});
|
|
286
269
|
});
|
|
287
270
|
|
|
288
271
|
// I am using a mobile/pc
|
|
@@ -304,6 +287,7 @@ When(/I am using an? (.*)/, async ( device ) => {
|
|
|
304
287
|
|
|
305
288
|
Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+)/i, async ( seconds ) => {
|
|
306
289
|
/* At any time, set a custom timeout for the scenario. */
|
|
290
|
+
// TODO: Add 1 second to this to allow other steps to not fail, like "I wait n seconds"
|
|
307
291
|
scope.timeout = parseInt( seconds ) * 1000;
|
|
308
292
|
|
|
309
293
|
// Cucumber timeouts are made custom for each step that needs it
|
|
@@ -338,18 +322,26 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
|
|
|
338
322
|
let supported_table = await scope.normalizeTable( scope, { raw_var_data });
|
|
339
323
|
|
|
340
324
|
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
|
|
341
|
-
// Until we reach the
|
|
325
|
+
// Until we reach the target page, hit an error, or time out (take too long)
|
|
342
326
|
while ( !id_matches ) {
|
|
343
327
|
let custom_timeout = new Promise(function( resolve, reject ) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
328
|
+
|
|
329
|
+
// Set up the timeout that avoids infinite loops
|
|
330
|
+
let on_page_timeout = async function() {
|
|
331
|
+
|
|
332
|
+
let non_reload_report_msg = `The target id was "${ target_id }", but it took `
|
|
333
|
+
+ `too long to try to reach it (over ${ scope.timeout/1000/60 } min). The `
|
|
334
|
+
+ `test got stuck on "${ id }".`
|
|
335
|
+
let error = non_reload_report_msg;
|
|
336
|
+
await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
|
|
337
|
+
}; // Ends on_page_timeout()
|
|
338
|
+
let page_timeout_id = setTimeout(on_page_timeout, scope.timeout);
|
|
350
339
|
|
|
351
340
|
// self-invoke an anonymous async function so we don't have to
|
|
352
341
|
// abstract the details of resolving and rejecting.
|
|
342
|
+
// If we need to call recursively, signature function might look like:
|
|
343
|
+
// func( scope, { resolve, reject, supported_table, page_timeout_id })
|
|
344
|
+
// Maybe list of timeout and/or interval ids in state that can all be cleared at once?
|
|
353
345
|
(async function() {
|
|
354
346
|
try {
|
|
355
347
|
let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
|
|
@@ -365,9 +357,12 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
|
|
|
365
357
|
if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
|
|
366
358
|
resolve();
|
|
367
359
|
} catch ( err ) {
|
|
360
|
+
let err_msg = `Error occurred when tried to answer fields on question "${ id }".`
|
|
361
|
+
scope.addToReport( scope, { type: `error`, value: err_msg });
|
|
362
|
+
// Throw error instead?
|
|
368
363
|
reject( err );
|
|
369
364
|
} finally {
|
|
370
|
-
clearTimeout(
|
|
365
|
+
clearTimeout( page_timeout_id ); // prevent temporary hang at end of tests
|
|
371
366
|
}
|
|
372
367
|
})();
|
|
373
368
|
}); // ends custom_timeout for setFields
|
|
@@ -736,31 +731,25 @@ Then(/the "([^"]+)" link opens a working page/, { timeout: -1 }, async ( linkTex
|
|
|
736
731
|
When('I tap to continue', { timeout: -1 }, async () => {
|
|
737
732
|
// Any selectors for this seem somewhat precarious
|
|
738
733
|
// No data will cause an automatic continue when `ensure_navigation` is true
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
);
|
|
734
|
+
|
|
735
|
+
// Exiting the custom timeout happens in scope.tapElement() where we can
|
|
736
|
+
// check if the server is reloading
|
|
737
|
+
await scope.setFields( scope, { var_data: [], ensure_navigation: true });
|
|
743
738
|
});
|
|
744
739
|
|
|
745
740
|
When(/I tap the "([^"]+)" tab/i, { timeout: -1 }, async ( tabId ) => {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
scope.timeout
|
|
749
|
-
)
|
|
741
|
+
// Exiting the custom timeout happens in scope.tapElement() if needed
|
|
742
|
+
await scope.tapTab( scope, tabId );
|
|
750
743
|
});
|
|
751
744
|
|
|
752
745
|
When(/I tap the "([^"]+)" element$/i, { timeout: -1 }, async ( elemSelector ) => {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
scope.timeout
|
|
756
|
-
)
|
|
746
|
+
// Exiting the custom timeout happens in scope.tapElement() if needed
|
|
747
|
+
await scope.tapElementBySelector( scope, elemSelector, 2000 );
|
|
757
748
|
});
|
|
758
749
|
|
|
759
750
|
When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
scope.timeout
|
|
763
|
-
)
|
|
751
|
+
// Exiting the custom timeout happens in scope.tapElement() if needed
|
|
752
|
+
await scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 )
|
|
764
753
|
});
|
|
765
754
|
|
|
766
755
|
When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
|
|
@@ -951,20 +940,18 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
|
|
|
951
940
|
*
|
|
952
941
|
* Variations:
|
|
953
942
|
* 1. I set the var "benefits['SSI']" to "false"
|
|
954
|
-
* 1. I set the
|
|
943
|
+
* 1. I set the variable "rent_interval" to "weekly"
|
|
955
944
|
*/
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
);
|
|
945
|
+
// Exiting the custom timeout happens in scope.tapElement() if needed
|
|
946
|
+
await scope.steps.set_regular_var( scope, var_name, answer )
|
|
947
|
+
|
|
960
948
|
});
|
|
961
949
|
|
|
962
950
|
When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
|
|
963
951
|
/* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
);
|
|
952
|
+
// This could theoretically tap an element, so
|
|
953
|
+
// exiting the custom timeout happens in scope.tapElement() if needed
|
|
954
|
+
await scope.steps.set_secret_var( scope, var_name, set_to_env_name )
|
|
968
955
|
});
|
|
969
956
|
|
|
970
957
|
|
|
@@ -983,10 +970,8 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
|
|
|
983
970
|
|
|
984
971
|
When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
|
|
985
972
|
/** Tap a link that doesn't navigate. Depends on the language of the text. */
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
scope.timeout
|
|
989
|
-
);
|
|
973
|
+
// exiting the custom timeout happens in scope.tapElement() if needed
|
|
974
|
+
await scope.steps.tap_term( scope, phrase );
|
|
990
975
|
});
|
|
991
976
|
|
|
992
977
|
Then('I sign', { timeout: -1 }, async () => {
|
|
@@ -1003,6 +988,9 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
|
|
|
1003
988
|
* WARNING: Must not contain any characters that are totally unique to that
|
|
1004
989
|
* page load. Just stick to the name of the file.
|
|
1005
990
|
*/
|
|
991
|
+
// TODO:
|
|
992
|
+
// This should be using `scope.tapElement()`? When someone clicks on a link to
|
|
993
|
+
// download a document, it sends a request to the server, which could be reloading
|
|
1006
994
|
return wrapPromiseWithTimeout(
|
|
1007
995
|
scope.steps.download( scope, filename ),
|
|
1008
996
|
scope.timeout
|
|
@@ -1095,6 +1083,7 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
|
|
|
1095
1083
|
//#####################################
|
|
1096
1084
|
|
|
1097
1085
|
After(async function(scenario) {
|
|
1086
|
+
|
|
1098
1087
|
// The accessibility failures don't happen til the end, so the rest of the test can continue
|
|
1099
1088
|
if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
|
|
1100
1089
|
// The fact that the scenario failed has already been noted earlier in the report
|
|
@@ -1119,6 +1108,7 @@ After(async function(scenario) {
|
|
|
1119
1108
|
value: `For security, ALKiln will not create a screenshot for this error. It's possible a secret is being used on this screen.`
|
|
1120
1109
|
});
|
|
1121
1110
|
} else {
|
|
1111
|
+
|
|
1122
1112
|
// Save/download a picture of the screen that's showing during the unexpected status
|
|
1123
1113
|
// Save one copy in the outer-most artifact folder
|
|
1124
1114
|
let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
|
|
@@ -1168,32 +1158,29 @@ After(async function(scenario) {
|
|
|
1168
1158
|
|
|
1169
1159
|
// If there is a page open, then sign out and close it
|
|
1170
1160
|
if ( scope.page ) {
|
|
1171
|
-
//
|
|
1172
|
-
|
|
1173
|
-
//
|
|
1174
|
-
// re-trying at that point.
|
|
1161
|
+
// Catch a possible reload error.
|
|
1162
|
+
|
|
1163
|
+
// TODO: implement and use scope.handle_possible_timeout()
|
|
1175
1164
|
try {
|
|
1165
|
+
// puppeteer will ensure proper timeout.
|
|
1176
1166
|
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
1167
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1168
|
+
} catch ( error ) {
|
|
1169
|
+
|
|
1170
|
+
if ( error.name === `TimeoutError` ) {
|
|
1171
|
+
// TODO: Should a more specific message about signing out get into the report no matter what?
|
|
1172
|
+
// If so, what should the message be in order to not get confused with these other messages?
|
|
1173
|
+
let non_reload_report_msg = `It took too long to sign out in preparation for the next test.`;
|
|
1174
|
+
await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
|
|
1175
|
+
} else {
|
|
1176
|
+
// Throw any non-timeout error
|
|
1177
|
+
let err_msg = `Error occurred when ALKiln tried to sign out in preparation for the next test.`
|
|
1178
|
+
scope.addToReport( scope, { type: `error`, value: err_msg });
|
|
1179
|
+
throw error;
|
|
1180
|
+
} // ends if error is timeout error
|
|
1181
|
+
|
|
1182
|
+
} // ends try/catch
|
|
1183
|
+
|
|
1197
1184
|
} // ends if scope.page
|
|
1198
1185
|
|
|
1199
1186
|
// Add report text to Scenario folder after everything has been added to the report
|
|
@@ -1205,6 +1192,9 @@ After(async function(scenario) {
|
|
|
1205
1192
|
});
|
|
1206
1193
|
|
|
1207
1194
|
AfterAll(async function() {
|
|
1195
|
+
// Stop collecting server response statuses
|
|
1196
|
+
scope.stop_tracking_server_status = true;
|
|
1197
|
+
|
|
1208
1198
|
let report = await scope.getPrintableReport( scope );
|
|
1209
1199
|
// Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
|
|
1210
1200
|
// May need newest version of cucumberjs
|
package/lib/utils/files.js
CHANGED
|
@@ -5,11 +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
|
|
11
|
-
|
|
12
|
-
let
|
|
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`;
|
|
13
22
|
return fancy_str;
|
|
14
23
|
}; // Ends files.readable_date()
|
|
15
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.
|
|
3
|
+
"version": "4.9.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'",
|
|
@@ -35,3 +35,28 @@ Scenario: Interview name includes url args with a wait
|
|
|
35
35
|
Scenario: I sign in
|
|
36
36
|
Given I sign in with the email "USER1_EMAIL" and the password "USER1_PASSWORD"
|
|
37
37
|
And I start the interview at "all_tests"
|
|
38
|
+
|
|
39
|
+
@fast @e7 @arbitraryurl
|
|
40
|
+
Scenario: I want to go to a full arbitrary url
|
|
41
|
+
Given the max seconds for each step in this scenario is 5
|
|
42
|
+
And the Scenario report should include:
|
|
43
|
+
"""
|
|
44
|
+
Trying to load the interview
|
|
45
|
+
"""
|
|
46
|
+
Given I start the interview at "https://apps-test.suffolklitlab.org/start/demo/questions"
|
|
47
|
+
Then I should see the phrase "What language do you speak?"
|
|
48
|
+
|
|
49
|
+
# WARNING: This Scenario may fail if the file moves or changes too much
|
|
50
|
+
@fast @failing @e8 @rfe8 @arbitraryurl
|
|
51
|
+
Scenario: Fail with no interview at arbitrary url
|
|
52
|
+
Given the final Scenario status should be "failed"
|
|
53
|
+
And the Scenario report should include:
|
|
54
|
+
"""
|
|
55
|
+
Trying to load the interview
|
|
56
|
+
"""
|
|
57
|
+
And the Scenario report should include:
|
|
58
|
+
"""
|
|
59
|
+
ALKiln could not find any interview question page
|
|
60
|
+
"""
|
|
61
|
+
And the max seconds for each step in this scenario is 5
|
|
62
|
+
Then I start the interview at "https://apps-test.suffolklitlab.org/list"
|
|
@@ -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
|
|
@@ -444,6 +444,7 @@ Scenario: Report still shows page id when I tap to continue without setting any
|
|
|
444
444
|
Scenario: Report still shows page id when I tap to continue without setting any fields
|
|
445
445
|
Tags: @reports @fast @rp1
|
|
446
446
|
---------------
|
|
447
|
+
Trying to load the interview
|
|
447
448
|
screen id: upload-files
|
|
448
449
|
screen id: group-of-complex-fields
|
|
449
450
|
| double_quote_dict['double_quote_key']['dq_two'] | true | |
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
let test_filename = function ( expected_prefix, result ) {
|
|
11
11
|
/* Test a name to see if it's valid output with a language defined. */
|
|
12
|
-
let expected_name_regex_str = `^${ expected_prefix }-${ names.base_filename }-\.{
|
|
12
|
+
let expected_name_regex_str = `^${ expected_prefix }-${ names.base_filename }-\.{40}$`;
|
|
13
13
|
if ( expected_prefix == `` ) {
|
|
14
|
-
expected_name_regex_str = `^${ names.base_filename }-\.{
|
|
14
|
+
expected_name_regex_str = `^${ names.base_filename }-\.{40}$`;
|
|
15
15
|
}
|
|
16
16
|
let regex = new RegExp( expected_name_regex_str );
|
|
17
17
|
|