@suffolklitlab/alkiln 4.6.1 → 4.7.1
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/.github/workflows/test_tests.yml +14 -34
- package/CHANGELOG.md +26 -0
- package/action.yml +6 -35
- package/lib/docassemble/setup.js +6 -0
- package/lib/scope.js +529 -111
- package/lib/steps.js +236 -25
- package/lib/utils/clean_reports.js +9 -3
- package/lib/utils/files.js +33 -0
- package/lib/utils/session_vars.js +56 -13
- package/package.json +2 -1
- package/tests/features/interactive_steps.feature +2 -2
- package/tests/features/observation_steps.feature +2 -2
- package/tests/features/random_input.feature +31 -0
- package/tests/features/story_table_formats.feature +49 -1
- package/tests/unit_tests/getSafeScenarioBaseFilename.fixtures.js +0 -2
- package/tests/unit_tests/getSafeScenarioBaseFilename.test.js +10 -21
- package/tests/unit_tests/getSafeScenarioFilename.test.js +6 -3
- package/tests/unit_tests/names.fixtures.js +8 -8
package/lib/steps.js
CHANGED
|
@@ -20,8 +20,10 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
20
20
|
const scope = require('./scope');
|
|
21
21
|
const log = require('./utils/log');
|
|
22
22
|
const session_vars = require('./utils/session_vars');
|
|
23
|
+
const files = require('./utils/files' );
|
|
24
|
+
|
|
23
25
|
/* Of Note:
|
|
24
|
-
- We're using `*=` because sometimes da text has funny characters in it that are hard to anticipate
|
|
26
|
+
- We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
29
|
|
|
@@ -70,7 +72,23 @@ BeforeAll(async () => {
|
|
|
70
72
|
scope.timeout = 30 * 1000;
|
|
71
73
|
scope.showif_timeout = 600;
|
|
72
74
|
scope.report = new Map();
|
|
73
|
-
|
|
75
|
+
|
|
76
|
+
// Store path names for files and docs created for the tests
|
|
77
|
+
scope.paths = {};
|
|
78
|
+
// Test setup should have created this filepath. Run setup if there's a problem with this.
|
|
79
|
+
scope.paths.artifacts = session_vars.get_artifacts_path_name();
|
|
80
|
+
// For GitHub tests, use the artifacts folder that was created during setup
|
|
81
|
+
// For local tests, make a new folder for each time scenarios start
|
|
82
|
+
if ( !session_vars.is_dev_env() ) {
|
|
83
|
+
// Make a new folder to store all the files for this run of the tests
|
|
84
|
+
scope.paths.artifacts = files.make_artifacts_folder();
|
|
85
|
+
// Save its name in a file. `session_vars` can get it back later.
|
|
86
|
+
session_vars.save_artifacts_path_name( scope.paths.artifacts );
|
|
87
|
+
}
|
|
88
|
+
scope.paths.report = `${ scope.paths.artifacts }/report.txt`;
|
|
89
|
+
|
|
90
|
+
scope.paths.debug_log = `${ scope.paths.artifacts }/${ log.debug_log_file }`;
|
|
91
|
+
fs.writeFileSync( scope.paths.debug_log, "" );
|
|
74
92
|
});
|
|
75
93
|
|
|
76
94
|
Before(async (scenario) => {
|
|
@@ -79,7 +97,14 @@ Before(async (scenario) => {
|
|
|
79
97
|
scope.expected_in_report = null;
|
|
80
98
|
|
|
81
99
|
await scope.addReportHeading(scope, {scenario});
|
|
100
|
+
// Make folder for this Scenario in the all-tests artifacts folder
|
|
82
101
|
scope.base_filename = await scope.getSafeScenarioBaseFilename(scope, {scenario});
|
|
102
|
+
// Add a date for uniqueness in case dev has accidentally copied a Scenario description
|
|
103
|
+
let date = files.readable_date();
|
|
104
|
+
scope.paths.scenario = `${ scope.paths.artifacts }/${ scope.base_filename }-${ date }`;
|
|
105
|
+
fs.mkdirSync( scope.paths.scenario );
|
|
106
|
+
// Store path name for this Scenario's individualized report
|
|
107
|
+
scope.paths.scenario_report = `${ scope.paths.scenario }/report.txt`;
|
|
83
108
|
|
|
84
109
|
// Downloads
|
|
85
110
|
scope.downloadComplete = false;
|
|
@@ -164,10 +189,9 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
164
189
|
// I've seen stuff take an extra moment or two. Shame to have it everywhere
|
|
165
190
|
try {
|
|
166
191
|
// Interview files should get downloaded to downloads folder
|
|
167
|
-
let docs_dir = await scope.getSafeScenarioFilename( scope, { prefix: `downloads` });
|
|
168
192
|
await scope.page._client.send('Page.setDownloadBehavior', {
|
|
169
193
|
behavior: 'allow',
|
|
170
|
-
downloadPath: path.resolve(
|
|
194
|
+
downloadPath: path.resolve( scope.paths.scenario ), // Save in root of this scenario
|
|
171
195
|
});
|
|
172
196
|
resolve();
|
|
173
197
|
} catch ( err ) { reject( err ); }
|
|
@@ -178,7 +202,11 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
178
202
|
await custom_download_behavior_timeout;
|
|
179
203
|
|
|
180
204
|
// ---- EVENT LISTENERS ----
|
|
181
|
-
|
|
205
|
+
|
|
206
|
+
// TODO: Wait for download to complete doesn't work. See notes in
|
|
207
|
+
// scope.js scope.detectDownloadComplete()
|
|
208
|
+
|
|
209
|
+
// ~Wait for possible download. From https://stackoverflow.com/a/51423688~
|
|
182
210
|
scope.page.on('response', async response => {
|
|
183
211
|
// Note: We can expect that only one file will be getting downloaded
|
|
184
212
|
// at any one time since each step completes synchronously
|
|
@@ -190,7 +218,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
190
218
|
// destroyed when the page closes anyway.
|
|
191
219
|
|
|
192
220
|
// Watch event on download folder or file
|
|
193
|
-
await fs.watchFile(
|
|
221
|
+
await fs.watchFile( scope.paths.scenario, function (curr, prev) {
|
|
194
222
|
// If current size eq to size from response then close
|
|
195
223
|
if (parseInt(curr.size) === parseInt(response._headers['content-length'])) {
|
|
196
224
|
// Lets the related function in `scope` know that the download
|
|
@@ -306,7 +334,8 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
|
|
|
306
334
|
// It also seems harsh, but more reliable.
|
|
307
335
|
// Ask developers what they'd like.
|
|
308
336
|
|
|
309
|
-
|
|
337
|
+
|
|
338
|
+
let supported_table = await scope.normalizeTable( scope, { raw_var_data });
|
|
310
339
|
|
|
311
340
|
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
|
|
312
341
|
// Until we reach the final page or hit an error
|
|
@@ -430,7 +459,7 @@ Then(/I take a screenshot ?(?:named "([^"]+)")?/, { timeout: -1 }, async ( name
|
|
|
430
459
|
/* Here to make writing tests more comfortable. */
|
|
431
460
|
When("I do nothing", async () => { await scope.afterStep(scope); }); // √
|
|
432
461
|
|
|
433
|
-
Then(
|
|
462
|
+
Then(/the (?:question|page|screen) id should be "([^"]+)"/i, { timeout: -1 }, async ( question_id ) => {
|
|
434
463
|
/* Looks for a sanitized version of the question id as it's written
|
|
435
464
|
* in the .yml. docassemble's way */
|
|
436
465
|
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, question_id );
|
|
@@ -613,7 +642,7 @@ Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkTe
|
|
|
613
642
|
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
614
643
|
|
|
615
644
|
let prop_obj = await link.getProperty('target');
|
|
616
|
-
let target = await
|
|
645
|
+
let target = await prop_obj.jsonValue();
|
|
617
646
|
|
|
618
647
|
let should_open_a_new_window = which_window === 'a new window';
|
|
619
648
|
let opens_a_new_window = target === '_blank';
|
|
@@ -650,7 +679,7 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
|
|
|
650
679
|
*/
|
|
651
680
|
|
|
652
681
|
// Get JSON variable value
|
|
653
|
-
let {
|
|
682
|
+
let { json_path, json } = await scope.savePageJSONData( scope );
|
|
654
683
|
let actual_value = json.variables[ var_name ];
|
|
655
684
|
|
|
656
685
|
// Add warning to report if value is not as expected
|
|
@@ -658,8 +687,8 @@ Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)"
|
|
|
658
687
|
await scope.addToReport( scope, {
|
|
659
688
|
type: `error`,
|
|
660
689
|
value: `The variable "${ var_name }" was not equal to the expected `
|
|
661
|
-
+ `value on the "${ scope.page_id }" screen. Check
|
|
662
|
-
+
|
|
690
|
+
+ `value on the "${ scope.page_id }" screen. Check `
|
|
691
|
+
+ `${ json_path } to see the page's JSON variables.`
|
|
663
692
|
});
|
|
664
693
|
}
|
|
665
694
|
|
|
@@ -735,11 +764,11 @@ When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -
|
|
|
735
764
|
});
|
|
736
765
|
|
|
737
766
|
When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
|
|
738
|
-
let { passed,
|
|
767
|
+
let { passed, axe_filepath } = await wrapPromiseWithTimeout(
|
|
739
768
|
scope.checkForA11y(scope),
|
|
740
769
|
scope.timeout
|
|
741
770
|
)
|
|
742
|
-
let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${
|
|
771
|
+
let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filepath }.`;
|
|
743
772
|
expect( passed, msg ).to.be.true;
|
|
744
773
|
});
|
|
745
774
|
|
|
@@ -752,10 +781,166 @@ When(/I check all pages for (?:accessibility|a11y) issues/i, {timeout: -1}, asyn
|
|
|
752
781
|
// UI element interaction
|
|
753
782
|
//#####################################
|
|
754
783
|
|
|
784
|
+
// -------------------------------------
|
|
785
|
+
// --- RANDOM INPUT ---
|
|
786
|
+
When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { timeout: -1 }, async ( max_pages_str ) => {
|
|
787
|
+
/** Give random answers until the user can't continue any more.
|
|
788
|
+
*
|
|
789
|
+
* Variations:
|
|
790
|
+
* I answer randomly for at most 1 page
|
|
791
|
+
* I answer randomly for at most 10 pages
|
|
792
|
+
* I answer randomly for at most 101 screens
|
|
793
|
+
*/
|
|
794
|
+
// Give each screenshots folder a time-stamped name in case the dev gets clever
|
|
795
|
+
// and use the same Scenario to run multiple randomized tests.
|
|
796
|
+
let timestamp = files.readable_date();
|
|
797
|
+
let random_input_path = `${ scope.paths.scenario }/random_answers_screenshots-${ timestamp }`
|
|
798
|
+
scope.paths.random_screenshots = random_input_path;
|
|
799
|
+
fs.mkdirSync( scope.paths.random_screenshots );
|
|
800
|
+
await scope.addToReport( scope, {
|
|
801
|
+
type: 'row',
|
|
802
|
+
value: `Testing interview with random input (in ${ random_input_path })`
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Force the dev to prevent infinite loops
|
|
806
|
+
let max_pages = parseInt( max_pages_str );
|
|
807
|
+
let curr_page_num = 0;
|
|
808
|
+
|
|
809
|
+
// While a continue-type button is on the current page, fill out
|
|
810
|
+
// the fields and then try to continue
|
|
811
|
+
let buttons_exist = true;
|
|
812
|
+
while ( buttons_exist && curr_page_num < max_pages ) {
|
|
813
|
+
|
|
814
|
+
curr_page_num = curr_page_num + 1;
|
|
815
|
+
let { id } = await scope.examinePageID( scope, `no target id` );
|
|
816
|
+
|
|
817
|
+
let no_content_err = `The interview seemed to run into an error page`
|
|
818
|
+
+ ` after the page with the question id "${ id }".`
|
|
819
|
+
+ ` See the screenshot in "${ random_input_path }".`;
|
|
820
|
+
|
|
821
|
+
let has_thrown = false;
|
|
822
|
+
// Ensure no infinite loop, but each page has a full timeout to run
|
|
823
|
+
let custom_timeout = new Promise(async function( resolve, reject ) {
|
|
824
|
+
|
|
825
|
+
// Set up timeout to stop infinite loops
|
|
826
|
+
let page_timeout_id = setTimeout(async function() {
|
|
827
|
+
|
|
828
|
+
// Make sure to only throw once
|
|
829
|
+
if ( has_thrown ) { return; }
|
|
830
|
+
else { has_thrown = true; }
|
|
831
|
+
|
|
832
|
+
let has_content = await scope.has_interview_content( scope );
|
|
833
|
+
|
|
834
|
+
if ( !has_content ) {
|
|
835
|
+
await scope.addToReport( scope, {
|
|
836
|
+
type: `error`,
|
|
837
|
+
value: no_content_err
|
|
838
|
+
});
|
|
839
|
+
reject( no_content_err );
|
|
840
|
+
|
|
841
|
+
} else {
|
|
842
|
+
let timeout_message = `The page with an id of "${ id }" took too long`
|
|
843
|
+
+ ` to fill with random answers (over ${ scope.timeout/1000/60 } min).`
|
|
844
|
+
await scope.addToReport( scope, { type: `error`, value: timeout_message });
|
|
845
|
+
reject( timeout_message );
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
}, scope.timeout );
|
|
849
|
+
|
|
850
|
+
// self-invoke an anonymous async function so we don't have to
|
|
851
|
+
// abstract the details of resolving and rejecting.
|
|
852
|
+
(async function() {
|
|
853
|
+
try {
|
|
854
|
+
|
|
855
|
+
// Set all the fields on the page
|
|
856
|
+
let buttons = await scope.steps.set_random_page_vars( scope );
|
|
857
|
+
|
|
858
|
+
// Take a screenshot of the page before continuing or ending
|
|
859
|
+
await scope.page.screenshot({
|
|
860
|
+
path: `${ scope.paths.random_screenshots }/random-${ Date.now() }.jpg`,
|
|
861
|
+
type: `jpeg`,
|
|
862
|
+
fullPage: true
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Prep for the next loop to possibly stop. Signature page buttons
|
|
866
|
+
// don't count as buttons in the way our field detects them
|
|
867
|
+
buttons_exist = buttons.length > 0;
|
|
868
|
+
// If there were any buttons, pick a random one to try to press
|
|
869
|
+
if ( buttons_exist ) {
|
|
870
|
+
await scope.set_random_input_for.buttons( scope, { fields: buttons });
|
|
871
|
+
} else {
|
|
872
|
+
// If can theoretically continue, try to. Also prep to
|
|
873
|
+
// stop or continue for the next loop
|
|
874
|
+
buttons_exist = await scope.continue_exists( scope );
|
|
875
|
+
if ( buttons_exist ) {
|
|
876
|
+
await scope.continue( scope );
|
|
877
|
+
} // ends if continue exists
|
|
878
|
+
} // ends if buttons_exist
|
|
879
|
+
|
|
880
|
+
let has_content = await scope.has_interview_content( scope );
|
|
881
|
+
if ( !has_content ) {
|
|
882
|
+
|
|
883
|
+
// Throw an error if there's no interview content
|
|
884
|
+
// Make sure to only throw once
|
|
885
|
+
if ( !has_thrown ) {
|
|
886
|
+
has_thrown = true;
|
|
887
|
+
await scope.addToReport( scope, {
|
|
888
|
+
type: `error`,
|
|
889
|
+
value: no_content_err
|
|
890
|
+
});
|
|
891
|
+
reject( no_content_err );
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
} // ends if !has_content
|
|
895
|
+
|
|
896
|
+
if ( buttons_exist ) {
|
|
897
|
+
process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
resolve();
|
|
901
|
+
|
|
902
|
+
} catch ( error ) {
|
|
903
|
+
|
|
904
|
+
// Make sure to only throw once
|
|
905
|
+
if ( has_thrown ) { return; }
|
|
906
|
+
else { has_thrown = true; }
|
|
907
|
+
|
|
908
|
+
// Throw a specific error if there's no interview content
|
|
909
|
+
let has_content = await scope.has_interview_content( scope );
|
|
910
|
+
if ( !has_content ) {
|
|
911
|
+
await scope.addToReport( scope, {
|
|
912
|
+
type: `error`,
|
|
913
|
+
value: no_content_err
|
|
914
|
+
});
|
|
915
|
+
reject( no_content_err );
|
|
916
|
+
|
|
917
|
+
// Otherwise throw a generic error
|
|
918
|
+
} else {
|
|
919
|
+
let err_msg = `The page with the question id "${ id }" errored when trying to fill`
|
|
920
|
+
+ ` it with random answers: ${ error.name }`
|
|
921
|
+
await scope.addToReport( scope, { type: `error`, value: err_msg });
|
|
922
|
+
reject( error );
|
|
923
|
+
} // ends if !has_content
|
|
924
|
+
|
|
925
|
+
} finally {
|
|
926
|
+
// prevent temporary hang at end of tests
|
|
927
|
+
clearTimeout( page_timeout_id );
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
930
|
+
|
|
931
|
+
}); // ends custom_timeout for set_random_page_vars
|
|
932
|
+
|
|
933
|
+
await custom_timeout;
|
|
934
|
+
|
|
935
|
+
} // ends while continue_exists
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
}); // Ends random input
|
|
939
|
+
|
|
755
940
|
// -------------------------------------
|
|
756
941
|
// --- VARIABLES-BASED STEPS ---
|
|
757
942
|
|
|
758
|
-
When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( var_name,
|
|
943
|
+
When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( var_name, answer ) => {
|
|
759
944
|
/* Set a non-story table variable (with or without a choice associated with it) to a value. Set:
|
|
760
945
|
* - Buttons associated with variables
|
|
761
946
|
* - Dropdowns
|
|
@@ -769,7 +954,7 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
|
|
|
769
954
|
* 1. I set the var "rent_interval" to "weekly"
|
|
770
955
|
*/
|
|
771
956
|
return wrapPromiseWithTimeout(
|
|
772
|
-
scope.steps.set_regular_var( scope, var_name,
|
|
957
|
+
scope.steps.set_regular_var( scope, var_name, answer ),
|
|
773
958
|
scope.timeout
|
|
774
959
|
);
|
|
775
960
|
});
|
|
@@ -926,25 +1111,40 @@ After(async function(scenario) {
|
|
|
926
1111
|
if ( scope.page ) { await scope.page.waitForTimeout( 60 * 1000 ); }
|
|
927
1112
|
}
|
|
928
1113
|
|
|
929
|
-
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
930
1114
|
if ( scope.page ) {
|
|
931
|
-
|
|
1115
|
+
|
|
1116
|
+
if ( !!scope.disable_error_screenshot ) {
|
|
932
1117
|
scope.addToReport(scope, {
|
|
933
1118
|
type: `row`,
|
|
934
1119
|
value: `For security, ALKiln will not create a screenshot for this error. It's possible a secret is being used on this screen.`
|
|
935
1120
|
});
|
|
936
1121
|
} else {
|
|
937
1122
|
// Save/download a picture of the screen that's showing during the unexpected status
|
|
938
|
-
|
|
939
|
-
await scope.
|
|
1123
|
+
// Save one copy in the outer-most artifact folder
|
|
1124
|
+
let scenario_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error_on` });
|
|
1125
|
+
await scope.page.screenshot({
|
|
1126
|
+
path: `${ scope.paths.artifacts }/${ scenario_filename }.jpg`,
|
|
1127
|
+
type: `jpeg`,
|
|
1128
|
+
fullPage: true
|
|
1129
|
+
});
|
|
1130
|
+
// Save another copy in the artifact's Scenario folder
|
|
1131
|
+
let screenshot_name = `error_on`;
|
|
1132
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
1133
|
+
screenshot_name += `-${ id }`;
|
|
1134
|
+
await scope.page.screenshot({
|
|
1135
|
+
path: `${ scope.paths.scenario }/${ screenshot_name }.jpg`,
|
|
1136
|
+
type: `jpeg`,
|
|
1137
|
+
fullPage: true
|
|
1138
|
+
});
|
|
940
1139
|
} // ends if scope.disable_error_screenshot
|
|
941
1140
|
} // ends if scope.page exists
|
|
942
1141
|
|
|
943
|
-
|
|
1142
|
+
// This has to come after the security message or security message doesn't print. Not sure why.
|
|
1143
|
+
if (scenario.result.status === `FAILED`) {
|
|
1144
|
+
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
1145
|
+
}
|
|
944
1146
|
|
|
945
|
-
|
|
946
|
-
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
947
|
-
}
|
|
1147
|
+
} // ends if not passed
|
|
948
1148
|
|
|
949
1149
|
let original_scenario_status = scenario.result.status;
|
|
950
1150
|
|
|
@@ -953,13 +1153,17 @@ After(async function(scenario) {
|
|
|
953
1153
|
let expected = scope.expected_status;
|
|
954
1154
|
scope.expected_status = null; // reset for next Scenario
|
|
955
1155
|
expect( scenario.result.status ).to.equal( expected );
|
|
1156
|
+
// If the status was as expected, pass this criteria
|
|
1157
|
+
scenario.result.status = `PASSED`;
|
|
956
1158
|
}
|
|
957
1159
|
if ( scope.expected_in_report && scope.expected_in_report.length > 0) {
|
|
958
1160
|
let expected = scope.expected_in_report;
|
|
959
1161
|
// Reset report values no matter what so they don't mess up future scenarios
|
|
960
1162
|
scope.expected_in_report = null;
|
|
961
1163
|
let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
|
|
1164
|
+
|
|
962
1165
|
if ( all_were_included ) { scenario.result.status = `PASSED`; }
|
|
1166
|
+
else { scenario.result.status = `FAILED`; }
|
|
963
1167
|
}
|
|
964
1168
|
|
|
965
1169
|
// If there is a page open, then sign out and close it
|
|
@@ -991,6 +1195,13 @@ After(async function(scenario) {
|
|
|
991
1195
|
await scope.page.close();
|
|
992
1196
|
scope.page = null;
|
|
993
1197
|
} // ends if scope.page
|
|
1198
|
+
|
|
1199
|
+
// Add report text to Scenario folder after everything has been added to the report
|
|
1200
|
+
let current_report_obj = scope.report.get( scope.scenario_id );
|
|
1201
|
+
let scenario_report_obj = [ scope.scenario_id, current_report_obj ]; // Mimic how loop works with `Map`
|
|
1202
|
+
let report = await scope.getPrintableScenario( scope, { scenario: scenario_report_obj });
|
|
1203
|
+
// Save the report as in the Scenario folder
|
|
1204
|
+
fs.writeFileSync( scope.paths.scenario_report, report );
|
|
994
1205
|
});
|
|
995
1206
|
|
|
996
1207
|
AfterAll(async function() {
|
|
@@ -1000,7 +1211,7 @@ AfterAll(async function() {
|
|
|
1000
1211
|
console.log( `\n\n${ report }` );
|
|
1001
1212
|
|
|
1002
1213
|
// Save/download the report as an artifact
|
|
1003
|
-
fs.writeFileSync(
|
|
1214
|
+
fs.writeFileSync( scope.paths.report, report );
|
|
1004
1215
|
|
|
1005
1216
|
// If there is a browser open, then close it
|
|
1006
1217
|
if (scope.browser) {
|
|
@@ -2,22 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const log = require('./log');
|
|
5
|
+
const session_vars = require('./session_vars');
|
|
5
6
|
|
|
6
7
|
// This is hardcoded information from `cucumber.js`. Once we move to cucumber 8, we should
|
|
7
|
-
// change that the cucumber.json and actually parse the info from it.
|
|
8
|
+
// change that the cucumber.json and actually parse the info from it.
|
|
9
|
+
// The file won't be used for anything other than detecting and removing color codes
|
|
8
10
|
let summary_file_name = 'cucumber-report.txt';
|
|
9
11
|
fs.readFile(summary_file_name, 'utf8', (err, data) => {
|
|
10
12
|
if (err) {
|
|
11
13
|
console.error(err);
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
16
|
+
|
|
14
17
|
// Replace terminal color codes, if present
|
|
15
18
|
// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#colors--graphics-mode
|
|
16
19
|
data = data.replace(/\x1b\[[0-9][0-9]?[0-9]?m/g, '');
|
|
17
|
-
|
|
20
|
+
|
|
21
|
+
// Put it in the correct file in the correct folder
|
|
22
|
+
let artifacts_path = session_vars.get_artifacts_path_name();
|
|
23
|
+
fs.appendFile( `${ artifacts_path }/${ log.debug_log_file }`, data, err => {
|
|
18
24
|
if (err) {
|
|
19
25
|
console.error(err);
|
|
20
26
|
return;
|
|
21
27
|
}
|
|
22
|
-
});
|
|
28
|
+
}); // Ends appendFileSync
|
|
23
29
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const safe_filename = require('sanitize-filename');
|
|
3
|
+
|
|
4
|
+
let files = {};
|
|
5
|
+
|
|
6
|
+
files.readable_date = function() {
|
|
7
|
+
/* Return the current timestamp in a human-readable filename-safe format to
|
|
8
|
+
* match the workflow artifact date format. */
|
|
9
|
+
let date = new Date(); // now
|
|
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
|
|
12
|
+
let fancy_str = str.replace(/\//g, '-').replace(',', ' at').replace(':', 'hrs ').replace(':', 'mins ') + 'secs UTC';
|
|
13
|
+
return fancy_str;
|
|
14
|
+
}; // Ends files.readable_date()
|
|
15
|
+
|
|
16
|
+
files.make_artifacts_folder = function (folder_name) {
|
|
17
|
+
/** Makes a folder in the root of the project, either with the given name
|
|
18
|
+
* or with a default name.
|
|
19
|
+
*
|
|
20
|
+
* Works for local development as well, unlike action.yml or the workflow file.
|
|
21
|
+
*/
|
|
22
|
+
if (folder_name) {
|
|
23
|
+
folder_name = safe_filename( folder_name );
|
|
24
|
+
} else {
|
|
25
|
+
folder_name = `alkiln_tests_output-${ files.readable_date() }`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fs.mkdirSync( folder_name );
|
|
29
|
+
return folder_name;
|
|
30
|
+
}; // Ends files.make_artifacts_folder()
|
|
31
|
+
|
|
32
|
+
module.exports = files;
|
|
33
|
+
|
|
@@ -21,9 +21,11 @@ const session_vars = {};
|
|
|
21
21
|
module.exports = session_vars;
|
|
22
22
|
|
|
23
23
|
// These default to null
|
|
24
|
+
// TODO: How to isolate debug elsewhere so this file can use the logger?
|
|
24
25
|
session_vars.get_debug = function () { return process.env.DEBUG || null; };
|
|
25
26
|
session_vars.get_dev_api_key = function () { return process.env.DOCASSEMBLE_DEVELOPER_API_KEY || null; };
|
|
26
27
|
session_vars.get_repo_url = function () { return process.env.REPO_URL || null; };
|
|
28
|
+
session_vars.is_dev_env = function () { return process.env.DEV || null; }
|
|
27
29
|
|
|
28
30
|
// More complex logic
|
|
29
31
|
session_vars.get_setup_timeout_ms = function () {
|
|
@@ -80,9 +82,10 @@ session_vars.get_languages = function () {
|
|
|
80
82
|
}; // Ends session_vars.get_languages()
|
|
81
83
|
|
|
82
84
|
|
|
83
|
-
const
|
|
85
|
+
const runtime_config_path = `runtime_config.json`;
|
|
86
|
+
const project_name_key = `da_project_name`;
|
|
84
87
|
session_vars.save_project_name = function ( project_name ) {
|
|
85
|
-
/* Saves the name of the project to a
|
|
88
|
+
/* Saves the name of the project to a json obj so deletion can use it later.
|
|
86
89
|
* Built for local as well as remote testing. Assumes a kosher string.
|
|
87
90
|
* We avoided a list because of concerns over getting out of date.
|
|
88
91
|
* Should we try to prevent someone overwriting a previous project name?
|
|
@@ -90,16 +93,23 @@ session_vars.save_project_name = function ( project_name ) {
|
|
|
90
93
|
* @param project_name { string } - purely alphanumeric string */
|
|
91
94
|
|
|
92
95
|
// Warn local developers if file already exists
|
|
93
|
-
if ( fs.existsSync(
|
|
96
|
+
if ( fs.existsSync( runtime_config_path ) ) {
|
|
94
97
|
let old_project_name = session_vars.get_project_name();
|
|
95
98
|
let session_project_already_exists_msg = `ALKiln WARNING: `
|
|
96
99
|
+ `This session had a project name already: ${ old_project_name }. `
|
|
97
100
|
+ `A new project has now been created called ${ project_name }. `
|
|
98
101
|
+ `Remember to delete ${ old_project_name }`;
|
|
99
102
|
console.warn( session_project_already_exists_msg );
|
|
103
|
+
} else {
|
|
104
|
+
// If the file doesn't exist, make one because next we need to read it
|
|
105
|
+
fs.writeFileSync( runtime_config_path, JSON.stringify( {}, null, 2 ) );
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
// Get the json, changing it, and re-write the file from scratch
|
|
109
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ) );
|
|
110
|
+
json[ project_name_key ] = project_name;
|
|
111
|
+
let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
|
|
112
|
+
|
|
103
113
|
if ( session_vars.get_debug() ) {
|
|
104
114
|
console.log( `ALKiln debug: Stored name of Project "${ project_name }" locally.` );
|
|
105
115
|
}
|
|
@@ -107,26 +117,59 @@ session_vars.save_project_name = function ( project_name ) {
|
|
|
107
117
|
}; // Ends save_project_name()
|
|
108
118
|
|
|
109
119
|
session_vars.get_project_name = function () {
|
|
110
|
-
/* Get the name of the current
|
|
111
|
-
let
|
|
112
|
-
|
|
120
|
+
/* Get the name of the current docassemble Project */
|
|
121
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ));
|
|
122
|
+
let project_name = json[ project_name_key ] || null;
|
|
123
|
+
if ( session_vars.get_debug() ) { console.log( `ALKiln debug: Project name from file is "${ project_name }".` ); }
|
|
113
124
|
return project_name;
|
|
114
125
|
}; // Ends get_project_name()
|
|
115
126
|
|
|
116
127
|
session_vars.delete_project_name = function () {
|
|
117
|
-
/*
|
|
118
|
-
// Avoid race conditions possibly caused by checking for existence first
|
|
119
|
-
// Delete file if possible
|
|
128
|
+
/* Empty the key storing the name of the docassemble Project */
|
|
120
129
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
// Get the json, changing it, and re-write the file from scratch
|
|
131
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ));
|
|
132
|
+
json[ project_name_key ] = ``;
|
|
133
|
+
console.log( `ALKiln: Deleted the data that stored the name of the Project.` );
|
|
134
|
+
fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
|
|
123
135
|
return true;
|
|
124
136
|
} catch ( error ) {
|
|
125
|
-
console.warn( `ALKiln WARNING: Could not delete the Project name
|
|
137
|
+
console.warn( `ALKiln WARNING: Could not delete the Project name data. Error: ${ error }.` );
|
|
126
138
|
return false;
|
|
127
139
|
}
|
|
128
140
|
};
|
|
129
141
|
|
|
142
|
+
// The file storing the artifacts path contains the name of the current artifacts folder.
|
|
143
|
+
// The directory name it contains will be created or changed with each run of all the tests.
|
|
144
|
+
// const file_storing_artifact_path = `_alkiln_test_artifacts_dir_name.json`;
|
|
145
|
+
const artifacts_path_key = `artifacts_path`;
|
|
146
|
+
session_vars.save_artifacts_path_name = function ( path_name ) {
|
|
147
|
+
/* Save a key with the name of the current artifacts folder in case it doesn't exist. */
|
|
148
|
+
|
|
149
|
+
// Get the json, changing it, and re-write the file from scratch
|
|
150
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ) );
|
|
151
|
+
json[ artifacts_path_key ] = path_name;
|
|
152
|
+
let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
|
|
153
|
+
|
|
154
|
+
if ( session_vars.get_debug() ) {
|
|
155
|
+
console.log( `ALKiln debug: Stored name of artifacts directory "${ path_name }" locally.` );
|
|
156
|
+
}
|
|
157
|
+
return file;
|
|
158
|
+
}; // Ends get_project_name()
|
|
159
|
+
|
|
160
|
+
session_vars.get_artifacts_path_name = function () {
|
|
161
|
+
/* Get the name of the current artifacts path. */
|
|
162
|
+
try {
|
|
163
|
+
let json = JSON.parse( fs.readFileSync( runtime_config_path ));
|
|
164
|
+
let path_name = json[ artifacts_path_key ];
|
|
165
|
+
|
|
166
|
+
if ( session_vars.get_debug() ) { console.log( `ALKiln debug: Artifacts directory is "${ path_name }".` ); }
|
|
167
|
+
return path_name;
|
|
168
|
+
} catch ( error ) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}; // Ends get_project_name()
|
|
172
|
+
|
|
130
173
|
/**
|
|
131
174
|
* Ensure all required environment variables exist. Add each missing variable
|
|
132
175
|
* to an error that will be thrown at the end.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suffolklitlab/alkiln",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.1",
|
|
4
4
|
"description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -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",
|
|
@@ -4,7 +4,7 @@ Feature: Interactive steps
|
|
|
4
4
|
# In tag names, 'i' is for 'interactive'
|
|
5
5
|
|
|
6
6
|
@slow @i1
|
|
7
|
-
Scenario: I set
|
|
7
|
+
Scenario: I set various values
|
|
8
8
|
Given I start the interview at "all_tests"
|
|
9
9
|
Then the question id should be "upload files"
|
|
10
10
|
When I set the var "upload_files_visible" to "some_png_1.png, some_png_2.png"
|
|
@@ -131,4 +131,4 @@ Scenario: tap element with an error
|
|
|
131
131
|
is there a typo?
|
|
132
132
|
"""
|
|
133
133
|
Given I start the interview at "test_taps"
|
|
134
|
-
And I tap the "Tests-not_there-tab" tab
|
|
134
|
+
And I tap the "Tests-not_there-tab" tab
|
|
@@ -98,7 +98,7 @@ Scenario: Test "Then I don’t continue" with an apostrophe
|
|
|
98
98
|
Then I don’t continue
|
|
99
99
|
And I will be told an answer is invalid
|
|
100
100
|
|
|
101
|
-
@fast @o9 @table
|
|
101
|
+
@fast @o9 @table @json
|
|
102
102
|
Scenario: I can match JSON page var to str
|
|
103
103
|
Given I start the interview at "all_tests.yml"
|
|
104
104
|
And I get to "showifs" with this data:
|
|
@@ -123,7 +123,7 @@ Scenario: I get the page's JSON
|
|
|
123
123
|
Given I start the interview at "all_tests"
|
|
124
124
|
Then I get the page's JSON variables and values
|
|
125
125
|
|
|
126
|
-
@fast @o11
|
|
126
|
+
@fast @o11 @screenshot
|
|
127
127
|
Scenario: I take a screenshot
|
|
128
128
|
Given I start the interview at "all_tests"
|
|
129
129
|
Then I take a screenshot
|