@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/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
|
|
@@ -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 () => {
|
|
@@ -790,35 +781,61 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
790
781
|
* I answer randomly for at most 10 pages
|
|
791
782
|
* I answer randomly for at most 101 screens
|
|
792
783
|
*/
|
|
793
|
-
|
|
784
|
+
// Give each screenshots folder a time-stamped name in case the dev gets clever
|
|
785
|
+
// and use the same Scenario to run multiple randomized tests.
|
|
786
|
+
let timestamp = files.readable_date();
|
|
787
|
+
let random_input_path = `${ scope.paths.scenario }/random_answers_screenshots-${ timestamp }`
|
|
788
|
+
scope.paths.random_screenshots = random_input_path;
|
|
794
789
|
fs.mkdirSync( scope.paths.random_screenshots );
|
|
790
|
+
await scope.addToReport( scope, {
|
|
791
|
+
type: 'row',
|
|
792
|
+
value: `Testing interview with random input (in ${ random_input_path })`
|
|
793
|
+
});
|
|
795
794
|
|
|
796
795
|
// Force the dev to prevent infinite loops
|
|
797
796
|
let max_pages = parseInt( max_pages_str );
|
|
798
797
|
let curr_page_num = 0;
|
|
799
798
|
|
|
800
|
-
// While a continue button is on the current page, fill out
|
|
799
|
+
// While a continue-type button is on the current page, fill out
|
|
801
800
|
// the fields and then try to continue
|
|
802
|
-
// let continue_exists = true;
|
|
803
|
-
// while ( continue_exists && curr_page_num < max_pages ) {
|
|
804
801
|
let buttons_exist = true;
|
|
805
802
|
while ( buttons_exist && curr_page_num < max_pages ) {
|
|
806
803
|
|
|
807
804
|
curr_page_num = curr_page_num + 1;
|
|
808
805
|
let { id } = await scope.examinePageID( scope, `no target id` );
|
|
809
806
|
|
|
807
|
+
let no_content_err = `The interview seemed to run into an error page`
|
|
808
|
+
+ ` after the page with the question id "${ id }".`
|
|
809
|
+
+ ` See the screenshot in "${ random_input_path }".`;
|
|
810
|
+
|
|
811
|
+
let has_thrown = false;
|
|
810
812
|
// Ensure no infinite loop, but each page has a full timeout to run
|
|
811
813
|
let custom_timeout = new Promise(async function( resolve, reject ) {
|
|
812
814
|
|
|
813
815
|
// Set up timeout to stop infinite loops
|
|
814
816
|
let page_timeout_id = setTimeout(async function() {
|
|
815
817
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
throw( timeout_message );
|
|
818
|
+
// Make sure to only throw once
|
|
819
|
+
if ( has_thrown ) { return; }
|
|
820
|
+
else { has_thrown = true; }
|
|
820
821
|
|
|
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 );
|
|
822
839
|
|
|
823
840
|
// self-invoke an anonymous async function so we don't have to
|
|
824
841
|
// abstract the details of resolving and rejecting.
|
|
@@ -830,7 +847,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
830
847
|
|
|
831
848
|
// Take a screenshot of the page before continuing or ending
|
|
832
849
|
await scope.page.screenshot({
|
|
833
|
-
path: `${ scope.paths.random_screenshots }
|
|
850
|
+
path: `${ scope.paths.random_screenshots }/random-${ Date.now() }.jpg`,
|
|
834
851
|
type: `jpeg`,
|
|
835
852
|
fullPage: true
|
|
836
853
|
});
|
|
@@ -848,7 +865,23 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
848
865
|
if ( buttons_exist ) {
|
|
849
866
|
await scope.continue( scope );
|
|
850
867
|
} // ends if continue exists
|
|
851
|
-
}
|
|
868
|
+
} // ends if buttons_exist
|
|
869
|
+
|
|
870
|
+
let has_content = await scope.has_interview_content( scope );
|
|
871
|
+
if ( !has_content ) {
|
|
872
|
+
|
|
873
|
+
// Throw an error if there's no interview content
|
|
874
|
+
// Make sure to only throw once
|
|
875
|
+
if ( !has_thrown ) {
|
|
876
|
+
has_thrown = true;
|
|
877
|
+
await scope.addToReport( scope, {
|
|
878
|
+
type: `error`,
|
|
879
|
+
value: no_content_err
|
|
880
|
+
});
|
|
881
|
+
reject( no_content_err );
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
} // ends if !has_content
|
|
852
885
|
|
|
853
886
|
if ( buttons_exist ) {
|
|
854
887
|
process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
|
|
@@ -856,11 +889,28 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
856
889
|
|
|
857
890
|
resolve();
|
|
858
891
|
|
|
859
|
-
} catch (
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
892
|
+
} catch ( error ) {
|
|
893
|
+
|
|
894
|
+
// Make sure to only throw once
|
|
895
|
+
if ( has_thrown ) { return; }
|
|
896
|
+
else { has_thrown = true; }
|
|
897
|
+
|
|
898
|
+
// Throw a specific error if there's no interview content
|
|
899
|
+
let has_content = await scope.has_interview_content( scope );
|
|
900
|
+
if ( !has_content ) {
|
|
901
|
+
await scope.addToReport( scope, {
|
|
902
|
+
type: `error`,
|
|
903
|
+
value: no_content_err
|
|
904
|
+
});
|
|
905
|
+
reject( no_content_err );
|
|
906
|
+
|
|
907
|
+
// Otherwise throw a generic error
|
|
908
|
+
} else {
|
|
909
|
+
let err_msg = `The page with the question id "${ id }" errored when trying to fill`
|
|
910
|
+
+ ` it with random answers: ${ error.name }`
|
|
911
|
+
await scope.addToReport( scope, { type: `error`, value: err_msg });
|
|
912
|
+
reject( error );
|
|
913
|
+
} // ends if !has_content
|
|
864
914
|
|
|
865
915
|
} finally {
|
|
866
916
|
// prevent temporary hang at end of tests
|
|
@@ -891,20 +941,18 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
|
|
|
891
941
|
*
|
|
892
942
|
* Variations:
|
|
893
943
|
* 1. I set the var "benefits['SSI']" to "false"
|
|
894
|
-
* 1. I set the
|
|
944
|
+
* 1. I set the variable "rent_interval" to "weekly"
|
|
895
945
|
*/
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
);
|
|
946
|
+
// Exiting the custom timeout happens in scope.tapElement() if needed
|
|
947
|
+
await scope.steps.set_regular_var( scope, var_name, answer )
|
|
948
|
+
|
|
900
949
|
});
|
|
901
950
|
|
|
902
951
|
When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
|
|
903
952
|
/* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
);
|
|
953
|
+
// This could theoretically tap an element, so
|
|
954
|
+
// exiting the custom timeout happens in scope.tapElement() if needed
|
|
955
|
+
await scope.steps.set_secret_var( scope, var_name, set_to_env_name )
|
|
908
956
|
});
|
|
909
957
|
|
|
910
958
|
|
|
@@ -923,10 +971,8 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
|
|
|
923
971
|
|
|
924
972
|
When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
|
|
925
973
|
/** Tap a link that doesn't navigate. Depends on the language of the text. */
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
scope.timeout
|
|
929
|
-
);
|
|
974
|
+
// exiting the custom timeout happens in scope.tapElement() if needed
|
|
975
|
+
await scope.steps.tap_term( scope, phrase );
|
|
930
976
|
});
|
|
931
977
|
|
|
932
978
|
Then('I sign', { timeout: -1 }, async () => {
|
|
@@ -943,6 +989,9 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
|
|
|
943
989
|
* WARNING: Must not contain any characters that are totally unique to that
|
|
944
990
|
* page load. Just stick to the name of the file.
|
|
945
991
|
*/
|
|
992
|
+
// TODO:
|
|
993
|
+
// This should be using `scope.tapElement()`? When someone clicks on a link to
|
|
994
|
+
// download a document, it sends a request to the server, which could be reloading
|
|
946
995
|
return wrapPromiseWithTimeout(
|
|
947
996
|
scope.steps.download( scope, filename ),
|
|
948
997
|
scope.timeout
|
|
@@ -1035,6 +1084,7 @@ Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected
|
|
|
1035
1084
|
//#####################################
|
|
1036
1085
|
|
|
1037
1086
|
After(async function(scenario) {
|
|
1087
|
+
|
|
1038
1088
|
// The accessibility failures don't happen til the end, so the rest of the test can continue
|
|
1039
1089
|
if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
|
|
1040
1090
|
// The fact that the scenario failed has already been noted earlier in the report
|
|
@@ -1051,14 +1101,15 @@ After(async function(scenario) {
|
|
|
1051
1101
|
if ( scope.page ) { await scope.page.waitForTimeout( 60 * 1000 ); }
|
|
1052
1102
|
}
|
|
1053
1103
|
|
|
1054
|
-
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
1055
1104
|
if ( scope.page ) {
|
|
1056
|
-
|
|
1105
|
+
|
|
1106
|
+
if ( !!scope.disable_error_screenshot ) {
|
|
1057
1107
|
scope.addToReport(scope, {
|
|
1058
1108
|
type: `row`,
|
|
1059
1109
|
value: `For security, ALKiln will not create a screenshot for this error. It's possible a secret is being used on this screen.`
|
|
1060
1110
|
});
|
|
1061
1111
|
} else {
|
|
1112
|
+
|
|
1062
1113
|
// Save/download a picture of the screen that's showing during the unexpected status
|
|
1063
1114
|
// Save one copy in the outer-most artifact folder
|
|
1064
1115
|
let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
|
|
@@ -1079,11 +1130,12 @@ After(async function(scenario) {
|
|
|
1079
1130
|
} // ends if scope.disable_error_screenshot
|
|
1080
1131
|
} // ends if scope.page exists
|
|
1081
1132
|
|
|
1082
|
-
|
|
1133
|
+
// This has to come after the security message or security message doesn't print. Not sure why.
|
|
1134
|
+
if (scenario.result.status === `FAILED`) {
|
|
1135
|
+
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
1136
|
+
}
|
|
1083
1137
|
|
|
1084
|
-
|
|
1085
|
-
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
1086
|
-
}
|
|
1138
|
+
} // ends if not passed
|
|
1087
1139
|
|
|
1088
1140
|
let original_scenario_status = scenario.result.status;
|
|
1089
1141
|
|
|
@@ -1092,43 +1144,44 @@ After(async function(scenario) {
|
|
|
1092
1144
|
let expected = scope.expected_status;
|
|
1093
1145
|
scope.expected_status = null; // reset for next Scenario
|
|
1094
1146
|
expect( scenario.result.status ).to.equal( expected );
|
|
1147
|
+
// If the status was as expected, pass this criteria
|
|
1148
|
+
scenario.result.status = `PASSED`;
|
|
1095
1149
|
}
|
|
1096
1150
|
if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
|
|
1097
1151
|
let expected = scope.expected_in_report;
|
|
1098
1152
|
// Reset report values no matter what so they don't mess up future scenarios
|
|
1099
1153
|
scope.expected_in_report = null;
|
|
1100
1154
|
let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
|
|
1155
|
+
|
|
1101
1156
|
if ( all_were_included ) { scenario.result.status = `PASSED`; }
|
|
1157
|
+
else { scenario.result.status = `FAILED`; }
|
|
1102
1158
|
}
|
|
1103
1159
|
|
|
1104
1160
|
// If there is a page open, then sign out and close it
|
|
1105
1161
|
if ( scope.page ) {
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
//
|
|
1109
|
-
// re-trying at that point.
|
|
1162
|
+
// Catch a possible reload error.
|
|
1163
|
+
|
|
1164
|
+
// TODO: implement and use scope.handle_possible_timeout()
|
|
1110
1165
|
try {
|
|
1166
|
+
// puppeteer will ensure proper timeout.
|
|
1111
1167
|
await scope.page.goto(`${ session_vars.get_da_server_url() }/user/sign-out`, {waitUntil: `domcontentloaded`});
|
|
1112
|
-
} catch ( err ) {
|
|
1113
|
-
if ( session_vars.DEBUG ) {
|
|
1114
|
-
console.log( `Trying to reset for the next test a second time because the first time failed.` );
|
|
1115
|
-
}
|
|
1116
1168
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1169
|
+
} catch ( error ) {
|
|
1170
|
+
|
|
1171
|
+
if ( error.name === `TimeoutError` ) {
|
|
1172
|
+
// TODO: Should a more specific message about signing out get into the report no matter what?
|
|
1173
|
+
// If so, what should the message be in order to not get confused with these other messages?
|
|
1174
|
+
let non_reload_report_msg = `It took too long to sign out in preparation for the next test.`;
|
|
1175
|
+
await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
|
|
1176
|
+
} else {
|
|
1177
|
+
// Throw any non-timeout error
|
|
1178
|
+
let err_msg = `Error occurred when ALKiln tried to sign out in preparation for the next test.`
|
|
1179
|
+
scope.addToReport( scope, { type: `error`, value: err_msg });
|
|
1180
|
+
throw error;
|
|
1181
|
+
} // ends if error is timeout error
|
|
1182
|
+
|
|
1183
|
+
} // ends try/catch
|
|
1184
|
+
|
|
1132
1185
|
} // ends if scope.page
|
|
1133
1186
|
|
|
1134
1187
|
// Add report text to Scenario folder after everything has been added to the report
|
|
@@ -1140,6 +1193,9 @@ After(async function(scenario) {
|
|
|
1140
1193
|
});
|
|
1141
1194
|
|
|
1142
1195
|
AfterAll(async function() {
|
|
1196
|
+
// Stop collecting server response statuses
|
|
1197
|
+
scope.stop_tracking_server_status = true;
|
|
1198
|
+
|
|
1143
1199
|
let report = await scope.getPrintableReport( scope );
|
|
1144
1200
|
// Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
|
|
1145
1201
|
// May need newest version of cucumberjs
|
package/lib/utils/files.js
CHANGED
|
@@ -5,10 +5,20 @@ let files = {};
|
|
|
5
5
|
|
|
6
6
|
files.readable_date = function() {
|
|
7
7
|
/* Return the current timestamp in a human-readable filename-safe format to
|
|
8
|
-
* match the workflow artifact date format.
|
|
8
|
+
* match the workflow artifact date format.
|
|
9
|
+
*
|
|
10
|
+
* See https://stackoverflow.com/a/48729396/14144258 and https://stackoverflow.com/a/6040556/14144258
|
|
11
|
+
*/
|
|
9
12
|
let date = new Date(); // now
|
|
10
|
-
let
|
|
11
|
-
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`;
|
|
12
22
|
return fancy_str;
|
|
13
23
|
}; // Ends files.readable_date()
|
|
14
24
|
|
|
@@ -25,6 +25,7 @@ module.exports = session_vars;
|
|
|
25
25
|
session_vars.get_debug = function () { return process.env.DEBUG || null; };
|
|
26
26
|
session_vars.get_dev_api_key = function () { return process.env.DOCASSEMBLE_DEVELOPER_API_KEY || null; };
|
|
27
27
|
session_vars.get_repo_url = function () { return process.env.REPO_URL || null; };
|
|
28
|
+
session_vars.get_server_reload_timeout = function () { return process.env.SERVER_RELOAD_TIMEOUT_SECONDS || null; };
|
|
28
29
|
session_vars.is_dev_env = function () { return process.env.DEV || null; }
|
|
29
30
|
|
|
30
31
|
// More complex logic
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suffolklitlab/alkiln",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "npm run unit && npm run langs_setup && npm run cucumber -- $@",
|
|
8
8
|
"cucumber_base": "run_cucumber(){ ./node_modules/.bin/cucumber-js \"$@\"; status=$?; ./lib/utils/clean_reports.js; return $status; }; run_cucumber",
|
|
9
|
-
"cucumber": "run_base(){ npm run cucumber_base -- \"$@\" tests/features/*.feature; }; run_base",
|
|
9
|
+
"cucumber": "run_base(){ npm run cucumber_base -- \"$@\" tests/features/*.feature --retry 1; }; run_base",
|
|
10
10
|
"cucumber_external": "run_cucumber(){ tmp_path=`pwd`; ( new_path=\"$1\"; shift; cd \"$new_path\"; npm run --prefix \"${tmp_path}\" cucumber_base -- \"$@\" \"$new_path\"/docassemble/*/data/sources/*.feature;) }; run_cucumber",
|
|
11
11
|
"langs": "npm run langs_setup && ./node_modules/.bin/cucumber-js --require ./lib/index.js tests/features/*_lang*.feature",
|
|
12
12
|
"langs_setup": "./lib/utils/langs.js 'tests/features/*.feature'",
|
|
@@ -132,8 +132,3 @@ Scenario: tap element with an error
|
|
|
132
132
|
"""
|
|
133
133
|
Given I start the interview at "test_taps"
|
|
134
134
|
And I tap the "Tests-not_there-tab" tab
|
|
135
|
-
|
|
136
|
-
@i5 @random
|
|
137
|
-
Scenario: I answer randomly till the end
|
|
138
|
-
Given I start the interview at "all_tests"
|
|
139
|
-
And I answer randomly for at most 50 pages
|
|
@@ -3,13 +3,29 @@ Feature: Assembly Line package-specific Steps
|
|
|
3
3
|
|
|
4
4
|
# In tag names, 'ri' is for 'random input'
|
|
5
5
|
|
|
6
|
-
@
|
|
6
|
+
@fast @ri1 @random
|
|
7
7
|
Scenario: I fill in random input till the end
|
|
8
8
|
Given I start the interview at "test_random_input"
|
|
9
9
|
And I answer randomly for at most 20 pages
|
|
10
10
|
Then the question id should be "end"
|
|
11
11
|
|
|
12
|
-
@
|
|
12
|
+
@fast @ri2 @random
|
|
13
13
|
Scenario: I fill in random input for 1 page
|
|
14
14
|
Given I start the interview at "test_random_input"
|
|
15
15
|
And I answer randomly for at most 1 page
|
|
16
|
+
|
|
17
|
+
# Should create two unique folders for random step screenshots which
|
|
18
|
+
# must be checked manually
|
|
19
|
+
@fast @ri3 @random
|
|
20
|
+
Scenario: I answer randomly till the end twice
|
|
21
|
+
Given I start the interview at "test_random_input"
|
|
22
|
+
And I answer randomly for at most 50 pages
|
|
23
|
+
Given I start the interview at "test_random_input"
|
|
24
|
+
And I answer randomly for at most 50 pages
|
|
25
|
+
|
|
26
|
+
@fast @ri4 @random @error
|
|
27
|
+
Scenario: Fail with error page from random input
|
|
28
|
+
Given the final Scenario status should be "failed"
|
|
29
|
+
Given I start the interview at "test_missing_var_error_screen"
|
|
30
|
+
And the max seconds for each step in this scenario is 10
|
|
31
|
+
And I answer randomly for at most 3 pages
|