@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/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
- log.add_to_debug_log(`Report:\ntype: ${ type }, value: ${ value }\n`);
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 ) { scope.scenario_id = `_report_${ Date.now() }` }
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 safe_filename( filename, { replacement: `_` });
219
+ return safer_name;
197
220
  }, // Ends scope.getSafeScenarioBaseFilename()
198
221
 
199
- getSafeScenarioFilename: async function( scope, options={} ) {
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
- // Ensure prefix is a string
206
- let prefix = options.prefix || '';
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
- // For now, it needs to be `_` for the action and .gitignore. In future,
209
- // aiming for `-`.
210
- if ( prefix ) {
211
- prefix = `${ prefix }_`;
232
+ if ( name.length > 0 ) {
233
+ name += `-`;
212
234
  }
213
235
 
214
- let safe_name = safe_filename(`${ prefix }${ Date.now() }-${ scope.base_filename }`, { replacement: `_` });
215
- let safer_name = safe_name.substring(0, (255 - 10)); // Allow room for extensions
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
- let filename = await scope.getSafeScenarioFilename( scope, {
221
- prefix: `downloads_${ scope.page_id }_JSON`
222
- });
223
- filename += `.json`;
224
- return filename;
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
- // Get a unique json file filename
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( filename, JSON.stringify( json, null, 2 ));
240
- return { filename, json };
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 that element.`;
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 axe_filename = null;
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: `aXe_failure_${ scope.page_id }`}) + '.json';
504
- fs.writeFileSync( axe_filename, JSON.stringify( axe_result, null, 2 ));
505
- let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_filename }.`;
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
- axe_filename: axe_filename
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
- let error_handle = await error_elem.getProperty( `textContent` );
575
- let system_error_text = await error_handle.jsonValue();
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
- result.error = system_error_text;
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, from_story_table=true }) {
599
- /* Return data derived from cucumber variable-setting data.
600
- * Table must have headers. Arrays are not supported. */
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.page.evaluateHandle(( selector, field ) => {
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 set_to = row.value;
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
- // Set field
1256
- if ( scope.setCustomDatatype[ custom_datatype ] === undefined ) {
1257
- // If no custom datatype, set a regular field
1258
- await scope.setField[ tag ]( scope, { handle, set_to });
1259
- } else {
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, set_to });
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
- return row;
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.setField`? Find the custom handle in the setter itself.
1402
+ // TODO: Just put this in `scope.enter_answer`? Find the custom handle in the setter itself.
1271
1403
  setCustomDatatype: {
1272
- "BirthDate": async function ( scope, { handle, set_to }) {
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 the ALToolbox custom birthdate
1406
+ * custom field container, sets an AssemblyLine three-part date's
1275
1407
  * field values. */
1276
- let date_parts = set_to.split(`/`);
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 and year
1428
+ // Set the day or year
1290
1429
  await scope.setText( scope, {
1291
1430
  handle: field,
1292
- set_to: date_parts[ field_i ]
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 BirthDate()
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
- setField: {
1305
- button: async function ( scope, { handle, set_to }) { await scope.tapElement( scope, handle ); },
1306
- textarea: async function ( scope, { handle, set_to }) {
1307
- await scope.setText( scope, { handle, set_to });
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, set_to }) {
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 `set_to` that is encoded correctly since
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
- let option = await scope.page.$(`option[value="${ set_to }"]`);
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( set_to ); // And use that value to set it
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: set_to });
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, set_to }) {
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, set_to }) {
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 ( set_to.toLowerCase() === 'false' ) { await label.evaluate( elem => { elem.style.background = 'tomato'; }); }
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, set_to }); }
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, set_to });
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 set_to_date = set_to;
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 (set_to.indexOf(`today`) !== -1) {
1524
+ if (answer.indexOf(`today`) !== -1) {
1365
1525
  // Find out how many days to add/subtract
1366
- let date_delta = set_to.replace(/\s/g, '');
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
- set_to_date = new Date();
1531
+ answer_date = new Date();
1372
1532
  // Add/subtract required number of days
1373
- set_to_date.setDate(set_to_date.getDate() + date_delta);
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
- set_to_date = ((set_to_date.getMonth() + 1).toString().padStart(2, 0) + '/' +
1377
- set_to_date.getDate().toString().padStart(2, 0) + '/' +
1378
- set_to_date.getFullYear().toString());
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, set_to: set_to_date });
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, set_to });
1543
+ await scope.setText( scope, { handle, answer });
1384
1544
  await scope.afterStep(scope, { waitForShowIf: true });
1385
1545
  }
1386
- }, // Ends scope.setField.input()
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, set_to }) {
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 = set_to.split(`,`);
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 "${set_to}".` });
1445
- throw ReferenceError(`Could not find "${set_to}".`)
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, set_to }) {
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 ( set_to.toLowerCase() !== status ) {
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, set_to }) {
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( set_to );
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 `setField` (which is singular)
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 msg = `Missing variable or variables on page.`;
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
- let safe_path = await scope.getSafeScenarioFilename(
1889
- scope, { prefix: `screenshot_${ name }` }
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: `./${ safe_path }.jpg`,
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, set_to ) => {
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: set_to, trigger: scope.trigger_not_needed_flag }],
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, set_to_env_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: set_to_env_name,
2149
+ value: answer_env_name,
1959
2150
  trigger: scope.trigger_not_needed_flag,
1960
- flags: {secret_name: set_to_env_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
  //#####################################