@suffolklitlab/alkiln 3.0.9
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 +77 -0
- package/CHANGELOG.md +381 -0
- package/README.md +18 -0
- package/action.yml +158 -0
- package/lib/DOM.md.html +63 -0
- package/lib/github_integration/install-branch.js +84 -0
- package/lib/github_integration/puppeteer-utils.js +224 -0
- package/lib/github_integration/setup.js +4 -0
- package/lib/github_integration/takedown.js +4 -0
- package/lib/index.js +2 -0
- package/lib/scope.js +1632 -0
- package/lib/steps.js +878 -0
- package/lib/utils/env_vars.js +173 -0
- package/lib/utils/langs.js +51 -0
- package/lib/world.js +10 -0
- package/package.json +53 -0
- package/tests/features/assembly_line.feature +43 -0
- package/tests/features/establishing_steps.feature +22 -0
- package/tests/features/example_test.feature +11 -0
- package/tests/features/interactive_steps.feature +85 -0
- package/tests/features/observation_steps.feature +97 -0
- package/tests/features/reports.feature +482 -0
- package/tests/features/some_png_1.png +0 -0
- package/tests/features/some_png_2.png +0 -0
- package/tests/features/story_table_formats.feature +77 -0
- package/tests/features/story_tables.feature +88 -0
- package/tests/unit_tests/ensureSpecialRows.test.js +34 -0
- package/tests/unit_tests/env_vars.test.js +53 -0
- package/tests/unit_tests/fields.fixtures.js +904 -0
- package/tests/unit_tests/getAllFields.test.js +142 -0
- package/tests/unit_tests/getMatchingRows.test.js +261 -0
- package/tests/unit_tests/getSafeScenarioBaseFilename.fixtures.js +12 -0
- package/tests/unit_tests/getSafeScenarioBaseFilename.test.js +62 -0
- package/tests/unit_tests/getSafeScenarioFilename.test.js +83 -0
- package/tests/unit_tests/html.fixtures.js +861 -0
- package/tests/unit_tests/matches.fixtures.js +168 -0
- package/tests/unit_tests/names.fixtures.js +35 -0
- package/tests/unit_tests/normalizeTable.test.js +22 -0
- package/tests/unit_tests/special_rows.fixtures.js +101 -0
- package/tests/unit_tests/tables.fixtures.js +428 -0
package/lib/steps.js
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
const { When, Then, And, Given, After, Before, BeforeAll, AfterAll, setDefaultTimeout } = require('cucumber');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const puppeteer = require('puppeteer');
|
|
6
|
+
const { v4: uuidv4 } = require('uuid');
|
|
7
|
+
|
|
8
|
+
const scope = require('./scope');
|
|
9
|
+
const env_vars = require('./utils/env_vars');
|
|
10
|
+
|
|
11
|
+
/* Of Note:
|
|
12
|
+
- We're using `*=` because sometimes da text has funny characters in it that are hard to anticipate
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
require("dotenv").config(); // Where did this go?
|
|
16
|
+
|
|
17
|
+
/* TODO:
|
|
18
|
+
1. 'choice' to be any kind of choice - radio, checkbox,
|
|
19
|
+
dropdown, etc. Have to research the different DOM for each
|
|
20
|
+
1. 'choice' to have a more specific way to access each item. For
|
|
21
|
+
example, for a list collect or other things that have multiple
|
|
22
|
+
things with the same text on the page.
|
|
23
|
+
1. Figure out how to test allowing felxibility for coder. For example:
|
|
24
|
+
there is placeholder text for the title of the form and if it's not
|
|
25
|
+
defined, placeholder text should appear (though that behavior may
|
|
26
|
+
bear discussion).
|
|
27
|
+
1. Add links to resources on:
|
|
28
|
+
1. taps that trigger navigation need Promise.all: https://stackoverflow.com/a/60789449/14144258
|
|
29
|
+
> I ran into a scenario, where there was the classic POST-303-GET
|
|
30
|
+
and an input[type=submit] was involved. It seems that in this case,
|
|
31
|
+
the tap of the button won't resolve until after the associated form's
|
|
32
|
+
submission and redirection, so the solution was to remove the waitForNavigation,
|
|
33
|
+
because it was executed after the redirection and thus was timing out.
|
|
34
|
+
|
|
35
|
+
I think Promise.all is what's taking care of these situations.
|
|
36
|
+
1. Listening for `targetchanged` or changed URL
|
|
37
|
+
1. Listening for responses
|
|
38
|
+
|
|
39
|
+
Should post example of detecting new page/no new page on submit when there
|
|
40
|
+
is a DOM change that you can detect. I suspect no request is
|
|
41
|
+
being sent, but I could be wrong. Haven't yet figured out how
|
|
42
|
+
to detect that.
|
|
43
|
+
|
|
44
|
+
regex thoughts: https://stackoverflow.com/questions/171480/regex-grabbing-values-between-quotation-marks
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// Hoping that most load and download times will be under 30 seconds.
|
|
48
|
+
// TODO: Check what average load and download times are in our current interviews.
|
|
49
|
+
setDefaultTimeout( 30 * 1000 );
|
|
50
|
+
|
|
51
|
+
let click_with = {
|
|
52
|
+
mobile: 'tap',
|
|
53
|
+
pc: 'click',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
BeforeAll(async () => {
|
|
57
|
+
scope.timeout = 30 * 1000;
|
|
58
|
+
scope.showif_timeout = 600;
|
|
59
|
+
scope.report = new Map();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
Before(async (scenario) => {
|
|
63
|
+
scope.scenario_id = uuidv4();
|
|
64
|
+
|
|
65
|
+
await scope.addReportHeading(scope, {scenario});
|
|
66
|
+
scope.base_filename = await scope.getSafeScenarioBaseFilename(scope, {scenario});
|
|
67
|
+
|
|
68
|
+
// Downloads
|
|
69
|
+
scope.downloadComplete = false;
|
|
70
|
+
scope.toDownload = '';
|
|
71
|
+
// Device interaction
|
|
72
|
+
scope.device = 'pc';
|
|
73
|
+
scope.activate = click_with[ scope.device ];
|
|
74
|
+
|
|
75
|
+
scope.page_id = null;
|
|
76
|
+
|
|
77
|
+
// Reset default timeout
|
|
78
|
+
scope.timeout = 30 * 1000;
|
|
79
|
+
setDefaultTimeout( 30 * 1000 );
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Add a check for an error page before each step? After each step?
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
//#####################################
|
|
86
|
+
//#####################################
|
|
87
|
+
// Establishing
|
|
88
|
+
//#####################################
|
|
89
|
+
//#####################################
|
|
90
|
+
|
|
91
|
+
Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}, async (file_name, language) => { // √
|
|
92
|
+
// NO CUCUMBER TIMEOUT. Handling our own timeouts to deal with initial scenario load.
|
|
93
|
+
// It involves wrapping everything in custom timeouts. See
|
|
94
|
+
// https://github.com/SuffolkLITLab/ALKiln/issues/389#issuecomment-984844240
|
|
95
|
+
let max_load_attempts = 3;
|
|
96
|
+
let num_attempts = 0;
|
|
97
|
+
// First test long load time discussion:
|
|
98
|
+
// https://github.com/SuffolkLITLab/ALKiln/issues/389#issuecomment-984844240
|
|
99
|
+
while ( num_attempts < max_load_attempts ) {
|
|
100
|
+
|
|
101
|
+
num_attempts++;
|
|
102
|
+
let timeout_message = `Failed to load "${ file_name }" after ${ num_attempts } tries. `
|
|
103
|
+
+ `Each try gave the page ${ scope.timeout/1000 } seconds to load.`;
|
|
104
|
+
|
|
105
|
+
let loaded = new Promise(async ( resolve, reject ) => {
|
|
106
|
+
// Exit if timeout is exceeded. Reject only if final attempt failed
|
|
107
|
+
let page_timeout = setTimeout(function() {
|
|
108
|
+
if ( num_attempts === max_load_attempts ) { reject( `${ timeout_message } Internal timeout.` ); }
|
|
109
|
+
else { resolve( false ); } // Give it another shot
|
|
110
|
+
}, scope.timeout);
|
|
111
|
+
// self-invoke an anonymous async function so we don't have to
|
|
112
|
+
// abstract the details of resolving and rejecting.
|
|
113
|
+
(async function() {
|
|
114
|
+
try {
|
|
115
|
+
await scope.load( scope, file_name );
|
|
116
|
+
resolve( true );
|
|
117
|
+
} catch ( err ) {
|
|
118
|
+
// Only fail on final attempt. Actual error should be logged by cucumber output.
|
|
119
|
+
if ( num_attempts === max_load_attempts ) {
|
|
120
|
+
// This may not be the most useful error for us framework developers, but
|
|
121
|
+
// we'll have the rejection error I hope
|
|
122
|
+
await scope.addToReport(scope, { type: `error`, value: timeout_message });
|
|
123
|
+
reject( err );
|
|
124
|
+
} else { resolve( false ); } // Give it another shot
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout( page_timeout ); // prevent temporary hang at end of tests
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
}); // ends loaded promise
|
|
130
|
+
await loaded;
|
|
131
|
+
if ( loaded === true ) { break; }
|
|
132
|
+
} // ends while attempting to load page
|
|
133
|
+
|
|
134
|
+
// The page timeout is set in here too in case someone set a custom timeout
|
|
135
|
+
// before a page was loaded.
|
|
136
|
+
// The function itself won't really timeout here, so we don't need to wrap it in
|
|
137
|
+
// a custom timeout
|
|
138
|
+
await scope.page.setDefaultTimeout( scope.timeout ); // overrides outer default timeout
|
|
139
|
+
|
|
140
|
+
// Allow developer to save download files
|
|
141
|
+
let custom_download_behavior_timeout = new Promise(async ( resolve, reject ) => {
|
|
142
|
+
let page_timeout = setTimeout(function() {reject( `Took too long to set download behavior.` ); }, scope.timeout);
|
|
143
|
+
// self-invoke an anonymous async function so we don't have to
|
|
144
|
+
// abstract the details of resolving and rejecting.
|
|
145
|
+
(async function() {
|
|
146
|
+
// I've seen stuff take an extra moment or two. Shame to have it everywhere
|
|
147
|
+
try {
|
|
148
|
+
// Interview files should get downloaded to downloads folder
|
|
149
|
+
let docs_dir = await scope.getSafeScenarioFilename( scope, { prefix: `downloads` });
|
|
150
|
+
await scope.page._client.send('Page.setDownloadBehavior', {
|
|
151
|
+
behavior: 'allow',
|
|
152
|
+
downloadPath: path.resolve( docs_dir ), // Save in root of this project
|
|
153
|
+
});
|
|
154
|
+
resolve();
|
|
155
|
+
} catch ( err ) { reject( err ); }
|
|
156
|
+
// prevent temporary hang at end of tests
|
|
157
|
+
finally { clearTimeout( page_timeout ); }
|
|
158
|
+
})();
|
|
159
|
+
}); // ends custom_download_behavior_timeout
|
|
160
|
+
await custom_download_behavior_timeout;
|
|
161
|
+
|
|
162
|
+
// ---- EVENT LISTENERS ----
|
|
163
|
+
// Wait for possible download. From https://stackoverflow.com/a/51423688
|
|
164
|
+
scope.page.on('response', async response => {
|
|
165
|
+
// Note: We can expect that only one file will be getting downloaded
|
|
166
|
+
// at any one time since each step completes synchronously
|
|
167
|
+
|
|
168
|
+
// If response has a file in it that matches the file we are downloading
|
|
169
|
+
if ( response._headers['content-disposition'] && (response._headers['content-disposition']).includes(`attachment;filename=${scope.toDownload}`) ) {
|
|
170
|
+
// Remove the event listener since we might not be closing the browser
|
|
171
|
+
scope.page.removeEventListener('response', arguments.callee);
|
|
172
|
+
|
|
173
|
+
// Watch event on download folder or file
|
|
174
|
+
await fs.watchFile( docs_dir, function (curr, prev) {
|
|
175
|
+
// If current size eq to size from response then close
|
|
176
|
+
if (parseInt(curr.size) === parseInt(response._headers['content-length'])) {
|
|
177
|
+
// Lets the related function in `scope` know that the download
|
|
178
|
+
// is done so that the next step can start.
|
|
179
|
+
scope.downloadComplete = true;
|
|
180
|
+
this.close();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}); // ends fs.watchFile
|
|
184
|
+
|
|
185
|
+
return false;
|
|
186
|
+
} // ends if this is the file the developer expected
|
|
187
|
+
}); // ends page.on 'response'
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
// TODO: Move language into a different step
|
|
191
|
+
scope.language = language;
|
|
192
|
+
let custom_lang_timeout = new Promise(async ( resolve, reject ) => {
|
|
193
|
+
let page_timeout = setTimeout(function() { reject( `It took to long to try to set the language "${ language }".` ); }, scope.timeout);
|
|
194
|
+
// self-invoke an anonymous async function so we don't have to
|
|
195
|
+
// abstract the details of resolving and rejecting.
|
|
196
|
+
(async function() {
|
|
197
|
+
try {
|
|
198
|
+
await scope.setLanguage( scope, { language });
|
|
199
|
+
resolve();
|
|
200
|
+
} catch ( err ) { reject( err ); }
|
|
201
|
+
// prevent temporary hang at end of tests
|
|
202
|
+
finally { clearTimeout( page_timeout ); }
|
|
203
|
+
})();
|
|
204
|
+
}); // ends custom_lang_timeout
|
|
205
|
+
await custom_lang_timeout;
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
let custom_aferStep_timeout = new Promise(async ( resolve, reject ) => {
|
|
209
|
+
let page_timeout = setTimeout(function() { reject(`\`afterStep()\` took too long.`); }, scope.timeout);
|
|
210
|
+
// self-invoke an anonymous async function so we don't have to
|
|
211
|
+
// abstract the details of resolving and rejecting.
|
|
212
|
+
(async function() {
|
|
213
|
+
// I've seen stuff take an extra moment or two. Shame to have it everywhere
|
|
214
|
+
try { await scope.afterStep(scope, {waitForTimeout: 200}); resolve(); }
|
|
215
|
+
catch ( err ) { reject( err ); }
|
|
216
|
+
// prevent temporary hang at end of tests
|
|
217
|
+
finally { clearTimeout( page_timeout ); }
|
|
218
|
+
})();
|
|
219
|
+
}); // ends custom_aferStep_timeout
|
|
220
|
+
await custom_aferStep_timeout;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// I am using a moble/pc
|
|
224
|
+
When(/I am using an? (.*)/, async ( device ) => {
|
|
225
|
+
// Let developer pick mobile device if they want to
|
|
226
|
+
// TODO: Add tablet?
|
|
227
|
+
if ( device ) {
|
|
228
|
+
if ( device.includes( 'mobile' ) || device.includes( 'phone' ) ) {
|
|
229
|
+
await scope.page.setUserAgent("Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36");
|
|
230
|
+
scope.device = 'mobile';
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
await scope.page.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3312.0 Safari/537.36");
|
|
234
|
+
scope.device = 'pc';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
scope.activate = click_with[ scope.device ];
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+)/i, async ( seconds ) => {
|
|
241
|
+
/* At any time, set a custom timeout for the scenario. */
|
|
242
|
+
scope.timeout = parseInt( seconds ) * 1000;
|
|
243
|
+
setDefaultTimeout( scope.timeout );
|
|
244
|
+
// If there is no page yet, we've ensured custom timeout will still be set
|
|
245
|
+
// appropriately in the interview loading step
|
|
246
|
+
if ( scope.page ) { await scope.page.setDefaultTimeout( scope.timeout ); }
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
//#####################################
|
|
250
|
+
//#####################################
|
|
251
|
+
// Story
|
|
252
|
+
//#####################################
|
|
253
|
+
//#####################################
|
|
254
|
+
|
|
255
|
+
When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)" with this data:?/, {timeout: -1}, async (target_id, raw_var_data) => {
|
|
256
|
+
/* NO CUCUMBER TIMEOUT. Must handle timeout ourselves:
|
|
257
|
+
* https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/timeouts.md#disable-timeouts
|
|
258
|
+
* Iterate in here to keep timeout reasonable */
|
|
259
|
+
|
|
260
|
+
// Note: Why do we not just stop when we've used all the vars in the table or
|
|
261
|
+
// reach the last var? Vars are not required to be given in sequence and pages that
|
|
262
|
+
// have non-button fields don't usually have a var assigned to the 'continue' button.
|
|
263
|
+
// Alternative: require vars even for 'continue' pages. Then in loop require at least one var
|
|
264
|
+
// to be found on a page before hitting 'continue'. That requires detecting navigation because
|
|
265
|
+
// pages with fields will not have a var for the continue button, so there will
|
|
266
|
+
// still have to be some automatic continuing.
|
|
267
|
+
// It also seems harsh, but more reliable.
|
|
268
|
+
// Ask developers what they'd like.
|
|
269
|
+
|
|
270
|
+
let supported_table = await scope.normalizeTable( scope, { var_data: raw_var_data.hashes() });
|
|
271
|
+
|
|
272
|
+
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id );
|
|
273
|
+
// Until we reach the final page or hit an error
|
|
274
|
+
while ( !id_matches ) {
|
|
275
|
+
let custom_timeout = new Promise(function( resolve, reject ) {
|
|
276
|
+
// Set up the timeout
|
|
277
|
+
let page_timeout = setTimeout(function() {
|
|
278
|
+
let timeout_message = `The target id was "${ target_id }", but it took too long` +
|
|
279
|
+
` to try to reach it (over ${ scope.timeout/1000/60 } min). The test got stuck on "${ id }".`
|
|
280
|
+
reject( timeout_message );
|
|
281
|
+
}, scope.timeout);
|
|
282
|
+
|
|
283
|
+
// self-invoke an anonymous async function so we don't have to
|
|
284
|
+
// abstract the details of resolving and rejecting.
|
|
285
|
+
(async function() {
|
|
286
|
+
try {
|
|
287
|
+
let { question_has_id, id, id_matches } = await scope.examinePageID( scope );
|
|
288
|
+
|
|
289
|
+
// Add special rows. Basically, do weirder things.
|
|
290
|
+
// TODO: Remove from_story_table - no longer needed if only called from in here
|
|
291
|
+
let ensured_var_data = await scope.ensureSpecialRows( scope, {
|
|
292
|
+
var_data: supported_table,
|
|
293
|
+
from_story_table: true,
|
|
294
|
+
});
|
|
295
|
+
// Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
|
|
296
|
+
let error = await scope.setFields(scope, { var_data: ensured_var_data, id, ensure_navigation: true });
|
|
297
|
+
if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
|
|
298
|
+
resolve();
|
|
299
|
+
} catch ( err ) {
|
|
300
|
+
reject( err );
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout( page_timeout ); // prevent temporary hang at end of tests
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
}); // ends custom_timeout for setFields
|
|
306
|
+
|
|
307
|
+
await custom_timeout;
|
|
308
|
+
|
|
309
|
+
// Prep for next loop
|
|
310
|
+
({ question_has_id, id, id_matches } = await scope.examinePageID( scope, target_id ));
|
|
311
|
+
|
|
312
|
+
} // ends while !id_matches
|
|
313
|
+
// Can anything unsatisfactory get through here?
|
|
314
|
+
|
|
315
|
+
// Add report strings from supported table, not an altered one
|
|
316
|
+
for ( let row of supported_table ) {
|
|
317
|
+
row.report_str = `${ await scope.convertToOriginalStoryTableRow(scope, { row_data: row }) }`;
|
|
318
|
+
}
|
|
319
|
+
// Sort into aphabetical order, as is recommended for writing story tables
|
|
320
|
+
let sorted_rows = supported_table.sort( function ( row1, row2 ) {
|
|
321
|
+
if( row1.report_str < row2.report_str ) { return -1; }
|
|
322
|
+
if( row1.report_str > row2.report_str ) { return 1; }
|
|
323
|
+
return 0;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Add full used table to report so it can be copied if needed.
|
|
327
|
+
// TODO: Should we/How do we get this in the final scenario report instead of here?
|
|
328
|
+
await scope.addToReport( scope, { type: `note`, value: `\n${ ' '.repeat(2) }Rows that got set:` });
|
|
329
|
+
await scope.addToReport( scope, { type: `step`, value: `${ ' '.repeat(4) }And I get the question id "${ target_id }" with this data:` });
|
|
330
|
+
await scope.addToReport(scope, { type: `row`, value: `${ ' '.repeat(6) }| var | value | trigger |`, });
|
|
331
|
+
let at_least_one_row_was_NOT_used = false;
|
|
332
|
+
for ( let row of sorted_rows ) {
|
|
333
|
+
if ( row.times_used > 0 ) {
|
|
334
|
+
await scope.addToReport(scope, { type: `row`, value: row.report_str, });
|
|
335
|
+
} else {
|
|
336
|
+
at_least_one_row_was_NOT_used = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Add unused rows if needed
|
|
341
|
+
if ( at_least_one_row_was_NOT_used ) {
|
|
342
|
+
await scope.addToReport( scope, { type: `note`, value: `${ ' '.repeat(2) }Unused rows:` });
|
|
343
|
+
for ( let row of sorted_rows ) {
|
|
344
|
+
if ( row.times_used === 0 ) {
|
|
345
|
+
await scope.addToReport(scope, { type: `row`, value: row.report_str, });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
await scope.addToReport(scope, { type: `note`, value: `${ ' '.repeat(2) }All rows were used` });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await scope.addToReport(scope, { type: `note`, value: `` });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
//#####################################
|
|
356
|
+
//#####################################
|
|
357
|
+
// Passive/Observational
|
|
358
|
+
//#####################################
|
|
359
|
+
//#####################################
|
|
360
|
+
|
|
361
|
+
// Need to see if it's possible to remove the need for this on most occasions
|
|
362
|
+
// I wait 1 second
|
|
363
|
+
// I wait .5 seconds
|
|
364
|
+
When(/I wait (\d*\.?\d+) seconds?/, async (seconds) => { // √
|
|
365
|
+
// Should this reset the 'navigated' flag?
|
|
366
|
+
// Shoudl any observational things reset the 'navigated' flag?
|
|
367
|
+
// Maybe the 'navitaged' flag should only be reset just before trying to
|
|
368
|
+
// navigate (after pressing a button)
|
|
369
|
+
await scope.afterStep(scope, {waitForTimeout: (parseFloat(seconds) * 1000)});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
Then(/I take a screenshot ?(?:named "([^"]+)")?/, async ( name ) => {
|
|
373
|
+
/* Save/download a screenshot. `name` does not have to be unique as
|
|
374
|
+
* the filename will be made unique. */
|
|
375
|
+
name = name || '';
|
|
376
|
+
let safe_path = await scope.getSafeScenarioFilename( scope, { prefix: `screenshot_${ name }` });
|
|
377
|
+
await scope.page.screenshot({ path: `./${ safe_path }.jpg`, type: 'jpeg', fullPage: true });
|
|
378
|
+
await scope.afterStep(scope);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
/* Here to make writing tests more comfortable. */
|
|
382
|
+
When("I do nothing", async () => { await scope.afterStep(scope); }); // √
|
|
383
|
+
|
|
384
|
+
Then('the question id should be {string}', async (question_id) => { // √
|
|
385
|
+
/* Looks for a santized version of the question id as it's written
|
|
386
|
+
* in the .yml. docassemble's way */
|
|
387
|
+
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, question_id );
|
|
388
|
+
|
|
389
|
+
// Trying to make helpful error messages
|
|
390
|
+
let no_id_msg = `Did not find any question id.`;
|
|
391
|
+
if ( !question_has_id ) { await scope.addToReport( scope, { type: `error`, value: no_id_msg }); }
|
|
392
|
+
expect( question_has_id , no_id_msg ).to.be.true;
|
|
393
|
+
|
|
394
|
+
let no_match_msg = `The question id was supposed to be "${ question_id }", but it's actually "${ id }".`;
|
|
395
|
+
if ( !id_matches ) { await scope.addToReport( scope, { type: `error`, value: no_match_msg }); }
|
|
396
|
+
expect( id_matches, no_match_msg ).to.be.true;
|
|
397
|
+
|
|
398
|
+
await scope.afterStep(scope);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Allow emphasis with capital letters
|
|
402
|
+
Then(/I should see the phrase "([^"]+)"/i, async ( phrase ) => { // √
|
|
403
|
+
/* In Chrome, this `innerText` gets only visible text */
|
|
404
|
+
const bodyText = await scope.page.$eval('body', elem => elem.innerText);
|
|
405
|
+
|
|
406
|
+
let msg = `The text "${ phrase }" SHOULD be on this page, but it's NOT.`;
|
|
407
|
+
if ( !bodyText.includes( phrase )) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
408
|
+
expect( bodyText, msg ).to.contain( phrase );
|
|
409
|
+
|
|
410
|
+
await scope.afterStep(scope);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Allow emphasis with capital letters
|
|
414
|
+
Then(/I should not see the phrase "([^"]+)"/i, async ( phrase ) => { // √
|
|
415
|
+
/* In Chrome, this `innerText` gets only visible text:
|
|
416
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
|
|
417
|
+
* "ignores hidden elements" */
|
|
418
|
+
const bodyText = await scope.page.$eval('body', elem => elem.innerText);
|
|
419
|
+
|
|
420
|
+
let msg = `The text "${ phrase }" should NOT be on this page, but it IS here.`;
|
|
421
|
+
if ( bodyText.includes( phrase )) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
422
|
+
expect( bodyText, msg ).not.to.contain( phrase );
|
|
423
|
+
|
|
424
|
+
await scope.afterStep(scope);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
Then('an element should have the id {string}', async ( id ) => { // √
|
|
428
|
+
const element = await scope.page.$('#' + id);
|
|
429
|
+
|
|
430
|
+
let msg = `No element on this page has the ID "${ id }".`;
|
|
431
|
+
if ( !element ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
432
|
+
expect( element, msg ).to.exist;
|
|
433
|
+
|
|
434
|
+
await scope.afterStep(scope);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// TODO:
|
|
438
|
+
// Just realized it's much more likely that the group name will be
|
|
439
|
+
// repeated than that the choice name will be repeated. Or maybe
|
|
440
|
+
// they're both equally likely to be repeated, but writing about the
|
|
441
|
+
// ordinal of the group would probably be more comfortable for a human.
|
|
442
|
+
// Need to think about this.
|
|
443
|
+
|
|
444
|
+
// let ordinal_regex = '?(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)?';
|
|
445
|
+
// // Haven't figured out ordinal_regex for group label yet
|
|
446
|
+
// // Allow "checkbox" and "radio"? Why would they be called the same thing?
|
|
447
|
+
// // TODO: Remove final pipe in regex
|
|
448
|
+
// let the_checkbox_is_as_expected = new RegExp(`the ${ ordinal_regex } ?(?:"([^"]+)")? (choice|checkbox|radio)(?: button)? ?(?:in "([^"]+)")? is (checked|unchecked|selected|unselected)`);
|
|
449
|
+
// Then(the_checkbox_is_as_expected, async (ordinal, label_text, choice_type, group_label_text, expected_status) => { // √
|
|
450
|
+
/* Non-dropdown non-combobox choices
|
|
451
|
+
* Examples of use:
|
|
452
|
+
* 1. the choice is unchecked
|
|
453
|
+
* 1. the radio is unchecked
|
|
454
|
+
* 1. the checkbox is unchecked
|
|
455
|
+
* 1. the only checkbox is unchecked
|
|
456
|
+
* 1. the third checkbox is checked
|
|
457
|
+
* 1. the "My court" checkbox is unchecked
|
|
458
|
+
* 1. the checkbox in "Which service" is checked
|
|
459
|
+
* 1. the third "My court" checkbox is checked
|
|
460
|
+
* 1. the third checkbox in "Which service" is checked
|
|
461
|
+
* 1. the "My court" checkbox in "Which service" is checked
|
|
462
|
+
* 1. the third "My court" checkbox in "Which service" is checked
|
|
463
|
+
* combos: none; a; b; c; a b; a c; b c; a b c;
|
|
464
|
+
*/
|
|
465
|
+
// let roles = [ choice_type ];
|
|
466
|
+
// if ( choice_type === 'choice' ) { roles = ['checkbox', 'radio']; }
|
|
467
|
+
|
|
468
|
+
// let elem = await scope.getSpecifiedChoice(scope, {ordinal, label_text, roles, group_label_text});
|
|
469
|
+
|
|
470
|
+
// // See if it's checked or not
|
|
471
|
+
// let is_checked = await elem.evaluate((elem) => {
|
|
472
|
+
// return elem.getAttribute('aria-checked') === 'true';
|
|
473
|
+
// });
|
|
474
|
+
// let what_it_should_be = expected_status === 'checked' || expected_status === 'selected';
|
|
475
|
+
// expect( is_checked ).to.equal( what_it_should_be );
|
|
476
|
+
|
|
477
|
+
// await scope.afterStep(scope);
|
|
478
|
+
// });
|
|
479
|
+
|
|
480
|
+
// This is phrase is confusing. It sounds like it's going to at least try to continue.
|
|
481
|
+
Then(/I (don't|can't|don’t|can’t|cannot|do not) continue/, async (unused) => { // √
|
|
482
|
+
/* Tests for detection of url change from button or link tap.
|
|
483
|
+
* Can other things trigger navigation? Re: other inputs things
|
|
484
|
+
* like 'Enter' acting like a click is a test for docassemble */
|
|
485
|
+
let msg = `The page should have stopped the user from continuing, but the user was able to continue.`;
|
|
486
|
+
if ( scope.navigated ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
487
|
+
expect( scope.navigated, msg ).to.be.false;
|
|
488
|
+
|
|
489
|
+
await scope.afterStep(scope);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
Then('I will be told an answer is invalid', async () => { // √
|
|
493
|
+
|
|
494
|
+
let { was_found, error_handlers } = await scope.checkForError( scope );
|
|
495
|
+
|
|
496
|
+
let no_error_msg = `No error message was found on the page`;
|
|
497
|
+
if ( !was_found ) { await scope.addToReport(scope, { type: `error`, value: no_error_msg }); }
|
|
498
|
+
expect( was_found, no_error_msg ).to.be.true;
|
|
499
|
+
|
|
500
|
+
let was_user_error = error_handlers.complex_user_error !== undefined || error_handlers.simple_user_error !== undefined;
|
|
501
|
+
let not_user_error_msg = `The error was a system error, not an error message to the user. Check the error screenshot.`;
|
|
502
|
+
if ( !was_user_error ) { await scope.addToReport(scope, { type: `error`, value: not_user_error_msg }); }
|
|
503
|
+
expect( was_user_error, not_user_error_msg ).to.be.true;
|
|
504
|
+
|
|
505
|
+
await scope.afterStep(scope);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
Then('I arrive at the next page', async () => { // √
|
|
509
|
+
/* Tests for detection of url change from button or link tap.
|
|
510
|
+
* Can other things trigger navigation? Re: other inputs things
|
|
511
|
+
* like 'Enter' acting like a click is a test for docassemble */
|
|
512
|
+
let msg = `User did not arrive at the next page.`
|
|
513
|
+
if ( !scope.navigated ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
514
|
+
expect( scope.navigated, msg ).to.be.true;
|
|
515
|
+
await scope.afterStep(scope);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Link observations have the 'Escape' button link in mind
|
|
519
|
+
Then('I should see the link {string}', async ( linkText ) => { // √
|
|
520
|
+
/* Loosley match link visible text. Should probably deprecate this. */
|
|
521
|
+
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
522
|
+
|
|
523
|
+
let msg = `Cannot find a link with the text "${ linkText }".`;
|
|
524
|
+
if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
525
|
+
expect( link, msg ).to.exist;
|
|
526
|
+
|
|
527
|
+
await scope.afterStep(scope);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
Then('I should see the link to {string}', async ( url ) => { // √
|
|
531
|
+
/* Strictly match href of link. */
|
|
532
|
+
let link = await scope.page.$(`*[href="${ url }"]`);
|
|
533
|
+
|
|
534
|
+
let msg = `Cannot find a link to "${ url }".`;
|
|
535
|
+
if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
536
|
+
expect(link, msg ).to.exist;
|
|
537
|
+
|
|
538
|
+
await scope.afterStep(scope);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) => { // √
|
|
542
|
+
/* Find link by its visible text and where it leads. */
|
|
543
|
+
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
544
|
+
// TODO: Possibly add a check here for the link text, though note this
|
|
545
|
+
// Step is not currently supported.
|
|
546
|
+
|
|
547
|
+
let prop_obj = await link.getProperty('href');
|
|
548
|
+
let actual_url = await prop_obj.jsonValue();
|
|
549
|
+
|
|
550
|
+
let msg = `Cannot find a link with the text "${ linkText }" leading to ${ expected_url }.`;
|
|
551
|
+
if ( actual_url !== expected_url ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
552
|
+
expect( actual_url ).to.equal( expected_url );
|
|
553
|
+
|
|
554
|
+
await scope.afterStep(scope);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkText, which_window) => { // √
|
|
558
|
+
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
559
|
+
|
|
560
|
+
let prop_obj = await link.getProperty('target');
|
|
561
|
+
let target = await await prop_obj.jsonValue();
|
|
562
|
+
|
|
563
|
+
let should_open_a_new_window = which_window === 'a new window';
|
|
564
|
+
let opens_a_new_window = target === '_blank';
|
|
565
|
+
let hasCorrectWindowTarget =
|
|
566
|
+
( should_open_a_new_window && opens_a_new_window )
|
|
567
|
+
|| ( !should_open_a_new_window && !opens_a_new_window );
|
|
568
|
+
|
|
569
|
+
let msg = `The link "${ linkText }" does NOT open in ${ which_window }.`;
|
|
570
|
+
if ( !hasCorrectWindowTarget ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
571
|
+
expect( hasCorrectWindowTarget, msg ).to.be.true;
|
|
572
|
+
|
|
573
|
+
await scope.afterStep(scope);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
//#####################################
|
|
577
|
+
//#####################################
|
|
578
|
+
// Actions
|
|
579
|
+
//#####################################
|
|
580
|
+
//#####################################
|
|
581
|
+
|
|
582
|
+
//#####################################
|
|
583
|
+
// Possible navigation
|
|
584
|
+
//#####################################
|
|
585
|
+
|
|
586
|
+
// Consider people wanting to use this for an in-interview page
|
|
587
|
+
// Also consider 'the link url "..." should open...'?
|
|
588
|
+
Then(/the "([^"]+)" link opens a working page/, async (linkText) => { // √
|
|
589
|
+
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
590
|
+
let prop_obj = await link.getProperty('href');
|
|
591
|
+
let actual_url = await prop_obj.jsonValue();
|
|
592
|
+
|
|
593
|
+
let linkPage = await scope.browser.newPage();
|
|
594
|
+
let response = await linkPage.goto(actual_url, {waitUntil: 'domcontentloaded'});
|
|
595
|
+
|
|
596
|
+
let msg = `The "${ linkText }" link is broken.`;
|
|
597
|
+
if ( !response.ok() ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
598
|
+
expect( response.ok(), msg ).to.be.true;
|
|
599
|
+
|
|
600
|
+
linkPage.close()
|
|
601
|
+
|
|
602
|
+
await scope.afterStep(scope);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
When('I tap to continue', async () => {
|
|
606
|
+
// Any selectors for this seem somewhat precarious
|
|
607
|
+
// No data will cause an automatic continue when `ensure_navigation` is true
|
|
608
|
+
await scope.setFields( scope, { var_data: [], ensure_navigation: true });
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
//#####################################
|
|
613
|
+
// UI element interaction
|
|
614
|
+
//#####################################
|
|
615
|
+
|
|
616
|
+
// -------------------------------------
|
|
617
|
+
// --- VARIABLES-BASED STEPS ---
|
|
618
|
+
|
|
619
|
+
When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, async ( var_name, set_to ) => {
|
|
620
|
+
/* Set any variable (with or without a choice associated with it) to a value. Set:
|
|
621
|
+
* - Buttons associated with variables
|
|
622
|
+
* - Dropdowns
|
|
623
|
+
* - Checkboxes (multiple choice checkboxes must be set to "true" or "false")
|
|
624
|
+
* - Radio buttons
|
|
625
|
+
* - Text inputs
|
|
626
|
+
* - Textareas
|
|
627
|
+
*
|
|
628
|
+
* Examples:
|
|
629
|
+
* 1. I set the var "benefits['SSI']" to "false"
|
|
630
|
+
* 1. I set the var "rent_interval" to "weekly"
|
|
631
|
+
*/
|
|
632
|
+
let var_data = await scope.normalizeTable( scope, {
|
|
633
|
+
var_data: [{ var: var_name, value: set_to, trigger: '' }],
|
|
634
|
+
});
|
|
635
|
+
// Don't continue if the variable doesn't navigate and if this variable cannot be set
|
|
636
|
+
// on this page, trigger an error
|
|
637
|
+
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
Then(/I upload "([^"]+)" to "([^"]+)"/, async ( filenames, var_name ) => {
|
|
641
|
+
/* Uploads sibling file (docx, jpg, etc) to input var. Can actually use
|
|
642
|
+
* 'I set the var "" to ""', but this is more explicit if it's desired. */
|
|
643
|
+
let var_data = await scope.normalizeTable( scope, {
|
|
644
|
+
var_data: [{ var: var_name, value: filenames, trigger: '' }],
|
|
645
|
+
});
|
|
646
|
+
// Don't continue if the variable doesn't navigate and if this variable cannot be set
|
|
647
|
+
// on this page, trigger an error
|
|
648
|
+
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
// -------------------------------------
|
|
653
|
+
// --- OTHER UI STEPS ---
|
|
654
|
+
|
|
655
|
+
When('I tap the defined text link {string}', async (phrase) => { // hold
|
|
656
|
+
/* Not sure what 'defined' means here. Maybe for terms? */
|
|
657
|
+
const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
|
|
658
|
+
|
|
659
|
+
let msg = `The term "${ phrase }" seems to be missing.`;
|
|
660
|
+
if ( !link ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
661
|
+
expect( link, msg ).to.exist;
|
|
662
|
+
|
|
663
|
+
await link.evaluate( elem => { return elem.click(); });
|
|
664
|
+
|
|
665
|
+
await scope.afterStep(scope, {waitForShowIf: true});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
Then('I sign', async () => {
|
|
669
|
+
await scope.sign( scope );
|
|
670
|
+
await scope.afterStep(scope);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
When(/I download "([^"]+)"$/, async (filename) => {
|
|
674
|
+
/* Taps the link that leads to the given filename to trigger downloading.
|
|
675
|
+
* and waits till the file has been downloaded before allowing the tests to continue.
|
|
676
|
+
WARNING: Cannot download the same file twice in a single scenario. */
|
|
677
|
+
let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
|
|
678
|
+
|
|
679
|
+
let msg = `"${ filename }" seems to be missing. Cannot find a link to that document.`;
|
|
680
|
+
if ( !elem ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
681
|
+
expect( elem, msg ).to.exist;
|
|
682
|
+
|
|
683
|
+
scope.toDownload = filename;
|
|
684
|
+
await elem.evaluate( elem => { return elem.click(); });
|
|
685
|
+
await scope.detectDownloadComplete( scope );
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
//#####################################
|
|
689
|
+
//#####################################
|
|
690
|
+
// Macros
|
|
691
|
+
//#####################################
|
|
692
|
+
//#####################################
|
|
693
|
+
|
|
694
|
+
When(/I set the name of ?(?:the var(?:iable)?)? "([^"]+)" to "([^"]+)"/, async (var_base, name_str) => {
|
|
695
|
+
/* Given a variable name and a space-separated string, fills the
|
|
696
|
+
* appropriate fields on an Assembly Line name question.
|
|
697
|
+
* It handles any of:
|
|
698
|
+
* 1. A one-word value to set name.text (ex: 'LandlordEtAl')
|
|
699
|
+
* 1. An Individual's name that it will set based on the number of words:
|
|
700
|
+
* 1. Two words - a first and last name (ex: "Tal Taylor")
|
|
701
|
+
* 1. Three words - a first, middle, and last name (ex: "Tal Tay Taylor")
|
|
702
|
+
* 1. Four words - a first, middle, last, and suffix (ex: "Tal Tay Taylor III")
|
|
703
|
+
*
|
|
704
|
+
* The string 'the var'/'the variable' part of the step is optional, as you can see below.
|
|
705
|
+
*
|
|
706
|
+
* Examples:
|
|
707
|
+
* 1. I set the name of "user" to "Uli Uster"
|
|
708
|
+
* 1. I set the name of the var "defendant" to "Dalia Day Defo Jr"
|
|
709
|
+
* 1. I set the name of the variable "defendant" to "Dalia Day Defo Jr"
|
|
710
|
+
*/
|
|
711
|
+
let name_patterns = [
|
|
712
|
+
[ `text` ],
|
|
713
|
+
[ `first`, `last` ],
|
|
714
|
+
[ `first`, `middle`, `last` ],
|
|
715
|
+
[ `first`, `middle`, `last`, `suffix` ],
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
let name_parts = name_str.split(' ');
|
|
719
|
+
let desired_pattern = name_patterns[( name_parts.length - 1 )];
|
|
720
|
+
|
|
721
|
+
for ( let parti = 0; parti < name_parts.length; parti++ ) {
|
|
722
|
+
let var_name = `${ var_base }.name.${ desired_pattern[ parti ] }`
|
|
723
|
+
let value = name_parts[ parti ];
|
|
724
|
+
await scope.setVar( scope, var_name, value );
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Continue to next page...?
|
|
728
|
+
await scope.afterStep(scope, {waitForShowIf: true});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
When(/I set the address of ?(?:the var(?:iable)?)? "([^"]+)" to "([^"]+)"/, async (var_base, address_str) => {
|
|
733
|
+
/* Given a variable name and a four-part address formatted as below,
|
|
734
|
+
* sets the appropriate Assembly Line address fields.
|
|
735
|
+
*
|
|
736
|
+
* The string 'the var'/'the variable' part of the step is optional, as you can see below.
|
|
737
|
+
*
|
|
738
|
+
* Examples:
|
|
739
|
+
* 1. When I set the address of "users[0]" to "112 Southampton St., Unit 1, Boston, MA 02118"
|
|
740
|
+
* 1. When I set the address of the variable "users[0]" to "112 Southampton St., Unit 1, Boston, MA 02118"
|
|
741
|
+
* 1. When I set the address of the var "users[0]" to "112 Southampton St., Unit 1, Boston, MA 02118"
|
|
742
|
+
*/
|
|
743
|
+
// Get all the parts of the address properly separated
|
|
744
|
+
// First, split apart each line of the address to [address line 1, address line 2, city, state zip]...
|
|
745
|
+
let split = address_str.split(/,\s*/);
|
|
746
|
+
// ...then, separate the state and the zip code...
|
|
747
|
+
let last_parts = (split.pop()).split(/\s+/);
|
|
748
|
+
// ...and add them back in, to end up with [address line 1, address line 2, city, state, zip].
|
|
749
|
+
let address_parts = split.concat( last_parts );
|
|
750
|
+
let start = `${ var_base }.address.`;
|
|
751
|
+
|
|
752
|
+
let index = 0;
|
|
753
|
+
await scope.setVar( scope, `${start}address`, address_parts[0] );
|
|
754
|
+
// Ensure it's without 'Unit '/'unit '
|
|
755
|
+
await scope.setVar( scope, `${start}unit`, (address_parts[1].toLowerCase()).replace(/unit\s*/, '') );
|
|
756
|
+
await scope.setVar( scope, `${start}city`, address_parts[2] );
|
|
757
|
+
await scope.setVar( scope, `${start}state`, address_parts[3] );
|
|
758
|
+
await scope.setVar( scope, `${start}zip`, address_parts[4] );
|
|
759
|
+
|
|
760
|
+
await scope.afterStep(scope, {waitForShowIf: true});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Next macro - download sequence?
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
//#####################################
|
|
768
|
+
//#####################################
|
|
769
|
+
// Framework Development
|
|
770
|
+
//#####################################
|
|
771
|
+
//#####################################
|
|
772
|
+
// TODO: Add Step for testing final report
|
|
773
|
+
|
|
774
|
+
Given(/the Scenario report should include/i, async ( report_text ) => {
|
|
775
|
+
/* WARNING: If a Step will fail, this Step must come first.
|
|
776
|
+
*
|
|
777
|
+
* Prepare to test that the report for a Scenario includes the given text.
|
|
778
|
+
* The actual test has to be done at the end of the Scenario as some text
|
|
779
|
+
* may be added to the report there. */
|
|
780
|
+
if ( !scope.expected_in_report ) {
|
|
781
|
+
scope.expected_in_report = [];
|
|
782
|
+
}
|
|
783
|
+
scope.expected_in_report.push( report_text );
|
|
784
|
+
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
Given(/the final Scenario status should be "(passed|failed)"/i, async ( expected_status ) => {
|
|
788
|
+
/* WARNING: If a Step will fail, this Step must come first.
|
|
789
|
+
*
|
|
790
|
+
* Prepare to test that the Scenario completes with the correct status. */
|
|
791
|
+
scope.expected_status = expected_status;
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
//#####################################
|
|
796
|
+
//#####################################
|
|
797
|
+
// After
|
|
798
|
+
//#####################################
|
|
799
|
+
//#####################################
|
|
800
|
+
|
|
801
|
+
After(async function(scenario) {
|
|
802
|
+
|
|
803
|
+
// Record status before internal report tests can interfere with values
|
|
804
|
+
await scope.setReportScenarioStatus( scope, { status: scenario.result.status });
|
|
805
|
+
|
|
806
|
+
if (scenario.result.status !== `passed`) {
|
|
807
|
+
if ( env_vars.DEBUG ) {
|
|
808
|
+
// When debugging locally, pause to allow dev to examine the page
|
|
809
|
+
console.log( `Error occurred while running test` );
|
|
810
|
+
await scope.page.waitFor( 60 * 1000 );
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
await scope.addToReport(scope, { type: `outcome`, value: `**-- Scenario Failed --**` });
|
|
814
|
+
// Save/download a picture of the screen during the error
|
|
815
|
+
let safe_filename = await scope.getSafeScenarioFilename( scope, { prefix: `error` });
|
|
816
|
+
await scope.page.screenshot({ path: `${ safe_filename }.jpg`, type: `jpeg`, fullPage: true });
|
|
817
|
+
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
let original_scenario_status = scenario.result.status;
|
|
821
|
+
|
|
822
|
+
// Internal tests of this framework
|
|
823
|
+
if ( scope.expected_status ) {
|
|
824
|
+
let expected = scope.expected_status;
|
|
825
|
+
scope.expected_status = null; // reset for next Scenario
|
|
826
|
+
expect( scenario.result.status ).to.equal( expected );
|
|
827
|
+
}
|
|
828
|
+
if ( scope.expected_in_report ) {
|
|
829
|
+
let expected = scope.expected_in_report;
|
|
830
|
+
// Reset report values no matter what so they don't mess up future scenarios
|
|
831
|
+
// Maybe this should be in `Before`, but it would be so out of context there...
|
|
832
|
+
scope.expected_in_report = [];
|
|
833
|
+
let all_were_included = await scope.reportIncludesAllExpected( scope, { expected });
|
|
834
|
+
if ( all_were_included ) { scenario.result.status = `passed`; }
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// If there is a page open, then sign out and close it
|
|
838
|
+
if ( scope.page ) {
|
|
839
|
+
// Make sure the new interview really starts fresh. Sometimes insufficient.
|
|
840
|
+
// Also, often is a failure point.
|
|
841
|
+
// Consider, instead, catching the reload error for a bad restart and just
|
|
842
|
+
// re-trying at that point.
|
|
843
|
+
try {
|
|
844
|
+
await scope.page.goto(`${ env_vars.BASE_URL }/user/sign-out`, {waitUntil: `domcontentloaded`});
|
|
845
|
+
} catch ( err ) {
|
|
846
|
+
await scope.addToReport(scope, {
|
|
847
|
+
type: `warning`,
|
|
848
|
+
value: `Trying to reset for the next test a second time because the first time failed.`
|
|
849
|
+
});
|
|
850
|
+
try {
|
|
851
|
+
await scope.page.goto(`${ env_vars.BASE_URL }/user/sign-out`, {waitUntil: `domcontentloaded`});
|
|
852
|
+
} catch ( err ) {
|
|
853
|
+
await scope.addToReport(scope, {
|
|
854
|
+
type: `warning`,
|
|
855
|
+
value: `This test ${ original_scenario_status }, but also there was an error while trying to reset for the next test. Re-running until you don't get this error would probably be a good idea.`
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
await scope.page.close();
|
|
861
|
+
scope.page = null;
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
AfterAll(async function() {
|
|
866
|
+
let report = await scope.getPrintableReport( scope );
|
|
867
|
+
// Log the report. Can't get this.attach() or this.log() to work in `After()` or here.
|
|
868
|
+
// May need newest version of cucumberjs
|
|
869
|
+
console.log( `\n\n${ report }` );
|
|
870
|
+
|
|
871
|
+
// Save/download the report as an artifact
|
|
872
|
+
fs.writeFileSync( `alkiln_report_${ Date.now() }.txt`, report );
|
|
873
|
+
|
|
874
|
+
// If there is a browser open, then close it
|
|
875
|
+
if (scope.browser) {
|
|
876
|
+
await scope.browser.close();
|
|
877
|
+
}
|
|
878
|
+
});
|