@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/scope.js
CHANGED
|
@@ -4,6 +4,8 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const { AxePuppeteer } = require('@axe-core/puppeteer');
|
|
6
6
|
const safe_filename = require("sanitize-filename");
|
|
7
|
+
// "en" is faster to import: https://github.com/faker-js/faker/issues/1114#issuecomment-1169532948
|
|
8
|
+
const faker = require('@faker-js/faker/locale/en').faker;
|
|
7
9
|
|
|
8
10
|
// Ours
|
|
9
11
|
const session_vars = require('./utils/session_vars');
|
|
@@ -11,6 +13,7 @@ const get_base_interview_url = require(`./docassemble/get_base_interview_url`);
|
|
|
11
13
|
const time = require('./utils/time');
|
|
12
14
|
const log = require('./utils/log');
|
|
13
15
|
const { waitForTimeout } = require('./utils/time');
|
|
16
|
+
const files = require('./utils/files' );
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
let ordinal_to_integer = {
|
|
@@ -50,6 +53,7 @@ let is_secret_var = function(row) {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
|
|
56
|
+
let misc_artifacts_dir = `_alkiln_test-misc_artifacts`;
|
|
53
57
|
|
|
54
58
|
module.exports = {
|
|
55
59
|
trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
|
|
@@ -63,7 +67,22 @@ module.exports = {
|
|
|
63
67
|
let scenario = await scope.makeSureReportPartsExist( scope );
|
|
64
68
|
scenario.lines.push({ type, value });
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
// TODO: Not sure how to use .add_to_debug_log() here considering possibly dynamic filepath
|
|
71
|
+
// log.add_to_debug_log(`Report:\ntype: ${ type }, value: ${ value }\n`);
|
|
72
|
+
|
|
73
|
+
// Ensure filepath for saving local unit test logs
|
|
74
|
+
if ( !scope.paths ) {
|
|
75
|
+
// TODO: would it be useful to put in existing artifacts folder if possible.
|
|
76
|
+
// Can this be usefully abstracted?
|
|
77
|
+
scope.paths = { debug_log: `${ misc_artifacts_dir }/unit_tests_logs-${ files.readable_date() }.txt` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
fs.mkdirSync( misc_artifacts_dir );
|
|
84
|
+
fs.appendFileSync(scope.paths.debug_log, `\nReport:\ntype: ${ type }, value: ${ value }`);
|
|
85
|
+
}
|
|
67
86
|
return scenario;
|
|
68
87
|
|
|
69
88
|
}, // Ends scope.addToReport()
|
|
@@ -72,7 +91,10 @@ module.exports = {
|
|
|
72
91
|
/* If a part of the report needs to exist right now, but doesn't exist,
|
|
73
92
|
* create it. */
|
|
74
93
|
if ( !scope.report ) { scope.report = new Map(); }
|
|
75
|
-
if ( !scope.scenario_id ) {
|
|
94
|
+
if ( !scope.scenario_id ) {
|
|
95
|
+
let date = files.readable_date();
|
|
96
|
+
scope.scenario_id = `_report_${ date }`;
|
|
97
|
+
}
|
|
76
98
|
if ( !scope.report.get( scope.scenario_id )) {
|
|
77
99
|
scope.report.set( scope.scenario_id, {} )
|
|
78
100
|
scope.report.get( scope.scenario_id ).status = `in progress`;
|
|
@@ -169,6 +191,7 @@ module.exports = {
|
|
|
169
191
|
let report = ``;
|
|
170
192
|
for ( let line of data.lines ) {
|
|
171
193
|
if ( line.type === `page_id` ) { report += `screen id: `; }
|
|
194
|
+
else if ( line.type === `outcome` ) { report += `\n`; }
|
|
172
195
|
else if ( line.type === `warning` ) { report += `WARNING: `; }
|
|
173
196
|
else if ( line.type === `error` ) { report += `\nERROR: ` }
|
|
174
197
|
report += `${ line.value }\n`;
|
|
@@ -187,41 +210,49 @@ module.exports = {
|
|
|
187
210
|
name_parts.push( scope.language );
|
|
188
211
|
}
|
|
189
212
|
name_parts.push( scenario.pickle.name );
|
|
190
|
-
for ( let tag of scenario.pickle.tags ) {
|
|
191
|
-
name_parts.push( tag.name );
|
|
192
|
-
}
|
|
193
213
|
|
|
194
214
|
let filename = name_parts.join(`-`);
|
|
215
|
+
let safe_name = safe_filename( filename, { replacement: `_` });
|
|
216
|
+
// Allow room for extensions, date, etc.
|
|
217
|
+
let safer_name = safe_name.substring(0, (255 - 45));
|
|
195
218
|
|
|
196
|
-
return
|
|
219
|
+
return safer_name;
|
|
197
220
|
}, // Ends scope.getSafeScenarioBaseFilename()
|
|
198
221
|
|
|
199
|
-
|
|
222
|
+
// TODO: Does this need a name change? getSafeScenarioBasedFilename?
|
|
223
|
+
getSafeScenarioFilename: async function( scope, options={ prefix: '' }) {
|
|
200
224
|
/* Return a string that's safe to be a filename. Use a timestamp,
|
|
201
225
|
* a prefix the caller wants, and the base filename. We hope it can
|
|
202
226
|
* contain non-english characters too. We do not offer a suffix option
|
|
203
227
|
* because it may be cut off if the filename is too long.
|
|
204
228
|
*/
|
|
205
|
-
|
|
206
|
-
|
|
229
|
+
let name = options.prefix || '';
|
|
230
|
+
|
|
207
231
|
// As long as it's not an empty string, add a separator between it and the rest of the name
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if ( prefix ) {
|
|
211
|
-
prefix = `${ prefix }_`;
|
|
232
|
+
if ( name.length > 0 ) {
|
|
233
|
+
name += `-`;
|
|
212
234
|
}
|
|
213
235
|
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
if ( scope.page ) {
|
|
237
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
238
|
+
name += `${ id }-`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
name += scope.base_filename;
|
|
242
|
+
name += `-${ files.readable_date() }`;
|
|
243
|
+
|
|
244
|
+
let safe_name = safe_filename( name, { replacement: `_` });
|
|
245
|
+
// Allow room for extensions, etc.
|
|
246
|
+
let safer_name = safe_name.substring(0, (255 - 10));
|
|
216
247
|
return safer_name;
|
|
217
248
|
}, // Ends scope.getSafeScenarioFilename()
|
|
218
249
|
|
|
219
250
|
getJSONFilename: async function( scope ) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return
|
|
251
|
+
// Get a unique path for a json file
|
|
252
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
253
|
+
let date = files.readable_date();
|
|
254
|
+
let json_path = path.join( scope.paths.scenario, `json_for-${ id }-${ date }.json` );
|
|
255
|
+
return json_path;
|
|
225
256
|
},
|
|
226
257
|
|
|
227
258
|
savePageJSONData: async function( scope ) {
|
|
@@ -231,13 +262,12 @@ module.exports = {
|
|
|
231
262
|
* @returns {string} info.filename The name of the file that was saved.
|
|
232
263
|
* @returns {Object} info.json The variables object.
|
|
233
264
|
*/
|
|
234
|
-
|
|
235
|
-
let filename = await scope.getJSONFilename( scope );
|
|
265
|
+
let json_path = await scope.getJSONFilename( scope );
|
|
236
266
|
// Get the data
|
|
237
267
|
let json = await scope.getPageJSON( scope );
|
|
238
268
|
// Save the data
|
|
239
|
-
fs.writeFileSync(
|
|
240
|
-
return {
|
|
269
|
+
fs.writeFileSync( json_path, JSON.stringify( json, null, 2 ));
|
|
270
|
+
return { json_path, json };
|
|
241
271
|
}, // Ends scope.savePageJSONData()
|
|
242
272
|
|
|
243
273
|
waitForShowIf: async function waitForShowIf(scope) {
|
|
@@ -327,7 +357,7 @@ module.exports = {
|
|
|
327
357
|
// This check might never be needed now as we try to match fields on the page
|
|
328
358
|
// first in user-triggered cases - users used to be able to access this almost
|
|
329
359
|
// directly, but they can't anymore. Our functions should know to only pass good things.
|
|
330
|
-
let msg = `Cannot find
|
|
360
|
+
let msg = `Cannot find the element to tap.`;
|
|
331
361
|
if ( !elem ) { await scope.addToReport(scope, { type: `error`, value: msg }); }
|
|
332
362
|
|
|
333
363
|
expect( elem, msg ).to.exist;
|
|
@@ -386,7 +416,14 @@ module.exports = {
|
|
|
386
416
|
|
|
387
417
|
detectDownloadComplete: async function detectDownloadComplete(scope, endTime) {
|
|
388
418
|
/* Resolve if a download flag has changed. Timeout after a bit less than
|
|
389
|
-
* the global permitted timeout
|
|
419
|
+
* the global permitted timeout
|
|
420
|
+
*
|
|
421
|
+
* TODO: Properly wait for download to complete. See
|
|
422
|
+
* https://github.com/SuffolkLITLab/ALKiln/issues/492 and
|
|
423
|
+
* https://stackoverflow.com/a/56951024 and
|
|
424
|
+
* https://github.com/puppeteer/puppeteer/issues/4676 and
|
|
425
|
+
* https://github.com/puppeteer/puppeteer/issues/299 and
|
|
426
|
+
*/
|
|
390
427
|
|
|
391
428
|
// Stop/resolve if too much time has passed
|
|
392
429
|
let expirationTime = scope.timeout - 200; // ms
|
|
@@ -454,7 +491,7 @@ module.exports = {
|
|
|
454
491
|
|
|
455
492
|
// Do your own waiting if you need to in the step
|
|
456
493
|
return to_manipulate;
|
|
457
|
-
}, // Ends setVar()
|
|
494
|
+
}, // Ends scope.setVar()
|
|
458
495
|
|
|
459
496
|
continue: async function ( scope ) {
|
|
460
497
|
/* Presses whatever button it finds that might lead to the next page. */
|
|
@@ -466,7 +503,25 @@ module.exports = {
|
|
|
466
503
|
await elem.evaluate( el => { return el.className });
|
|
467
504
|
// Waits for navigation or user error
|
|
468
505
|
await scope.tapElement( scope, elem );
|
|
469
|
-
},
|
|
506
|
+
}, // Ends scope.continue()
|
|
507
|
+
|
|
508
|
+
continue_exists: async function ( scope ) {
|
|
509
|
+
/** Return true if a button that will allow continuing appears
|
|
510
|
+
* on this page. Otherwise return false. */
|
|
511
|
+
|
|
512
|
+
// Possible to find this on an event screen with `buttons:`, like an exit button
|
|
513
|
+
// <button type="submit" class="btn btn-da btn-danger" name="X211bHRpcGxlX2Nob2ljZQ" value="0">exit</button>
|
|
514
|
+
// An actual continue button that doesn't set a variable
|
|
515
|
+
// <button class="btn btn-da btn-primary" type="submit">Continue</button>
|
|
516
|
+
// One that sets a variable:
|
|
517
|
+
// <button type="submit" class="btn btn-da btn-primary" name="Zm9v" value="True">Continue</button>
|
|
518
|
+
// `buttons:` can be used in question blocks as choices
|
|
519
|
+
let regular = await scope.page.$( `fieldset.da-field-buttons button[type="submit"]` );
|
|
520
|
+
let signature = await scope.page.$( `fieldset .dasigsave` );
|
|
521
|
+
|
|
522
|
+
return regular !== null || signature !== null;
|
|
523
|
+
|
|
524
|
+
}, // Ends scope.continue_exists()
|
|
470
525
|
|
|
471
526
|
tapTab: async function ( scope, tab_id ) {
|
|
472
527
|
let elem = await scope.page.$(`#${tab_id}`);
|
|
@@ -497,12 +552,13 @@ module.exports = {
|
|
|
497
552
|
axe_result.passes = [];
|
|
498
553
|
axe_result.inapplicable = [];
|
|
499
554
|
|
|
500
|
-
let
|
|
555
|
+
let axe_path = null;
|
|
501
556
|
const passed = axe_result.violations.length == 0;
|
|
502
557
|
if (!passed) {
|
|
503
|
-
axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `
|
|
504
|
-
|
|
505
|
-
|
|
558
|
+
let axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `aXe_failure`}) + '.json';
|
|
559
|
+
axe_path = `${ scope.paths.scenario }/${ axe_filename }`;
|
|
560
|
+
fs.writeFileSync( axe_path, JSON.stringify( axe_result, null, 2 ));
|
|
561
|
+
let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_path }.`;
|
|
506
562
|
await scope.addToReport( scope, {
|
|
507
563
|
type: `error`,
|
|
508
564
|
value: msg
|
|
@@ -512,7 +568,7 @@ module.exports = {
|
|
|
512
568
|
}
|
|
513
569
|
return {
|
|
514
570
|
passed: axe_result.violations.length == 0,
|
|
515
|
-
|
|
571
|
+
axe_filepath: axe_path
|
|
516
572
|
};
|
|
517
573
|
},
|
|
518
574
|
|
|
@@ -562,6 +618,9 @@ module.exports = {
|
|
|
562
618
|
let result = { error: ``, elem: null };
|
|
563
619
|
|
|
564
620
|
let winner = await Promise.race([
|
|
621
|
+
// Signature page
|
|
622
|
+
scope.page.waitForSelector( `#dasigpage`, { timeout: scope.timeout }),
|
|
623
|
+
// Any other valid page
|
|
565
624
|
scope.page.waitForSelector( `#daMainQuestion`, { timeout: scope.timeout }),
|
|
566
625
|
scope.page.waitForSelector( `#da-retry`, { timeout: scope.timeout }),
|
|
567
626
|
]);
|
|
@@ -571,10 +630,14 @@ module.exports = {
|
|
|
571
630
|
let error_id_elem = await scope.page.$( `#da-retry` );
|
|
572
631
|
if ( error_id_elem ) {
|
|
573
632
|
let error_elem = await scope.page.$( `blockquote` );
|
|
574
|
-
|
|
575
|
-
|
|
633
|
+
if (error_elem !== null) {
|
|
634
|
+
let error_handle = await error_elem.getProperty( `textContent` );
|
|
635
|
+
let system_error_text = await error_handle.jsonValue();
|
|
576
636
|
|
|
577
|
-
|
|
637
|
+
result.error = system_error_text;
|
|
638
|
+
} else {
|
|
639
|
+
result.error = "Docassemble ran into an error on the page, but ALKiln does not know what the error is. If a screenshot was taken, check the screenshot";
|
|
640
|
+
}
|
|
578
641
|
}
|
|
579
642
|
|
|
580
643
|
return result;
|
|
@@ -595,9 +658,40 @@ module.exports = {
|
|
|
595
658
|
}
|
|
596
659
|
}, // Ends scope.setLanguage()
|
|
597
660
|
|
|
598
|
-
normalizeTable: async function ( scope, { var_data,
|
|
599
|
-
/* Return data derived from cucumber variable-setting data
|
|
600
|
-
*
|
|
661
|
+
normalizeTable: async function ( scope, { var_data=null, raw_var_data=null }) {
|
|
662
|
+
/* Return data derived from cucumber variable-setting data, from either an object format
|
|
663
|
+
* (var_data) or an array format (raw_var_data). `var_data` only is used when calling
|
|
664
|
+
* normalizeTable internally.
|
|
665
|
+
*/
|
|
666
|
+
|
|
667
|
+
if (!var_data && ! raw_var_data) {
|
|
668
|
+
let no_data_msg = `ALKiln Internal error: normalizeTable called w/o var_data or raw_var_data:` +
|
|
669
|
+
` open an issue at https://github.com/SuffolkLITLab/ALKiln/issues/new if you see this!`;
|
|
670
|
+
await scope.addToReport( scope, { type: `error`, value: no_data_msg });
|
|
671
|
+
throw Error(no_data_msg);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Always use the raw var data if present; it has the full first row
|
|
675
|
+
if (raw_var_data) {
|
|
676
|
+
var_data = raw_var_data.hashes();
|
|
677
|
+
if (!('var' in var_data[0]) || !('value' in var_data[0])) {
|
|
678
|
+
let raw_array = raw_var_data.raw();
|
|
679
|
+
var_data = [];
|
|
680
|
+
for (let row_raw of raw_array) {
|
|
681
|
+
if (row_raw.length < 2) {
|
|
682
|
+
let bad_table_msg = `Your Story Table definition needs to be changed. The best practice`
|
|
683
|
+
+ ` is to make your table 3 columns wide with a first row in this format: "| var | value | trigger |".`
|
|
684
|
+
+ ` The first row of your table is actually "${ raw_array[0] }"`;
|
|
685
|
+
await scope.addToReport( scope, { type: `error`, value: bad_table_msg})
|
|
686
|
+
throw Error(bad_table_msg);
|
|
687
|
+
} else if (row_raw.length == 2) {
|
|
688
|
+
var_data.push({var: row_raw[0], value: row_raw[1]});
|
|
689
|
+
} else {
|
|
690
|
+
var_data.push({var: row_raw[0], value: row_raw[1], trigger: row_raw[2]});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
601
695
|
|
|
602
696
|
// Support tables with no 'trigger' column
|
|
603
697
|
let supported_table = [];
|
|
@@ -932,6 +1026,9 @@ module.exports = {
|
|
|
932
1026
|
// names.var_names.push( layer_2_decoded );
|
|
933
1027
|
}
|
|
934
1028
|
|
|
1029
|
+
// Detecting valid variable names/characters
|
|
1030
|
+
// https://stackoverflow.com/a/23377268
|
|
1031
|
+
|
|
935
1032
|
return var_names;
|
|
936
1033
|
}, // Ends scope.getPossibleVarNames()
|
|
937
1034
|
|
|
@@ -1198,13 +1295,7 @@ module.exports = {
|
|
|
1198
1295
|
await scope.page.waitForSelector('#dasigcanvas');
|
|
1199
1296
|
}
|
|
1200
1297
|
|
|
1201
|
-
let handle = await scope.
|
|
1202
|
-
let elem = document.querySelector( selector );
|
|
1203
|
-
// `elem` has never been null
|
|
1204
|
-
if ( elem.nextSibling && elem.nextSibling.tag === `label` ) {
|
|
1205
|
-
return elem.nextSibling;
|
|
1206
|
-
} else { return elem; }
|
|
1207
|
-
}, field.selector, field );
|
|
1298
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
1208
1299
|
|
|
1209
1300
|
let disabled = await handle.evaluate(( elem )=> {
|
|
1210
1301
|
// da's header covers the top of the section containing the fields which means
|
|
@@ -1225,15 +1316,39 @@ module.exports = {
|
|
|
1225
1316
|
await scope.page.waitForTimeout( 400 );
|
|
1226
1317
|
}
|
|
1227
1318
|
|
|
1228
|
-
let { tag, type } = field;
|
|
1229
1319
|
// We use the last match. We wrote a warning for the user if there were
|
|
1230
1320
|
// multiple matches in the story that the last value will be used. In future,
|
|
1231
1321
|
// Steps will be able to set this data gradually and we don't want the developer
|
|
1232
1322
|
// to get confused thinking that an earlier Step was used instead of a later one.
|
|
1233
1323
|
let row = matching_input_rows[ matching_input_rows.length - 1 ];
|
|
1234
|
-
let
|
|
1324
|
+
let answer = row.value;
|
|
1325
|
+
|
|
1326
|
+
await scope.funnel_the_answer( scope, { handle, answer, field });
|
|
1327
|
+
|
|
1328
|
+
return row;
|
|
1329
|
+
}, // Ends scope.setVariable()
|
|
1330
|
+
|
|
1331
|
+
get_handle_from_field: async function ( scope, { field }) {
|
|
1332
|
+
/** Get the element's puppeteer handle using the field's selector. */
|
|
1333
|
+
|
|
1334
|
+
let handle = await scope.page.evaluateHandle(( selector ) => {
|
|
1335
|
+
let elem = document.querySelector( selector );
|
|
1336
|
+
// If the element has a sibling that's a label, we need to
|
|
1337
|
+
// interact with that label instead.
|
|
1338
|
+
// `elem` has never been null from what we've seen
|
|
1339
|
+
if ( elem.nextSibling && elem.nextSibling.tag === `label` ) {
|
|
1340
|
+
return elem.nextSibling;
|
|
1341
|
+
} else { return elem; }
|
|
1342
|
+
// Pass in the selector as an argument
|
|
1343
|
+
}, field.selector );
|
|
1344
|
+
|
|
1345
|
+
return handle;
|
|
1346
|
+
}, // Ends scope.get_handle_from_field()
|
|
1347
|
+
|
|
1348
|
+
get_custom_datatype: async function( scope, { handle }) {
|
|
1349
|
+
/** If this is part of a custom datatype, returns an element,
|
|
1350
|
+
* otherwise returns null. */
|
|
1235
1351
|
|
|
1236
|
-
// Detect a custom type and get the element of that custom type if it exists
|
|
1237
1352
|
let custom_datatype = await handle.evaluate(( elem )=> {
|
|
1238
1353
|
// If the element is in a form group**, then extract the form group's
|
|
1239
1354
|
// datatype value.
|
|
@@ -1252,28 +1367,52 @@ module.exports = {
|
|
|
1252
1367
|
return custom_datatype;
|
|
1253
1368
|
});
|
|
1254
1369
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1370
|
+
return custom_datatype;
|
|
1371
|
+
}, // Ends scope.is_custom_datatype()
|
|
1372
|
+
|
|
1373
|
+
funnel_the_answer: async function( scope, { handle, field, answer }) {
|
|
1374
|
+
/** Use the tag or custom datatype to funnel the answer to the right
|
|
1375
|
+
* field. Pages without a handle should pass in `null` for the
|
|
1376
|
+
* handle.
|
|
1377
|
+
*/
|
|
1378
|
+
|
|
1379
|
+
let custom_datatype = null;
|
|
1380
|
+
// Pages with elements like `canvas` for signatures won't have a handle
|
|
1381
|
+
// to pass in. They should pass in `null` for the handle and shouldn't
|
|
1382
|
+
// need a handle in order to function.
|
|
1383
|
+
if ( handle !== null ) {
|
|
1384
|
+
// Get the element of that custom type if it exists
|
|
1385
|
+
custom_datatype = await scope.get_custom_datatype( scope, { handle });
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// If there is a custom datatype function available, use it to set the field
|
|
1389
|
+
if ( scope.setCustomDatatype[ custom_datatype ] !== undefined ) {
|
|
1260
1390
|
// Set da package custom fields. TODO: find `custom_handle` in the setter?
|
|
1261
1391
|
let custom_handle = await handle.evaluateHandle(( elem )=> { return elem.closest( `.da-form-group` ) });
|
|
1262
|
-
await scope.setCustomDatatype[ custom_datatype ]( scope, { handle: custom_handle,
|
|
1392
|
+
await scope.setCustomDatatype[ custom_datatype ]( scope, { handle: custom_handle, answer });
|
|
1393
|
+
} else {
|
|
1394
|
+
// If no custom datatype, set a regular field
|
|
1395
|
+
await scope.enter_answer[ field.tag ]( scope, { handle, answer });
|
|
1263
1396
|
}
|
|
1264
1397
|
|
|
1265
|
-
|
|
1266
|
-
}, // Ends scope.setVariable()
|
|
1398
|
+
}, // Ends scope.funnel_the_answer()
|
|
1267
1399
|
|
|
1268
1400
|
// Handle setting values for da custom datatypes. E.g. `da-field-container-datatype-BirthDate`
|
|
1269
1401
|
// TODO: Make this more easily extensible.
|
|
1270
|
-
// TODO: Just put this in `scope.
|
|
1402
|
+
// TODO: Just put this in `scope.enter_answer`? Find the custom handle in the setter itself.
|
|
1271
1403
|
setCustomDatatype: {
|
|
1272
|
-
|
|
1404
|
+
al_date: async function ( scope, { handle, answer, is_past }) {
|
|
1273
1405
|
/* Given a date of the format 'mm/dd/yyyy' and the handle of the
|
|
1274
|
-
* custom field container, sets
|
|
1406
|
+
* custom field container, sets an AssemblyLine three-part date's
|
|
1275
1407
|
* field values. */
|
|
1276
|
-
|
|
1408
|
+
|
|
1409
|
+
// If it doesn't have the right format
|
|
1410
|
+
if ( answer === null || !answer.includes(`/`) ) {
|
|
1411
|
+
// Do nothing
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
let date_parts = answer.split(`/`);
|
|
1277
1416
|
let fields = await handle.$$(`select, input:not([type="hidden"])`);
|
|
1278
1417
|
for ( let field_i = 0; field_i < fields.length; field_i++ ) {
|
|
1279
1418
|
let field = fields[ field_i ];
|
|
@@ -1286,55 +1425,76 @@ module.exports = {
|
|
|
1286
1425
|
// Set the month
|
|
1287
1426
|
await field.select( date_parts[ 0 ] );
|
|
1288
1427
|
} else {
|
|
1289
|
-
// Set the day
|
|
1428
|
+
// Set the day or year
|
|
1290
1429
|
await scope.setText( scope, {
|
|
1291
1430
|
handle: field,
|
|
1292
|
-
|
|
1431
|
+
answer: date_parts[ field_i ]
|
|
1293
1432
|
});
|
|
1294
1433
|
}
|
|
1295
1434
|
|
|
1296
1435
|
} // For each field
|
|
1297
1436
|
|
|
1298
1437
|
await scope.afterStep( scope );
|
|
1299
|
-
}, // Ends
|
|
1438
|
+
}, // Ends scope.setCustomDatatype.al_date()
|
|
1439
|
+
|
|
1440
|
+
BirthDate: async function ( scope, { handle, answer }) {
|
|
1441
|
+
/** Given a date of the format 'mm/dd/yyyy' and the handle of the
|
|
1442
|
+
* custom field container, sets the ALToolbox custom birthdate
|
|
1443
|
+
* field values. */
|
|
1300
1444
|
|
|
1445
|
+
await scope.setCustomDatatype.al_date( scope, { handle, answer })
|
|
1446
|
+
}, // Ends scope.setCustomDatatype.BirthDate()
|
|
1447
|
+
|
|
1448
|
+
ThreePartsDate: async function ( scope, { handle, answer }) {
|
|
1449
|
+
/** Given a date of the format 'mm/dd/yyyy' and the handle of the
|
|
1450
|
+
* custom field container, sets the ALToolbox custom ThreePartsDate
|
|
1451
|
+
* field values. */
|
|
1452
|
+
await scope.setCustomDatatype.al_date( scope, { handle, answer })
|
|
1453
|
+
}, // Ends scope.setCustomDatatype.ThreePartsDate()
|
|
1301
1454
|
|
|
1302
1455
|
}, // ends setCustomDatatype {}
|
|
1303
1456
|
|
|
1304
|
-
|
|
1305
|
-
button: async function ( scope, { handle,
|
|
1306
|
-
textarea: async function ( scope, { handle,
|
|
1307
|
-
await scope.setText( scope, { handle,
|
|
1457
|
+
enter_answer: {
|
|
1458
|
+
button: async function ( scope, { handle, answer }) { await scope.tapElement( scope, handle ); },
|
|
1459
|
+
textarea: async function ( scope, { handle, answer }) {
|
|
1460
|
+
await scope.setText( scope, { handle, answer });
|
|
1308
1461
|
await scope.afterStep(scope); // No showifs for this one?
|
|
1309
1462
|
},
|
|
1310
|
-
select: async function ( scope, { handle,
|
|
1463
|
+
select: async function ( scope, { handle, answer }) {
|
|
1464
|
+
// Note: I don't remember what's really going on in here.
|
|
1465
|
+
// I will note that values of `select` elements are strings,
|
|
1466
|
+
// not variables, so we don't need to check for variable names.
|
|
1467
|
+
|
|
1311
1468
|
// A dropdown's option value can be one of two things
|
|
1312
1469
|
// Try to find the element using the first value
|
|
1313
1470
|
|
|
1314
|
-
// TODO: Can we somehow pass in a `
|
|
1471
|
+
// TODO: Can we somehow pass in a `answer` that is encoded correctly since
|
|
1315
1472
|
// we're getting the fields from the DOM to begin with? We'll probably
|
|
1316
1473
|
// need another prop because we need the `option` human value to
|
|
1317
1474
|
// match the story table
|
|
1318
1475
|
|
|
1319
1476
|
// TODO: Change this to search in the actual handle instead of just $
|
|
1320
|
-
|
|
1477
|
+
// There could be multiple fields on the page with the same option
|
|
1478
|
+
// values.
|
|
1479
|
+
// let option = await handle.$(`option[value="${ answer }"]`);
|
|
1480
|
+
let option = await scope.page.$(`option[value="${ answer }"]`);
|
|
1321
1481
|
if ( option ) {
|
|
1322
|
-
await handle.select(
|
|
1482
|
+
await handle.select( answer ); // And use that value to set it
|
|
1323
1483
|
|
|
1324
1484
|
// If that literal value isn't on the page, it should be a base64 encoded value
|
|
1325
|
-
// TODO: Can these be double encoded?
|
|
1485
|
+
// TODO: Can these be double encoded? Can they be encoded like objects?
|
|
1326
1486
|
} else {
|
|
1327
|
-
let base64_name = await scope.toBase64( scope, { utf8_str:
|
|
1487
|
+
let base64_name = await scope.toBase64( scope, { utf8_str: answer });
|
|
1328
1488
|
await handle.select( base64_name );
|
|
1329
1489
|
}
|
|
1330
1490
|
|
|
1331
1491
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1332
1492
|
},
|
|
1333
|
-
canvas: async function ( scope, { handle,
|
|
1493
|
+
canvas: async function ( scope, { handle, answer }) {
|
|
1334
1494
|
await scope.sign( scope );
|
|
1335
1495
|
await scope.afterStep( scope );
|
|
1336
1496
|
},
|
|
1337
|
-
input: async function ( scope, { handle,
|
|
1497
|
+
input: async function ( scope, { handle, answer }) {
|
|
1338
1498
|
/* Set value of some `input` element to the given value. */
|
|
1339
1499
|
|
|
1340
1500
|
let type = await handle.evaluate( elem => { return elem.getAttribute('type'); });
|
|
@@ -1343,50 +1503,60 @@ module.exports = {
|
|
|
1343
1503
|
let [label] = await handle.$x(`following-sibling::label`);
|
|
1344
1504
|
|
|
1345
1505
|
if ( session_vars.get_debug() ) {
|
|
1346
|
-
if (
|
|
1506
|
+
if ( answer.toLowerCase() === 'false' ) { await label.evaluate( elem => { elem.style.background = 'tomato'; }); }
|
|
1347
1507
|
else { await label.evaluate( elem => { elem.style.background = 'teal'; }); }
|
|
1348
1508
|
let name = await label.evaluate( elem => { return elem.getAttribute('for'); });
|
|
1349
1509
|
}
|
|
1350
1510
|
|
|
1351
1511
|
if ( type === `radio` ) { await label.evaluate( elem => { return elem.click(); }); }
|
|
1352
|
-
else { await scope.setCheckbox( scope, { label,
|
|
1512
|
+
else { await scope.setCheckbox( scope, { label, answer }); }
|
|
1353
1513
|
|
|
1354
1514
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1355
1515
|
|
|
1356
1516
|
} else if ( type === `file` ) {
|
|
1357
|
-
await scope.uploadFiles( scope, { handle,
|
|
1517
|
+
await scope.uploadFiles( scope, { handle, answer });
|
|
1358
1518
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1359
1519
|
|
|
1360
1520
|
} else if (type == `date`) {
|
|
1361
|
-
let
|
|
1521
|
+
let answer_date = answer;
|
|
1362
1522
|
// If the date is specified in the format "today" or "today + 2" or "today-1"
|
|
1363
1523
|
// it will need some pre-processing.
|
|
1364
|
-
if (
|
|
1524
|
+
if (answer.indexOf(`today`) !== -1) {
|
|
1365
1525
|
// Find out how many days to add/subtract
|
|
1366
|
-
let date_delta =
|
|
1526
|
+
let date_delta = answer.replace(/\s/g, '');
|
|
1367
1527
|
date_delta = date_delta.match(/today([+-]\d+)?/)[1];
|
|
1368
1528
|
date_delta = date_delta == undefined ? 0 : date_delta;
|
|
1369
1529
|
date_delta = parseInt(date_delta);
|
|
1370
1530
|
// Get today's date
|
|
1371
|
-
|
|
1531
|
+
answer_date = new Date();
|
|
1372
1532
|
// Add/subtract required number of days
|
|
1373
|
-
|
|
1533
|
+
answer_date.setDate(answer_date.getDate() + date_delta);
|
|
1374
1534
|
// Convert to the docassemble date field format: mm/dd/yyyy
|
|
1375
1535
|
// Reference: https://gomakethings.com/setting-a-date-input-to-todays-date-with-vanilla-js/
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1536
|
+
answer_date = ((answer_date.getMonth() + 1).toString().padStart(2, 0) + '/' +
|
|
1537
|
+
answer_date.getDate().toString().padStart(2, 0) + '/' +
|
|
1538
|
+
answer_date.getFullYear().toString());
|
|
1379
1539
|
}
|
|
1380
|
-
await scope.setText( scope, { handle,
|
|
1540
|
+
await scope.setText( scope, { handle, answer: answer_date });
|
|
1381
1541
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1382
1542
|
} else {
|
|
1383
|
-
await scope.setText( scope, { handle,
|
|
1543
|
+
await scope.setText( scope, { handle, answer });
|
|
1384
1544
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1385
1545
|
}
|
|
1386
|
-
}, // Ends scope.
|
|
1387
|
-
|
|
1546
|
+
}, // Ends scope.enter_answer.input()
|
|
1547
|
+
hidden: async function ( scope, { handle, answer }) {
|
|
1548
|
+
// Do nothing. The only way the code should get here is if
|
|
1549
|
+
// this wasn't a custom datatype we can handle.
|
|
1550
|
+
// If we were to write a message, I don't know what we can
|
|
1551
|
+
// write - this may be a different custom datatype or this
|
|
1552
|
+
// could be a special da field that we shouldn't worry the
|
|
1553
|
+
// dev about at all.
|
|
1554
|
+
return;
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
}, // ends scope.enter_answer
|
|
1388
1558
|
|
|
1389
|
-
uploadFiles: async function ( scope, { handle,
|
|
1559
|
+
uploadFiles: async function ( scope, { handle, answer }) {
|
|
1390
1560
|
/* Try to upload one or more files to a file-type input field. */
|
|
1391
1561
|
|
|
1392
1562
|
// Tests could be in one of two places. Figure out which directory they're in
|
|
@@ -1419,7 +1589,7 @@ module.exports = {
|
|
|
1419
1589
|
}
|
|
1420
1590
|
|
|
1421
1591
|
// Allow a typo where someone forgot to include a space after a comma
|
|
1422
|
-
let file_names =
|
|
1592
|
+
let file_names = answer.split(`,`);
|
|
1423
1593
|
|
|
1424
1594
|
// If a file exist, add it to a list of files to be uploaded. If it doesn't, add a warning.
|
|
1425
1595
|
let paths = [];
|
|
@@ -1441,8 +1611,8 @@ module.exports = {
|
|
|
1441
1611
|
}
|
|
1442
1612
|
|
|
1443
1613
|
if ( paths.length === 0 ) {
|
|
1444
|
-
await scope.addToReport( scope, { type: `error`, value: `Could not find "${
|
|
1445
|
-
throw ReferenceError(`Could not find "${
|
|
1614
|
+
await scope.addToReport( scope, { type: `error`, value: `Could not find "${answer}".` });
|
|
1615
|
+
throw ReferenceError(`Could not find "${answer}".`)
|
|
1446
1616
|
}
|
|
1447
1617
|
// Prepare to wait for upload to complete
|
|
1448
1618
|
let name = await handle.evaluate( elem => { return elem.getAttribute(`name`); });
|
|
@@ -1467,20 +1637,20 @@ module.exports = {
|
|
|
1467
1637
|
|
|
1468
1638
|
}, // Ends scope.uploadFiles()
|
|
1469
1639
|
|
|
1470
|
-
setCheckbox: async function ( scope, { label,
|
|
1640
|
+
setCheckbox: async function ( scope, { label, answer }) {
|
|
1471
1641
|
// Depending on the current value/status of a checkbox and the desired
|
|
1472
1642
|
// value to set, either taps the checkbox or leaves it alone.
|
|
1473
1643
|
let status = await label.evaluate( elem => { return elem.getAttribute('aria-checked'); });
|
|
1474
|
-
if (
|
|
1644
|
+
if ( answer.toLowerCase() !== status ) {
|
|
1475
1645
|
await label.evaluate( elem => { return elem.click(); });
|
|
1476
1646
|
}
|
|
1477
1647
|
}, // Ends scope.setCheckbox()
|
|
1478
1648
|
|
|
1479
|
-
setText: async function ( scope, { handle,
|
|
1649
|
+
setText: async function ( scope, { handle, answer }) {
|
|
1480
1650
|
// Set text in some kind of field (input text, input date, textarea, etc.)
|
|
1481
1651
|
await handle.evaluate( el => { el.value = '' });
|
|
1482
1652
|
await handle.focus();
|
|
1483
|
-
await handle.type(
|
|
1653
|
+
await handle.type( answer );
|
|
1484
1654
|
setTimeout(async () => {
|
|
1485
1655
|
}, 500)
|
|
1486
1656
|
},
|
|
@@ -1527,7 +1697,7 @@ module.exports = {
|
|
|
1527
1697
|
}, // Ends scope.examinePageID()
|
|
1528
1698
|
|
|
1529
1699
|
// TODO: Rename var_data to input_data?
|
|
1530
|
-
// Note: Totally different from `
|
|
1700
|
+
// Note: Totally different from `enter_answer` (which is singular)
|
|
1531
1701
|
// TODO: If a step triggers this and is not able to set a value, should it error?
|
|
1532
1702
|
// TODO: Rename to setFieldsOfOnePage? setPageFields?
|
|
1533
1703
|
setFields: async function ( scope, {
|
|
@@ -1656,7 +1826,8 @@ module.exports = {
|
|
|
1656
1826
|
}
|
|
1657
1827
|
}
|
|
1658
1828
|
if ( num_unused_rows > 0 ) {
|
|
1659
|
-
let
|
|
1829
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
1830
|
+
let msg = `Missing variable or variables on page "${id}"`;
|
|
1660
1831
|
await scope.addToReport(scope, { type: `error`, value: msg });
|
|
1661
1832
|
expect( num_unused_rows, msg ).to.equal( 0 );
|
|
1662
1833
|
}
|
|
@@ -1802,6 +1973,20 @@ module.exports = {
|
|
|
1802
1973
|
}
|
|
1803
1974
|
}, // Ends scope.getThereIsAnotherValue()
|
|
1804
1975
|
|
|
1976
|
+
// throw_if_no_interview_content()?
|
|
1977
|
+
has_interview_content: async function ( scope ) {
|
|
1978
|
+
/** Return false if ther's no element indicating a da interview question,
|
|
1979
|
+
* like a signature page, other fields, or interview text. Assumes that page has already
|
|
1980
|
+
* finished navigating. */
|
|
1981
|
+
let elem_da_question = await scope.page.$( `#daMainQuestion` );
|
|
1982
|
+
let elem_da_signature = await scope.page.$( `#dasigpage` );
|
|
1983
|
+
if ( elem_da_question === null && elem_da_signature === null ) {
|
|
1984
|
+
return false;
|
|
1985
|
+
} else {
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1988
|
+
}, // Ends scope.has_interview_content()
|
|
1989
|
+
|
|
1805
1990
|
waitUntilContinued: async function ( scope, { url = '', id = '' }) {
|
|
1806
1991
|
/** Given a url, attempt to continue until the page url changes or an error
|
|
1807
1992
|
is found. Return the error object no matter what. */
|
|
@@ -1885,12 +2070,18 @@ module.exports = {
|
|
|
1885
2070
|
/* Download and save a screenshot. `name` does not have to be
|
|
1886
2071
|
* unique as the filename will be made unique. */
|
|
1887
2072
|
name = name || '';
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
);
|
|
2073
|
+
|
|
2074
|
+
let date = files.readable_date();
|
|
2075
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
2076
|
+
// Include the custom name if it has one
|
|
2077
|
+
if ( name.length > 0 ) {
|
|
2078
|
+
name = `screenshot-${ name }-on-${ id }-${ date }`;
|
|
2079
|
+
} else {
|
|
2080
|
+
name = `screenshot_on-${ id }-${ date }`;
|
|
2081
|
+
}
|
|
1891
2082
|
|
|
1892
2083
|
await scope.page.screenshot({
|
|
1893
|
-
path: `./${
|
|
2084
|
+
path: `./${ scope.paths.scenario }/${ name }.jpg`,
|
|
1894
2085
|
type: 'jpeg',
|
|
1895
2086
|
fullPage: true
|
|
1896
2087
|
});
|
|
@@ -1930,7 +2121,7 @@ module.exports = {
|
|
|
1930
2121
|
await scope.afterStep(scope);
|
|
1931
2122
|
}, // Ends link_works()
|
|
1932
2123
|
|
|
1933
|
-
set_regular_var: async ( scope, var_name,
|
|
2124
|
+
set_regular_var: async ( scope, var_name, answer ) => {
|
|
1934
2125
|
/** Set a non-story table variable (with or without a choice associated with it) to a value. Set:
|
|
1935
2126
|
* - Buttons associated with variables
|
|
1936
2127
|
* - Dropdowns
|
|
@@ -1941,23 +2132,23 @@ module.exports = {
|
|
|
1941
2132
|
*/
|
|
1942
2133
|
// Let kiln know not to test against trigger variable values, even for proxy vars
|
|
1943
2134
|
let var_data = await scope.normalizeTable( scope, {
|
|
1944
|
-
var_data: [{ var: var_name, value:
|
|
2135
|
+
var_data: [{ var: var_name, value: answer, trigger: scope.trigger_not_needed_flag }],
|
|
1945
2136
|
});
|
|
1946
2137
|
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
1947
2138
|
// if this variable cannot be set on this page, trigger an error
|
|
1948
2139
|
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
1949
2140
|
}, // Ends set_regular_var()
|
|
1950
2141
|
|
|
1951
|
-
set_secret_var: async ( scope, var_name,
|
|
2142
|
+
set_secret_var: async ( scope, var_name, answer_env_name ) => {
|
|
1952
2143
|
/** Sets a non-story table variable to a "secret" value (i.e. an env var) */
|
|
1953
2144
|
// Prevent pictures of a screen with a secret.
|
|
1954
2145
|
scope.disable_error_screenshot = true;
|
|
1955
2146
|
let var_data = await scope.normalizeTable( scope, {
|
|
1956
2147
|
var_data: [{
|
|
1957
2148
|
var: var_name,
|
|
1958
|
-
value:
|
|
2149
|
+
value: answer_env_name,
|
|
1959
2150
|
trigger: scope.trigger_not_needed_flag,
|
|
1960
|
-
flags: {secret_name:
|
|
2151
|
+
flags: {secret_name: answer_env_name}
|
|
1961
2152
|
}],
|
|
1962
2153
|
});
|
|
1963
2154
|
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
@@ -1990,6 +2181,9 @@ module.exports = {
|
|
|
1990
2181
|
* WARNING: Cannot download the same file twice in a single scenario.
|
|
1991
2182
|
* WARNING: Must not have any characters that are totally unique to that
|
|
1992
2183
|
* page load. It's usually just the name of the file.
|
|
2184
|
+
*
|
|
2185
|
+
* TODO: Properly wait for download to complete. See notes in
|
|
2186
|
+
* scope.js scope.detectDownloadComplete()
|
|
1993
2187
|
*/
|
|
1994
2188
|
let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
|
|
1995
2189
|
|
|
@@ -2126,10 +2320,234 @@ module.exports = {
|
|
|
2126
2320
|
});
|
|
2127
2321
|
|
|
2128
2322
|
await scope.afterStep(scope, {waitForShowIf: false});
|
|
2129
|
-
}, // Ends sign_in
|
|
2323
|
+
}, // Ends scope.steps.sign_in()
|
|
2324
|
+
|
|
2325
|
+
set_random_page_vars: async ( scope ) => {
|
|
2326
|
+
/** Answer inputs randomly. */
|
|
2327
|
+
|
|
2328
|
+
let html = await scope.page.content();
|
|
2329
|
+
// These are the fields on the current page. Their handles should all exist.
|
|
2330
|
+
let fields = await scope.getAllFields( scope, { html: html });
|
|
2331
|
+
let buttons = [];
|
|
2332
|
+
|
|
2333
|
+
// For every field on the page
|
|
2334
|
+
for ( let field of fields ) {
|
|
2335
|
+
// fields[n].tag is HTML tag of node, as a string
|
|
2336
|
+
|
|
2337
|
+
// If the field is a button, save it in a list of buttons that we'll deal with later
|
|
2338
|
+
if ( field.tag === `button` ) {
|
|
2339
|
+
buttons.push( field );
|
|
2340
|
+
} else {
|
|
2341
|
+
// Try to give a random answer for that element tag
|
|
2342
|
+
await scope.set_random_input_for[ field.tag ]( scope, { field: field });
|
|
2343
|
+
process.stdout.write(`\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
} // End for all page fields
|
|
2347
|
+
|
|
2348
|
+
return buttons;
|
|
2349
|
+
|
|
2350
|
+
}, // Ends scope.steps.set_random_page_vars()
|
|
2130
2351
|
|
|
2131
2352
|
}, // ends scope.steps
|
|
2132
2353
|
|
|
2354
|
+
set_random_input_for: {
|
|
2355
|
+
a: async function (scope, { fields }) {
|
|
2356
|
+
// We don't do anything with links for random input tests
|
|
2357
|
+
},
|
|
2358
|
+
buttons: async function (scope, { fields }) {
|
|
2359
|
+
// Get a random button
|
|
2360
|
+
let field = faker.helpers.arrayElement( fields );
|
|
2361
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2362
|
+
// Send it to be tapped
|
|
2363
|
+
await scope.funnel_the_answer( scope, { handle, field, answer: `doesn't matter` });
|
|
2364
|
+
},
|
|
2365
|
+
textarea: async function (scope, { field }) {
|
|
2366
|
+
// Get random paragraph of text
|
|
2367
|
+
let answer = faker.lorem.paragraph();
|
|
2368
|
+
// Get the element's puppeteer handler
|
|
2369
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2370
|
+
// Type the random text into the textarea
|
|
2371
|
+
await scope.funnel_the_answer( scope, { handle, field, answer: answer });
|
|
2372
|
+
},
|
|
2373
|
+
select: async function (scope, { field }) {
|
|
2374
|
+
// Get the values of all the options of the field
|
|
2375
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2376
|
+
// Get the values of all the choices
|
|
2377
|
+
let answer_choices = await handle.evaluate(function ( elem ) {
|
|
2378
|
+
let values = Array.from(elem.querySelectorAll(`option`)).map(element=>element.value);
|
|
2379
|
+
return values;
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
// Remove the "no selection" option from the list ('')
|
|
2383
|
+
// TODO: blue sky - for optional fields allow no selection
|
|
2384
|
+
if ( answer_choices[0] === `` ) {
|
|
2385
|
+
answer_choices.shift();
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Select randomly between them
|
|
2389
|
+
let answer = faker.helpers.arrayElement( answer_choices );
|
|
2390
|
+
|
|
2391
|
+
// Set that value
|
|
2392
|
+
await scope.funnel_the_answer( scope, { handle, field, answer: answer });
|
|
2393
|
+
|
|
2394
|
+
},
|
|
2395
|
+
canvas: async function (scope, { field }) {
|
|
2396
|
+
// WARNING: Do not test this in headless mode.
|
|
2397
|
+
// If we do this in headless mode, the test will fail. It signs, but
|
|
2398
|
+
// can't continue. Not sure why.
|
|
2399
|
+
await scope.funnel_the_answer( scope, { field, handle: null, answer: `doesn't matter` });
|
|
2400
|
+
},
|
|
2401
|
+
input: async function (scope, { field }) {
|
|
2402
|
+
/** Handles various `type`s of input element fields, except:
|
|
2403
|
+
*
|
|
2404
|
+
* Does not handle "file" type
|
|
2405
|
+
* Does not handle custom data values yet, like BirthDate, because they have
|
|
2406
|
+
* `type="hidden"` which also matches other special da fields on the
|
|
2407
|
+
* page that we SHOULDN'T interact with.
|
|
2408
|
+
* Does not handle range, which is not a `type`, but it is an input element
|
|
2409
|
+
*/
|
|
2410
|
+
|
|
2411
|
+
// Use the type instead of the tag to get get the answer for
|
|
2412
|
+
// an input field
|
|
2413
|
+
let type = field.type;
|
|
2414
|
+
|
|
2415
|
+
// Custom datatype question: Do all custom datatypes hide their
|
|
2416
|
+
// original field? Probably not...
|
|
2417
|
+
// TODO: Move custom datatype checking to the caller of this
|
|
2418
|
+
// function somehow - in future, not all custom datatypes will
|
|
2419
|
+
// be inputs _and_ this won't handle, for example, a `select` field
|
|
2420
|
+
// that is part of a custom datatype with a hidden field - this
|
|
2421
|
+
// doesn't take care of that situation.
|
|
2422
|
+
|
|
2423
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2424
|
+
|
|
2425
|
+
// If this is a custom datatype
|
|
2426
|
+
let custom_datatype = await scope.get_custom_datatype( scope, { handle });
|
|
2427
|
+
let is_custom_datatype = scope.get_random_input_for[ custom_datatype ] !== undefined;
|
|
2428
|
+
if ( is_custom_datatype ) {
|
|
2429
|
+
|
|
2430
|
+
// And the custom datatype has a hidden field (which will be the original field)
|
|
2431
|
+
let hidden_field = await scope.get_custom_datatype_hidden_field( scope, { handle });
|
|
2432
|
+
if ( hidden_field !== null ) {
|
|
2433
|
+
// only set the value once - just for the original field
|
|
2434
|
+
if ( type === `hidden` ) {
|
|
2435
|
+
|
|
2436
|
+
// set the value
|
|
2437
|
+
let answer = await scope.get_random_input_for[ custom_datatype ]( scope );
|
|
2438
|
+
await scope.funnel_the_answer( scope, { handle, field, answer });
|
|
2439
|
+
|
|
2440
|
+
}
|
|
2441
|
+
// Otherwise ignore the non-original field
|
|
2442
|
+
|
|
2443
|
+
// Untested: If no hidden field exists, just try to set the custom
|
|
2444
|
+
// datatype field
|
|
2445
|
+
} else {
|
|
2446
|
+
// set the value
|
|
2447
|
+
let answer = await scope.get_random_input_for[ custom_datatype ]( scope );
|
|
2448
|
+
await scope.funnel_the_answer( scope, { handle, field, answer });
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// If this is not a custom datatype
|
|
2452
|
+
} else {
|
|
2453
|
+
|
|
2454
|
+
// `file` type inputs come next to an input with a type of ''.
|
|
2455
|
+
// Maybe others do too.
|
|
2456
|
+
if ( type !== `` && type !== `hidden` ) {
|
|
2457
|
+
// Field types that we can't yet handle, but plan to in future
|
|
2458
|
+
if ( type === `file` ) {
|
|
2459
|
+
await scope.addToReport( scope, {
|
|
2460
|
+
type: `warning`,
|
|
2461
|
+
value: `Sorry, ALKiln's random input tests cannot yet handle "${ type }" fields.`
|
|
2462
|
+
});
|
|
2463
|
+
} else {
|
|
2464
|
+
let answer = await scope.get_random_input_for[ type ]( scope );
|
|
2465
|
+
await scope.funnel_the_answer( scope, { handle, field, answer });
|
|
2466
|
+
} // ends if field type is one we cover
|
|
2467
|
+
} // ends if field type exists
|
|
2468
|
+
|
|
2469
|
+
} // ends if is custom datatype
|
|
2470
|
+
|
|
2471
|
+
}, // ends scope.set_random_input_for.input
|
|
2472
|
+
}, // ends scope.set_random_input_for
|
|
2473
|
+
|
|
2474
|
+
get_custom_datatype_hidden_field: async function (scope, { handle }) {
|
|
2475
|
+
/** If there's a hidden field in a custom datatype, returns that
|
|
2476
|
+
* DOM node, otherwise returns null. */
|
|
2477
|
+
|
|
2478
|
+
let hidden_field = await handle.evaluate(( elem )=> {
|
|
2479
|
+
// If the element is in a form group**, then extract the form group's
|
|
2480
|
+
// datatype value.
|
|
2481
|
+
// ** For now the only custom datatype we have is in a form group. In
|
|
2482
|
+
// future we should look into what happens with a `fieldset` for
|
|
2483
|
+
// `choices:`, etc.
|
|
2484
|
+
let custom_datatype = null, matches = null;
|
|
2485
|
+
let group = elem.closest( `.da-form-group` );
|
|
2486
|
+
// Returns an element or null
|
|
2487
|
+
let hidden_child = group.querySelector( `[type="hidden"]` );
|
|
2488
|
+
|
|
2489
|
+
return hidden_child;
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
return hidden_field;
|
|
2493
|
+
}, // Ends scope.get_custom_datatype_hidden_field()
|
|
2494
|
+
|
|
2495
|
+
get_random_input_for: {
|
|
2496
|
+
checkbox: async function ( scope ) {
|
|
2497
|
+
return `${ Math.random() > .5 }`;
|
|
2498
|
+
},
|
|
2499
|
+
radio: async function ( scope ) {
|
|
2500
|
+
return `${ Math.random() > .5 }`;
|
|
2501
|
+
},
|
|
2502
|
+
text: async function ( scope ) {
|
|
2503
|
+
return faker.random.word();
|
|
2504
|
+
},
|
|
2505
|
+
number: async function ( scope ) {
|
|
2506
|
+
let answer = faker.helpers.arrayElement( [`1`, `2`, `3`], 1 );
|
|
2507
|
+
return answer;
|
|
2508
|
+
},
|
|
2509
|
+
date: async function ( scope ) {
|
|
2510
|
+
let past = faker.date.past(25);
|
|
2511
|
+
let future = faker.date.future(1);
|
|
2512
|
+
|
|
2513
|
+
let date = null;
|
|
2514
|
+
if ( Math.random() > .5 ) {
|
|
2515
|
+
date = past;
|
|
2516
|
+
} else {
|
|
2517
|
+
date = future;
|
|
2518
|
+
}
|
|
2519
|
+
// Ensure 2-digit number for month and day
|
|
2520
|
+
let month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
2521
|
+
let day = ("0" + (date.getDay() + 1)).slice(-2);
|
|
2522
|
+
// Format the string in the way ALKiln can read it
|
|
2523
|
+
return `${ month }/${ day }/${ date.getFullYear() }`;
|
|
2524
|
+
},
|
|
2525
|
+
currency: async function ( scope ) {
|
|
2526
|
+
return `${ Math.random() * 100 }`;
|
|
2527
|
+
},
|
|
2528
|
+
email: async function ( scope ) {
|
|
2529
|
+
return faker.internet.email( faker.random.word(), faker.random.word(), `example.com` );
|
|
2530
|
+
},
|
|
2531
|
+
password: async function ( scope ) {
|
|
2532
|
+
return faker.internet.password();
|
|
2533
|
+
},
|
|
2534
|
+
hidden: async function ( scope ) {
|
|
2535
|
+
return null;
|
|
2536
|
+
},
|
|
2537
|
+
BirthDate: async function ( scope ) {
|
|
2538
|
+
let date = faker.date.birthdate({ max: 70, min: 0, mode: `age` });
|
|
2539
|
+
// Ensure 2-digit number for month and day
|
|
2540
|
+
let month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
2541
|
+
let day = ("0" + (date.getDay() + 1)).slice(-2);
|
|
2542
|
+
// Format the string in the way ALKiln can read it
|
|
2543
|
+
let date_str = `${ month }/${ day }/${ date.getFullYear() }`;
|
|
2544
|
+
return date_str;
|
|
2545
|
+
},
|
|
2546
|
+
ThreePartsDate: async function ( scope ) {
|
|
2547
|
+
return await scope.get_random_input_for.date( scope );
|
|
2548
|
+
},
|
|
2549
|
+
}, // ends scope.get_random_input_for()
|
|
2550
|
+
|
|
2133
2551
|
|
|
2134
2552
|
//#####################################
|
|
2135
2553
|
//#####################################
|