@suffolklitlab/alkiln 4.6.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test_tests.yml +14 -34
- package/CHANGELOG.md +17 -0
- package/action.yml +6 -35
- package/lib/docassemble/setup.js +6 -0
- package/lib/scope.js +471 -105
- package/lib/steps.js +163 -17
- package/lib/utils/clean_reports.js +9 -3
- package/lib/utils/files.js +32 -0
- package/lib/utils/session_vars.js +56 -13
- package/package.json +2 -1
- package/tests/features/interactive_steps.feature +11 -2
- package/tests/features/observation_steps.feature +2 -2
- package/tests/features/random_input.feature +15 -0
- package/tests/features/story_tables.feature +1 -0
- 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`;
|
|
@@ -187,41 +209,49 @@ module.exports = {
|
|
|
187
209
|
name_parts.push( scope.language );
|
|
188
210
|
}
|
|
189
211
|
name_parts.push( scenario.pickle.name );
|
|
190
|
-
for ( let tag of scenario.pickle.tags ) {
|
|
191
|
-
name_parts.push( tag.name );
|
|
192
|
-
}
|
|
193
212
|
|
|
194
213
|
let filename = name_parts.join(`-`);
|
|
214
|
+
let safe_name = safe_filename( filename, { replacement: `_` });
|
|
215
|
+
// Allow room for extensions, date, etc.
|
|
216
|
+
let safer_name = safe_name.substring(0, (255 - 45));
|
|
195
217
|
|
|
196
|
-
return
|
|
218
|
+
return safer_name;
|
|
197
219
|
}, // Ends scope.getSafeScenarioBaseFilename()
|
|
198
220
|
|
|
199
|
-
|
|
221
|
+
// TODO: Does this need a name change? getSafeScenarioBasedFilename?
|
|
222
|
+
getSafeScenarioFilename: async function( scope, options={ prefix: '' }) {
|
|
200
223
|
/* Return a string that's safe to be a filename. Use a timestamp,
|
|
201
224
|
* a prefix the caller wants, and the base filename. We hope it can
|
|
202
225
|
* contain non-english characters too. We do not offer a suffix option
|
|
203
226
|
* because it may be cut off if the filename is too long.
|
|
204
227
|
*/
|
|
205
|
-
|
|
206
|
-
|
|
228
|
+
let name = options.prefix || '';
|
|
229
|
+
|
|
207
230
|
// As long as it's not an empty string, add a separator between it and the rest of the name
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
if ( name.length > 0 ) {
|
|
232
|
+
name += `-`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if ( scope.page ) {
|
|
236
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
237
|
+
name += `${ id }-`;
|
|
212
238
|
}
|
|
213
239
|
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
name += scope.base_filename;
|
|
241
|
+
name += `-${ files.readable_date() }`;
|
|
242
|
+
|
|
243
|
+
let safe_name = safe_filename( name, { replacement: `_` });
|
|
244
|
+
// Allow room for extensions, etc.
|
|
245
|
+
let safer_name = safe_name.substring(0, (255 - 10));
|
|
216
246
|
return safer_name;
|
|
217
247
|
}, // Ends scope.getSafeScenarioFilename()
|
|
218
248
|
|
|
219
249
|
getJSONFilename: async function( scope ) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return
|
|
250
|
+
// Get a unique path for a json file
|
|
251
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
252
|
+
let date = files.readable_date();
|
|
253
|
+
let json_path = path.join( scope.paths.scenario, `json_for-${ id }-${ date }.json` );
|
|
254
|
+
return json_path;
|
|
225
255
|
},
|
|
226
256
|
|
|
227
257
|
savePageJSONData: async function( scope ) {
|
|
@@ -231,13 +261,12 @@ module.exports = {
|
|
|
231
261
|
* @returns {string} info.filename The name of the file that was saved.
|
|
232
262
|
* @returns {Object} info.json The variables object.
|
|
233
263
|
*/
|
|
234
|
-
|
|
235
|
-
let filename = await scope.getJSONFilename( scope );
|
|
264
|
+
let json_path = await scope.getJSONFilename( scope );
|
|
236
265
|
// Get the data
|
|
237
266
|
let json = await scope.getPageJSON( scope );
|
|
238
267
|
// Save the data
|
|
239
|
-
fs.writeFileSync(
|
|
240
|
-
return {
|
|
268
|
+
fs.writeFileSync( json_path, JSON.stringify( json, null, 2 ));
|
|
269
|
+
return { json_path, json };
|
|
241
270
|
}, // Ends scope.savePageJSONData()
|
|
242
271
|
|
|
243
272
|
waitForShowIf: async function waitForShowIf(scope) {
|
|
@@ -386,7 +415,14 @@ module.exports = {
|
|
|
386
415
|
|
|
387
416
|
detectDownloadComplete: async function detectDownloadComplete(scope, endTime) {
|
|
388
417
|
/* Resolve if a download flag has changed. Timeout after a bit less than
|
|
389
|
-
* the global permitted timeout
|
|
418
|
+
* the global permitted timeout
|
|
419
|
+
*
|
|
420
|
+
* TODO: Properly wait for download to complete. See
|
|
421
|
+
* https://github.com/SuffolkLITLab/ALKiln/issues/492 and
|
|
422
|
+
* https://stackoverflow.com/a/56951024 and
|
|
423
|
+
* https://github.com/puppeteer/puppeteer/issues/4676 and
|
|
424
|
+
* https://github.com/puppeteer/puppeteer/issues/299 and
|
|
425
|
+
*/
|
|
390
426
|
|
|
391
427
|
// Stop/resolve if too much time has passed
|
|
392
428
|
let expirationTime = scope.timeout - 200; // ms
|
|
@@ -454,7 +490,7 @@ module.exports = {
|
|
|
454
490
|
|
|
455
491
|
// Do your own waiting if you need to in the step
|
|
456
492
|
return to_manipulate;
|
|
457
|
-
}, // Ends setVar()
|
|
493
|
+
}, // Ends scope.setVar()
|
|
458
494
|
|
|
459
495
|
continue: async function ( scope ) {
|
|
460
496
|
/* Presses whatever button it finds that might lead to the next page. */
|
|
@@ -466,7 +502,25 @@ module.exports = {
|
|
|
466
502
|
await elem.evaluate( el => { return el.className });
|
|
467
503
|
// Waits for navigation or user error
|
|
468
504
|
await scope.tapElement( scope, elem );
|
|
469
|
-
},
|
|
505
|
+
}, // Ends scope.continue()
|
|
506
|
+
|
|
507
|
+
continue_exists: async function ( scope ) {
|
|
508
|
+
/** Return true if a button that will allow continuing appears
|
|
509
|
+
* on this page. Otherwise return false. */
|
|
510
|
+
|
|
511
|
+
// Possible to find this on an event screen with `buttons:`, like an exit button
|
|
512
|
+
// <button type="submit" class="btn btn-da btn-danger" name="X211bHRpcGxlX2Nob2ljZQ" value="0">exit</button>
|
|
513
|
+
// An actual continue button that doesn't set a variable
|
|
514
|
+
// <button class="btn btn-da btn-primary" type="submit">Continue</button>
|
|
515
|
+
// One that sets a variable:
|
|
516
|
+
// <button type="submit" class="btn btn-da btn-primary" name="Zm9v" value="True">Continue</button>
|
|
517
|
+
// `buttons:` can be used in question blocks as choices
|
|
518
|
+
let regular = await scope.page.$( `fieldset.da-field-buttons button[type="submit"]` );
|
|
519
|
+
let signature = await scope.page.$( `fieldset .dasigsave` );
|
|
520
|
+
|
|
521
|
+
return regular !== null || signature !== null;
|
|
522
|
+
|
|
523
|
+
}, // Ends scope.continue_exists()
|
|
470
524
|
|
|
471
525
|
tapTab: async function ( scope, tab_id ) {
|
|
472
526
|
let elem = await scope.page.$(`#${tab_id}`);
|
|
@@ -497,12 +551,13 @@ module.exports = {
|
|
|
497
551
|
axe_result.passes = [];
|
|
498
552
|
axe_result.inapplicable = [];
|
|
499
553
|
|
|
500
|
-
let
|
|
554
|
+
let axe_path = null;
|
|
501
555
|
const passed = axe_result.violations.length == 0;
|
|
502
556
|
if (!passed) {
|
|
503
|
-
axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `
|
|
504
|
-
|
|
505
|
-
|
|
557
|
+
let axe_filename = await scope.getSafeScenarioFilename( scope, {prefix: `aXe_failure`}) + '.json';
|
|
558
|
+
axe_path = `${ scope.paths.scenario }/${ axe_filename }`;
|
|
559
|
+
fs.writeFileSync( axe_path, JSON.stringify( axe_result, null, 2 ));
|
|
560
|
+
let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_path }.`;
|
|
506
561
|
await scope.addToReport( scope, {
|
|
507
562
|
type: `error`,
|
|
508
563
|
value: msg
|
|
@@ -512,7 +567,7 @@ module.exports = {
|
|
|
512
567
|
}
|
|
513
568
|
return {
|
|
514
569
|
passed: axe_result.violations.length == 0,
|
|
515
|
-
|
|
570
|
+
axe_filepath: axe_path
|
|
516
571
|
};
|
|
517
572
|
},
|
|
518
573
|
|
|
@@ -666,7 +721,7 @@ module.exports = {
|
|
|
666
721
|
// All the different types of fields
|
|
667
722
|
// buttons, canvases, inputs of all kinds, selects (dropdowns), textareas. Are there more?
|
|
668
723
|
// Will deal with `option` once inside `select`
|
|
669
|
-
let all_nodes = $( `#dasigpage, fieldset button, .daquestionactionbutton, fieldset input, .da-form-group input, .da-form-group select, .da-form-group textarea` );
|
|
724
|
+
let all_nodes = $( `#dasigpage, fieldset button, .daquestionactionbutton, fieldset input, .da-form-group input, .da-form-group select, form select, .da-form-group textarea` );
|
|
670
725
|
for ( let node of all_nodes ) {
|
|
671
726
|
// Decision: Do not abstract the below. There's too much data to pass around for it to make sense
|
|
672
727
|
let $node = $( node );
|
|
@@ -932,6 +987,9 @@ module.exports = {
|
|
|
932
987
|
// names.var_names.push( layer_2_decoded );
|
|
933
988
|
}
|
|
934
989
|
|
|
990
|
+
// Detecting valid variable names/characters
|
|
991
|
+
// https://stackoverflow.com/a/23377268
|
|
992
|
+
|
|
935
993
|
return var_names;
|
|
936
994
|
}, // Ends scope.getPossibleVarNames()
|
|
937
995
|
|
|
@@ -1198,13 +1256,7 @@ module.exports = {
|
|
|
1198
1256
|
await scope.page.waitForSelector('#dasigcanvas');
|
|
1199
1257
|
}
|
|
1200
1258
|
|
|
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 );
|
|
1259
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
1208
1260
|
|
|
1209
1261
|
let disabled = await handle.evaluate(( elem )=> {
|
|
1210
1262
|
// da's header covers the top of the section containing the fields which means
|
|
@@ -1225,15 +1277,39 @@ module.exports = {
|
|
|
1225
1277
|
await scope.page.waitForTimeout( 400 );
|
|
1226
1278
|
}
|
|
1227
1279
|
|
|
1228
|
-
let { tag, type } = field;
|
|
1229
1280
|
// We use the last match. We wrote a warning for the user if there were
|
|
1230
1281
|
// multiple matches in the story that the last value will be used. In future,
|
|
1231
1282
|
// Steps will be able to set this data gradually and we don't want the developer
|
|
1232
1283
|
// to get confused thinking that an earlier Step was used instead of a later one.
|
|
1233
1284
|
let row = matching_input_rows[ matching_input_rows.length - 1 ];
|
|
1234
|
-
let
|
|
1285
|
+
let answer = row.value;
|
|
1286
|
+
|
|
1287
|
+
await scope.funnel_the_answer( scope, { handle, answer, field });
|
|
1288
|
+
|
|
1289
|
+
return row;
|
|
1290
|
+
}, // Ends scope.setVariable()
|
|
1291
|
+
|
|
1292
|
+
get_handle_from_field: async function ( scope, { field }) {
|
|
1293
|
+
/** Get the element's puppeteer handle using the field's selector. */
|
|
1294
|
+
|
|
1295
|
+
let handle = await scope.page.evaluateHandle(( selector ) => {
|
|
1296
|
+
let elem = document.querySelector( selector );
|
|
1297
|
+
// If the element has a sibling that's a label, we need to
|
|
1298
|
+
// interact with that label instead.
|
|
1299
|
+
// `elem` has never been null from what we've seen
|
|
1300
|
+
if ( elem.nextSibling && elem.nextSibling.tag === `label` ) {
|
|
1301
|
+
return elem.nextSibling;
|
|
1302
|
+
} else { return elem; }
|
|
1303
|
+
// Pass in the selector as an argument
|
|
1304
|
+
}, field.selector );
|
|
1305
|
+
|
|
1306
|
+
return handle;
|
|
1307
|
+
}, // Ends scope.get_handle_from_field()
|
|
1308
|
+
|
|
1309
|
+
get_custom_datatype: async function( scope, { handle }) {
|
|
1310
|
+
/** If this is part of a custom datatype, returns an element,
|
|
1311
|
+
* otherwise returns null. */
|
|
1235
1312
|
|
|
1236
|
-
// Detect a custom type and get the element of that custom type if it exists
|
|
1237
1313
|
let custom_datatype = await handle.evaluate(( elem )=> {
|
|
1238
1314
|
// If the element is in a form group**, then extract the form group's
|
|
1239
1315
|
// datatype value.
|
|
@@ -1252,28 +1328,52 @@ module.exports = {
|
|
|
1252
1328
|
return custom_datatype;
|
|
1253
1329
|
});
|
|
1254
1330
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1331
|
+
return custom_datatype;
|
|
1332
|
+
}, // Ends scope.is_custom_datatype()
|
|
1333
|
+
|
|
1334
|
+
funnel_the_answer: async function( scope, { handle, field, answer }) {
|
|
1335
|
+
/** Use the tag or custom datatype to funnel the answer to the right
|
|
1336
|
+
* field. Pages without a handle should pass in `null` for the
|
|
1337
|
+
* handle.
|
|
1338
|
+
*/
|
|
1339
|
+
|
|
1340
|
+
let custom_datatype = null;
|
|
1341
|
+
// Pages with elements like `canvas` for signatures won't have a handle
|
|
1342
|
+
// to pass in. They should pass in `null` for the handle and shouldn't
|
|
1343
|
+
// need a handle in order to function.
|
|
1344
|
+
if ( handle !== null ) {
|
|
1345
|
+
// Get the element of that custom type if it exists
|
|
1346
|
+
custom_datatype = await scope.get_custom_datatype( scope, { handle });
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// If there is a custom datatype function available, use it to set the field
|
|
1350
|
+
if ( scope.setCustomDatatype[ custom_datatype ] !== undefined ) {
|
|
1260
1351
|
// Set da package custom fields. TODO: find `custom_handle` in the setter?
|
|
1261
1352
|
let custom_handle = await handle.evaluateHandle(( elem )=> { return elem.closest( `.da-form-group` ) });
|
|
1262
|
-
await scope.setCustomDatatype[ custom_datatype ]( scope, { handle: custom_handle,
|
|
1353
|
+
await scope.setCustomDatatype[ custom_datatype ]( scope, { handle: custom_handle, answer });
|
|
1354
|
+
} else {
|
|
1355
|
+
// If no custom datatype, set a regular field
|
|
1356
|
+
await scope.enter_answer[ field.tag ]( scope, { handle, answer });
|
|
1263
1357
|
}
|
|
1264
1358
|
|
|
1265
|
-
|
|
1266
|
-
}, // Ends scope.setVariable()
|
|
1359
|
+
}, // Ends scope.funnel_the_answer()
|
|
1267
1360
|
|
|
1268
1361
|
// Handle setting values for da custom datatypes. E.g. `da-field-container-datatype-BirthDate`
|
|
1269
1362
|
// TODO: Make this more easily extensible.
|
|
1270
|
-
// TODO: Just put this in `scope.
|
|
1363
|
+
// TODO: Just put this in `scope.enter_answer`? Find the custom handle in the setter itself.
|
|
1271
1364
|
setCustomDatatype: {
|
|
1272
|
-
|
|
1365
|
+
al_date: async function ( scope, { handle, answer, is_past }) {
|
|
1273
1366
|
/* Given a date of the format 'mm/dd/yyyy' and the handle of the
|
|
1274
|
-
* custom field container, sets
|
|
1367
|
+
* custom field container, sets an AssemblyLine three-part date's
|
|
1275
1368
|
* field values. */
|
|
1276
|
-
|
|
1369
|
+
|
|
1370
|
+
// If it doesn't have the right format
|
|
1371
|
+
if ( answer === null || !answer.includes(`/`) ) {
|
|
1372
|
+
// Do nothing
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
let date_parts = answer.split(`/`);
|
|
1277
1377
|
let fields = await handle.$$(`select, input:not([type="hidden"])`);
|
|
1278
1378
|
for ( let field_i = 0; field_i < fields.length; field_i++ ) {
|
|
1279
1379
|
let field = fields[ field_i ];
|
|
@@ -1286,55 +1386,76 @@ module.exports = {
|
|
|
1286
1386
|
// Set the month
|
|
1287
1387
|
await field.select( date_parts[ 0 ] );
|
|
1288
1388
|
} else {
|
|
1289
|
-
// Set the day
|
|
1389
|
+
// Set the day or year
|
|
1290
1390
|
await scope.setText( scope, {
|
|
1291
1391
|
handle: field,
|
|
1292
|
-
|
|
1392
|
+
answer: date_parts[ field_i ]
|
|
1293
1393
|
});
|
|
1294
1394
|
}
|
|
1295
1395
|
|
|
1296
1396
|
} // For each field
|
|
1297
1397
|
|
|
1298
1398
|
await scope.afterStep( scope );
|
|
1299
|
-
}, // Ends
|
|
1399
|
+
}, // Ends scope.setCustomDatatype.al_date()
|
|
1400
|
+
|
|
1401
|
+
BirthDate: async function ( scope, { handle, answer }) {
|
|
1402
|
+
/** Given a date of the format 'mm/dd/yyyy' and the handle of the
|
|
1403
|
+
* custom field container, sets the ALToolbox custom birthdate
|
|
1404
|
+
* field values. */
|
|
1300
1405
|
|
|
1406
|
+
await scope.setCustomDatatype.al_date( scope, { handle, answer })
|
|
1407
|
+
}, // Ends scope.setCustomDatatype.BirthDate()
|
|
1408
|
+
|
|
1409
|
+
ThreePartsDate: async function ( scope, { handle, answer }) {
|
|
1410
|
+
/** Given a date of the format 'mm/dd/yyyy' and the handle of the
|
|
1411
|
+
* custom field container, sets the ALToolbox custom ThreePartsDate
|
|
1412
|
+
* field values. */
|
|
1413
|
+
await scope.setCustomDatatype.al_date( scope, { handle, answer })
|
|
1414
|
+
}, // Ends scope.setCustomDatatype.ThreePartsDate()
|
|
1301
1415
|
|
|
1302
1416
|
}, // ends setCustomDatatype {}
|
|
1303
1417
|
|
|
1304
|
-
|
|
1305
|
-
button: async function ( scope, { handle,
|
|
1306
|
-
textarea: async function ( scope, { handle,
|
|
1307
|
-
await scope.setText( scope, { handle,
|
|
1418
|
+
enter_answer: {
|
|
1419
|
+
button: async function ( scope, { handle, answer }) { await scope.tapElement( scope, handle ); },
|
|
1420
|
+
textarea: async function ( scope, { handle, answer }) {
|
|
1421
|
+
await scope.setText( scope, { handle, answer });
|
|
1308
1422
|
await scope.afterStep(scope); // No showifs for this one?
|
|
1309
1423
|
},
|
|
1310
|
-
select: async function ( scope, { handle,
|
|
1424
|
+
select: async function ( scope, { handle, answer }) {
|
|
1425
|
+
// Note: I don't remember what's really going on in here.
|
|
1426
|
+
// I will note that values of `select` elements are strings,
|
|
1427
|
+
// not variables, so we don't need to check for variable names.
|
|
1428
|
+
|
|
1311
1429
|
// A dropdown's option value can be one of two things
|
|
1312
1430
|
// Try to find the element using the first value
|
|
1313
1431
|
|
|
1314
|
-
// TODO: Can we somehow pass in a `
|
|
1432
|
+
// TODO: Can we somehow pass in a `answer` that is encoded correctly since
|
|
1315
1433
|
// we're getting the fields from the DOM to begin with? We'll probably
|
|
1316
1434
|
// need another prop because we need the `option` human value to
|
|
1317
1435
|
// match the story table
|
|
1318
1436
|
|
|
1319
1437
|
// TODO: Change this to search in the actual handle instead of just $
|
|
1320
|
-
|
|
1438
|
+
// There could be multiple fields on the page with the same option
|
|
1439
|
+
// values.
|
|
1440
|
+
// let option = await handle.$(`option[value="${ answer }"]`);
|
|
1441
|
+
let option = await scope.page.$(`option[value="${ answer }"]`);
|
|
1321
1442
|
if ( option ) {
|
|
1322
|
-
await handle.select(
|
|
1443
|
+
await handle.select( answer ); // And use that value to set it
|
|
1323
1444
|
|
|
1324
1445
|
// If that literal value isn't on the page, it should be a base64 encoded value
|
|
1325
|
-
// TODO: Can these be double encoded?
|
|
1446
|
+
// TODO: Can these be double encoded? Can they be encoded like objects?
|
|
1326
1447
|
} else {
|
|
1327
|
-
let base64_name = await scope.toBase64( scope, { utf8_str:
|
|
1448
|
+
let base64_name = await scope.toBase64( scope, { utf8_str: answer });
|
|
1328
1449
|
await handle.select( base64_name );
|
|
1329
1450
|
}
|
|
1330
1451
|
|
|
1331
1452
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1332
1453
|
},
|
|
1333
|
-
canvas: async function ( scope, { handle,
|
|
1454
|
+
canvas: async function ( scope, { handle, answer }) {
|
|
1334
1455
|
await scope.sign( scope );
|
|
1335
1456
|
await scope.afterStep( scope );
|
|
1336
1457
|
},
|
|
1337
|
-
input: async function ( scope, { handle,
|
|
1458
|
+
input: async function ( scope, { handle, answer }) {
|
|
1338
1459
|
/* Set value of some `input` element to the given value. */
|
|
1339
1460
|
|
|
1340
1461
|
let type = await handle.evaluate( elem => { return elem.getAttribute('type'); });
|
|
@@ -1343,50 +1464,60 @@ module.exports = {
|
|
|
1343
1464
|
let [label] = await handle.$x(`following-sibling::label`);
|
|
1344
1465
|
|
|
1345
1466
|
if ( session_vars.get_debug() ) {
|
|
1346
|
-
if (
|
|
1467
|
+
if ( answer.toLowerCase() === 'false' ) { await label.evaluate( elem => { elem.style.background = 'tomato'; }); }
|
|
1347
1468
|
else { await label.evaluate( elem => { elem.style.background = 'teal'; }); }
|
|
1348
1469
|
let name = await label.evaluate( elem => { return elem.getAttribute('for'); });
|
|
1349
1470
|
}
|
|
1350
1471
|
|
|
1351
1472
|
if ( type === `radio` ) { await label.evaluate( elem => { return elem.click(); }); }
|
|
1352
|
-
else { await scope.setCheckbox( scope, { label,
|
|
1473
|
+
else { await scope.setCheckbox( scope, { label, answer }); }
|
|
1353
1474
|
|
|
1354
1475
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1355
1476
|
|
|
1356
1477
|
} else if ( type === `file` ) {
|
|
1357
|
-
await scope.uploadFiles( scope, { handle,
|
|
1478
|
+
await scope.uploadFiles( scope, { handle, answer });
|
|
1358
1479
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1359
1480
|
|
|
1360
1481
|
} else if (type == `date`) {
|
|
1361
|
-
let
|
|
1482
|
+
let answer_date = answer;
|
|
1362
1483
|
// If the date is specified in the format "today" or "today + 2" or "today-1"
|
|
1363
1484
|
// it will need some pre-processing.
|
|
1364
|
-
if (
|
|
1485
|
+
if (answer.indexOf(`today`) !== -1) {
|
|
1365
1486
|
// Find out how many days to add/subtract
|
|
1366
|
-
let date_delta =
|
|
1487
|
+
let date_delta = answer.replace(/\s/g, '');
|
|
1367
1488
|
date_delta = date_delta.match(/today([+-]\d+)?/)[1];
|
|
1368
1489
|
date_delta = date_delta == undefined ? 0 : date_delta;
|
|
1369
1490
|
date_delta = parseInt(date_delta);
|
|
1370
1491
|
// Get today's date
|
|
1371
|
-
|
|
1492
|
+
answer_date = new Date();
|
|
1372
1493
|
// Add/subtract required number of days
|
|
1373
|
-
|
|
1494
|
+
answer_date.setDate(answer_date.getDate() + date_delta);
|
|
1374
1495
|
// Convert to the docassemble date field format: mm/dd/yyyy
|
|
1375
1496
|
// Reference: https://gomakethings.com/setting-a-date-input-to-todays-date-with-vanilla-js/
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1497
|
+
answer_date = ((answer_date.getMonth() + 1).toString().padStart(2, 0) + '/' +
|
|
1498
|
+
answer_date.getDate().toString().padStart(2, 0) + '/' +
|
|
1499
|
+
answer_date.getFullYear().toString());
|
|
1379
1500
|
}
|
|
1380
|
-
await scope.setText( scope, { handle,
|
|
1501
|
+
await scope.setText( scope, { handle, answer: answer_date });
|
|
1381
1502
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1382
1503
|
} else {
|
|
1383
|
-
await scope.setText( scope, { handle,
|
|
1504
|
+
await scope.setText( scope, { handle, answer });
|
|
1384
1505
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1385
1506
|
}
|
|
1386
|
-
}, // Ends scope.
|
|
1387
|
-
|
|
1507
|
+
}, // Ends scope.enter_answer.input()
|
|
1508
|
+
hidden: async function ( scope, { handle, answer }) {
|
|
1509
|
+
// Do nothing. The only way the code should get here is if
|
|
1510
|
+
// this wasn't a custom datatype we can handle.
|
|
1511
|
+
// If we were to write a message, I don't know what we can
|
|
1512
|
+
// write - this may be a different custom datatype or this
|
|
1513
|
+
// could be a special da field that we shouldn't worry the
|
|
1514
|
+
// dev about at all.
|
|
1515
|
+
return;
|
|
1516
|
+
},
|
|
1517
|
+
|
|
1518
|
+
}, // ends scope.enter_answer
|
|
1388
1519
|
|
|
1389
|
-
uploadFiles: async function ( scope, { handle,
|
|
1520
|
+
uploadFiles: async function ( scope, { handle, answer }) {
|
|
1390
1521
|
/* Try to upload one or more files to a file-type input field. */
|
|
1391
1522
|
|
|
1392
1523
|
// Tests could be in one of two places. Figure out which directory they're in
|
|
@@ -1419,7 +1550,7 @@ module.exports = {
|
|
|
1419
1550
|
}
|
|
1420
1551
|
|
|
1421
1552
|
// Allow a typo where someone forgot to include a space after a comma
|
|
1422
|
-
let file_names =
|
|
1553
|
+
let file_names = answer.split(`,`);
|
|
1423
1554
|
|
|
1424
1555
|
// If a file exist, add it to a list of files to be uploaded. If it doesn't, add a warning.
|
|
1425
1556
|
let paths = [];
|
|
@@ -1441,8 +1572,8 @@ module.exports = {
|
|
|
1441
1572
|
}
|
|
1442
1573
|
|
|
1443
1574
|
if ( paths.length === 0 ) {
|
|
1444
|
-
await scope.addToReport( scope, { type: `error`, value: `Could not find "${
|
|
1445
|
-
throw ReferenceError(`Could not find "${
|
|
1575
|
+
await scope.addToReport( scope, { type: `error`, value: `Could not find "${answer}".` });
|
|
1576
|
+
throw ReferenceError(`Could not find "${answer}".`)
|
|
1446
1577
|
}
|
|
1447
1578
|
// Prepare to wait for upload to complete
|
|
1448
1579
|
let name = await handle.evaluate( elem => { return elem.getAttribute(`name`); });
|
|
@@ -1467,20 +1598,20 @@ module.exports = {
|
|
|
1467
1598
|
|
|
1468
1599
|
}, // Ends scope.uploadFiles()
|
|
1469
1600
|
|
|
1470
|
-
setCheckbox: async function ( scope, { label,
|
|
1601
|
+
setCheckbox: async function ( scope, { label, answer }) {
|
|
1471
1602
|
// Depending on the current value/status of a checkbox and the desired
|
|
1472
1603
|
// value to set, either taps the checkbox or leaves it alone.
|
|
1473
1604
|
let status = await label.evaluate( elem => { return elem.getAttribute('aria-checked'); });
|
|
1474
|
-
if (
|
|
1605
|
+
if ( answer.toLowerCase() !== status ) {
|
|
1475
1606
|
await label.evaluate( elem => { return elem.click(); });
|
|
1476
1607
|
}
|
|
1477
1608
|
}, // Ends scope.setCheckbox()
|
|
1478
1609
|
|
|
1479
|
-
setText: async function ( scope, { handle,
|
|
1610
|
+
setText: async function ( scope, { handle, answer }) {
|
|
1480
1611
|
// Set text in some kind of field (input text, input date, textarea, etc.)
|
|
1481
1612
|
await handle.evaluate( el => { el.value = '' });
|
|
1482
1613
|
await handle.focus();
|
|
1483
|
-
await handle.type(
|
|
1614
|
+
await handle.type( answer );
|
|
1484
1615
|
setTimeout(async () => {
|
|
1485
1616
|
}, 500)
|
|
1486
1617
|
},
|
|
@@ -1527,7 +1658,7 @@ module.exports = {
|
|
|
1527
1658
|
}, // Ends scope.examinePageID()
|
|
1528
1659
|
|
|
1529
1660
|
// TODO: Rename var_data to input_data?
|
|
1530
|
-
// Note: Totally different from `
|
|
1661
|
+
// Note: Totally different from `enter_answer` (which is singular)
|
|
1531
1662
|
// TODO: If a step triggers this and is not able to set a value, should it error?
|
|
1532
1663
|
// TODO: Rename to setFieldsOfOnePage? setPageFields?
|
|
1533
1664
|
setFields: async function ( scope, {
|
|
@@ -1656,7 +1787,8 @@ module.exports = {
|
|
|
1656
1787
|
}
|
|
1657
1788
|
}
|
|
1658
1789
|
if ( num_unused_rows > 0 ) {
|
|
1659
|
-
let
|
|
1790
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
1791
|
+
let msg = `Missing variable or variables on page "${id}"`;
|
|
1660
1792
|
await scope.addToReport(scope, { type: `error`, value: msg });
|
|
1661
1793
|
expect( num_unused_rows, msg ).to.equal( 0 );
|
|
1662
1794
|
}
|
|
@@ -1885,12 +2017,18 @@ module.exports = {
|
|
|
1885
2017
|
/* Download and save a screenshot. `name` does not have to be
|
|
1886
2018
|
* unique as the filename will be made unique. */
|
|
1887
2019
|
name = name || '';
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
);
|
|
2020
|
+
|
|
2021
|
+
let date = files.readable_date();
|
|
2022
|
+
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
2023
|
+
// Include the custom name if it has one
|
|
2024
|
+
if ( name.length > 0 ) {
|
|
2025
|
+
name = `screenshot-${ name }-on-${ id }-${ date }`;
|
|
2026
|
+
} else {
|
|
2027
|
+
name = `screenshot_on-${ id }-${ date }`;
|
|
2028
|
+
}
|
|
1891
2029
|
|
|
1892
2030
|
await scope.page.screenshot({
|
|
1893
|
-
path: `./${
|
|
2031
|
+
path: `./${ scope.paths.scenario }/${ name }.jpg`,
|
|
1894
2032
|
type: 'jpeg',
|
|
1895
2033
|
fullPage: true
|
|
1896
2034
|
});
|
|
@@ -1930,7 +2068,7 @@ module.exports = {
|
|
|
1930
2068
|
await scope.afterStep(scope);
|
|
1931
2069
|
}, // Ends link_works()
|
|
1932
2070
|
|
|
1933
|
-
set_regular_var: async ( scope, var_name,
|
|
2071
|
+
set_regular_var: async ( scope, var_name, answer ) => {
|
|
1934
2072
|
/** Set a non-story table variable (with or without a choice associated with it) to a value. Set:
|
|
1935
2073
|
* - Buttons associated with variables
|
|
1936
2074
|
* - Dropdowns
|
|
@@ -1941,23 +2079,23 @@ module.exports = {
|
|
|
1941
2079
|
*/
|
|
1942
2080
|
// Let kiln know not to test against trigger variable values, even for proxy vars
|
|
1943
2081
|
let var_data = await scope.normalizeTable( scope, {
|
|
1944
|
-
var_data: [{ var: var_name, value:
|
|
2082
|
+
var_data: [{ var: var_name, value: answer, trigger: scope.trigger_not_needed_flag }],
|
|
1945
2083
|
});
|
|
1946
2084
|
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
1947
2085
|
// if this variable cannot be set on this page, trigger an error
|
|
1948
2086
|
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
1949
2087
|
}, // Ends set_regular_var()
|
|
1950
2088
|
|
|
1951
|
-
set_secret_var: async ( scope, var_name,
|
|
2089
|
+
set_secret_var: async ( scope, var_name, answer_env_name ) => {
|
|
1952
2090
|
/** Sets a non-story table variable to a "secret" value (i.e. an env var) */
|
|
1953
2091
|
// Prevent pictures of a screen with a secret.
|
|
1954
2092
|
scope.disable_error_screenshot = true;
|
|
1955
2093
|
let var_data = await scope.normalizeTable( scope, {
|
|
1956
2094
|
var_data: [{
|
|
1957
2095
|
var: var_name,
|
|
1958
|
-
value:
|
|
2096
|
+
value: answer_env_name,
|
|
1959
2097
|
trigger: scope.trigger_not_needed_flag,
|
|
1960
|
-
flags: {secret_name:
|
|
2098
|
+
flags: {secret_name: answer_env_name}
|
|
1961
2099
|
}],
|
|
1962
2100
|
});
|
|
1963
2101
|
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
@@ -1990,6 +2128,9 @@ module.exports = {
|
|
|
1990
2128
|
* WARNING: Cannot download the same file twice in a single scenario.
|
|
1991
2129
|
* WARNING: Must not have any characters that are totally unique to that
|
|
1992
2130
|
* page load. It's usually just the name of the file.
|
|
2131
|
+
*
|
|
2132
|
+
* TODO: Properly wait for download to complete. See notes in
|
|
2133
|
+
* scope.js scope.detectDownloadComplete()
|
|
1993
2134
|
*/
|
|
1994
2135
|
let [elem] = await scope.page.$x(`//a[contains(@href, "${ filename }")]`);
|
|
1995
2136
|
|
|
@@ -2126,10 +2267,235 @@ module.exports = {
|
|
|
2126
2267
|
});
|
|
2127
2268
|
|
|
2128
2269
|
await scope.afterStep(scope, {waitForShowIf: false});
|
|
2129
|
-
}, // Ends sign_in
|
|
2270
|
+
}, // Ends scope.steps.sign_in()
|
|
2271
|
+
|
|
2272
|
+
set_random_page_vars: async ( scope ) => {
|
|
2273
|
+
/** Answer inputs randomly. */
|
|
2274
|
+
|
|
2275
|
+
let html = await scope.page.content();
|
|
2276
|
+
// These are the fields on the current page. Their handles should all exist.
|
|
2277
|
+
let fields = await scope.getAllFields( scope, { html: html });
|
|
2278
|
+
let buttons = [];
|
|
2279
|
+
|
|
2280
|
+
// For every field on the page
|
|
2281
|
+
for ( let field of fields ) {
|
|
2282
|
+
|
|
2283
|
+
// fields[n].tag is HTML tag of node, as a string
|
|
2284
|
+
|
|
2285
|
+
// If the field is a button, save it in a list of buttons that we'll deal with later
|
|
2286
|
+
if ( field.tag === `button` ) {
|
|
2287
|
+
buttons.push( field );
|
|
2288
|
+
} else {
|
|
2289
|
+
// Try to give a random answer for that element tag
|
|
2290
|
+
await scope.set_random_input_for[ field.tag ]( scope, { field: field });
|
|
2291
|
+
process.stdout.write(`\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
} // End for all page fields
|
|
2295
|
+
|
|
2296
|
+
return buttons;
|
|
2297
|
+
|
|
2298
|
+
}, // Ends scope.steps.set_random_page_vars()
|
|
2130
2299
|
|
|
2131
2300
|
}, // ends scope.steps
|
|
2132
2301
|
|
|
2302
|
+
set_random_input_for: {
|
|
2303
|
+
a: async function (scope, { fields }) {
|
|
2304
|
+
// We don't do anything with links for random input tests
|
|
2305
|
+
},
|
|
2306
|
+
buttons: async function (scope, { fields }) {
|
|
2307
|
+
// Get a random button
|
|
2308
|
+
let field = faker.helpers.arrayElement( fields );
|
|
2309
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2310
|
+
// Send it to be tapped
|
|
2311
|
+
await scope.funnel_the_answer( scope, { handle, field, answer: `doesn't matter` });
|
|
2312
|
+
},
|
|
2313
|
+
textarea: async function (scope, { field }) {
|
|
2314
|
+
// Get random paragraph of text
|
|
2315
|
+
let answer = faker.lorem.paragraph();
|
|
2316
|
+
// Get the element's puppeteer handler
|
|
2317
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2318
|
+
// Type the random text into the textarea
|
|
2319
|
+
await scope.funnel_the_answer( scope, { handle, field, answer: answer });
|
|
2320
|
+
},
|
|
2321
|
+
select: async function (scope, { field }) {
|
|
2322
|
+
// Get the values of all the options of the field
|
|
2323
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2324
|
+
// Get the values of all the choices
|
|
2325
|
+
let answer_choices = await handle.evaluate(function ( elem ) {
|
|
2326
|
+
let values = Array.from(elem.querySelectorAll(`option`)).map(element=>element.value);
|
|
2327
|
+
return values;
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
// Remove the "no selection" option from the list ('')
|
|
2331
|
+
// TODO: blue sky - for optional fields allow no selection
|
|
2332
|
+
if ( answer_choices[0] === `` ) {
|
|
2333
|
+
answer_choices.shift();
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// Select randomly between them
|
|
2337
|
+
let answer = faker.helpers.arrayElement( answer_choices );
|
|
2338
|
+
|
|
2339
|
+
// Set that value
|
|
2340
|
+
await scope.funnel_the_answer( scope, { handle, field, answer: answer });
|
|
2341
|
+
|
|
2342
|
+
},
|
|
2343
|
+
canvas: async function (scope, { field }) {
|
|
2344
|
+
// WARNING: Do not test this in headless mode.
|
|
2345
|
+
// If we do this in headless mode, the test will fail. It signs, but
|
|
2346
|
+
// can't continue. Not sure why.
|
|
2347
|
+
await scope.funnel_the_answer( scope, { field, handle: null, answer: `doesn't matter` });
|
|
2348
|
+
},
|
|
2349
|
+
input: async function (scope, { field }) {
|
|
2350
|
+
/** Handles various `type`s of input element fields, except:
|
|
2351
|
+
*
|
|
2352
|
+
* Does not handle "file" type
|
|
2353
|
+
* Does not handle custom data values yet, like BirthDate, because they have
|
|
2354
|
+
* `type="hidden"` which also matches other special da fields on the
|
|
2355
|
+
* page that we SHOULDN'T interact with.
|
|
2356
|
+
* Does not handle range, which is not a `type`, but it is an input element
|
|
2357
|
+
*/
|
|
2358
|
+
|
|
2359
|
+
// Use the type instead of the tag to get get the answer for
|
|
2360
|
+
// an input field
|
|
2361
|
+
let type = field.type;
|
|
2362
|
+
|
|
2363
|
+
// Custom datatype question: Do all custom datatypes hide their
|
|
2364
|
+
// original field? Probably not...
|
|
2365
|
+
// TODO: Move custom datatype checking to the caller of this
|
|
2366
|
+
// function somehow - in future, not all custom datatypes will
|
|
2367
|
+
// be inputs _and_ this won't handle, for example, a `select` field
|
|
2368
|
+
// that is part of a custom datatype with a hidden field - this
|
|
2369
|
+
// doesn't take care of that situation.
|
|
2370
|
+
|
|
2371
|
+
let handle = await scope.get_handle_from_field( scope, { field: field });
|
|
2372
|
+
|
|
2373
|
+
// If this is a custom datatype
|
|
2374
|
+
let custom_datatype = await scope.get_custom_datatype( scope, { handle });
|
|
2375
|
+
let is_custom_datatype = scope.get_random_input_for[ custom_datatype ] !== undefined;
|
|
2376
|
+
if ( is_custom_datatype ) {
|
|
2377
|
+
|
|
2378
|
+
// And the custom datatype has a hidden field (which will be the original field)
|
|
2379
|
+
let hidden_field = await scope.get_custom_datatype_hidden_field( scope, { handle });
|
|
2380
|
+
if ( hidden_field !== null ) {
|
|
2381
|
+
// only set the value once - just for the original field
|
|
2382
|
+
if ( type === `hidden` ) {
|
|
2383
|
+
|
|
2384
|
+
// set the value
|
|
2385
|
+
let answer = await scope.get_random_input_for[ custom_datatype ]( scope );
|
|
2386
|
+
await scope.funnel_the_answer( scope, { handle, field, answer });
|
|
2387
|
+
|
|
2388
|
+
}
|
|
2389
|
+
// Otherwise ignore the non-original field
|
|
2390
|
+
|
|
2391
|
+
// Untested: If no hidden field exists, just try to set the custom
|
|
2392
|
+
// datatype field
|
|
2393
|
+
} else {
|
|
2394
|
+
// set the value
|
|
2395
|
+
let answer = await scope.get_random_input_for[ custom_datatype ]( scope );
|
|
2396
|
+
await scope.funnel_the_answer( scope, { handle, field, answer });
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// If this is not a custom datatype
|
|
2400
|
+
} else {
|
|
2401
|
+
|
|
2402
|
+
// `file` type inputs come next to an input with a type of ''.
|
|
2403
|
+
// Maybe others do too.
|
|
2404
|
+
if ( type !== `` && type !== `hidden` ) {
|
|
2405
|
+
// Field types that we can't yet handle, but plan to in future
|
|
2406
|
+
if ( type === `file` ) {
|
|
2407
|
+
await scope.addToReport( scope, {
|
|
2408
|
+
type: `warning`,
|
|
2409
|
+
value: `Sorry, ALKiln's random-input cannot yet handle "${ type }" fields.`
|
|
2410
|
+
});
|
|
2411
|
+
} else {
|
|
2412
|
+
let answer = await scope.get_random_input_for[ type ]( scope );
|
|
2413
|
+
await scope.funnel_the_answer( scope, { handle, field, answer });
|
|
2414
|
+
} // ends if field type is one we cover
|
|
2415
|
+
} // ends if field type exists
|
|
2416
|
+
|
|
2417
|
+
} // ends if is custom datatype
|
|
2418
|
+
|
|
2419
|
+
}, // ends scope.set_random_input_for.input
|
|
2420
|
+
}, // ends scope.set_random_input_for
|
|
2421
|
+
|
|
2422
|
+
get_custom_datatype_hidden_field: async function (scope, { handle }) {
|
|
2423
|
+
/** If there's a hidden field in a custom datatype, returns that
|
|
2424
|
+
* DOM node, otherwise returns null. */
|
|
2425
|
+
|
|
2426
|
+
let hidden_field = await handle.evaluate(( elem )=> {
|
|
2427
|
+
// If the element is in a form group**, then extract the form group's
|
|
2428
|
+
// datatype value.
|
|
2429
|
+
// ** For now the only custom datatype we have is in a form group. In
|
|
2430
|
+
// future we should look into what happens with a `fieldset` for
|
|
2431
|
+
// `choices:`, etc.
|
|
2432
|
+
let custom_datatype = null, matches = null;
|
|
2433
|
+
let group = elem.closest( `.da-form-group` );
|
|
2434
|
+
// Returns an element or null
|
|
2435
|
+
let hidden_child = group.querySelector( `[type="hidden"]` );
|
|
2436
|
+
|
|
2437
|
+
return hidden_child;
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
return hidden_field;
|
|
2441
|
+
}, // Ends scope.get_custom_datatype_hidden_field()
|
|
2442
|
+
|
|
2443
|
+
get_random_input_for: {
|
|
2444
|
+
checkbox: async function ( scope ) {
|
|
2445
|
+
return `${ Math.random() > .5 }`;
|
|
2446
|
+
},
|
|
2447
|
+
radio: async function ( scope ) {
|
|
2448
|
+
return `${ Math.random() > .5 }`;
|
|
2449
|
+
},
|
|
2450
|
+
text: async function ( scope ) {
|
|
2451
|
+
return faker.random.word();
|
|
2452
|
+
},
|
|
2453
|
+
number: async function ( scope ) {
|
|
2454
|
+
let answer = faker.helpers.arrayElement( [`1`, `2`, `3`], 1 );
|
|
2455
|
+
return answer;
|
|
2456
|
+
},
|
|
2457
|
+
date: async function ( scope ) {
|
|
2458
|
+
let past = faker.date.past(25);
|
|
2459
|
+
let future = faker.date.future(1);
|
|
2460
|
+
|
|
2461
|
+
let date = null;
|
|
2462
|
+
if ( Math.random() > .5 ) {
|
|
2463
|
+
date = past;
|
|
2464
|
+
} else {
|
|
2465
|
+
date = future;
|
|
2466
|
+
}
|
|
2467
|
+
// Ensure 2-digit number for month and day
|
|
2468
|
+
let month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
2469
|
+
let day = ("0" + (date.getDay() + 1)).slice(-2);
|
|
2470
|
+
// Format the string in the way ALKiln can read it
|
|
2471
|
+
return `${ month }/${ day }/${ date.getFullYear() }`;
|
|
2472
|
+
},
|
|
2473
|
+
currency: async function ( scope ) {
|
|
2474
|
+
return `${ Math.random() * 100 }`;
|
|
2475
|
+
},
|
|
2476
|
+
email: async function ( scope ) {
|
|
2477
|
+
return faker.internet.email( faker.random.word(), faker.random.word(), `example.com` );
|
|
2478
|
+
},
|
|
2479
|
+
password: async function ( scope ) {
|
|
2480
|
+
return faker.internet.password();
|
|
2481
|
+
},
|
|
2482
|
+
hidden: async function ( scope ) {
|
|
2483
|
+
return null;
|
|
2484
|
+
},
|
|
2485
|
+
BirthDate: async function ( scope ) {
|
|
2486
|
+
let date = faker.date.birthdate({ max: 70, min: 0, mode: `age` });
|
|
2487
|
+
// Ensure 2-digit number for month and day
|
|
2488
|
+
let month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
2489
|
+
let day = ("0" + (date.getDay() + 1)).slice(-2);
|
|
2490
|
+
// Format the string in the way ALKiln can read it
|
|
2491
|
+
let date_str = `${ month }/${ day }/${ date.getFullYear() }`;
|
|
2492
|
+
return date_str;
|
|
2493
|
+
},
|
|
2494
|
+
ThreePartsDate: async function ( scope ) {
|
|
2495
|
+
return await scope.get_random_input_for.date( scope );
|
|
2496
|
+
},
|
|
2497
|
+
}, // ends scope.get_random_input_for()
|
|
2498
|
+
|
|
2133
2499
|
|
|
2134
2500
|
//#####################################
|
|
2135
2501
|
//#####################################
|