@suffolklitlab/alkiln 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/lib/docassemble/docassemble_api_interface.js +18 -0
- package/lib/scope.js +309 -48
- package/lib/steps.js +191 -135
- package/lib/utils/files.js +13 -3
- package/lib/utils/session_vars.js +1 -0
- package/package.json +2 -2
- package/tests/features/interactive_steps.feature +0 -5
- package/tests/features/random_input.feature +18 -2
- package/tests/features/reload.feature +155 -0
- package/tests/features/story_table_formats.feature +49 -1
- package/tests/unit_tests/getSafeScenarioFilename.test.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -40,12 +40,35 @@ Format:
|
|
|
40
40
|
-->
|
|
41
41
|
<!-- ## [Unreleased] -->
|
|
42
42
|
|
|
43
|
+
## [4.8.0] - 2022-08-30
|
|
44
|
+
### Changed
|
|
45
|
+
- Date for filenames. Format date in y-m-d format instead of d-m-y that it had by default. Include ms.
|
|
46
|
+
|
|
47
|
+
## [4.7.2] - 2022-08-09
|
|
48
|
+
### Changed
|
|
49
|
+
- Inform the developer when a test failure may have been caused by a server reload. See https://github.com/SuffolkLITLab/ALKiln/issues/392.
|
|
50
|
+
- Retry all failing tests once.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## [4.7.1] - 2022-08-07
|
|
54
|
+
### Changed
|
|
55
|
+
- Ensure unique folder names for random input screenshots in case developer uses multiple random steps in one Scenario.
|
|
56
|
+
- Ensure random input detects unexpected interview errors.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- Fix failure when signature page is the first screen.
|
|
60
|
+
- Internal tests failing even when failure status is as expected.
|
|
61
|
+
- Double printing in report of scenario failure, one of them even when Scenario was possibly non-failure, like skipped.
|
|
62
|
+
|
|
43
63
|
## [4.7.0] - 2022-07-27
|
|
44
64
|
### Added
|
|
45
|
-
- Generate random input to fill out a simple form automatically
|
|
65
|
+
- Generate random input to fill out a simple form automatically. Does not yet ensure that random screenshot folder names are unique, or that error screens are detected as errors. 4.7.1 implements those.
|
|
46
66
|
|
|
47
67
|
### Changed
|
|
48
68
|
- Add page id to the "Missing Variable or variables on page" error report
|
|
69
|
+
- Allows Story Tables to not include a header row, as long as they have 2 or 3 columns.
|
|
70
|
+
This isn't the suggested way at the moment, since including headers is more readable,
|
|
71
|
+
but can prevent confusing and unnecessary errors.
|
|
49
72
|
|
|
50
73
|
## [4.6.2] - 2022-06-28
|
|
51
74
|
### Changed
|
|
@@ -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.
|
|
@@ -191,6 +194,7 @@ module.exports = {
|
|
|
191
194
|
let report = ``;
|
|
192
195
|
for ( let line of data.lines ) {
|
|
193
196
|
if ( line.type === `page_id` ) { report += `screen id: `; }
|
|
197
|
+
else if ( line.type === `outcome` ) { report += `\n`; }
|
|
194
198
|
else if ( line.type === `warning` ) { report += `WARNING: `; }
|
|
195
199
|
else if ( line.type === `error` ) { report += `\nERROR: ` }
|
|
196
200
|
report += `${ line.value }\n`;
|
|
@@ -356,41 +360,54 @@ module.exports = {
|
|
|
356
360
|
// This check might never be needed now as we try to match fields on the page
|
|
357
361
|
// first in user-triggered cases - users used to be able to access this almost
|
|
358
362
|
// directly, but they can't anymore. Our functions should know to only pass good things.
|
|
359
|
-
let msg = `Cannot find
|
|
363
|
+
let msg = `Cannot find the element to tap.`;
|
|
360
364
|
if ( !elem ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
361
365
|
|
|
362
366
|
expect( elem, msg ).to.exist;
|
|
363
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
|
+
|
|
364
371
|
let start_url = await scope.page.url(); // so we can later check for navigation
|
|
365
372
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// and other convos around it. We haven't made it public that the device
|
|
373
|
-
// can customized. Until we do that, we'll just use "click" all the time.
|
|
374
|
-
if (!tabToWaitFor) {
|
|
375
|
-
// Click with no navigation will end immediately
|
|
376
|
-
clickWait = Promise.all([
|
|
377
|
-
elem.evaluate( (el) => { return el.click(); }),
|
|
378
|
-
scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
|
379
|
-
]);
|
|
380
|
-
} else {
|
|
381
|
-
// Same thing as above, but the promise doesn't wait for navigation.
|
|
382
|
-
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' }) );
|
|
383
379
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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?
|
|
394
411
|
// Maybe url change + trigger var change? Maybe just trigger var?
|
|
395
412
|
let end_url = await scope.page.url();
|
|
396
413
|
let navigated = start_url !== end_url;
|
|
@@ -413,6 +430,156 @@ module.exports = {
|
|
|
413
430
|
return winner;
|
|
414
431
|
}, // Ends scope.tapElement()
|
|
415
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
|
+
|
|
416
583
|
detectDownloadComplete: async function detectDownloadComplete(scope, endTime) {
|
|
417
584
|
/* Resolve if a download flag has changed. Timeout after a bit less than
|
|
418
585
|
* the global permitted timeout
|
|
@@ -585,18 +752,36 @@ module.exports = {
|
|
|
585
752
|
/* Try to load the page. Should we also pass a `timeout` arg or just
|
|
586
753
|
* use scope.timeout? */
|
|
587
754
|
|
|
588
|
-
// If there is no browser open, start a new one
|
|
589
|
-
if (!scope.browser) {
|
|
590
|
-
scope.browser = await scope.driver.launch({ headless: !session_vars.get_debug(), devtools: session_vars.get_debug() });
|
|
591
|
-
}
|
|
592
|
-
if ( !scope.page ) { scope.page = await scope.browser.newPage(); }
|
|
593
|
-
|
|
594
755
|
if ( !file_name.includes('.yml') ) { file_name = `${ file_name }.yml` }
|
|
595
756
|
let base_url = await get_base_interview_url();
|
|
596
757
|
let interview_url = `${ base_url }${ file_name }`;
|
|
597
758
|
if ( session_vars.get_debug() ) { console.log( interview_url ); }
|
|
598
759
|
|
|
599
|
-
|
|
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
|
|
600
785
|
|
|
601
786
|
let load_result = await scope.getLoadData( scope );
|
|
602
787
|
// Errors, or reaching timeout, causes an exception and is shown to the developer at the end
|
|
@@ -617,6 +802,9 @@ module.exports = {
|
|
|
617
802
|
let result = { error: ``, elem: null };
|
|
618
803
|
|
|
619
804
|
let winner = await Promise.race([
|
|
805
|
+
// Signature page
|
|
806
|
+
scope.page.waitForSelector( `#dasigpage`, { timeout: scope.timeout }),
|
|
807
|
+
// Any other valid page
|
|
620
808
|
scope.page.waitForSelector( `#daMainQuestion`, { timeout: scope.timeout }),
|
|
621
809
|
scope.page.waitForSelector( `#da-retry`, { timeout: scope.timeout }),
|
|
622
810
|
]);
|
|
@@ -626,10 +814,14 @@ module.exports = {
|
|
|
626
814
|
let error_id_elem = await scope.page.$( `#da-retry` );
|
|
627
815
|
if ( error_id_elem ) {
|
|
628
816
|
let error_elem = await scope.page.$( `blockquote` );
|
|
629
|
-
|
|
630
|
-
|
|
817
|
+
if (error_elem !== null) {
|
|
818
|
+
let error_handle = await error_elem.getProperty( `textContent` );
|
|
819
|
+
let system_error_text = await error_handle.jsonValue();
|
|
631
820
|
|
|
632
|
-
|
|
821
|
+
result.error = system_error_text;
|
|
822
|
+
} else {
|
|
823
|
+
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";
|
|
824
|
+
}
|
|
633
825
|
}
|
|
634
826
|
|
|
635
827
|
return result;
|
|
@@ -650,9 +842,40 @@ module.exports = {
|
|
|
650
842
|
}
|
|
651
843
|
}, // Ends scope.setLanguage()
|
|
652
844
|
|
|
653
|
-
normalizeTable: async function ( scope, { var_data,
|
|
654
|
-
/* Return data derived from cucumber variable-setting data
|
|
655
|
-
*
|
|
845
|
+
normalizeTable: async function ( scope, { var_data=null, raw_var_data=null }) {
|
|
846
|
+
/* Return data derived from cucumber variable-setting data, from either an object format
|
|
847
|
+
* (var_data) or an array format (raw_var_data). `var_data` only is used when calling
|
|
848
|
+
* normalizeTable internally.
|
|
849
|
+
*/
|
|
850
|
+
|
|
851
|
+
if (!var_data && ! raw_var_data) {
|
|
852
|
+
let no_data_msg = `ALKiln Internal error: normalizeTable called w/o var_data or raw_var_data:` +
|
|
853
|
+
` open an issue at https://github.com/SuffolkLITLab/ALKiln/issues/new if you see this!`;
|
|
854
|
+
await scope.addToReport( scope, { type: `error`, value: no_data_msg });
|
|
855
|
+
throw Error(no_data_msg);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Always use the raw var data if present; it has the full first row
|
|
859
|
+
if (raw_var_data) {
|
|
860
|
+
var_data = raw_var_data.hashes();
|
|
861
|
+
if (!('var' in var_data[0]) || !('value' in var_data[0])) {
|
|
862
|
+
let raw_array = raw_var_data.raw();
|
|
863
|
+
var_data = [];
|
|
864
|
+
for (let row_raw of raw_array) {
|
|
865
|
+
if (row_raw.length < 2) {
|
|
866
|
+
let bad_table_msg = `Your Story Table definition needs to be changed. The best practice`
|
|
867
|
+
+ ` is to make your table 3 columns wide with a first row in this format: "| var | value | trigger |".`
|
|
868
|
+
+ ` The first row of your table is actually "${ raw_array[0] }"`;
|
|
869
|
+
await scope.addToReport( scope, { type: `error`, value: bad_table_msg})
|
|
870
|
+
throw Error(bad_table_msg);
|
|
871
|
+
} else if (row_raw.length == 2) {
|
|
872
|
+
var_data.push({var: row_raw[0], value: row_raw[1]});
|
|
873
|
+
} else {
|
|
874
|
+
var_data.push({var: row_raw[0], value: row_raw[1], trigger: row_raw[2]});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
656
879
|
|
|
657
880
|
// Support tables with no 'trigger' column
|
|
658
881
|
let supported_table = [];
|
|
@@ -1518,7 +1741,9 @@ module.exports = {
|
|
|
1518
1741
|
}, // ends scope.enter_answer
|
|
1519
1742
|
|
|
1520
1743
|
uploadFiles: async function ( scope, { handle, answer }) {
|
|
1521
|
-
/* 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
|
+
*/
|
|
1522
1747
|
|
|
1523
1748
|
// Tests could be in one of two places. Figure out which directory they're in
|
|
1524
1749
|
// Makes an absolute path from the directory in which `npm run` was used plus our internal folders
|
|
@@ -1934,6 +2159,20 @@ module.exports = {
|
|
|
1934
2159
|
}
|
|
1935
2160
|
}, // Ends scope.getThereIsAnotherValue()
|
|
1936
2161
|
|
|
2162
|
+
// throw_if_no_interview_content()?
|
|
2163
|
+
has_interview_content: async function ( scope ) {
|
|
2164
|
+
/** Return false if ther's no element indicating a da interview question,
|
|
2165
|
+
* like a signature page, other fields, or interview text. Assumes that page has already
|
|
2166
|
+
* finished navigating. */
|
|
2167
|
+
let elem_da_question = await scope.page.$( `#daMainQuestion` );
|
|
2168
|
+
let elem_da_signature = await scope.page.$( `#dasigpage` );
|
|
2169
|
+
if ( elem_da_question === null && elem_da_signature === null ) {
|
|
2170
|
+
return false;
|
|
2171
|
+
} else {
|
|
2172
|
+
return true;
|
|
2173
|
+
}
|
|
2174
|
+
}, // Ends scope.has_interview_content()
|
|
2175
|
+
|
|
1937
2176
|
waitUntilContinued: async function ( scope, { url = '', id = '' }) {
|
|
1938
2177
|
/** Given a url, attempt to continue until the page url changes or an error
|
|
1939
2178
|
is found. Return the error object no matter what. */
|
|
@@ -2057,12 +2296,14 @@ module.exports = {
|
|
|
2057
2296
|
let actual_url = await prop_obj.jsonValue();
|
|
2058
2297
|
|
|
2059
2298
|
let linkPage = await scope.browser.newPage();
|
|
2299
|
+
// This should not check for a server reload timeout error - it could be going anywhere.
|
|
2060
2300
|
let response = await linkPage.goto( actual_url, { waitUntil: 'domcontentloaded' });
|
|
2061
2301
|
|
|
2062
2302
|
let msg = `The "${ linkText }" link is broken.`;
|
|
2063
2303
|
if ( !response.ok() ) { await scope.addToReport( scope, { type: `error`, value: msg }); }
|
|
2064
2304
|
expect( response.ok(), msg ).to.be.true;
|
|
2065
2305
|
|
|
2306
|
+
// This may cause problems if the linked page is the interview page
|
|
2066
2307
|
linkPage.close()
|
|
2067
2308
|
|
|
2068
2309
|
await scope.afterStep(scope);
|
|
@@ -2111,7 +2352,7 @@ module.exports = {
|
|
|
2111
2352
|
if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
2112
2353
|
expect( link, msg ).to.exist;
|
|
2113
2354
|
|
|
2114
|
-
await
|
|
2355
|
+
await scope.tapElement( scope, link );
|
|
2115
2356
|
|
|
2116
2357
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2117
2358
|
}, // Ends tap_term()
|
|
@@ -2139,6 +2380,7 @@ module.exports = {
|
|
|
2139
2380
|
expect( elem, msg ).to.exist;
|
|
2140
2381
|
|
|
2141
2382
|
scope.toDownload = filename;
|
|
2383
|
+
// Should this be using `scope.tapElement()`?
|
|
2142
2384
|
await elem.evaluate( elem => { return elem.click(); });
|
|
2143
2385
|
await scope.detectDownloadComplete( scope );
|
|
2144
2386
|
}, // Ends download()
|
|
@@ -2231,8 +2473,26 @@ module.exports = {
|
|
|
2231
2473
|
|
|
2232
2474
|
// Go to the sign in page
|
|
2233
2475
|
let login_url = `${ session_vars.get_da_server_url() }/user/sign-in`;
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
|
2236
2496
|
|
|
2237
2497
|
let email = process.env[ email_secret_name ];
|
|
2238
2498
|
let password = process.env[ password_secret_name ];
|
|
@@ -2248,13 +2508,15 @@ module.exports = {
|
|
|
2248
2508
|
if ( password === undefined ) {
|
|
2249
2509
|
await scope.addToReport( scope, { type: `error`, value: password_msg })
|
|
2250
2510
|
}
|
|
2511
|
+
|
|
2251
2512
|
expect( email, email_msg ).to.not.equal( undefined );
|
|
2252
2513
|
expect( password, password_msg ).to.not.equal( undefined );
|
|
2253
2514
|
|
|
2254
2515
|
// Try to log in
|
|
2255
2516
|
await scope.page.type( `#email`, email );
|
|
2256
2517
|
await scope.page.type( `#password`, password );
|
|
2257
|
-
await scope.page.$
|
|
2518
|
+
let elem = await scope.page.$( `button[name="submit"]` );
|
|
2519
|
+
await scope.tapElement( scope, elem );
|
|
2258
2520
|
|
|
2259
2521
|
// Wait to see the success message.
|
|
2260
2522
|
// This is a bit of an assumption and needs success and failure testing
|
|
@@ -2279,7 +2541,6 @@ module.exports = {
|
|
|
2279
2541
|
|
|
2280
2542
|
// For every field on the page
|
|
2281
2543
|
for ( let field of fields ) {
|
|
2282
|
-
|
|
2283
2544
|
// fields[n].tag is HTML tag of node, as a string
|
|
2284
2545
|
|
|
2285
2546
|
// If the field is a button, save it in a list of buttons that we'll deal with later
|
|
@@ -2406,7 +2667,7 @@ module.exports = {
|
|
|
2406
2667
|
if ( type === `file` ) {
|
|
2407
2668
|
await scope.addToReport( scope, {
|
|
2408
2669
|
type: `warning`,
|
|
2409
|
-
value: `Sorry, ALKiln's random
|
|
2670
|
+
value: `Sorry, ALKiln's random input tests cannot yet handle "${ type }" fields.`
|
|
2410
2671
|
});
|
|
2411
2672
|
} else {
|
|
2412
2673
|
let answer = await scope.get_random_input_for[ type ]( scope );
|