@suffolklitlab/alkiln 5.17.1 → 5.17.2-deps
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/al_da_image_latest.yml +64 -3
- package/.github/workflows/github_server.yml +1 -1
- package/.github/workflows/playground.yml +1 -1
- package/CHANGELOG.md +44 -4
- package/action.yml +1 -1
- package/docs/decisions/017_post_navigation_custom_behavior_2026_10_26.md +136 -0
- package/lib/run_cucumber.js +1 -1
- package/lib/scope.js +655 -237
- package/lib/steps.js +137 -60
- package/models.json +19 -0
- package/npm-shrinkwrap.json +1154 -2211
- package/package.json +14 -14
- package/pi-session-2026-11-04T23-59-20-761Z_01a1a900-beb8-70ca-a64d-033dc70709e8.html +4411 -0
- package/tests/log_codes/log_code_expected_instances.txt +10 -1
- package/tests/unit_tests/navigation.test.js +72 -0
package/lib/scope.js
CHANGED
|
@@ -211,6 +211,7 @@ module.exports = {
|
|
|
211
211
|
*
|
|
212
212
|
* TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
|
|
213
213
|
* TODO: Update to latest cucumberjs, which has `AfterStep`
|
|
214
|
+
* Discuss: Move system error detection to afterStep()?
|
|
214
215
|
*/
|
|
215
216
|
|
|
216
217
|
if ( scope.page ) {
|
|
@@ -232,8 +233,8 @@ module.exports = {
|
|
|
232
233
|
let error_text_prop = await error_elem.getProperty( 'textContent' );
|
|
233
234
|
let system_error_text = await error_text_prop.jsonValue();
|
|
234
235
|
|
|
235
|
-
reports.addToReport(scope, { type: `error`, code: `ALK0101`, value: system_error_text });
|
|
236
|
-
throw
|
|
236
|
+
let rpt_msg = reports.addToReport(scope, { type: `error`, code: `ALK0101`, value: system_error_text });
|
|
237
|
+
throw Error( rpt_msg );
|
|
237
238
|
};
|
|
238
239
|
|
|
239
240
|
// Wait for elements that might get revealed
|
|
@@ -254,10 +255,44 @@ module.exports = {
|
|
|
254
255
|
if ( options.waitForTimeout ) { await time.waitForTimeout( options.waitForTimeout ); }
|
|
255
256
|
}, // Ends afterStep
|
|
256
257
|
|
|
257
|
-
tapElementAndNavigate: async function(scope, {
|
|
258
|
-
|
|
258
|
+
tapElementAndNavigate: async function(scope, {
|
|
259
|
+
elem,
|
|
260
|
+
post_nav_handlers=null,
|
|
261
|
+
waitForTimeout=null
|
|
262
|
+
}) {
|
|
263
|
+
/**
|
|
264
|
+
* Tap a given element and expect successful navigation. Can tap action
|
|
265
|
+
* button, link, continue button, etc. Unfortunately, we need more ways to
|
|
266
|
+
* detect navigation. Ideas for detecting navigation:
|
|
267
|
+
* https://github.com/SuffolkLITLab/ALKiln/issues/229.
|
|
268
|
+
*
|
|
269
|
+
* @param {Object} scope - Tests state object
|
|
270
|
+
* @param {Object} kwargs - Named arguments
|
|
271
|
+
* @param {ElementHandle} kwargs.elem - Valid puppeteer element handle
|
|
272
|
+
* @param {int} [kwargs.waitForTimeout] - Max seconds before timing out
|
|
273
|
+
* @param {[function]} [kwargs.post_nav_handlers] - Functions to handle page
|
|
274
|
+
* errors like system errors or invalid answers' markers
|
|
275
|
+
*
|
|
276
|
+
* @returns {Object} kwrvs - Named return values
|
|
277
|
+
* @returns {bool} kwrvs.was_found - True if the check found any errors
|
|
278
|
+
* @returns {bool} kwrvs.ok - False if the check found any errors. Discuss:
|
|
279
|
+
* Should we only use `ok` for internal errors?
|
|
280
|
+
* @param {user|system} kwrvs.type - "user" with invalid answers, "system"
|
|
281
|
+
* with da or AL server errors
|
|
282
|
+
* @param {[ElementHandle]} kwrvs.error_handles - List of puppeteer element
|
|
283
|
+
* handles the check found. There can be multiple invalid answer elems.
|
|
284
|
+
* */
|
|
285
|
+
await scope.log_and_update_if_new_page_id( scope );
|
|
286
|
+
|
|
287
|
+
// TODO: We sometimes log multiple error messages for an error because we're
|
|
288
|
+
// logging at multiple levels. Can we refactor to log them just once?
|
|
289
|
+
|
|
290
|
+
let { id: initial_id } = await scope.examinePageID( scope, 'none to match' );
|
|
291
|
+
let error_data = await scope.checkForErrors( scope );
|
|
292
|
+
// Saves a lot of time in cases where we must otherwise wait for timeout
|
|
293
|
+
await scope.throw_on_system_error( scope, { error_data, id: initial_id });
|
|
259
294
|
|
|
260
|
-
let start_url = await scope.page.url(); //
|
|
295
|
+
let start_url = await scope.page.url(); // for navigation detection later
|
|
261
296
|
|
|
262
297
|
await scope.tapElementAndStayOnPage(scope, {
|
|
263
298
|
elem,
|
|
@@ -267,30 +302,17 @@ module.exports = {
|
|
|
267
302
|
]
|
|
268
303
|
});
|
|
269
304
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
305
|
+
let navigated = false;
|
|
306
|
+
({ error_data, navigated } = await scope.wait_until_continued( scope, {
|
|
307
|
+
url: start_url, id: initial_id, post_nav_handlers
|
|
308
|
+
}));
|
|
274
309
|
|
|
275
|
-
//
|
|
276
|
-
if ( navigated ) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// If navigation, wait for 200ms to let things finish loading and moving
|
|
283
|
-
await scope.afterStep(scope, {
|
|
284
|
-
waitForTimeout: ( waitForTimeout || 200 ),
|
|
285
|
-
navigated: true
|
|
286
|
-
});
|
|
287
|
-
} else {
|
|
288
|
-
// Discuss: Fail here or later since the test didn't manage to navigate?
|
|
289
|
-
await scope.afterStep(scope, {
|
|
290
|
-
waitForTimeout: ( waitForTimeout || 0 ),
|
|
291
|
-
navigated: false
|
|
292
|
-
});
|
|
293
|
-
}
|
|
310
|
+
// If navigation, wait for 200ms to let things finish loading and moving
|
|
311
|
+
if ( navigated ) { waitForTimeout ??= 200; }
|
|
312
|
+
await scope.afterStep( scope, {
|
|
313
|
+
waitForTimeout: waitForTimeout, navigated,
|
|
314
|
+
});
|
|
315
|
+
return error_data;
|
|
294
316
|
}, // Ends scope.tapElementAndNavigate()
|
|
295
317
|
|
|
296
318
|
nav_race: function(scope, opts={}) {
|
|
@@ -344,16 +366,27 @@ module.exports = {
|
|
|
344
366
|
// for that text too. Also, puppeteer will ensure proper timeout.
|
|
345
367
|
await Promise.race([
|
|
346
368
|
click_promises,
|
|
347
|
-
// This is meant to detect user-visible alerts/errors,
|
|
369
|
+
// This is meant to detect user-visible da or AL alerts/errors,
|
|
348
370
|
// not breaking errors in the testing code.
|
|
349
371
|
// No need for aborters since puppeteer has its own timeout
|
|
350
|
-
|
|
351
|
-
|
|
372
|
+
|
|
373
|
+
// If this accidentally navigates, these will detect that the page has
|
|
374
|
+
// navigated.
|
|
375
|
+
// Discuss: Is intention to stay on this page? So throw on navigation?
|
|
376
|
+
|
|
377
|
+
// Discuss: `validation code` message can be on a page even after user fixed
|
|
378
|
+
// values. Always dismiss `validation code` before trying to continue? Or
|
|
379
|
+
// just detect navigation through requests and responses, which we need to
|
|
380
|
+
// switch to anyway.
|
|
381
|
+
// TODO: Do we need to check these elements' bounding boxes first since
|
|
382
|
+
// puppeteer doesn't recognize if parents are hidden?
|
|
383
|
+
scope.page.waitForSelector(`#daflash .alert-danger`, { visible: true }),
|
|
352
384
|
scope.page.waitForSelector('.da-has-error', { visible: true }),
|
|
353
385
|
]);
|
|
354
386
|
} catch ( error ) {
|
|
355
387
|
if ( error.name === `TimeoutError` ) {
|
|
356
|
-
|
|
388
|
+
// TODO: Discuss - this is a navigation timeout even though the tap shouldn't navigate
|
|
389
|
+
let non_reload_report_msg = `After ALKiln tapped "${ tapperText }" it took too long to detect a change on the page.`;
|
|
357
390
|
await scope.handle_page_timeout_error( scope, { non_reload_report_data: {
|
|
358
391
|
code: `ALK0103`,
|
|
359
392
|
message: non_reload_report_msg,
|
|
@@ -361,8 +394,8 @@ module.exports = {
|
|
|
361
394
|
} else {
|
|
362
395
|
// Throw any non-timeout error
|
|
363
396
|
let err_msg = `An error occurred when ALKiln tried to tap "${ tapperText }".`
|
|
364
|
-
reports.addToReport( scope, { type: `error`, code: `ALK0104`, value: err_msg });
|
|
365
|
-
throw
|
|
397
|
+
let rpt_msg = reports.addToReport( scope, { type: `error`, code: `ALK0104`, value: [ err_msg, error ].join(`\n`) });
|
|
398
|
+
throw Error( rpt_msg );
|
|
366
399
|
} // ends if error is timeout error
|
|
367
400
|
|
|
368
401
|
} // ends try/catch
|
|
@@ -557,25 +590,39 @@ module.exports = {
|
|
|
557
590
|
return false;
|
|
558
591
|
},
|
|
559
592
|
|
|
593
|
+
// ⚠️ Any selectors I find seem somewhat precarious.
|
|
560
594
|
continue_button_selector_da_v_1_9_n_and_earlier: `fieldset.da-field-buttons button[type="submit"].btn-primary`,
|
|
561
595
|
continue_button_selector_da_v_1_10_n_and_later: `button[type="submit"]#da-continue-button`,
|
|
562
596
|
continue_button_selector_backup_da_v_1_9_n_and_earlier: `.da-fieldset.da-field-buttons button[type="submit"].btn-primary`,
|
|
597
|
+
// Discuss: Use only the broadest selector or keep these first 2 as well?
|
|
563
598
|
signature_cont_btn_selector_da_v_1_9_n_and_earlier: `fieldset .dasigsave`,
|
|
564
|
-
|
|
565
599
|
signature_cont_btn_selector_backup_da_v_1_9_n_and_earlier: `.da-fieldset .dasigsave`,
|
|
566
600
|
// ⚠️ docassemble >= 1.10.n, 2 signature page continue buttons. One visible on
|
|
567
601
|
// wide screen, one visible on narrow screen. We need to get the visible
|
|
568
602
|
// one, but puppeteer v22.15.0 doesn't recognize an element with a hidden
|
|
569
603
|
// ancestor as invisible, so we need to get both `.dasigsave` and test for
|
|
570
|
-
// visibility
|
|
604
|
+
// visibility with the bounding box for now.
|
|
571
605
|
signature_cont_btn_selector_da_v_1_10_n_and_later: `.dasigsave`,
|
|
572
|
-
continue: async function ( scope ) {
|
|
606
|
+
continue: async function ( scope, { post_nav_handlers=null }={}) {
|
|
573
607
|
/**
|
|
574
608
|
* Presses whatever button it finds that might lead to the next page.
|
|
575
609
|
*
|
|
576
|
-
* @param {
|
|
610
|
+
* @param {Object} scope - Tests state object
|
|
611
|
+
* @param {Object} obj - Named arguments
|
|
612
|
+
* @param {[function]} [obj.post_nav_handlers] - Functions to handle page
|
|
613
|
+
* errors like system errors or invalid answers' markers
|
|
614
|
+
*
|
|
615
|
+
* @returns {Object} obj - Named return values
|
|
616
|
+
* @returns {bool} obj.was_found - True if the check found any errors
|
|
617
|
+
* @returns {bool} obj.ok - False if the check found any errors. Discuss:
|
|
618
|
+
* Should we only use `ok` for internal errors?
|
|
619
|
+
* @param {user|system} obj.type - "user" with invalid answers, "system"
|
|
620
|
+
* with da or AL server errors
|
|
621
|
+
* @param {[ElementHandle]} obj.error_handles - List of puppeteer element
|
|
622
|
+
* handles the check found. There can be multiple invalid answer elems.
|
|
577
623
|
* */
|
|
578
|
-
|
|
624
|
+
await scope.log_and_update_if_new_page_id( scope );
|
|
625
|
+
|
|
579
626
|
let elem_lists = [
|
|
580
627
|
// non-signature pages (these are the most consistent ways)
|
|
581
628
|
await scope.page.$$(scope.continue_button_selector_da_v_1_9_n_and_earlier, { visible: true }),
|
|
@@ -606,7 +653,10 @@ module.exports = {
|
|
|
606
653
|
}
|
|
607
654
|
|
|
608
655
|
// Waits for navigation or user error
|
|
609
|
-
await scope.tapElementAndNavigate( scope, {
|
|
656
|
+
let error_data = await scope.tapElementAndNavigate( scope, {
|
|
657
|
+
elem: continue_button, post_nav_handlers
|
|
658
|
+
});
|
|
659
|
+
return error_data;
|
|
610
660
|
}, // Ends scope.continue()
|
|
611
661
|
|
|
612
662
|
continue_exists: async function ( scope ) {
|
|
@@ -632,7 +682,6 @@ module.exports = {
|
|
|
632
682
|
let signature_da_v_1_10_x = await scope.page.$(scope.signature_cont_btn_selector_da_v_1_10_n_and_later, { visible: true });
|
|
633
683
|
|
|
634
684
|
return regular !== null || regular_da_v_1_10_x !== null || regular_backup !== null || signature !== null || signature_backup !== null || signature_da_v_1_10_x !== null;
|
|
635
|
-
|
|
636
685
|
}, // Ends scope.continue_exists()
|
|
637
686
|
|
|
638
687
|
tapTab: async function ( scope, tab_id ) {
|
|
@@ -646,8 +695,9 @@ module.exports = {
|
|
|
646
695
|
await scope.waitForTab( scope, { tab_id });
|
|
647
696
|
},
|
|
648
697
|
|
|
649
|
-
checkForA11y: async function (scope ) {
|
|
698
|
+
checkForA11y: async function ( scope ) {
|
|
650
699
|
/** Return if there were any violations and the file containing aXe output with more details of the checks */
|
|
700
|
+
// Discuss: What problem was the page id solving? When was there a mismatch?
|
|
651
701
|
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
652
702
|
if ( scope.page_id !== id ) {
|
|
653
703
|
reports.addToReport(scope, { type: `page_id info`, code: `ALK0108`, value: `${ id }`, });
|
|
@@ -682,13 +732,30 @@ module.exports = {
|
|
|
682
732
|
},
|
|
683
733
|
|
|
684
734
|
tapSelectorToNavigate: async function ( scope, selector, waitForTimeout ) {
|
|
735
|
+
/**
|
|
736
|
+
* Taps an element matching the selector string, expecting to navigate.
|
|
737
|
+
*
|
|
738
|
+
* @param {Object} scope - Tests state object
|
|
739
|
+
* @param {str} selector - Selector of the element to tap
|
|
740
|
+
* @param {int} waitForTimeout - Max seconds before timing out
|
|
741
|
+
*
|
|
742
|
+
* @returns {Object} obj - Named return values
|
|
743
|
+
* @returns {bool} obj.was_found - True if the check found any errors
|
|
744
|
+
* @returns {bool} obj.ok - False if the check found any errors. Discuss:
|
|
745
|
+
* Should we only use `ok` for internal errors?
|
|
746
|
+
* @param {user|system} obj.type - "user" with invalid answers, "system"
|
|
747
|
+
* with da or AL server errors
|
|
748
|
+
* @param {[ElementHandle]} obj.error_handles - List of puppeteer element
|
|
749
|
+
* handles the check found. There can be multiple invalid answer elems.
|
|
750
|
+
* */
|
|
685
751
|
let elem = await scope.page.$(`${ selector }`);
|
|
686
|
-
let error_msg = `Couldn't find "${ selector }" on the page
|
|
752
|
+
let error_msg = `Couldn't find "${ selector }" on the page. Is there a typo?`;
|
|
687
753
|
if ( !elem ) {
|
|
688
754
|
reports.addToReport(scope, { type: `error`, code: `ALK0111`, value: error_msg });
|
|
689
755
|
}
|
|
690
756
|
expect( elem, error_msg ).to.exist;
|
|
691
|
-
await scope.tapElementAndNavigate( scope, { elem, waitForTimeout });
|
|
757
|
+
let error_data = await scope.tapElementAndNavigate( scope, { elem, waitForTimeout });
|
|
758
|
+
return error_data;
|
|
692
759
|
},
|
|
693
760
|
|
|
694
761
|
tapSelectorAndStay: async function ( scope, { selector }) {
|
|
@@ -1132,7 +1199,6 @@ module.exports = {
|
|
|
1132
1199
|
// TODO: Do we need daaota as well? ("all of the above")
|
|
1133
1200
|
}
|
|
1134
1201
|
|
|
1135
|
-
// Am I doing this wrong? the guesses aren't just the name, right?
|
|
1136
1202
|
let var_names = await scope.getPossibleDecodedNames( scope, {
|
|
1137
1203
|
not_decoded: encoded_name,
|
|
1138
1204
|
field_like_names,
|
|
@@ -1155,6 +1221,8 @@ module.exports = {
|
|
|
1155
1221
|
values.push( node.attribs.value );
|
|
1156
1222
|
}
|
|
1157
1223
|
|
|
1224
|
+
// TODO: Try replacing null coalescing with using the guesses as var
|
|
1225
|
+
// names.
|
|
1158
1226
|
field_data.guesses ??= await scope.add_values_if_needed( scope, { var_names, values });
|
|
1159
1227
|
if ( scope.is_button_ish( scope, { field: field_data }) ) {
|
|
1160
1228
|
// Discuss: This makes sure buttons are matched with table rows last. Is
|
|
@@ -1195,12 +1263,21 @@ module.exports = {
|
|
|
1195
1263
|
* */
|
|
1196
1264
|
let question_data = { ok: false, status: -1 };
|
|
1197
1265
|
let question_response = null;
|
|
1198
|
-
let err_msg = `Docassemble couldn't process this page's JSON. ALKiln will`
|
|
1199
|
-
+ ` do its best, but may fail. We would love a report for this.`;
|
|
1200
1266
|
|
|
1201
1267
|
try {
|
|
1202
|
-
question_response = await scope?.page?.evaluate?.( async (
|
|
1268
|
+
question_response = await scope?.page?.evaluate?.( async () => {
|
|
1203
1269
|
/**
|
|
1270
|
+
* Return da "question data" from the server for the current page
|
|
1271
|
+
*
|
|
1272
|
+
* ‼️ On system error caused by `GET` request, AL's da v1.9.n first
|
|
1273
|
+
* detects the error, but it stays invisible to the DOM since the
|
|
1274
|
+
* page hasn't been reloaded. Then, when the test presses to
|
|
1275
|
+
* continue, a new error appears. Unfortunately, the old error is
|
|
1276
|
+
* lost, so we won't be able to retrieve it through the DOM. We may
|
|
1277
|
+
* want to switch to catching errors during the field-getting part
|
|
1278
|
+
* of the life-cycle if we want to be able to handle this robustly.
|
|
1279
|
+
* I'm not sure what other issues will arise at that point.
|
|
1280
|
+
*
|
|
1204
1281
|
* @returns {object} -
|
|
1205
1282
|
* { ok: false }; // ok -> response status or false
|
|
1206
1283
|
* | { ok, status, ...data, ...( await response.json() ) }
|
|
@@ -1208,7 +1285,9 @@ module.exports = {
|
|
|
1208
1285
|
* | { ok, status, alk_msg: err_msg }
|
|
1209
1286
|
* | { ok: false, alk_msg: err_msg, error: response_err.message }
|
|
1210
1287
|
* */
|
|
1211
|
-
let
|
|
1288
|
+
let q_resp_err_msg = `Docassemble couldn't process this page's JSON. ALKiln's`
|
|
1289
|
+
+ ` will do its best, but may fail. We would love a report for this.`;
|
|
1290
|
+
let data = { ok: false, alk_msg: null };
|
|
1212
1291
|
try {
|
|
1213
1292
|
let response = await fetch(
|
|
1214
1293
|
window.location.href.replace(`interview?`, `interview?json=1&`)
|
|
@@ -1217,41 +1296,29 @@ module.exports = {
|
|
|
1217
1296
|
data.ok = response.ok;
|
|
1218
1297
|
data.status = response.status;
|
|
1219
1298
|
|
|
1220
|
-
// ‼️ On system error caused by `GET` request, AL's da v1.9.n first
|
|
1221
|
-
// detects the error, but it stays invisible to the DOM since the
|
|
1222
|
-
// page hasn't been reloaded. Then, when the test presses to
|
|
1223
|
-
// continue, a new error appears. Unfortunately, the old error is
|
|
1224
|
-
// lost, so we won't be able to retrieve it through the DOM. We may
|
|
1225
|
-
// want to switch to catching errors during the field-getting part
|
|
1226
|
-
// of the life-cycle if we want to be able to handle this robustly.
|
|
1227
|
-
// I'm not sure what other issues will arise at that point.
|
|
1228
|
-
|
|
1229
1299
|
let content_type = response?.headers?.get?.(`content-type`);
|
|
1230
1300
|
if ( content_type?.startsWith?.(`application/json`) ) {
|
|
1231
1301
|
return { ...data, ...( await response.json() ) };
|
|
1232
1302
|
|
|
1233
1303
|
} else if ( content_type?.startsWith?.(`text/plain`) ) {
|
|
1234
|
-
|
|
1235
|
-
// doesn't handle, making its error page unhelpful. In future, we
|
|
1236
|
-
// do more with the actual page response.
|
|
1237
|
-
return { ...data, alk_msg: err_msg, html: await response.text() };
|
|
1304
|
+
return { ...data, alk_msg: q_resp_err_msg, html: await response.text() };
|
|
1238
1305
|
|
|
1239
1306
|
} else {
|
|
1240
|
-
// Research: How can we get in here? What other content-types that
|
|
1241
|
-
// could get give response ok?
|
|
1242
|
-
return { ...data, alk_msg:
|
|
1307
|
+
// Research: How can we get in here? What other content-types that
|
|
1308
|
+
// we could get give response ok?
|
|
1309
|
+
return { ...data, alk_msg: q_resp_err_msg, content_type };
|
|
1243
1310
|
} // ends content-type check
|
|
1244
1311
|
|
|
1245
1312
|
} catch ( response_err ) {
|
|
1246
1313
|
// Error might be caused by non-da page author navigated to when, for
|
|
1247
1314
|
// example, testing an "Exit" destination.
|
|
1248
|
-
return { ok: false, alk_msg:
|
|
1315
|
+
return { ok: false, alk_msg: q_resp_err_msg, error: response_err.message, }
|
|
1249
1316
|
} // ends fetch try
|
|
1250
1317
|
|
|
1251
1318
|
return data;
|
|
1252
|
-
}
|
|
1319
|
+
} ); // ends page.evaluate() call
|
|
1253
1320
|
} catch ( question_data_error ) {
|
|
1254
|
-
question_response = { ok: false, alk_msg:
|
|
1321
|
+
question_response = { ok: false, alk_msg: q_resp_err_msg, error: question_data_error.message, }
|
|
1255
1322
|
}
|
|
1256
1323
|
|
|
1257
1324
|
question_data = question_response || question_data;
|
|
@@ -1444,7 +1511,13 @@ module.exports = {
|
|
|
1444
1511
|
// Really need everything to be unique here if at all possible. Shall we resort
|
|
1445
1512
|
// to parents and n-th child?
|
|
1446
1513
|
// Exceptions: Some specific fields can be duplicates without being dangerous
|
|
1447
|
-
if (
|
|
1514
|
+
if (
|
|
1515
|
+
matching_nodes.length > 1
|
|
1516
|
+
&& !selector.includes(`[class="file-caption-name"]`)
|
|
1517
|
+
&& !selector.includes(`fileinput-cancel`) // File upload
|
|
1518
|
+
&& !selector.includes(`fileinput-remove`) // File upload
|
|
1519
|
+
&& !selector.includes(`kv-file`) // File upload
|
|
1520
|
+
) {
|
|
1448
1521
|
let msg = `${ matching_nodes.length } items on this page matched the base64 encoded name '${ selector }'. You may be setting the same variable in multiple places on this page.`;
|
|
1449
1522
|
reports.addToReport(scope, { type: `warning`, code: `ALK0125`, value: msg });
|
|
1450
1523
|
}
|
|
@@ -1946,7 +2019,7 @@ module.exports = {
|
|
|
1946
2019
|
);
|
|
1947
2020
|
if ( final_matching_rows.length == 0) {
|
|
1948
2021
|
log.debug({ code: `ALK0132`, context: `matching`, },
|
|
1949
|
-
`0 matching rows
|
|
2022
|
+
`0 matching rows for this field on the page:`, field
|
|
1950
2023
|
);
|
|
1951
2024
|
}
|
|
1952
2025
|
|
|
@@ -2046,7 +2119,6 @@ module.exports = {
|
|
|
2046
2119
|
// Pass in the selector as an argument
|
|
2047
2120
|
}, field.selector );
|
|
2048
2121
|
|
|
2049
|
-
|
|
2050
2122
|
return handle;
|
|
2051
2123
|
}, // Ends scope.get_handle_from_field()
|
|
2052
2124
|
|
|
@@ -2180,10 +2252,12 @@ module.exports = {
|
|
|
2180
2252
|
|
|
2181
2253
|
enter_answer: {
|
|
2182
2254
|
button: async function ( scope, { handle, answer }) {
|
|
2255
|
+
// For ambiguous tap discussion, see https://github.com/SuffolkLITLab/ALKiln/issues/1127
|
|
2183
2256
|
await scope.tapElementAndNavigate( scope, { elem: handle });
|
|
2184
2257
|
return true;
|
|
2185
2258
|
},
|
|
2186
2259
|
a: async function ( scope, { handle, answer }) {
|
|
2260
|
+
// For ambiguous tap discussion, see https://github.com/SuffolkLITLab/ALKiln/issues/1127
|
|
2187
2261
|
await scope.tapElementAndNavigate( scope, { elem: handle });
|
|
2188
2262
|
return true;
|
|
2189
2263
|
},
|
|
@@ -2256,7 +2330,7 @@ module.exports = {
|
|
|
2256
2330
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
2257
2331
|
|
|
2258
2332
|
} else if ( type === `file` ) {
|
|
2259
|
-
row_was_used = await scope.
|
|
2333
|
+
row_was_used = await scope.upload_files( scope, { handle, answer });
|
|
2260
2334
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
2261
2335
|
|
|
2262
2336
|
} else if (type == `date`) {
|
|
@@ -2336,7 +2410,7 @@ module.exports = {
|
|
|
2336
2410
|
return valid_date;
|
|
2337
2411
|
}, // Ends scope.today_to_date()
|
|
2338
2412
|
|
|
2339
|
-
|
|
2413
|
+
find_files: async function( scope, {to_find_names}) {
|
|
2340
2414
|
/** Returns a list of paths to the sources folder associated
|
|
2341
2415
|
* with the given list of file names.
|
|
2342
2416
|
*
|
|
@@ -2393,9 +2467,9 @@ module.exports = {
|
|
|
2393
2467
|
return [];
|
|
2394
2468
|
}
|
|
2395
2469
|
return paths;
|
|
2396
|
-
}, // Ends scope.
|
|
2470
|
+
}, // Ends scope.find_files()
|
|
2397
2471
|
|
|
2398
|
-
|
|
2472
|
+
upload_files: async function ( scope, { handle, answer }) {
|
|
2399
2473
|
/* Try to upload one or more files to a file-type input field.
|
|
2400
2474
|
* Does not submit, so no danger of timeout with server reload.
|
|
2401
2475
|
*
|
|
@@ -2405,8 +2479,8 @@ module.exports = {
|
|
|
2405
2479
|
// First make sure the list of files was defined. Don't remove spaces yet -
|
|
2406
2480
|
// allow a typo where someone forgot to include a space after a comma.
|
|
2407
2481
|
let to_find_names = answer.split(`,`);
|
|
2408
|
-
let paths = await scope.
|
|
2409
|
-
if (paths.length == 0) {
|
|
2482
|
+
let paths = await scope.find_files(scope, {to_find_names});
|
|
2483
|
+
if ( paths.length == 0 ) {
|
|
2410
2484
|
// The row wasn't used
|
|
2411
2485
|
return false;
|
|
2412
2486
|
}
|
|
@@ -2418,16 +2492,17 @@ module.exports = {
|
|
|
2418
2492
|
// it's not completely necessary. If the gobal `setTimeout` fires then the
|
|
2419
2493
|
// `evaluate` won't be far behind. They both use about the same timeout
|
|
2420
2494
|
// duration.
|
|
2421
|
-
let finish_uploading = handle.evaluate( async function
|
|
2495
|
+
let finish_uploading = handle.evaluate( async function (
|
|
2422
2496
|
elem, timeout, answer
|
|
2423
2497
|
) {
|
|
2424
2498
|
return new Promise (( resolve, reject ) => {
|
|
2425
2499
|
/**
|
|
2426
2500
|
* Resolve when upload completes. Logging in here will log to the hidden
|
|
2427
2501
|
* browser. Must instead pass information out with `resolve`.
|
|
2502
|
+
*
|
|
2503
|
+
* Warn about uploading the same file multiple times. It won't protect
|
|
2504
|
+
* against accidentally uploading same file to a different field.
|
|
2428
2505
|
* */
|
|
2429
|
-
// Warn about uploading the same file multiple times
|
|
2430
|
-
// Won't protect against accidentally uploading same file to diff field
|
|
2431
2506
|
|
|
2432
2507
|
// If elem.files, the author has previously set this field. Warn them.
|
|
2433
2508
|
// Example prev_filenames: "C:\\fakepath\\name_of_file.png"
|
|
@@ -2447,13 +2522,13 @@ module.exports = {
|
|
|
2447
2522
|
|
|
2448
2523
|
let result = {
|
|
2449
2524
|
ok: false, prev_filenames, type: null,
|
|
2450
|
-
code: `ALK0283`, level: `
|
|
2525
|
+
code: `ALK0283`, level: `info`, msgs: [], error: null,
|
|
2451
2526
|
};
|
|
2452
2527
|
|
|
2453
|
-
if ( prev_filenames ) {
|
|
2528
|
+
if (( prev_filenames?.length || 0) > 0 ) {
|
|
2454
2529
|
result.level = `warn`; // log level
|
|
2455
2530
|
result.type = `warning`; // report type
|
|
2456
|
-
shared_names = shared_names_list.join(`", "`);
|
|
2531
|
+
let shared_names = shared_names_list.join(`", "`);
|
|
2457
2532
|
|
|
2458
2533
|
if ( shared_names_list.length > 0 ) {
|
|
2459
2534
|
if ( all_matched ) {
|
|
@@ -2471,9 +2546,10 @@ module.exports = {
|
|
|
2471
2546
|
|
|
2472
2547
|
let timeout_id = null;
|
|
2473
2548
|
let resolve_on_change = function() {
|
|
2549
|
+
// bootstrap-fileinput starts reading
|
|
2474
2550
|
result.ok = true; // May contain warnings
|
|
2475
2551
|
clearTimeout( timeout_id );
|
|
2476
|
-
result.msgs.push(`
|
|
2552
|
+
result.msgs.push(`Started uploading these files: "${ answer }"`);
|
|
2477
2553
|
resolve( result );
|
|
2478
2554
|
};
|
|
2479
2555
|
|
|
@@ -2494,7 +2570,7 @@ module.exports = {
|
|
|
2494
2570
|
}); // ends Promise
|
|
2495
2571
|
|
|
2496
2572
|
}, scope.timeout - 100, answer ); // ends page.evaluate()
|
|
2497
|
-
|
|
2573
|
+
|
|
2498
2574
|
await handle.uploadFile(...paths);
|
|
2499
2575
|
|
|
2500
2576
|
// Wait for upload to finish. If already done, execution will continue
|
|
@@ -2515,9 +2591,31 @@ module.exports = {
|
|
|
2515
2591
|
if ( level === `throw` ) { log_type = `throw`; }
|
|
2516
2592
|
log[ log_type ]({ code, level, error, context: `file_upload`}, ...msgs );
|
|
2517
2593
|
|
|
2594
|
+
// LLM Claude (version?) description: While the bootstrap-fileinput is
|
|
2595
|
+
// reading, it disables the input, adds `is-locked` to its container
|
|
2596
|
+
// (lock()), and adds `file-thumb-loading` (_toggleLoading("show")). We'll
|
|
2597
|
+
// wait till these are gone. See GitHub #1125. @plocket tested these
|
|
2598
|
+
// classes out and said they seem legit.
|
|
2599
|
+
await scope.page.waitForFunction(
|
|
2600
|
+
( el ) => {
|
|
2601
|
+
const container = el.closest(`.file-input`);
|
|
2602
|
+
return (
|
|
2603
|
+
!el.disabled
|
|
2604
|
+
&& !( container?.classList?.contains?.(`is-locked`)
|
|
2605
|
+
|| container?.classList?.contains?.(`file-thumb-loading`)
|
|
2606
|
+
));
|
|
2607
|
+
},
|
|
2608
|
+
{ timeout: scope.timeout - 100 },
|
|
2609
|
+
handle,
|
|
2610
|
+
);
|
|
2611
|
+
|
|
2612
|
+
log.debug({ code: `ALK0297`, context: `file_upload`, level: `info`, },
|
|
2613
|
+
`Finished uploading files`
|
|
2614
|
+
);
|
|
2615
|
+
|
|
2518
2616
|
// Return whether successfully used row
|
|
2519
2617
|
return ok;
|
|
2520
|
-
}, // Ends scope.
|
|
2618
|
+
}, // Ends scope.upload_files()
|
|
2521
2619
|
|
|
2522
2620
|
setCheckbox: async function ( scope, { label, answer }) {
|
|
2523
2621
|
// Depending on the current value/status of a checkbox and the desired
|
|
@@ -2622,32 +2720,28 @@ module.exports = {
|
|
|
2622
2720
|
return risky_id.toLowerCase().replace(/[^A-Za-z0-9]+/g, '-');
|
|
2623
2721
|
},
|
|
2624
2722
|
|
|
2625
|
-
examinePageID: async function ( scope,
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
let
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
// TODO: What are good names for these properties?
|
|
2648
|
-
// question_has_id? found_any_id?
|
|
2649
|
-
// class_matches_id? class_and_id_do_match? id_and_page_id_match?
|
|
2650
|
-
return { question_has_id: found_any_question_id, id: id, id_matches: ids_match };
|
|
2723
|
+
examinePageID: async function ( scope, page_id = `` ) {
|
|
2724
|
+
/** Looks for a sanitized version of the question id as it's written
|
|
2725
|
+
* in the .yml. docassemble's way in python:
|
|
2726
|
+
* re.sub(r'[^A-Za-z0-9]+', '-', interview_status.question.id.lower())
|
|
2727
|
+
*
|
|
2728
|
+
* TODO: Replace use of this method with refactored functionality in
|
|
2729
|
+
* `current_id_matches`, `id_as_class_name`, and
|
|
2730
|
+
* `current_page_id_class_name`
|
|
2731
|
+
*
|
|
2732
|
+
* @param {Object} scope - Tests state object
|
|
2733
|
+
* @param {Object} obj - Named arguments
|
|
2734
|
+
* @param {str} obj.id_to_match - A docassemble page id, possibly in .yml
|
|
2735
|
+
* format
|
|
2736
|
+
*
|
|
2737
|
+
* @returns { question_has_id: bool, id: str, id_matches: bool }
|
|
2738
|
+
* */
|
|
2739
|
+
// let id_to_match = page_id.toLowerCase().replace(/[^A-Za-z0-9]+/g, '-');
|
|
2740
|
+
let id_to_match = await scope.id_as_class_name( scope, { page_id });
|
|
2741
|
+
let current_id = await scope.current_page_id_class_name( scope );
|
|
2742
|
+
let question_has_id = !!current_id || false;
|
|
2743
|
+
let id_matches = id_to_match === current_id;
|
|
2744
|
+
return { id: current_id, question_has_id, id_matches };
|
|
2651
2745
|
}, // Ends scope.examinePageID()
|
|
2652
2746
|
|
|
2653
2747
|
// TODO: Rename var_data to input_data?
|
|
@@ -2659,7 +2753,7 @@ module.exports = {
|
|
|
2659
2753
|
ensure_navigation=true,
|
|
2660
2754
|
ensure_all_vars_used=false
|
|
2661
2755
|
}) {
|
|
2662
|
-
|
|
2756
|
+
/** Non-recursive function to set as many variables as possible before
|
|
2663
2757
|
* continuing to the next page. Important to avoid recursion because of async ops.
|
|
2664
2758
|
* If it's not data in a table, the code can be more permissive about some input.
|
|
2665
2759
|
*
|
|
@@ -2669,19 +2763,23 @@ module.exports = {
|
|
|
2669
2763
|
* if navigation is not triggered another way.
|
|
2670
2764
|
* @param ensure_all_vars_used {bool} [false] = Default false. If true, errors if any
|
|
2671
2765
|
* object in var_data is not used during running of this function.
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2766
|
+
*
|
|
2767
|
+
* @returns {Object} obj - Named return values
|
|
2768
|
+
* @returns {bool} obj.was_found - True if the check found any errors
|
|
2769
|
+
* @returns {bool} obj.ok - False if the check found any errors. Discuss:
|
|
2770
|
+
* Should we only use `ok` for internal errors?
|
|
2771
|
+
* @param {user|system} obj.type - "user" with invalid answers, "system"
|
|
2772
|
+
* with da or AL server errors
|
|
2773
|
+
* @param {[ElementHandle]} obj.error_handles - List of puppeteer element
|
|
2774
|
+
* handles the check found. There can be multiple invalid answer elems.
|
|
2775
|
+
* */
|
|
2776
|
+
log.debug({ code: `ALK0221`, before: `\n~~~~~~~~~~~~~~\n`, },
|
|
2777
|
+
`Starting scope.setFields()`
|
|
2675
2778
|
);
|
|
2676
2779
|
|
|
2677
2780
|
let error_data = null;
|
|
2678
2781
|
|
|
2679
|
-
|
|
2680
|
-
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
2681
|
-
if ( scope.page_id !== id ) {
|
|
2682
|
-
reports.addToReport(scope, { type: `page_id info`, code: `ALK0138`, value: `${ id }`, });
|
|
2683
|
-
scope.page_id = id;
|
|
2684
|
-
}
|
|
2782
|
+
let id = await scope.log_and_update_if_new_page_id( scope );
|
|
2685
2783
|
|
|
2686
2784
|
let html = await scope.page.content();
|
|
2687
2785
|
// These are the fields on the current page. Their handles should all exist.
|
|
@@ -2692,7 +2790,6 @@ module.exports = {
|
|
|
2692
2790
|
// again until nothing more can be set.
|
|
2693
2791
|
let something_new_was_set = true; // So loop can start
|
|
2694
2792
|
while ( something_new_was_set ) {
|
|
2695
|
-
|
|
2696
2793
|
something_new_was_set = false; // start fresh
|
|
2697
2794
|
// Go through each field on the page, seeing if we can set its variable
|
|
2698
2795
|
for ( let field of fields ) {
|
|
@@ -2728,13 +2825,11 @@ module.exports = {
|
|
|
2728
2825
|
if ( session_vars.get_debug() ) { await time.waitForTimeout(2000); }
|
|
2729
2826
|
|
|
2730
2827
|
// Press a continue button with a story table variable name if it exists
|
|
2731
|
-
let
|
|
2732
|
-
let error = {};
|
|
2828
|
+
let navigated = false;
|
|
2733
2829
|
for ( let field of fields ) {
|
|
2734
2830
|
// All other fields should have been set
|
|
2735
2831
|
if ( scope.is_button_ish( scope, { field }) && field.type === 'submit' ) {
|
|
2736
2832
|
let page_url = await scope.page.url();
|
|
2737
|
-
|
|
2738
2833
|
let row_used = await scope.setVariable( scope, { field, var_data, });
|
|
2739
2834
|
if ( row_used ) {
|
|
2740
2835
|
|
|
@@ -2746,23 +2841,12 @@ module.exports = {
|
|
|
2746
2841
|
|
|
2747
2842
|
// Esp important for `.target_number`
|
|
2748
2843
|
await scope.increment_row_use( scope, { row: row_used });
|
|
2844
|
+
({ error_data, navigated } = await scope.wait_until_continued( scope, {
|
|
2845
|
+
url: page_url, id
|
|
2846
|
+
}));
|
|
2749
2847
|
|
|
2750
|
-
//
|
|
2751
|
-
|
|
2752
|
-
// Have to catch errors right now or may get stuck in an infinite loop?
|
|
2753
|
-
// TODO: Don't catch errors - maybe the developer expected/wanted an error
|
|
2754
|
-
// For now they'll have to stop the story table early and do the rest the slow way
|
|
2755
|
-
// TODO: Discuss: add the option of throwing when an error happens? Per page?
|
|
2756
|
-
|
|
2757
|
-
// The developer said to continue by defining this row.
|
|
2758
|
-
// For now, that means this page should not cause any kind of error.
|
|
2759
|
-
// Make sure no system or user error messages appear
|
|
2760
|
-
// Discuss moving this out of here. Having to pass in `id` just for
|
|
2761
|
-
// these messages smells bad. Con: Already a lot of code out there.
|
|
2762
|
-
error_data = await scope.waitUntilContinued( scope, { url: page_url, id });
|
|
2763
|
-
|
|
2764
|
-
did_navigate = true;
|
|
2765
|
-
log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
|
|
2848
|
+
// pressed button to set var successfully
|
|
2849
|
+
log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`);
|
|
2766
2850
|
break;
|
|
2767
2851
|
|
|
2768
2852
|
} // ends if row_used
|
|
@@ -2780,43 +2864,35 @@ module.exports = {
|
|
|
2780
2864
|
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
2781
2865
|
let msg = `In a linear Step all the values you give MUST be filled in, but ALKiln was unable `
|
|
2782
2866
|
+ `to find a field on the page with the "${ id }" for the variable "${ original_row.var }" that `
|
|
2783
|
-
+ `could be set to "${ original_row.value }".
|
|
2784
|
-
|
|
2785
|
-
|
|
2867
|
+
+ `could be set to "${ original_row.value }". Alternatively, you`
|
|
2868
|
+
+ ` may have duplicate rows in your table. See this Scenario's HTML`
|
|
2869
|
+
+ ` file for details.`;
|
|
2870
|
+
let err_msg = reports.addToReport(scope, { type: `error`, code: `ALK0141`, value: msg });
|
|
2871
|
+
throw err_msg;
|
|
2786
2872
|
}
|
|
2787
2873
|
}
|
|
2788
2874
|
}
|
|
2789
2875
|
|
|
2790
2876
|
// If didn't already press a continue button, press one now.
|
|
2791
|
-
if ( !
|
|
2792
|
-
// All fields on this page should
|
|
2877
|
+
if ( !navigated && ensure_navigation ) {
|
|
2878
|
+
// All fields on this page should have been taken care of already
|
|
2793
2879
|
let page_url = await scope.page.url();
|
|
2794
|
-
await scope.continue( scope );
|
|
2795
|
-
// Have to catch errors right now or may get stuck in an infinite loop?
|
|
2796
|
-
// TODO: Don't catch errors - maybe the developer expected/wanted an error
|
|
2797
|
-
// For now they'll have to stop the story table early and do the rest the slow way
|
|
2798
|
-
// TODO: Discuss: add the option of throwing when an error happens? Per page?
|
|
2799
|
-
|
|
2800
|
-
// Make sure no system or user error messages appear
|
|
2801
|
-
// Discuss moving this out of here. Having to pass in `id` just for
|
|
2802
|
-
// these messages smells bad. Con: Already a lot of code out there.
|
|
2880
|
+
error_data = await scope.continue( scope );
|
|
2803
2881
|
|
|
2804
|
-
|
|
2882
|
+
navigated = true;
|
|
2883
|
+
log.stdout({}, `\x1b[36m${ '>' }\x1b[0m`); // continued successfully automatically
|
|
2884
|
+
} // ends if !navigated && ensure_navigation
|
|
2805
2885
|
|
|
2806
|
-
|
|
2807
|
-
log.stdout({}, `\x1b[36m${ '>' }\x1b[0m`); // continued successfully
|
|
2808
|
-
} // ends if !did_navigate && ensure_navigation
|
|
2809
|
-
|
|
2810
|
-
if ( did_navigate && scope.check_all_for_a11y ) {
|
|
2886
|
+
if ( navigated && scope.check_all_for_a11y ) {
|
|
2811
2887
|
let { passed } = await scope.checkForA11y(scope);
|
|
2812
2888
|
if (!passed) {
|
|
2813
2889
|
scope.passed_all_for_a11y = false;
|
|
2814
2890
|
}
|
|
2815
2891
|
}
|
|
2816
2892
|
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2893
|
+
log.debug({ code: `ALK0288`, context: `scenario`, level: `note`,
|
|
2894
|
+
before: `\n~~~~~~~~~~~~~~\n` }, `Finished scope.setFields()`
|
|
2895
|
+
);
|
|
2820
2896
|
return error_data;
|
|
2821
2897
|
// TODO: Maybe we should return something else or something in addition about used vars
|
|
2822
2898
|
}, // Ends scope.setFields()
|
|
@@ -2824,7 +2900,8 @@ module.exports = {
|
|
|
2824
2900
|
is_button_ish: function ( scope, { field = null }) {
|
|
2825
2901
|
/**
|
|
2826
2902
|
* Return true if the field is similar enough to a button, else false. This
|
|
2827
|
-
* does NOT determine whether the field submits the form
|
|
2903
|
+
* does NOT determine whether the field submits the form, but does
|
|
2904
|
+
* assume the intention is navigation.
|
|
2828
2905
|
*
|
|
2829
2906
|
* Discuss: Use "btn" class as well?
|
|
2830
2907
|
*
|
|
@@ -2836,10 +2913,87 @@ module.exports = {
|
|
|
2836
2913
|
if ( field.tag === `button`
|
|
2837
2914
|
|| field.selector.includes(`role="button"`)
|
|
2838
2915
|
|| field.selector.includes(`.dasigsave`)
|
|
2916
|
+
|| field.selector.includes(`.daquestionactionbutton`)
|
|
2839
2917
|
) { return true; }
|
|
2840
2918
|
return false;
|
|
2841
2919
|
},
|
|
2842
2920
|
|
|
2921
|
+
log_and_update_if_new_page_id: async function ( scope ) {
|
|
2922
|
+
/**
|
|
2923
|
+
* Log the page id if we're on a new page. Abstracting this avoids adding
|
|
2924
|
+
* multiple log codes.
|
|
2925
|
+
*
|
|
2926
|
+
* Discuss: This triggers 2 puppeteer evals. Only 1 is really necessary. How
|
|
2927
|
+
* many pages id checks would make that speed hit matter? Consider copying
|
|
2928
|
+
* `.current_id_matches()` code in here to get the id just once.
|
|
2929
|
+
*
|
|
2930
|
+
* @param {Object} scope - Tests state object
|
|
2931
|
+
*
|
|
2932
|
+
* @returns {str} - the current page .yml id as an HTML class
|
|
2933
|
+
* */
|
|
2934
|
+
let ids_match = await scope.current_id_matches( scope, {
|
|
2935
|
+
id_to_match: scope.page_id
|
|
2936
|
+
});
|
|
2937
|
+
if ( !ids_match ) {
|
|
2938
|
+
let new_id = await scope.current_page_id_class_name( scope );
|
|
2939
|
+
reports.addToReport(scope, { type: `page_id info`, code: `ALK0138`, value: `${ new_id }`, });
|
|
2940
|
+
scope.page_id = new_id;
|
|
2941
|
+
}
|
|
2942
|
+
return scope.page_id;
|
|
2943
|
+
},
|
|
2944
|
+
|
|
2945
|
+
current_id_matches: async function ( scope, { id_to_match=`` }) {
|
|
2946
|
+
/**
|
|
2947
|
+
* Returns true if the given docassemble page id matches the current page id
|
|
2948
|
+
*
|
|
2949
|
+
* @param {Object} scope - Tests state object
|
|
2950
|
+
* @param {Object} obj - Named arguments
|
|
2951
|
+
* @param {str} obj.id_to_match - A docassemble page id, possibly in .yml
|
|
2952
|
+
* format
|
|
2953
|
+
*
|
|
2954
|
+
* @returns {bool} - true if the ids match, otherwise false. Missing ids will match.
|
|
2955
|
+
* */
|
|
2956
|
+
let class_to_match = scope.id_as_class_name( scope, { page_id: id_to_match });
|
|
2957
|
+
let current_id_class = await scope.current_page_id_class_name( scope );
|
|
2958
|
+
return class_to_match === current_id_class;
|
|
2959
|
+
},
|
|
2960
|
+
|
|
2961
|
+
id_as_class_name: function ( scope, { page_id=`` }) {
|
|
2962
|
+
/**
|
|
2963
|
+
* Converts a given page id string (possibly originating from the interview
|
|
2964
|
+
* .yml) as a class name as docassemble does, then returns that value.
|
|
2965
|
+
* This is useful because the .yml page id is included in the DOM as a
|
|
2966
|
+
* class name. docassemble's python code:
|
|
2967
|
+
*
|
|
2968
|
+
* re.sub(r'[^A-Za-z0-9]+', '-', interview_status.question.id.lower())
|
|
2969
|
+
*
|
|
2970
|
+
* @param {Object} scope - Tests state object
|
|
2971
|
+
* @param {Object} obj - Named arguments
|
|
2972
|
+
* @param {str} obj.page_id - A docassemble page id, possibly in .yml format
|
|
2973
|
+
*
|
|
2974
|
+
* @returns {str | null} - the given str as a docassemble-formatted
|
|
2975
|
+
* HTML class name or `null` if `page_id` is not a string.
|
|
2976
|
+
* */
|
|
2977
|
+
return page_id?.toLowerCase?.().replace(/[^A-Za-z0-9]+/g, `-`) || null;
|
|
2978
|
+
},
|
|
2979
|
+
|
|
2980
|
+
current_page_id_class_name: async function ( scope ) {
|
|
2981
|
+
/**
|
|
2982
|
+
* Returns the current page id as an HTML class name or null if missing
|
|
2983
|
+
*
|
|
2984
|
+
* @param {Object} scope - Tests state object
|
|
2985
|
+
*
|
|
2986
|
+
* @returns {str | null} - Current page id as a class name or `null` if none
|
|
2987
|
+
* exists
|
|
2988
|
+
* */
|
|
2989
|
+
// `.className` is a space-sparated list of the node's classes
|
|
2990
|
+
let possible_id_classes = await scope.page.$eval(`body`, function (elem) {
|
|
2991
|
+
return elem.className
|
|
2992
|
+
});
|
|
2993
|
+
let id_matches = possible_id_classes?.match(/question-([^ ]+)/);
|
|
2994
|
+
return id_matches?.[1] || null;
|
|
2995
|
+
},
|
|
2996
|
+
|
|
2843
2997
|
increment_row_use: async function ( scope, { row }) {
|
|
2844
2998
|
/** Accumulate number of times this row was used. Esp important for `.target_number`.
|
|
2845
2999
|
* Mutates `row` and its props. */
|
|
@@ -3072,105 +3226,369 @@ module.exports = {
|
|
|
3072
3226
|
}
|
|
3073
3227
|
}, // Ends scope.has_interview_content()
|
|
3074
3228
|
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3229
|
+
// ⚠️ Keep text of `id=...` unique from others to identify problems better
|
|
3230
|
+
wait_until_continued: async function ( scope, {
|
|
3231
|
+
url=``, id=`Unknown id`, post_nav_handlers=null
|
|
3232
|
+
}) {
|
|
3233
|
+
/**
|
|
3234
|
+
* Given a url and id of an initial page, attempt to detect navigation.
|
|
3235
|
+
* After the results of the navigation attempt, run the post_nav_handlers.
|
|
3236
|
+
*
|
|
3237
|
+
* ‼️ ALWAYS include required critical error handlers in `post_nav_handlers`
|
|
3238
|
+
* that will throw errors. We need the post-navigation handlers in this
|
|
3239
|
+
* order:
|
|
3240
|
+
*
|
|
3241
|
+
* 1.(required) A page with a visible system error is a critical error and always
|
|
3242
|
+
* trumps other errors, so it comes first
|
|
3243
|
+
* 2. (optional) Invalid answers and other mmore specific handlers trump unspecified
|
|
3244
|
+
* nav errs
|
|
3245
|
+
* 3. (required) Navigation attempt that timed out is a critical error that lacks
|
|
3246
|
+
* further information
|
|
3247
|
+
* 4. (optional) Unspecified non-navigation catchall comes last
|
|
3248
|
+
*
|
|
3249
|
+
* See docs/decisions/017_post_navigation_custom_behavior_2026_10_26.md for
|
|
3250
|
+
* a deeper discussion.
|
|
3251
|
+
*
|
|
3252
|
+
* Unfortunately, our navigation detection game is weaksauce. Navigation
|
|
3253
|
+
* detection thoughts: https://github.com/SuffolkLITLab/ALKiln/issues/229
|
|
3254
|
+
*
|
|
3255
|
+
* @param {Object} scope - Tests state object
|
|
3256
|
+
* @param {Object} kwargs - Named arguments
|
|
3257
|
+
* @param {str} kwargs.url - Assumed to be the url of the initial page
|
|
3258
|
+
* @param {str} kwargs.id - ID of the initial page
|
|
3259
|
+
* @param {[function]} [kwargs.post_nav_handlers] - Functions to handle page
|
|
3260
|
+
* errors like system errors, user errors (invalid answers), and
|
|
3261
|
+
* navigation failure
|
|
3262
|
+
*
|
|
3263
|
+
* @returns {Object} kwrvs - Named return values
|
|
3264
|
+
* @returns {bool} kwrvs.navigated - True if the interview got to a new page
|
|
3265
|
+
* @returns {Object} kwrvs.error_data - Data for any errors found
|
|
3266
|
+
* @returns {bool} kwrvs.error_data.was_found - True if the check found any
|
|
3267
|
+
* errors
|
|
3268
|
+
* @returns {bool} kwrvs.error_data.ok - False if the check found any errors.
|
|
3269
|
+
* Discuss: Should we only use `ok` for internal errors?
|
|
3270
|
+
* @returns {`user`|`system`} kwrvs.error_data.type - "user" with invalid
|
|
3271
|
+
* answers, "system" with da or AL server errors
|
|
3272
|
+
* @returns {[ElementHandle]} kwrvs.error_data.error_handles - List of
|
|
3273
|
+
* puppeteer element handles the check found. There can be multiple
|
|
3274
|
+
* invalid answer elements.
|
|
3275
|
+
* */
|
|
3276
|
+
await scope.log_and_update_if_new_page_id( scope );
|
|
3081
3277
|
|
|
3082
|
-
|
|
3083
|
-
|
|
3278
|
+
post_nav_handlers ??= [
|
|
3279
|
+
scope.throw_on_system_error,
|
|
3280
|
+
scope.throw_on_invalid_answers,
|
|
3281
|
+
scope.throw_on_non_navigation_timeout,
|
|
3282
|
+
scope.throw_on_non_navigation_catchall,
|
|
3283
|
+
];
|
|
3284
|
+
scope.throw_if_missing_required_post_nav_handlers( scope, { handlers: post_nav_handlers })
|
|
3084
3285
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3286
|
+
let initial_id = id;
|
|
3287
|
+
let initial_url = url;
|
|
3288
|
+
let curr_url = await scope.page.url();
|
|
3289
|
+
let error_data = await scope.checkForErrors( scope );
|
|
3290
|
+
|
|
3291
|
+
// Attempt to solve weird race condition issues where page has not properly
|
|
3292
|
+
// continued. May exceed actual current timeout.
|
|
3293
|
+
// Discuss: Use a global clock to properly track of remaining time to avoid
|
|
3294
|
+
// scope.timeout - 100 and such? Any such cucumber or puppeteer feature?
|
|
3295
|
+
let timed_out = false; // Lazy timeout?
|
|
3296
|
+
let nav_timeout = setTimeout(function(){ timed_out = true }, scope.timeout - 100);
|
|
3297
|
+
while ( curr_url === initial_url && error_data.ok && !timed_out ) {
|
|
3087
3298
|
await time.waitForTimeout( 50 );
|
|
3299
|
+
error_data = await scope.checkForErrors( scope );
|
|
3088
3300
|
curr_url = await scope.page.url();
|
|
3089
|
-
error_data = await scope.checkForError( scope );
|
|
3090
3301
|
}
|
|
3302
|
+
clearTimeout( nav_timeout );
|
|
3303
|
+
|
|
3304
|
+
// TODO: Review architecture - can we refactor nav out of global state?
|
|
3305
|
+
scope.navigated = curr_url !== initial_url;
|
|
3091
3306
|
|
|
3307
|
+
error_data = await scope.checkForErrors( scope );
|
|
3092
3308
|
if ( !error_data.ok ) {
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3309
|
+
for ( let handler of post_nav_handlers ) {
|
|
3310
|
+
await handler( scope, { id: initial_id, error_data, timed_out });
|
|
3311
|
+
}
|
|
3096
3312
|
}
|
|
3097
3313
|
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
expect( timedOut, timeout_err_message ).to.be.false;
|
|
3314
|
+
return { error_data, navigated: curr_url !== initial_url };
|
|
3315
|
+
}, // Ends scope.wait_until_continued()
|
|
3101
3316
|
|
|
3102
|
-
|
|
3317
|
+
throw_if_missing_required_post_nav_handlers: function( scope, { handlers=[] }={}) {
|
|
3318
|
+
/**
|
|
3319
|
+
* Return true if the list of functions contains the required handlers,
|
|
3320
|
+
* otherwise throw an internal error.
|
|
3321
|
+
*
|
|
3322
|
+
* @param {Object} scope - Tests state object
|
|
3323
|
+
* @param {Object} kwargs - Named arguments
|
|
3324
|
+
* @param {[func]} kwargs.handlers - List of functions
|
|
3325
|
+
*
|
|
3326
|
+
* @returns {bool} - True if function completes successfully
|
|
3327
|
+
* */
|
|
3328
|
+
let missing_handler_errors = [];
|
|
3329
|
+
if ( !handlers.includes( scope.throw_on_system_error ) ) {
|
|
3330
|
+
let missing_system_msg = log.debug({ code: `ALK0295`, context: `internal nav_handlers`, level: `error` },
|
|
3331
|
+
`ALKiln's internal list of post-navigation handlers must include "scope.throw_on_system_error()"`
|
|
3332
|
+
);
|
|
3333
|
+
missing_handler_errors.push( missing_system_msg );
|
|
3334
|
+
}
|
|
3335
|
+
if ( !handlers.includes( scope.throw_on_non_navigation_timeout ) ) {
|
|
3336
|
+
let missing_timeout_msg = log.debug({ code: `ALK0296`, context: `internal nav_handlers`, level: `error` },
|
|
3337
|
+
`ALKiln's internal list of post-navigation handlers must include "scope.throw_on_non_navigation_timeout()"`
|
|
3338
|
+
);
|
|
3339
|
+
missing_handler_errors.push( missing_timeout_msg );
|
|
3340
|
+
}
|
|
3103
3341
|
|
|
3104
|
-
|
|
3105
|
-
|
|
3342
|
+
if ( missing_handler_errors.length > 0 ) {
|
|
3343
|
+
throw ReferenceError( missing_handler_errors.join(`\n`) );
|
|
3344
|
+
}
|
|
3106
3345
|
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
* returns those elements. The error elements are probably exclusive, but
|
|
3110
|
-
* we're not sure. */
|
|
3346
|
+
return true;
|
|
3347
|
+
},
|
|
3111
3348
|
|
|
3112
|
-
|
|
3349
|
+
validation_err_selector: `#daflash .alert-danger`,
|
|
3350
|
+
// Discuss: `validation code` message can be on a page even after user fixed
|
|
3351
|
+
// values. Always dismiss `validation code` before trying to continue? Or
|
|
3352
|
+
// just detect navigation through requests and responses, which we need to
|
|
3353
|
+
// switch to anyway.
|
|
3354
|
+
// ⚠️ 'display:' and 'none' MUST be separated by a space
|
|
3355
|
+
invalid_field_selector: `xpath/.//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`,
|
|
3356
|
+
// ⚠️ Assumptions: ": Error" stays in the metadata of the page and #damain
|
|
3357
|
+
// only appears on da error pages
|
|
3358
|
+
sys_err_selector: `xpath/.//meta[contains(@content, ": Error")]|.//*[@id="damain"]//h1[contains(text(), "Error")]`,
|
|
3359
|
+
// `al_custom_error_action` in base64. Replaces system error pages, but acts
|
|
3360
|
+
// like a question screen. Shows "The error details are no longer
|
|
3361
|
+
// available..." message when GET requests error (default da message?)
|
|
3362
|
+
al_err_selector: `.question-custom-error-action .daattributions #trigger[data-variable="YWxfY3VzdG9tX2Vycm9yX2FjdGlvbg"]`,
|
|
3363
|
+
checkForErrors: async function ( scope ) {
|
|
3364
|
+
/**
|
|
3365
|
+
* Return any elements from invalid user answers or system errors & other
|
|
3366
|
+
* data. Avoid throwing. Invalid answers may be the author's goal.
|
|
3367
|
+
*
|
|
3368
|
+
* Note: May later want the actual text of the invalid answer messages.
|
|
3369
|
+
*
|
|
3370
|
+
* @param {Object} scope - Tests state object
|
|
3371
|
+
*
|
|
3372
|
+
* @returns {Object} obj - Named return values
|
|
3373
|
+
* @returns {bool} obj.was_found - True if the check found any errors
|
|
3374
|
+
* @returns {bool} obj.ok - False if the check found any errors. Discuss:
|
|
3375
|
+
* Should we only use `ok` for internal errors?
|
|
3376
|
+
* @param {user|system} obj.type - "user" with invalid answers, "system"
|
|
3377
|
+
* with da or AL server errors
|
|
3378
|
+
* @param {[ElementHandle]} obj.error_handles - List of puppeteer element
|
|
3379
|
+
* handles the check found. There can be multiple invalid answer elems.
|
|
3380
|
+
* */
|
|
3381
|
+
// ⚠️ Keep these error objs in this order. For now, the first item on the
|
|
3382
|
+
// list is the one we later use, and `system` errors have priority.
|
|
3113
3383
|
let possible_error_types = [
|
|
3114
|
-
|
|
3115
|
-
{ type: `
|
|
3116
|
-
|
|
3117
|
-
{ type: `user`,
|
|
3118
|
-
{ type: `system`, handlers: await scope.page.$$(`xpath/.//meta[@content="docassemble: Error"]` ) },
|
|
3119
|
-
// Hardcoding the `al_custom_error_action` screen (that's what the base64 here is). It hides actual errors and looks like a continue screen, but isn't.
|
|
3120
|
-
{ type: `system`, handlers: await scope.page.$$('.question-custom-error-action .daattributions #trigger[data-variable="YWxfY3VzdG9tX2Vycm9yX2FjdGlvbg"]') },
|
|
3384
|
+
{ type: `system`, handles: await scope.page.$$( scope.sys_err_selector ) },
|
|
3385
|
+
{ type: `system`, handles: await scope.page.$$( scope.al_err_selector ) },
|
|
3386
|
+
{ type: `user`, handles: await scope.page.$$( scope.validation_err_selector ) },
|
|
3387
|
+
{ type: `user`, handles: await scope.page.$$( scope.invalid_field_selector ) },
|
|
3121
3388
|
];
|
|
3122
3389
|
|
|
3123
|
-
|
|
3390
|
+
// Can have multiple user errors which we may get the text of in future
|
|
3391
|
+
let error_handles = [];
|
|
3124
3392
|
let was_found = false;
|
|
3125
3393
|
let type = null;
|
|
3126
3394
|
for ( let one_err_type of possible_error_types ) {
|
|
3127
|
-
if ( one_err_type.
|
|
3128
|
-
// If there's a page visible (locally), give us as much time to
|
|
3129
|
-
// Because of previous actions, this may
|
|
3130
|
-
//
|
|
3131
|
-
//
|
|
3395
|
+
if ( one_err_type.handles?.[0] ) {
|
|
3396
|
+
// If there's a page visible (locally), give us almost as much time to
|
|
3397
|
+
// examine it as timeout allows. Because of previous actions, this may
|
|
3398
|
+
// still be a bit too long.
|
|
3399
|
+
// Also, if you're debugging and you close the window early, the process
|
|
3400
|
+
// will still take this long to exit after the tests finish. May fix
|
|
3401
|
+
// in future, but it's just during debugging, so yolo for now!
|
|
3132
3402
|
if ( session_vars.get_debug() ) {
|
|
3133
3403
|
await time.waitForTimeout( ( scope.timeout - (1 * 1000)) );
|
|
3134
3404
|
}
|
|
3405
|
+
|
|
3135
3406
|
was_found = true;
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3407
|
+
error_handles = error_handles.concat( one_err_type.handles )
|
|
3408
|
+
if ( !type || one_err_type.type == `system` ) {
|
|
3409
|
+
type = one_err_type.type
|
|
3410
|
+
}
|
|
3411
|
+
} // ends if first handler
|
|
3412
|
+
} // ends for each error type
|
|
3141
3413
|
|
|
3142
|
-
return { was_found, ok: !was_found, type,
|
|
3143
|
-
}, // Ends scope.
|
|
3414
|
+
return { was_found, ok: !was_found, type, error_handles };
|
|
3415
|
+
}, // Ends scope.checkForErrors()
|
|
3144
3416
|
|
|
3145
|
-
|
|
3146
|
-
error_data
|
|
3417
|
+
throw_on_system_error: async function ( scope, {
|
|
3418
|
+
error_data=null, id=`Unknown ID`
|
|
3147
3419
|
}) {
|
|
3148
|
-
/**
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
if (
|
|
3154
|
-
let
|
|
3155
|
-
let report_msg = reports.addToReport(scope, {
|
|
3156
|
-
|
|
3420
|
+
/**
|
|
3421
|
+
* Throw if there is a system/server error. Include some page text.
|
|
3422
|
+
*
|
|
3423
|
+
* @param {Object} scope - Tests state object
|
|
3424
|
+
* */
|
|
3425
|
+
if ( error_data?.type === `system` ) {
|
|
3426
|
+
let alk_msg = [`The test ran into a system error before or on the page with the id "${ id }". The picture of this page, if available, may show more.`];
|
|
3427
|
+
let report_msg = reports.addToReport(scope, {
|
|
3428
|
+
type: `error`, code: `ALK0147`,
|
|
3429
|
+
value: [ alk_msg, ``, await scope.get_system_err( scope ) ].join(`\n`),
|
|
3430
|
+
});
|
|
3431
|
+
throw Error( report_msg );
|
|
3157
3432
|
}
|
|
3158
3433
|
|
|
3159
3434
|
return scope;
|
|
3160
|
-
}, // Ends scope.
|
|
3435
|
+
}, // Ends scope.throw_on_system_error()
|
|
3161
3436
|
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3437
|
+
get_system_err: async function ( scope, { num_lines=8 }={}) {
|
|
3438
|
+
/**
|
|
3439
|
+
* Returns a sample of error-related text on a docassemble or Assembly Line
|
|
3440
|
+
* system error page
|
|
3441
|
+
*
|
|
3442
|
+
* Note: YWxfZm9ybWF0dGVkX2Vycm9yX21lc3NhZ2U is "al_formatted_error_message"
|
|
3443
|
+
*
|
|
3444
|
+
* @param {Object} scope - Tests state object
|
|
3445
|
+
* @param {Object} obj - Named arguments
|
|
3446
|
+
* @param {int} obj.num_lines - Number of lines of HTML text to return
|
|
3447
|
+
*
|
|
3448
|
+
* @returns {str} - A sample of the HTML related to the system error
|
|
3449
|
+
* */
|
|
3450
|
+
let truncated_text = `Unable to get the text on this page. Check the picture of this page if possible.`
|
|
3451
|
+
try {
|
|
3452
|
+
|
|
3453
|
+
let page_err_text = await scope.page.evaluate(() => {
|
|
3454
|
+
return (
|
|
3455
|
+
( document.querySelector(`#YWxfZm9ybWF0dGVkX2Vycm9yX21lc3NhZ2U`)
|
|
3456
|
+
|| document.querySelector(`.dabody`)
|
|
3457
|
+
).textContent
|
|
3458
|
+
);
|
|
3459
|
+
});
|
|
3460
|
+
|
|
3461
|
+
log.debug({ code: `ALK0287`, context: `page_error`, level: `info` },
|
|
3462
|
+
`Full text of page error:`, page_err_text
|
|
3463
|
+
);
|
|
3464
|
+
|
|
3465
|
+
truncated_text = page_err_text.split(`\n`)
|
|
3466
|
+
.slice( 0, num_lines )
|
|
3467
|
+
.join(`\n`)
|
|
3468
|
+
.trim()
|
|
3469
|
+
+ `\n...`;
|
|
3470
|
+
|
|
3471
|
+
} catch ( pupp_err ) {
|
|
3472
|
+
log.debug({ code: `ALK0292`, context: `internal`, level: `note` },
|
|
3473
|
+
`Skipped getting system error screen text with puppeteerjs:`, pupp_err,
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
return truncated_text;
|
|
3478
|
+
}, // Ends scope.get_system_err()
|
|
3479
|
+
|
|
3480
|
+
throw_on_invalid_answers: async function ( scope, {
|
|
3481
|
+
error_data={}, id=`Unknown ID`
|
|
3482
|
+
}={}) {
|
|
3483
|
+
/**
|
|
3484
|
+
* Throw an error if the user has given invalid answers. If the author
|
|
3485
|
+
* simply wants to detect invalid answers and avoid throwing, use
|
|
3486
|
+
* `scope.checkForErrors()`.
|
|
3487
|
+
*
|
|
3488
|
+
* Discuss: Change our architecture to handle throwing and reporting in some
|
|
3489
|
+
* central location down/up-stream. Here, for example, I want to add an
|
|
3490
|
+
* error to the report, but "I can't continue" is catching this error and
|
|
3491
|
+
* letting the test pass. How can we handle that? Allow editing the report
|
|
3492
|
+
* after the fact? Throwing the report message with log code and all and
|
|
3493
|
+
* count on the caller to report it?
|
|
3494
|
+
*
|
|
3495
|
+
* @param {Object} scope - Tests state object
|
|
3496
|
+
* @param {Object} [obj] - Named arguments
|
|
3497
|
+
* @param {Object} [obj.error_data] - Maybe data for invalid answers' markers
|
|
3498
|
+
* @param {str} [obj.id] - ID of the page that generated errors, if they exist
|
|
3499
|
+
*
|
|
3500
|
+
* @returns {Object} - Error data given
|
|
3501
|
+
* */
|
|
3166
3502
|
if ( !error_data.ok && error_data.type === `user` ) {
|
|
3167
|
-
let err_msg = `
|
|
3168
|
-
let report_msg = reports.addToReport(scope, { type: `
|
|
3169
|
-
|
|
3503
|
+
let err_msg = `The user got an invalid answer on the page with the id "${ id }". You can check the picture of this page unless you used secrets.`;
|
|
3504
|
+
let report_msg = reports.addToReport(scope, { type: `error`, code: `ALK0148`, value: err_msg });
|
|
3505
|
+
throw Error( report_msg );
|
|
3170
3506
|
}
|
|
3171
3507
|
|
|
3172
|
-
return
|
|
3173
|
-
}, // Ends scope.
|
|
3508
|
+
return error_data;
|
|
3509
|
+
}, // Ends scope.throw_on_invalid_answers()
|
|
3510
|
+
|
|
3511
|
+
warn_on_invalid_answers: async function ( scope, {
|
|
3512
|
+
error_data={}, id=`Unknown ID`
|
|
3513
|
+
}={}) {
|
|
3514
|
+
/**
|
|
3515
|
+
* Add a warning to the report if the page has invalid answer indicators.
|
|
3516
|
+
*
|
|
3517
|
+
* @param {Object} scope - Tests state object
|
|
3518
|
+
* @param {Object} [obj] - Named arguments
|
|
3519
|
+
* @param {Object} [obj.error_data] - Maybe data for invalid answers' markers
|
|
3520
|
+
* @param {str} [obj.id] - ID of the page that generated errors, if they exist
|
|
3521
|
+
*
|
|
3522
|
+
* @returns {Object} - Error data given
|
|
3523
|
+
* */
|
|
3524
|
+
let initial_id = id;
|
|
3525
|
+
if ( !error_data.ok && error_data.type === `user` ) {
|
|
3526
|
+
// TOOD: Add a deprecation message to Steps that call this
|
|
3527
|
+
let report_msg = reports.addToReport(scope, {
|
|
3528
|
+
type: `warning`, code: `ALK0290`, value: `The user got an invalid answer on the page with the id "${ initial_id }".`
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
return error_data;
|
|
3533
|
+
}, // Ends scope.warn_on_invalid_answers()
|
|
3534
|
+
|
|
3535
|
+
throw_on_non_navigation_timeout: async function ( scope, {
|
|
3536
|
+
timed_out=false, id=`Unknown ID`
|
|
3537
|
+
}={}) {
|
|
3538
|
+
/**
|
|
3539
|
+
* Throw an error if the page hasn't navigated and the nav check timed out.
|
|
3540
|
+
*
|
|
3541
|
+
* @param {Object} scope - Tests state object
|
|
3542
|
+
* @param {Object} [obj] - Named arguments
|
|
3543
|
+
* @param {Object} [obj.timed_out] - Whether navigation timed out
|
|
3544
|
+
* @param {str} [obj.id] - ID of the page that generated errors, if they exist
|
|
3545
|
+
*
|
|
3546
|
+
* @returns {undefined}
|
|
3547
|
+
* */
|
|
3548
|
+
let initial_id = id;
|
|
3549
|
+
let timeout_err_message = `The test tried to navigate away from the page with the id "${ initial_id }", but it took longer than ${ (scope.timeout/1000)/60 } min. Unless you used a secret, you can check the picture of this page.`
|
|
3550
|
+
if ( !scope.navigated && timed_out ) {
|
|
3551
|
+
reports.addToReport(scope, { type: `error`, code: `ALK0146`, value: timeout_err_message });
|
|
3552
|
+
throw Error( timeout_err_message );
|
|
3553
|
+
}
|
|
3554
|
+
},
|
|
3555
|
+
|
|
3556
|
+
throw_on_non_navigation_catchall: async function ( scope, { id=`Unknown ID` }={}) {
|
|
3557
|
+
/**
|
|
3558
|
+
* Throw an error if the page hasn't navigated. Used in cases where we
|
|
3559
|
+
* haven't identified why the interaction didn't navigate.
|
|
3560
|
+
*
|
|
3561
|
+
* @param {Object} scope - Tests state object
|
|
3562
|
+
* @param {Object} [obj] - Named arguments
|
|
3563
|
+
* @param {str} [obj.id] - ID of the page that generated errors, if they exist
|
|
3564
|
+
*
|
|
3565
|
+
* @returns {undefined}
|
|
3566
|
+
* */
|
|
3567
|
+
let initial_id = id;
|
|
3568
|
+
if ( !scope.navigated ) {
|
|
3569
|
+
let non_nav_msg = `Was unable to navigate away from page with the id "${ initial_id }".`
|
|
3570
|
+
reports.addToReport(scope, { type: `error`, code: `ALK0291`, value: non_nav_msg });
|
|
3571
|
+
throw Error( non_nav_msg );
|
|
3572
|
+
}
|
|
3573
|
+
},
|
|
3574
|
+
|
|
3575
|
+
warn_on_non_navigation_catchall: async function ( scope, { id=`Unknown ID` }={}) {
|
|
3576
|
+
/**
|
|
3577
|
+
* Warn if the page hasn't navigated. Used in cases where we haven't
|
|
3578
|
+
* identified why the interaction didn't navigate.
|
|
3579
|
+
*
|
|
3580
|
+
* @param {Object} scope - Tests state object
|
|
3581
|
+
* @param {Object} [obj] - Named arguments
|
|
3582
|
+
* @param {str} [obj.id] - ID of the page that generated errors, if they exist
|
|
3583
|
+
*
|
|
3584
|
+
* @returns {undefined}
|
|
3585
|
+
* */
|
|
3586
|
+
let initial_id = id;
|
|
3587
|
+
if ( !scope.navigated ) {
|
|
3588
|
+
let non_nav_msg = `Was unable to navigate away from page with the id "${ initial_id }".`
|
|
3589
|
+
reports.addToReport(scope, { type: `warning`, code: `ALK0293`, value: non_nav_msg });
|
|
3590
|
+
}
|
|
3591
|
+
},
|
|
3174
3592
|
|
|
3175
3593
|
take_a_screenshot: async ( scope,{ path }) => {
|
|
3176
3594
|
/* Takes a jpeg screenshot. Avoids destroying signatures. */
|
|
@@ -3393,7 +3811,7 @@ module.exports = {
|
|
|
3393
3811
|
/** Given a fetch response headers' Content-Disposition str, return the
|
|
3394
3812
|
* filename of the response's file.
|
|
3395
3813
|
*
|
|
3396
|
-
* @
|
|
3814
|
+
* @returns { string | null } - Name of fetched file
|
|
3397
3815
|
* */
|
|
3398
3816
|
let filename = default_name;
|
|
3399
3817
|
if ( disposition ) {
|
|
@@ -3415,7 +3833,7 @@ module.exports = {
|
|
|
3415
3833
|
}, // Ends scope.steps.get_response_filename()
|
|
3416
3834
|
|
|
3417
3835
|
compare_pdfs: async function (scope, {existing_pdf_path, new_pdf_path}) {
|
|
3418
|
-
let existing_paths = await scope.
|
|
3836
|
+
let existing_paths = await scope.find_files(scope, {to_find_names: [existing_pdf_path]});
|
|
3419
3837
|
if (existing_paths.length == 0) {
|
|
3420
3838
|
// The row wasn't used
|
|
3421
3839
|
let msg = `Could not find the existing PDF at ${ existing_pdf_path }`;
|
|
@@ -3809,7 +4227,7 @@ module.exports = {
|
|
|
3809
4227
|
let handle = await scope.page.waitForSelector(`#dasigcanvas`);
|
|
3810
4228
|
// We cannot be sure of knowing the name of the signer and so want to
|
|
3811
4229
|
// indicate it's random, but we do want something unique
|
|
3812
|
-
await scope.funnel_the_answer( scope, { field, handle, answer: `Random ${ faker.
|
|
4230
|
+
await scope.funnel_the_answer( scope, { field, handle, answer: `Random ${ faker.person.lastName() }` });
|
|
3813
4231
|
},
|
|
3814
4232
|
input: async function (scope, { field }) {
|
|
3815
4233
|
/** Handles various `type`s of input element fields, except:
|
|
@@ -3912,7 +4330,7 @@ module.exports = {
|
|
|
3912
4330
|
return `${ Math.random() > .5 }`;
|
|
3913
4331
|
},
|
|
3914
4332
|
text: async function ( scope ) {
|
|
3915
|
-
return faker.
|
|
4333
|
+
return faker.word.sample();
|
|
3916
4334
|
},
|
|
3917
4335
|
number: async function ( scope ) {
|
|
3918
4336
|
let answer = faker.helpers.arrayElement( [`1`, `2`, `3`], 1 );
|
|
@@ -3938,7 +4356,7 @@ module.exports = {
|
|
|
3938
4356
|
return `${ Math.random() * 100 }`;
|
|
3939
4357
|
},
|
|
3940
4358
|
email: async function ( scope ) {
|
|
3941
|
-
return faker.internet.email( faker.
|
|
4359
|
+
return faker.internet.email( faker.word.adjective(), `-`, faker.word.noun(), `example.com` );
|
|
3942
4360
|
},
|
|
3943
4361
|
password: async function ( scope ) {
|
|
3944
4362
|
return faker.internet.password();
|