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