@suffolklitlab/alkiln 5.17.0 → 5.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/{da_image_v_sha_7d13853.yml → al_da_image_latest.yml} +8 -3
- package/.github/workflows/github_server.yml +5 -3
- package/.github/workflows/playground.yml +2 -2
- package/CHANGELOG.md +22 -1
- package/lib/docassemble/docassemble_api_interface.js +3 -0
- package/lib/scope.js +627 -152
- package/lib/steps.js +40 -34
- package/lib/utils/Log.js +10 -8
- package/lib/utils/session_vars.js +25 -8
- package/package.json +1 -1
- package/tests/log_codes/log_code_expected_instances.txt +12 -2
- package/tests/unit_tests/constrained_random_answers/valid.fixtures.js +3 -1
- package/tests/unit_tests/fields.fixtures.js +19 -8
- package/tests/unit_tests/getAllFields.test.js +1 -1
- package/tests/unit_tests/html.fixtures.js +3 -1
package/lib/scope.js
CHANGED
|
@@ -347,6 +347,7 @@ module.exports = {
|
|
|
347
347
|
// This is meant to detect user-visible alerts/errors,
|
|
348
348
|
// not breaking errors in the testing code.
|
|
349
349
|
// No need for aborters since puppeteer has its own timeout
|
|
350
|
+
// TODO: Be more specific. Authors may use `.alert-danger` themselves.
|
|
350
351
|
scope.page.waitForSelector('.alert-danger', { visible: true }),
|
|
351
352
|
scope.page.waitForSelector('.da-has-error', { visible: true }),
|
|
352
353
|
]);
|
|
@@ -556,22 +557,56 @@ module.exports = {
|
|
|
556
557
|
return false;
|
|
557
558
|
},
|
|
558
559
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
560
|
+
continue_button_selector_da_v_1_9_n_and_earlier: `fieldset.da-field-buttons button[type="submit"].btn-primary`,
|
|
561
|
+
continue_button_selector_da_v_1_10_n_and_later: `button[type="submit"]#da-continue-button`,
|
|
562
|
+
continue_button_selector_backup_da_v_1_9_n_and_earlier: `.da-fieldset.da-field-buttons button[type="submit"].btn-primary`,
|
|
563
|
+
signature_cont_btn_selector_da_v_1_9_n_and_earlier: `fieldset .dasigsave`,
|
|
564
|
+
|
|
565
|
+
signature_cont_btn_selector_backup_da_v_1_9_n_and_earlier: `.da-fieldset .dasigsave`,
|
|
566
|
+
// ⚠️ docassemble >= 1.10.n, 2 signature page continue buttons. One visible on
|
|
567
|
+
// wide screen, one visible on narrow screen. We need to get the visible
|
|
568
|
+
// one, but puppeteer v22.15.0 doesn't recognize an element with a hidden
|
|
569
|
+
// ancestor as invisible, so we need to get both `.dasigsave` and test for
|
|
570
|
+
// visibility our own way for now.
|
|
571
|
+
signature_cont_btn_selector_da_v_1_10_n_and_later: `.dasigsave`,
|
|
563
572
|
continue: async function ( scope ) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Presses whatever button it finds that might lead to the next page.
|
|
575
|
+
*
|
|
576
|
+
* @param {object} scope - Tests state object
|
|
577
|
+
* */
|
|
578
|
+
// ⚠️ Any selectors I find seem somewhat precarious.
|
|
579
|
+
let elem_lists = [
|
|
580
|
+
// non-signature pages (these are the most consistent ways)
|
|
581
|
+
await scope.page.$$(scope.continue_button_selector_da_v_1_9_n_and_earlier, { visible: true }),
|
|
582
|
+
await scope.page.$$(scope.continue_button_selector_da_v_1_10_n_and_later, { visible: true }),
|
|
583
|
+
await scope.page.$$(scope.continue_button_selector_backup_da_v_1_9_n_and_earlier, { visible: true }),
|
|
584
|
+
// signature pages
|
|
585
|
+
await scope.page.$$(scope.signature_cont_btn_selector_da_v_1_9_n_and_earlier, { visible: true }),
|
|
586
|
+
await scope.page.$$(scope.signature_cont_btn_selector_backup_da_v_1_9_n_and_earlier, { visible: true }),
|
|
587
|
+
// Two possible buttons if `visible: true` doesn't work
|
|
588
|
+
await scope.page.$$(scope.signature_cont_btn_selector_da_v_1_10_n_and_later, { visible: true }),
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
// ⚠️ Avoid whichever `.dasigsave` button is hidden on signature screens. We
|
|
592
|
+
// must do this ourselves because puppeteer v22.15.0 doesn't seem to
|
|
593
|
+
// recognize when an ancestor of an element is `display: none`.
|
|
594
|
+
let continue_button = null;
|
|
595
|
+
for ( let possible_cont_buttons of elem_lists ) {
|
|
596
|
+
let found = false;
|
|
597
|
+
for ( let possible_cont_button of possible_cont_buttons ) {
|
|
598
|
+
let bounding_box = await possible_cont_button.boundingBox();
|
|
599
|
+
if ( bounding_box?.height !== 0 && bounding_box?.width !== 0) {
|
|
600
|
+
continue_button = possible_cont_button;
|
|
601
|
+
found = true;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if ( found ) { break; }
|
|
606
|
+
}
|
|
607
|
+
|
|
573
608
|
// Waits for navigation or user error
|
|
574
|
-
await scope.tapElementAndNavigate( scope, { elem });
|
|
609
|
+
await scope.tapElementAndNavigate( scope, { elem: continue_button });
|
|
575
610
|
}, // Ends scope.continue()
|
|
576
611
|
|
|
577
612
|
continue_exists: async function ( scope ) {
|
|
@@ -585,13 +620,18 @@ module.exports = {
|
|
|
585
620
|
// <button type="submit" class="btn btn-da btn-primary" name="Zm9v" value="True">Continue</button>
|
|
586
621
|
// It will not find event screen `buttons:`, like an exit button or a restart button
|
|
587
622
|
// <button type="submit" class="btn btn-da btn-danger" name="X211bHRpcGxlX2Nob2ljZQ" value="0">exit</button>
|
|
588
|
-
// `buttons:` can be used in question blocks as choices
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
let
|
|
592
|
-
let
|
|
623
|
+
// ⚠️ `buttons:` can be used in question blocks as choices
|
|
624
|
+
|
|
625
|
+
// Multiple matches not needed. We're just checking for presence.
|
|
626
|
+
let regular = await scope.page.$(scope.continue_button_selector_da_v_1_9_n_and_earlier, { visible: true });
|
|
627
|
+
let regular_da_v_1_10_x = await scope.page.$(scope.continue_button_selector_da_v_1_10_n_and_later, { visible: true });
|
|
628
|
+
let regular_backup = await scope.page.$(scope.continue_button_selector_backup_da_v_1_9_n_and_earlier, { visible: true });
|
|
593
629
|
|
|
594
|
-
|
|
630
|
+
let signature = await scope.page.$(scope.signature_cont_btn_selector_da_v_1_9_n_and_earlier, { visible: true });
|
|
631
|
+
let signature_backup = await scope.page.$(scope.signature_cont_btn_selector_backup_da_v_1_9_n_and_earlier, { visible: true });
|
|
632
|
+
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
|
+
|
|
634
|
+
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;
|
|
595
635
|
|
|
596
636
|
}, // Ends scope.continue_exists()
|
|
597
637
|
|
|
@@ -750,6 +790,14 @@ module.exports = {
|
|
|
750
790
|
set_interview_session_data: function ( scope, { vars = null }) {
|
|
751
791
|
/** Sets this interview's session data for this scenario if it doesn't
|
|
752
792
|
* already exist.
|
|
793
|
+
*
|
|
794
|
+
* Discuss: Keep a record of insertion order of .interviews uids so we can
|
|
795
|
+
* tell which interview is most recent and the order in which interviews
|
|
796
|
+
* were visited.
|
|
797
|
+
*
|
|
798
|
+
* TODO: If we don't already, when deleting interviews, use
|
|
799
|
+
* scenario.interviews to get the relevant uids instead of just the most
|
|
800
|
+
* recent uid? Multiple tests may be running.
|
|
753
801
|
*
|
|
754
802
|
* @returns final_vars { object | null }
|
|
755
803
|
* @returns final_vars.i { str } File path of interview
|
|
@@ -931,8 +979,12 @@ module.exports = {
|
|
|
931
979
|
* as some kind of sophisticated data? With their selectors, for example.
|
|
932
980
|
* We will choose selectors that will continue to identify the node, eve
|
|
933
981
|
* after a hidden field is revealed, etc.
|
|
934
|
-
*
|
|
935
|
-
*
|
|
982
|
+
*
|
|
983
|
+
* TODO: Not sure where to bring in cheerio. Pass in `$` instead?
|
|
984
|
+
*
|
|
985
|
+
* TODO: Consider getting more data here so that other functionality can use
|
|
986
|
+
* the data to reduce use of `page.evaluate()`.
|
|
987
|
+
* */
|
|
936
988
|
let fields = [];
|
|
937
989
|
|
|
938
990
|
const $ = cheerio.load( html ); // Could this be on `scope`?
|
|
@@ -960,55 +1012,87 @@ module.exports = {
|
|
|
960
1012
|
// Some `show if` fields have `names` like '_field_n' instead of their var name. Ex: _field_1
|
|
961
1013
|
let field_like_names = await scope.getFieldNamesDict( scope, { $: $ });
|
|
962
1014
|
|
|
963
|
-
// All the different types of fields
|
|
964
|
-
|
|
965
|
-
//
|
|
966
|
-
|
|
1015
|
+
// All the different types of fields. Will deal with `option` once inside `select`
|
|
1016
|
+
|
|
1017
|
+
// All these hidden inputs moved inside the da form element. We want to keep
|
|
1018
|
+
// some hidden inputs, so we'll have to exclude the new neighbors.
|
|
1019
|
+
let not = `:not([name="${ [
|
|
1020
|
+
`_checkboxes`, `csrf_token`, `_event`, `_question_name`, `_tracker`, `_datatypes`, `_visible`, `_varnames`
|
|
1021
|
+
].join('"]):not([name="') }"])`;
|
|
1022
|
+
let all_nodes = $(
|
|
1023
|
+
`#dasigpage canvas`
|
|
1024
|
+
+ `, fieldset button, .da-fieldset button, .daquestionactionbutton, #daform button, #daform *[role="button"]`
|
|
1025
|
+
+ `, fieldset input${ not }, .da-fieldset input${ not }, .da-form-group input${ not }, #daform input${ not }`
|
|
1026
|
+
+ `, .da-form-group select, #daform select, #daform form select`
|
|
1027
|
+
+ `, .da-form-group textarea, #daform textarea`
|
|
1028
|
+
);
|
|
1029
|
+
let all_buttons = [];
|
|
1030
|
+
|
|
1031
|
+
// // TODO: When various versions of da resolve the `GET` errors, re-activate
|
|
1032
|
+
// // this code out here instead of just for canvas elements.
|
|
1033
|
+
// let question_data = await scope.get_question_data( scope );
|
|
1034
|
+
|
|
967
1035
|
for ( let node of all_nodes ) {
|
|
968
|
-
// Decision: Do not abstract the below. There's too much data to pass around for it to make sense
|
|
969
1036
|
let $node = $( node );
|
|
970
1037
|
let field_data = {
|
|
971
1038
|
selector: '',
|
|
972
1039
|
tag: node.name,
|
|
973
|
-
guesses:
|
|
974
|
-
type:
|
|
1040
|
+
guesses: null,
|
|
1041
|
+
type: node.attribs.type || ``,
|
|
975
1042
|
trigger: trigger,
|
|
976
1043
|
};
|
|
977
|
-
if ( node.attribs.type ) { field_data.type = node.attribs.type; }
|
|
978
1044
|
|
|
979
1045
|
// Often used for the variable name
|
|
980
|
-
let
|
|
981
|
-
|
|
982
|
-
//
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
//
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1046
|
+
let encoded_name = $node.attr( `name` );
|
|
1047
|
+
|
|
1048
|
+
if ( field_data.tag === `canvas` ) { // $(`#dasigpage`).length > 0 && $(`#dasigpage`)[0].name ) {
|
|
1049
|
+
|
|
1050
|
+
let question_data = await scope.get_question_data( scope );
|
|
1051
|
+
|
|
1052
|
+
// ⚠️ Cheerio is unable to detect that some `.dasigsave` buttons are
|
|
1053
|
+
// hidden. Ignore buttons here. Take care of it with puppeteer later.
|
|
1054
|
+
|
|
1055
|
+
// Try for guesses that are most likely to be associated with the canvas
|
|
1056
|
+
encoded_name ??= scope.canvas_name.try_for_best_encoded( scope, {
|
|
1057
|
+
existing_guesses: field_data.guesses, $, trigger
|
|
1058
|
+
});
|
|
1059
|
+
if ( !encoded_name && !field_data.guesses ) {
|
|
1060
|
+
field_data.guesses = scope.canvas_name.try_for_mid_guesses( scope, {
|
|
1061
|
+
question_data
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Resort to names that may have proxy vars, such as x and i. See
|
|
1066
|
+
// docassemble generic objects and index vars. They are more
|
|
1067
|
+
// ambiguous since they could match multiple variables.
|
|
1068
|
+
|
|
1069
|
+
if ( !encoded_name && !field_data.guesses ) {
|
|
1070
|
+
field_data.guesses = scope.canvas_name.try_for_worse_guesses( scope, {
|
|
1071
|
+
question_data
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if ( !encoded_name && !field_data.guesses ) {
|
|
1075
|
+
encoded_name = scope.canvas_name.try_for_worse_encoded( scope, {
|
|
1076
|
+
question_data
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
// ✏️ var_names handles an undefined encoded_name just fine
|
|
1080
|
+
|
|
1081
|
+
// If we have none of those, we need to tell the user we have too little
|
|
1082
|
+
// information to set the signature field variable We'll fail later
|
|
1083
|
+
// when we can take a proper screenshot.
|
|
1084
|
+
if ( !field_data.guesses && !encoded_name ) {
|
|
1002
1085
|
reports.addToReport(scope, {
|
|
1003
1086
|
type: `error`,
|
|
1004
1087
|
code: `ALK0123`,
|
|
1005
|
-
value: `You cannot sign a signature page this
|
|
1006
|
-
+ `interview's HTML is missing data. Use the "I sign"
|
|
1007
|
-
+ `instead. You can also read documentation about `
|
|
1008
|
-
+ `how to
|
|
1088
|
+
value: `You cannot sign a signature page with this ALKiln Step `
|
|
1089
|
+
+ `because the interview's HTML is missing data. Use the "I sign" `
|
|
1090
|
+
+ `Step instead. You can also read documentation about `
|
|
1091
|
+
+ `how to add the "trigger" to the interview HTML.`
|
|
1009
1092
|
});
|
|
1093
|
+
// Discuss: Throw here?
|
|
1010
1094
|
}
|
|
1011
|
-
}
|
|
1095
|
+
} // end if signature page
|
|
1012
1096
|
|
|
1013
1097
|
// The selector we'll use to set the DOM node's value later
|
|
1014
1098
|
if ( !field_data.selector ) {
|
|
@@ -1021,19 +1105,19 @@ module.exports = {
|
|
|
1021
1105
|
let href = node.attribs.href;
|
|
1022
1106
|
let match = href.match( /action=([^&]*)/ );
|
|
1023
1107
|
if ( match ) {
|
|
1024
|
-
|
|
1108
|
+
encoded_name = match[1];
|
|
1025
1109
|
} else {
|
|
1026
1110
|
// Some action buttons just use urls instead of da actions
|
|
1027
|
-
// This means the first guess of the name should be the
|
|
1111
|
+
// This means the first guess of the name should be the encoded_name
|
|
1028
1112
|
// TODO: This only replaces the chars at the start of the string. Is that
|
|
1029
1113
|
// intentional? Does this `.replace()` need the `g` flag?
|
|
1030
|
-
|
|
1114
|
+
encoded_name = href.replace(/^[A-Za-z0-9]*/, '_');
|
|
1031
1115
|
}
|
|
1032
1116
|
}
|
|
1033
1117
|
|
|
1034
|
-
if ( !
|
|
1118
|
+
if ( !encoded_name ) {
|
|
1035
1119
|
// Some fields, like comboboxes, have a weird var name setup
|
|
1036
|
-
|
|
1120
|
+
encoded_name = $node.attr( `id` );
|
|
1037
1121
|
}
|
|
1038
1122
|
|
|
1039
1123
|
// ====================
|
|
@@ -1044,12 +1128,13 @@ module.exports = {
|
|
|
1044
1128
|
if ( $node.hasClass('danota-checkbox') || $node.parents(`.da-field-container-datatype-object_radio`).name ) {
|
|
1045
1129
|
is_none_of_the_above = true;
|
|
1046
1130
|
let first_sib = $(node.parent).find( 'input' )[0];
|
|
1047
|
-
|
|
1131
|
+
encoded_name = first_sib.attribs.name;
|
|
1048
1132
|
// TODO: Do we need daaota as well? ("all of the above")
|
|
1049
1133
|
}
|
|
1050
1134
|
|
|
1135
|
+
// Am I doing this wrong? the guesses aren't just the name, right?
|
|
1051
1136
|
let var_names = await scope.getPossibleDecodedNames( scope, {
|
|
1052
|
-
not_decoded:
|
|
1137
|
+
not_decoded: encoded_name,
|
|
1053
1138
|
field_like_names,
|
|
1054
1139
|
is_none_of_the_above: is_none_of_the_above,
|
|
1055
1140
|
proxies,
|
|
@@ -1070,16 +1155,260 @@ module.exports = {
|
|
|
1070
1155
|
values.push( node.attribs.value );
|
|
1071
1156
|
}
|
|
1072
1157
|
|
|
1073
|
-
field_data.guesses
|
|
1074
|
-
|
|
1158
|
+
field_data.guesses ??= await scope.add_values_if_needed( scope, { var_names, values });
|
|
1159
|
+
if ( scope.is_button_ish( scope, { field: field_data }) ) {
|
|
1160
|
+
// Discuss: This makes sure buttons are matched with table rows last. Is
|
|
1161
|
+
// that useful? Appropriate?
|
|
1162
|
+
all_buttons.push( field_data );
|
|
1163
|
+
} else {
|
|
1164
|
+
fields.push( field_data );
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1075
1167
|
} // ends for every node
|
|
1076
1168
|
|
|
1169
|
+
let on_page = [ ...fields, ...all_buttons ];
|
|
1170
|
+
|
|
1077
1171
|
log.debug({ code: `ALK0124`, context: `page fields`, },
|
|
1078
|
-
|
|
1172
|
+
JSON.stringify( on_page, null, 2 )
|
|
1079
1173
|
);
|
|
1080
|
-
return
|
|
1174
|
+
return on_page;
|
|
1081
1175
|
}, // Ends scope.getAllFields()
|
|
1082
1176
|
|
|
1177
|
+
get_question_data: async function ( scope ) {
|
|
1178
|
+
/**
|
|
1179
|
+
* Return server data about the current question.
|
|
1180
|
+
*
|
|
1181
|
+
* da v1.10.n or greater removes some info from the DOM, such as on the
|
|
1182
|
+
* signature page, so we need to supplement our info when possible. We can
|
|
1183
|
+
* do this by making a GET request for the current page data in the
|
|
1184
|
+
* browser. See https://docassemble.org/docs/api.html#session_question. If
|
|
1185
|
+
* we had an API key for the user or an admin, we could use the API, but
|
|
1186
|
+
* we cannot guarantee having that API key without making a lot more work
|
|
1187
|
+
* for authors.
|
|
1188
|
+
*
|
|
1189
|
+
* Discuss: Do we need to wait longer for a server response, as we do for a
|
|
1190
|
+
* page response?
|
|
1191
|
+
*
|
|
1192
|
+
* @param {Object} scope - Tests state object
|
|
1193
|
+
*
|
|
1194
|
+
* @returns {Object} - the question & field data from the current page
|
|
1195
|
+
* */
|
|
1196
|
+
let question_data = { ok: false, status: -1 };
|
|
1197
|
+
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
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
question_response = await scope?.page?.evaluate?.( async ( err_msg ) => {
|
|
1203
|
+
/**
|
|
1204
|
+
* @returns {object} -
|
|
1205
|
+
* { ok: false }; // ok -> response status or false
|
|
1206
|
+
* | { ok, status, ...data, ...( await response.json() ) }
|
|
1207
|
+
* | { ok, status, alk_msg: err_msg, ...data, html: await response.text() }
|
|
1208
|
+
* | { ok, status, alk_msg: err_msg }
|
|
1209
|
+
* | { ok: false, alk_msg: err_msg, error: response_err.message }
|
|
1210
|
+
* */
|
|
1211
|
+
let data = { ok: false, alk_msg: null }; // ok -> did we get JSON or not
|
|
1212
|
+
try {
|
|
1213
|
+
let response = await fetch(
|
|
1214
|
+
window.location.href.replace(`interview?`, `interview?json=1&`)
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
data.ok = response.ok;
|
|
1218
|
+
data.status = response.status;
|
|
1219
|
+
|
|
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
|
+
let content_type = response?.headers?.get?.(`content-type`);
|
|
1230
|
+
if ( content_type?.startsWith?.(`application/json`) ) {
|
|
1231
|
+
return { ...data, ...( await response.json() ) };
|
|
1232
|
+
|
|
1233
|
+
} else if ( content_type?.startsWith?.(`text/plain`) ) {
|
|
1234
|
+
// NOTE: Assembly Line will obscure docassemble error content it
|
|
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() };
|
|
1238
|
+
|
|
1239
|
+
} else {
|
|
1240
|
+
// Research: How can we get in here? What other content-types that we
|
|
1241
|
+
// could get give response ok?
|
|
1242
|
+
return { ...data, alk_msg: err_msg, content_type };
|
|
1243
|
+
} // ends content-type check
|
|
1244
|
+
|
|
1245
|
+
} catch ( response_err ) {
|
|
1246
|
+
// Error might be caused by non-da page author navigated to when, for
|
|
1247
|
+
// example, testing an "Exit" destination.
|
|
1248
|
+
return { ok: false, alk_msg: err_msg, error: response_err.message, }
|
|
1249
|
+
} // ends fetch try
|
|
1250
|
+
|
|
1251
|
+
return data;
|
|
1252
|
+
}, err_msg ); // ends page.evaluate() call
|
|
1253
|
+
} catch ( question_data_error ) {
|
|
1254
|
+
question_response = { ok: false, alk_msg: err_msg, error: question_data_error.message, }
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
question_data = question_response || question_data;
|
|
1258
|
+
// If we didn't get JSON response, we're assuming fetch ran into a problem.
|
|
1259
|
+
// This isn't critical info. Other ALKiln code will throw if warranted.
|
|
1260
|
+
if ( !question_data.ok ) {
|
|
1261
|
+
scope.log_question_data_not_ok( scope, { data: question_data });
|
|
1262
|
+
return { ok: false };
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return question_data;
|
|
1266
|
+
}, // Ends get_question_data
|
|
1267
|
+
|
|
1268
|
+
log_question_data_not_ok: function ( scope, { data={} }) {
|
|
1269
|
+
/**
|
|
1270
|
+
* Log question_data response props appropriately.
|
|
1271
|
+
*
|
|
1272
|
+
* @param {Object} scope - Tests state object
|
|
1273
|
+
* @param {Object} obj - Named args
|
|
1274
|
+
* @param {Object} obj.data - Data for a non-JSON response to fetching
|
|
1275
|
+
* docassemble current question data
|
|
1276
|
+
*
|
|
1277
|
+
* Possible data shapes:
|
|
1278
|
+
* { ok: false }; // ok -> response.ok or false
|
|
1279
|
+
* | { ok, status, ...data, ...( await response.json() ) }
|
|
1280
|
+
* | { ok, status, alk_msg: err_msg, ...data, html: await response.text() }
|
|
1281
|
+
* | { ok, status, alk_msg: err_msg, content_type }
|
|
1282
|
+
* | { ok: false, alk_msg: err_msg, error: response_err.message }
|
|
1283
|
+
*
|
|
1284
|
+
* @returns {string} - Log text
|
|
1285
|
+
* */
|
|
1286
|
+
let default_msg = `ALKlin skipped processing the current page's server data.`;
|
|
1287
|
+
let qst_msgs = [ data.alk_msg || default_msg, ]
|
|
1288
|
+
|
|
1289
|
+
if ( data.status || data.html || data.error || data.content_type ) { qst_msgs.push(``); }
|
|
1290
|
+
|
|
1291
|
+
if ( data.status ) { qst_msgs.push(`Response status: ${ data.status }`); }
|
|
1292
|
+
if ( data.content_type ) { qst_msgs.push(`Content type: ${ data.content_type }`) }
|
|
1293
|
+
if ( data.html ) {
|
|
1294
|
+
let $ = cheerio.load( data.html );
|
|
1295
|
+
qst_msgs.push( $('#damain')?.text?.() || data.html );
|
|
1296
|
+
}
|
|
1297
|
+
if ( data.error ) { qst_msgs.push( data.error ); }
|
|
1298
|
+
|
|
1299
|
+
return log.debug({ level: `warn`, code: `ALK0282`, context: `question_data`,
|
|
1300
|
+
}, qst_msgs.join(`\n`));
|
|
1301
|
+
},
|
|
1302
|
+
|
|
1303
|
+
canvas_name: {
|
|
1304
|
+
try_for_best_encoded: function( scope, { existing_guesses, $, trigger }) {
|
|
1305
|
+
/**
|
|
1306
|
+
* Returns the canvas' encoded var instance name if possible. Otherwise
|
|
1307
|
+
* returns null.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {Object} scope - Tests state object
|
|
1310
|
+
* @param {Object} obj - Named arguments
|
|
1311
|
+
* @param {Object} obj.existing_guesses - List of existing guesses
|
|
1312
|
+
* @param {Object} obj.$ - cheerio
|
|
1313
|
+
* @param {Object} obj.trigger - Name of the var the interview is seeking
|
|
1314
|
+
*
|
|
1315
|
+
* @returns {string} - Encoded variable instance name
|
|
1316
|
+
*
|
|
1317
|
+
* @example
|
|
1318
|
+
* scope.canvas_name.try_for_best_encoded( scope, { existing_guesses, $, trigger }
|
|
1319
|
+
* // cHJveHlfbGlzdFswXS5zaWduYXR1cmU // decoded: people[0]...
|
|
1320
|
+
* */
|
|
1321
|
+
if ( existing_guesses?.length > 0 ) { return null; }
|
|
1322
|
+
return (
|
|
1323
|
+
// v1.9.n: If da server's config value of `restrict input
|
|
1324
|
+
// variables` set to True, `_save_as` should work to get the var
|
|
1325
|
+
// name. v 1.10.n, unknown
|
|
1326
|
+
$(`input[name="_save_as"]`)?.attr?.(`value`)
|
|
1327
|
+
|| trigger // From ALKiln custom HTML author may or may not be using
|
|
1328
|
+
|| null
|
|
1329
|
+
);
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1332
|
+
try_for_mid_guesses: function( scope, { question_data }) {
|
|
1333
|
+
/**
|
|
1334
|
+
* If possible, returns a list of one string - a decoded instance name
|
|
1335
|
+
* that likely belongs to the canvas var. Otherwise returns null.
|
|
1336
|
+
* Example: people[0].signature.
|
|
1337
|
+
*
|
|
1338
|
+
* @param {Object} scope - Tests state object
|
|
1339
|
+
* @param {Object} obj - Named arguments
|
|
1340
|
+
* @param {Object} obj.question_data - Docassemble server data about the
|
|
1341
|
+
* current question
|
|
1342
|
+
*
|
|
1343
|
+
* @returns {array} - List of one decoded canvas element name
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* scope.canvas_name.try_for_mid_guesses( scope, { field_data, question_data })
|
|
1347
|
+
* // people[0].signature // If it's the first one in the list, that is
|
|
1348
|
+
* */
|
|
1349
|
+
// `event_list` contains decoded instance names of variables. Sadly, this
|
|
1350
|
+
// code assumes the first item of `event_list` is the canvas name. Note,
|
|
1351
|
+
// though, that I've been unable to trigger an event_list with multiple
|
|
1352
|
+
// items.
|
|
1353
|
+
if ( question_data?.event_list?.[0] ) {
|
|
1354
|
+
return [ question_data.event_list[0] ];
|
|
1355
|
+
}
|
|
1356
|
+
return null;
|
|
1357
|
+
},
|
|
1358
|
+
|
|
1359
|
+
try_for_worse_guesses: function( scope, { question_data }) {
|
|
1360
|
+
/**
|
|
1361
|
+
* If possible, returns a list of one string - a decoded var name that
|
|
1362
|
+
* likely belongs to the canvas, but may contain x, i, or other such
|
|
1363
|
+
* proxy variable names instead of the intrinsic name. Otherwise returns
|
|
1364
|
+
* null. Example: x[i].signature. Note that I've been unable to trigger
|
|
1365
|
+
* a `fields` list with multiple items.
|
|
1366
|
+
*
|
|
1367
|
+
* @param {Object} scope - Tests state object
|
|
1368
|
+
* @param {Object} obj - Named arguments
|
|
1369
|
+
* @param {Object} obj.question_data - Docassemble server data about the
|
|
1370
|
+
* current question
|
|
1371
|
+
*
|
|
1372
|
+
* @returns {[string]} - List of one decoded canvas, maybe proxy, var name
|
|
1373
|
+
*
|
|
1374
|
+
* @example
|
|
1375
|
+
* scope.canvas_name.try_for_mid_guesses( scope, { question_data })
|
|
1376
|
+
* // x[i].signature
|
|
1377
|
+
* */
|
|
1378
|
+
// The canvas should be the only field on the page, so the assumption that
|
|
1379
|
+
// the canvas's name is the first item of `fields` should be reasonable.
|
|
1380
|
+
if ( question_data?.fields?.[0]?.variable_name ) {
|
|
1381
|
+
return [ question_data.fields[0].variable_name ];
|
|
1382
|
+
}
|
|
1383
|
+
return null;
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1386
|
+
try_for_worse_encoded: function( scope, { question_data }) {
|
|
1387
|
+
/**
|
|
1388
|
+
* If possible, returns an encoded var name that likely belongs to the
|
|
1389
|
+
* canvas, but may contain x, i, or other such proxy variable names
|
|
1390
|
+
* instead of the intrinsic name. Otherwise returns null. Example:
|
|
1391
|
+
* x[i].signature. Note that I've been unable to trigger a `fields` list
|
|
1392
|
+
* with multiple items.
|
|
1393
|
+
*
|
|
1394
|
+
* @param {Object} scope - Tests state object
|
|
1395
|
+
* @param {Object} obj - Named arguments
|
|
1396
|
+
* @param {Object} obj.question_data - Docassemble server data about the
|
|
1397
|
+
* current question
|
|
1398
|
+
*
|
|
1399
|
+
* @returns {string} - Encoded canvas, maybe proxy, var name
|
|
1400
|
+
*
|
|
1401
|
+
* @example
|
|
1402
|
+
* scope.canvas_name.try_for_mid_guesses( scope, { question_data })
|
|
1403
|
+
* // eFtpXS5zaWduYXR1cmU // Decodes to x[i].signature
|
|
1404
|
+
* */
|
|
1405
|
+
// The canvas should be the only field on the page, so assuming the
|
|
1406
|
+
// canvas's name the first item of `fields` should be reasonable.
|
|
1407
|
+
// Last resort, encoded name. Example: eFtpXS5zaWduYXR1cmU which decodes to (x[i]...).
|
|
1408
|
+
return question_data?.fields?.[0]?.variable_name_encoded;
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
|
|
1083
1412
|
getPageJSON: async function ( scope ) {
|
|
1084
1413
|
/* Get the JSON data of the current page. */
|
|
1085
1414
|
return await scope.page.evaluate(async function ( elem ) {
|
|
@@ -1103,6 +1432,7 @@ module.exports = {
|
|
|
1103
1432
|
if ( node.attribs.value ) { selector += `[value="${ node.attribs.value }"]`; }
|
|
1104
1433
|
if ( node.attribs.id ) { selector += `[id="${ node.attribs.id }"]`; }
|
|
1105
1434
|
if ( node.attribs.class ) { selector += `[class="${ node.attribs.class }"]`; }
|
|
1435
|
+
if ( node.attribs.role ) { selector += `[role="${ node.attribs.role }"]`; }
|
|
1106
1436
|
if ( $( node ).attr( 'data-linknum' )) { selector += `[data-linknum="${ $( node ).attr( 'data-linknum' ) }"]`; }
|
|
1107
1437
|
// Everything should be in #daquestion and that's also how our unit tests and fixtures work.
|
|
1108
1438
|
selector = '#daquestion ' + selector;
|
|
@@ -1122,12 +1452,12 @@ module.exports = {
|
|
|
1122
1452
|
return selector;
|
|
1123
1453
|
}, // Ends scope.getSelector()
|
|
1124
1454
|
|
|
1125
|
-
|
|
1455
|
+
add_values_if_needed: async function ( scope, { var_names, values }) {
|
|
1126
1456
|
/* Return variable names and values combined in ways that can
|
|
1127
1457
|
* try to match table rows. Tables can have two properties:
|
|
1128
|
-
* variable name and value. */
|
|
1458
|
+
* variable name and value. Despite it's name, non-mutating. */
|
|
1129
1459
|
log.debug({ code: `ALK0126`, context: `matching`, },
|
|
1130
|
-
`scope.
|
|
1460
|
+
`scope.add_values_if_needed() var_names:`, var_names, `values:`, values
|
|
1131
1461
|
);
|
|
1132
1462
|
let possible_table_rows = [];
|
|
1133
1463
|
for ( let var_name of var_names ) {
|
|
@@ -1156,7 +1486,7 @@ module.exports = {
|
|
|
1156
1486
|
} // end iterate through row possibilities
|
|
1157
1487
|
|
|
1158
1488
|
return possible_table_rows;
|
|
1159
|
-
}, // Ends scope.
|
|
1489
|
+
}, // Ends scope.add_values_if_needed()
|
|
1160
1490
|
|
|
1161
1491
|
getFieldNamesDict: async function ( scope, { $ }) {
|
|
1162
1492
|
/* Returns an object containing a map of the `_field_n` names and
|
|
@@ -1397,7 +1727,7 @@ module.exports = {
|
|
|
1397
1727
|
if ( opt.attribs.value !== undefined ) {
|
|
1398
1728
|
options.push( opt.attribs.value );
|
|
1399
1729
|
}
|
|
1400
|
-
// Dropdowns created w/ objects are base64 encoded; but it's handled in
|
|
1730
|
+
// Dropdowns created w/ objects are base64 encoded; but it's handled in add_values_if_needed
|
|
1401
1731
|
}
|
|
1402
1732
|
return options;
|
|
1403
1733
|
}, // Ends scope.getOptionValues()
|
|
@@ -1716,6 +2046,7 @@ module.exports = {
|
|
|
1716
2046
|
// Pass in the selector as an argument
|
|
1717
2047
|
}, field.selector );
|
|
1718
2048
|
|
|
2049
|
+
|
|
1719
2050
|
return handle;
|
|
1720
2051
|
}, // Ends scope.get_handle_from_field()
|
|
1721
2052
|
|
|
@@ -1852,6 +2183,10 @@ module.exports = {
|
|
|
1852
2183
|
await scope.tapElementAndNavigate( scope, { elem: handle });
|
|
1853
2184
|
return true;
|
|
1854
2185
|
},
|
|
2186
|
+
a: async function ( scope, { handle, answer }) {
|
|
2187
|
+
await scope.tapElementAndNavigate( scope, { elem: handle });
|
|
2188
|
+
return true;
|
|
2189
|
+
},
|
|
1855
2190
|
textarea: async function ( scope, { handle, answer }) {
|
|
1856
2191
|
await scope.setText( scope, { handle, answer });
|
|
1857
2192
|
await scope.afterStep(scope); // No showifs for this one?
|
|
@@ -1895,7 +2230,7 @@ module.exports = {
|
|
|
1895
2230
|
/* Set value of some `input` element to the given value. */
|
|
1896
2231
|
let row_was_used = true;
|
|
1897
2232
|
|
|
1898
|
-
let type = await handle.evaluate( elem => {
|
|
2233
|
+
let type = await handle.evaluate( elem => {
|
|
1899
2234
|
if (elem) {
|
|
1900
2235
|
return elem.getAttribute('type');
|
|
1901
2236
|
} else {
|
|
@@ -2076,28 +2411,112 @@ module.exports = {
|
|
|
2076
2411
|
return false;
|
|
2077
2412
|
}
|
|
2078
2413
|
|
|
2079
|
-
//
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2414
|
+
// ✏️ The code below is a great indication that Log and `reports`
|
|
2415
|
+
// architecture may be less than ideal, though js doesn't help.
|
|
2416
|
+
|
|
2417
|
+
// TODO: Pass in a promise aborter to page.evaluate() if possible, though
|
|
2418
|
+
// it's not completely necessary. If the gobal `setTimeout` fires then the
|
|
2419
|
+
// `evaluate` won't be far behind. They both use about the same timeout
|
|
2420
|
+
// duration.
|
|
2421
|
+
let finish_uploading = handle.evaluate( async function detect_finished_upload(
|
|
2422
|
+
elem, timeout, answer
|
|
2423
|
+
) {
|
|
2424
|
+
return new Promise (( resolve, reject ) => {
|
|
2425
|
+
/**
|
|
2426
|
+
* Resolve when upload completes. Logging in here will log to the hidden
|
|
2427
|
+
* browser. Must instead pass information out with `resolve`.
|
|
2428
|
+
* */
|
|
2429
|
+
// Warn about uploading the same file multiple times
|
|
2430
|
+
// Won't protect against accidentally uploading same file to diff field
|
|
2431
|
+
|
|
2432
|
+
// If elem.files, the author has previously set this field. Warn them.
|
|
2433
|
+
// Example prev_filenames: "C:\\fakepath\\name_of_file.png"
|
|
2434
|
+
let prev_filenames = [];
|
|
2435
|
+
let shared_names_list = [];
|
|
2436
|
+
let all_matched = true;
|
|
2437
|
+
for ( let file_obj of elem.files ) {
|
|
2438
|
+
let filename = file_obj.name.slice( 12, file_obj.name.length );
|
|
2439
|
+
prev_filenames.push( filename );
|
|
2440
|
+
// May over-match with names that end the same way
|
|
2441
|
+
if ( answer.includes( filename ) ) {
|
|
2442
|
+
shared_names_list.push( filename );
|
|
2443
|
+
} else {
|
|
2444
|
+
all_matched = false;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
let result = {
|
|
2449
|
+
ok: false, prev_filenames, type: null,
|
|
2450
|
+
code: `ALK0283`, level: `debug`, msgs: [], error: null,
|
|
2451
|
+
};
|
|
2084
2452
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2453
|
+
if ( prev_filenames ) {
|
|
2454
|
+
result.level = `warn`; // log level
|
|
2455
|
+
result.type = `warning`; // report type
|
|
2456
|
+
shared_names = shared_names_list.join(`", "`);
|
|
2457
|
+
|
|
2458
|
+
if ( shared_names_list.length > 0 ) {
|
|
2459
|
+
if ( all_matched ) {
|
|
2460
|
+
result.code = `ALK0284`;
|
|
2461
|
+
result.msgs.push(`Previously, you uploaded these files to this field: "${ answer }". Just now you tried to upload these files to this field: "${ shared_names }". Those names closely match. Did you mean them to be different?`);
|
|
2462
|
+
} else {
|
|
2463
|
+
result.code = `ALK0285`;
|
|
2464
|
+
result.msgs.push(`Previously, you uploaded these files to this field: "${ answer }". Just now you tried to upload these files to this field: "${ shared_names }". Some of those names closely match. Did you mean them to be different?`);
|
|
2465
|
+
}
|
|
2466
|
+
} else {
|
|
2467
|
+
result.code = `ALK0286`;
|
|
2468
|
+
result.msgs.push(`You already uploaded some files to the current field. Did you mean to do that?`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
let timeout_id = null;
|
|
2473
|
+
let resolve_on_change = function() {
|
|
2474
|
+
result.ok = true; // May contain warnings
|
|
2475
|
+
clearTimeout( timeout_id );
|
|
2476
|
+
result.msgs.push(`Finished uploading these files: "${ answer }"`);
|
|
2477
|
+
resolve( result );
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
timeout_id = setTimeout(() => {
|
|
2481
|
+
elem.removeEventListener(`change`, resolve_on_change );
|
|
2482
|
+
result.level = `throw`;
|
|
2483
|
+
result.type = `error`;
|
|
2484
|
+
result.code = `ALK0281`;
|
|
2485
|
+
// Discuss: Will the trace be confusing from here?
|
|
2486
|
+
result.msgs.push(`It took more than ${( timeout - 100 )/1000 } seconds to try to upload the file(s) "${ answer }".`);
|
|
2487
|
+
// ⚠️ Unable to pass Error out of page.evaluate. Must use string.
|
|
2488
|
+
result.error = `Timeout error: It took too long to upload the files.`;
|
|
2489
|
+
resolve( result );
|
|
2490
|
+
}, timeout );
|
|
2491
|
+
|
|
2492
|
+
elem.value = null;
|
|
2493
|
+
elem.addEventListener(`change`, resolve_on_change );
|
|
2494
|
+
}); // ends Promise
|
|
2495
|
+
|
|
2496
|
+
}, scope.timeout - 100, answer ); // ends page.evaluate()
|
|
2089
2497
|
|
|
2090
|
-
// Upload
|
|
2091
2498
|
await handle.uploadFile(...paths);
|
|
2092
2499
|
|
|
2093
|
-
//
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2500
|
+
// Wait for upload to finish. If already done, execution will continue
|
|
2501
|
+
let { ok, type, level, code, error, msgs } = await finish_uploading;
|
|
2502
|
+
|
|
2503
|
+
let report_vals = msgs;
|
|
2504
|
+
if ( error ) {
|
|
2505
|
+
// ⚠️ Unable to get Error out of page.evaluate. Must create it here.
|
|
2506
|
+
error = new Error( error );
|
|
2507
|
+
report_vals.push( error );
|
|
2508
|
+
}
|
|
2509
|
+
if ( type !== null ) {
|
|
2510
|
+
reports.addToReport(scope, { type, code, value: report_vals.join(`\n`) });
|
|
2511
|
+
}
|
|
2098
2512
|
|
|
2099
|
-
//
|
|
2100
|
-
|
|
2513
|
+
// The ternary calls, but the need for it smells like bad code to me.
|
|
2514
|
+
let log_type = `debug`;
|
|
2515
|
+
if ( level === `throw` ) { log_type = `throw`; }
|
|
2516
|
+
log[ log_type ]({ code, level, error, context: `file_upload`}, ...msgs );
|
|
2517
|
+
|
|
2518
|
+
// Return whether successfully used row
|
|
2519
|
+
return ok;
|
|
2101
2520
|
}, // Ends scope.uploadFiles()
|
|
2102
2521
|
|
|
2103
2522
|
setCheckbox: async function ( scope, { label, answer }) {
|
|
@@ -2150,7 +2569,7 @@ module.exports = {
|
|
|
2150
2569
|
let canvas = await scope.page.waitForSelector( `#dasigcanvas` );
|
|
2151
2570
|
let msg = `Could not find a signature field.`;
|
|
2152
2571
|
if ( !canvas ) { reports.addToReport(scope, { type: `error`, code: `ALK0137`, value: msg }); }
|
|
2153
|
-
expect( canvas, msg ).to.
|
|
2572
|
+
expect( canvas, msg ).to.not.be.null;
|
|
2154
2573
|
|
|
2155
2574
|
// Provides functionality to draw name arg on canvas
|
|
2156
2575
|
if (name !== undefined) {
|
|
@@ -2163,7 +2582,9 @@ module.exports = {
|
|
|
2163
2582
|
let bounding_box = await canvas.boundingBox();
|
|
2164
2583
|
// We draw the dot because it allows the developer to tap continue
|
|
2165
2584
|
// Docassemble does not allow to tap continue unless the user interacts with the canvas
|
|
2166
|
-
|
|
2585
|
+
let pen_x = (bounding_box.x + bounding_box.width / 2) + Math.random() * 50;
|
|
2586
|
+
let pen_y = (bounding_box.y + bounding_box.height / 2) + Math.random() * 50;
|
|
2587
|
+
await scope.page.mouse.move( pen_x, pen_y );
|
|
2167
2588
|
await scope.page.mouse.down();
|
|
2168
2589
|
// await scope.page.mouse.move(1, 1); // Too big? Wait a while before drawing?
|
|
2169
2590
|
await scope.page.mouse.up();
|
|
@@ -2253,6 +2674,8 @@ module.exports = {
|
|
|
2253
2674
|
`scope.setFields()`
|
|
2254
2675
|
);
|
|
2255
2676
|
|
|
2677
|
+
let error_data = null;
|
|
2678
|
+
|
|
2256
2679
|
// Only record the page id once for each page to which we navigate
|
|
2257
2680
|
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
2258
2681
|
if ( scope.page_id !== id ) {
|
|
@@ -2278,7 +2701,9 @@ module.exports = {
|
|
|
2278
2701
|
if ( field.was_set ) { continue; }
|
|
2279
2702
|
// Avoid navigating in this `while`. Will do this later so we get to
|
|
2280
2703
|
// set all fields in here first. TODO: Can links (<a>) set vars?
|
|
2281
|
-
if (
|
|
2704
|
+
if (
|
|
2705
|
+
scope.is_button_ish( scope, { field }) && field.type === `submit`
|
|
2706
|
+
) { continue; }
|
|
2282
2707
|
|
|
2283
2708
|
// Try to set other types of fields
|
|
2284
2709
|
let row_used = await scope.setVariable( scope, { field, var_data });
|
|
@@ -2307,7 +2732,7 @@ module.exports = {
|
|
|
2307
2732
|
let error = {};
|
|
2308
2733
|
for ( let field of fields ) {
|
|
2309
2734
|
// All other fields should have been set
|
|
2310
|
-
if (
|
|
2735
|
+
if ( scope.is_button_ish( scope, { field }) && field.type === 'submit' ) {
|
|
2311
2736
|
let page_url = await scope.page.url();
|
|
2312
2737
|
|
|
2313
2738
|
let row_used = await scope.setVariable( scope, { field, var_data, });
|
|
@@ -2334,10 +2759,7 @@ module.exports = {
|
|
|
2334
2759
|
// Make sure no system or user error messages appear
|
|
2335
2760
|
// Discuss moving this out of here. Having to pass in `id` just for
|
|
2336
2761
|
// these messages smells bad. Con: Already a lot of code out there.
|
|
2337
|
-
|
|
2338
|
-
// if ( error.was_found ) {
|
|
2339
|
-
// await scope.throwPageError( scope, { id: id });
|
|
2340
|
-
// }
|
|
2762
|
+
error_data = await scope.waitUntilContinued( scope, { url: page_url, id });
|
|
2341
2763
|
|
|
2342
2764
|
did_navigate = true;
|
|
2343
2765
|
log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
|
|
@@ -2351,17 +2773,16 @@ module.exports = {
|
|
|
2351
2773
|
if ( ensure_all_vars_used ) {
|
|
2352
2774
|
for ( let row of var_data ) {
|
|
2353
2775
|
if ( row.times_used === 0 ) {
|
|
2354
|
-
let values_used_status = 'The page is missing a value';
|
|
2355
2776
|
let source_row = row;
|
|
2356
2777
|
// TODO: Make the row object flatter so we don't need to dig in this way
|
|
2357
2778
|
if ( row.source ) { source_row = row.source; }
|
|
2358
2779
|
let original_row = source_row.original;
|
|
2359
2780
|
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
2360
2781
|
let msg = `In a linear Step all the values you give MUST be filled in, but ALKiln was unable `
|
|
2361
|
-
+ `to find a field on the page "${id}" for the variable "${ original_row.var }" that `
|
|
2782
|
+
+ `to find a field on the page with the "${ id }" for the variable "${ original_row.var }" that `
|
|
2362
2783
|
+ `could be set to "${ original_row.value }". See this Scenario's HTML file for details.`;
|
|
2363
2784
|
reports.addToReport(scope, { type: `error`, code: `ALK0141`, value: msg });
|
|
2364
|
-
expect(
|
|
2785
|
+
expect( 'The page is missing a field', msg ).to.equal( '' );
|
|
2365
2786
|
}
|
|
2366
2787
|
}
|
|
2367
2788
|
}
|
|
@@ -2380,11 +2801,7 @@ module.exports = {
|
|
|
2380
2801
|
// Discuss moving this out of here. Having to pass in `id` just for
|
|
2381
2802
|
// these messages smells bad. Con: Already a lot of code out there.
|
|
2382
2803
|
|
|
2383
|
-
|
|
2384
|
-
error = await scope.waitUntilContinued( scope, { url: page_url, id });
|
|
2385
|
-
// if ( error.was_found ) {
|
|
2386
|
-
// await scope.throwPageError( scope, { id: id });
|
|
2387
|
-
// }
|
|
2804
|
+
error_data = await scope.waitUntilContinued( scope, { url: page_url, id });
|
|
2388
2805
|
|
|
2389
2806
|
did_navigate = true;
|
|
2390
2807
|
log.stdout({}, `\x1b[36m${ '>' }\x1b[0m`); // continued successfully
|
|
@@ -2400,10 +2817,29 @@ module.exports = {
|
|
|
2400
2817
|
// TODO: Since we now continue only once no matter what, we can put
|
|
2401
2818
|
// the error in the caller of the function, though we may want
|
|
2402
2819
|
// to pass the symbol to print as well.
|
|
2403
|
-
return
|
|
2820
|
+
return error_data;
|
|
2404
2821
|
// TODO: Maybe we should return something else or something in addition about used vars
|
|
2405
2822
|
}, // Ends scope.setFields()
|
|
2406
2823
|
|
|
2824
|
+
is_button_ish: function ( scope, { field = null }) {
|
|
2825
|
+
/**
|
|
2826
|
+
* Return true if the field is similar enough to a button, else false. This
|
|
2827
|
+
* does NOT determine whether the field submits the form.
|
|
2828
|
+
*
|
|
2829
|
+
* Discuss: Use "btn" class as well?
|
|
2830
|
+
*
|
|
2831
|
+
* @param {Object} scope - Tests state object
|
|
2832
|
+
* @param {Object} field - One field from `.getAllFields()`
|
|
2833
|
+
*
|
|
2834
|
+
* @returns bool
|
|
2835
|
+
* */
|
|
2836
|
+
if ( field.tag === `button`
|
|
2837
|
+
|| field.selector.includes(`role="button"`)
|
|
2838
|
+
|| field.selector.includes(`.dasigsave`)
|
|
2839
|
+
) { return true; }
|
|
2840
|
+
return false;
|
|
2841
|
+
},
|
|
2842
|
+
|
|
2407
2843
|
increment_row_use: async function ( scope, { row }) {
|
|
2408
2844
|
/** Accumulate number of times this row was used. Esp important for `.target_number`.
|
|
2409
2845
|
* Mutates `row` and its props. */
|
|
@@ -2641,16 +3077,22 @@ module.exports = {
|
|
|
2641
3077
|
is found. Return the error object no matter what. */
|
|
2642
3078
|
let prev_url = url;
|
|
2643
3079
|
let curr_url = await scope.page.url();
|
|
2644
|
-
let
|
|
3080
|
+
let error_data = await scope.checkForError( scope );
|
|
2645
3081
|
|
|
2646
3082
|
let timedOut = false; // Lazy timeout?
|
|
2647
3083
|
let cont_timeout = setTimeout(function(){ timedOut = true }, scope.timeout);
|
|
2648
3084
|
|
|
2649
3085
|
// Attempt to solve weird race condition issues where page has not properly continued
|
|
2650
|
-
while ( curr_url === prev_url &&
|
|
3086
|
+
while ( curr_url === prev_url && error_data.ok && !timedOut ) {
|
|
2651
3087
|
await time.waitForTimeout( 50 );
|
|
2652
3088
|
curr_url = await scope.page.url();
|
|
2653
|
-
|
|
3089
|
+
error_data = await scope.checkForError( scope );
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
if ( !error_data.ok ) {
|
|
3093
|
+
await scope.throw_system_error( scope, { error_data, id: id });
|
|
3094
|
+
// If trying to continue, shouldn't be running into user errors either
|
|
3095
|
+
await scope.throw_invalid_answer( scope, { error_data, id: id });
|
|
2654
3096
|
}
|
|
2655
3097
|
|
|
2656
3098
|
let timeout_err_message = `The test tried to continue to the next page, but it took longer than ${ (scope.timeout/1000)/60 } min. Check the picture of this page. If a secret variable was used, there will be no picture.`
|
|
@@ -2659,7 +3101,7 @@ module.exports = {
|
|
|
2659
3101
|
|
|
2660
3102
|
clearTimeout( cont_timeout );
|
|
2661
3103
|
|
|
2662
|
-
return
|
|
3104
|
+
return error_data;
|
|
2663
3105
|
}, // Ends scope.waitUntilContinued()
|
|
2664
3106
|
|
|
2665
3107
|
checkForError: async function ( scope ) {
|
|
@@ -2667,20 +3109,22 @@ module.exports = {
|
|
|
2667
3109
|
* returns those elements. The error elements are probably exclusive, but
|
|
2668
3110
|
* we're not sure. */
|
|
2669
3111
|
|
|
2670
|
-
// All results are lists to match `$x`
|
|
2671
|
-
let
|
|
2672
|
-
|
|
3112
|
+
// All results are lists to match puppeteerjs `$x`
|
|
3113
|
+
let possible_error_types = [
|
|
3114
|
+
// TODO: Be more specific. Author can also use a `.alert-danger` call out.
|
|
3115
|
+
{ type: `user`, handlers: await scope.page.$$(`.alert-danger`) },
|
|
2673
3116
|
// The below does not work if there's not a space between ':' and 'none'
|
|
2674
|
-
|
|
2675
|
-
|
|
3117
|
+
{ type: `user`, handlers: await scope.page.$$(`xpath/.//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`) },
|
|
3118
|
+
{ type: `system`, handlers: await scope.page.$$(`xpath/.//meta[@content="docassemble: Error"]` ) },
|
|
2676
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.
|
|
2677
|
-
|
|
2678
|
-
|
|
3120
|
+
{ type: `system`, handlers: await scope.page.$$('.question-custom-error-action .daattributions #trigger[data-variable="YWxfY3VzdG9tX2Vycm9yX2FjdGlvbg"]') },
|
|
3121
|
+
];
|
|
2679
3122
|
|
|
2680
|
-
let error_handlers =
|
|
3123
|
+
let error_handlers = []; // TODO: Refactor to be single item
|
|
2681
3124
|
let was_found = false;
|
|
2682
|
-
|
|
2683
|
-
|
|
3125
|
+
let type = null;
|
|
3126
|
+
for ( let one_err_type of possible_error_types ) {
|
|
3127
|
+
if ( one_err_type.handlers[0] ) {
|
|
2684
3128
|
// If there's a page visible (locally), give us as much time to examine it as timeout allows.
|
|
2685
3129
|
// Because of previous actions, this may still be a bit too long.
|
|
2686
3130
|
// Also, if you're debugging and you close the window early, the process will still
|
|
@@ -2689,36 +3133,53 @@ module.exports = {
|
|
|
2689
3133
|
await time.waitForTimeout( ( scope.timeout - (1 * 1000)) );
|
|
2690
3134
|
}
|
|
2691
3135
|
was_found = true;
|
|
2692
|
-
|
|
3136
|
+
type = one_err_type.type
|
|
3137
|
+
error_handlers.push( one_err_type.handlers[0] );
|
|
3138
|
+
break;
|
|
2693
3139
|
}
|
|
2694
3140
|
}
|
|
2695
3141
|
|
|
2696
|
-
return { was_found, error_handlers };
|
|
3142
|
+
return { was_found, ok: !was_found, type, error_handlers };
|
|
2697
3143
|
}, // Ends scope.checkForError()
|
|
2698
3144
|
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
3145
|
+
throw_system_error: async function ( scope, {
|
|
3146
|
+
error_data = null, error_handlers = null, id = ``, error_text = ``
|
|
3147
|
+
}) {
|
|
3148
|
+
/** Throw a cucumber error with the error that appeared on the page. */
|
|
3149
|
+
let was_system_error = error_data?.type === `system`
|
|
3150
|
+
|| error_handlers?.system_error !== undefined
|
|
3151
|
+
|| error_handlers?.al_custom_error !== undefined;
|
|
3152
|
+
|
|
3153
|
+
if ( was_system_error ) {
|
|
3154
|
+
let err_msg = `The test ran into a system error when it tried to continue to the next page after "${ id }". Also check the picture of this page.`;
|
|
3155
|
+
let report_msg = reports.addToReport(scope, { type: `error`, code: `ALK0147`, value: err_msg });
|
|
3156
|
+
expect( was_system_error, report_msg ).to.be.false;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
return scope;
|
|
3160
|
+
}, // Ends scope.throw_system_error()
|
|
2705
3161
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
3162
|
+
throw_invalid_answer: async function ( scope, {
|
|
3163
|
+
error_data = {}, id = ''
|
|
3164
|
+
}) {
|
|
3165
|
+
// It was a user error, which the author may be expecting
|
|
3166
|
+
if ( !error_data.ok && error_data.type === `user` ) {
|
|
3167
|
+
let err_msg = `There was an invalid answer when the test tried to continue to the next page after ${ id }. Check the picture of this page.`;
|
|
3168
|
+
let report_msg = reports.addToReport(scope, { type: `warning`, code: `ALK0148`, value: err_msg });
|
|
3169
|
+
expect( error_data.type === `user`, report_msg ).to.be.false;
|
|
2711
3170
|
}
|
|
2712
3171
|
|
|
2713
3172
|
return scope;
|
|
2714
|
-
}, // Ends scope.
|
|
3173
|
+
}, // Ends scope.throw_invalid_answer()
|
|
2715
3174
|
|
|
2716
3175
|
take_a_screenshot: async ( scope,{ path }) => {
|
|
2717
3176
|
/* Takes a jpeg screenshot. Avoids destroying signatures. */
|
|
2718
3177
|
|
|
2719
3178
|
let fullPage = true;
|
|
2720
|
-
|
|
2721
|
-
signature_elem =
|
|
3179
|
+
// Use only $ as opposed to $$ - just checking for presence of any element
|
|
3180
|
+
let signature_elem = await scope.page.$(scope.signature_cont_btn_selector_da_v_1_9_n_and_earlier, { visible: true })
|
|
3181
|
+
|| await scope.page.$(scope.signature_cont_btn_selector_backup_da_v_1_9_n_and_earlier, { visible: true })
|
|
3182
|
+
|| await scope.page.$(scope.signature_cont_btn_selector_da_v_1_10_n_and_later, { visible: true });
|
|
2722
3183
|
if ( signature_elem !== null ) {
|
|
2723
3184
|
fullPage = false;
|
|
2724
3185
|
}
|
|
@@ -2948,8 +3409,6 @@ module.exports = {
|
|
|
2948
3409
|
}
|
|
2949
3410
|
|
|
2950
3411
|
// TODO: Add debug log here
|
|
2951
|
-
// console.log(`filename:`, filename);
|
|
2952
|
-
|
|
2953
3412
|
// TODO: Add to the report if we had to use the default name
|
|
2954
3413
|
|
|
2955
3414
|
return filename;
|
|
@@ -3236,7 +3695,12 @@ module.exports = {
|
|
|
3236
3695
|
},
|
|
3237
3696
|
|
|
3238
3697
|
set_random_page_vars: async ( scope ) => {
|
|
3239
|
-
/**
|
|
3698
|
+
/**
|
|
3699
|
+
* Give random answers to every field.
|
|
3700
|
+
*
|
|
3701
|
+
* ⚠️ Keep hidden fields! With some customdatatypes, we set their values.
|
|
3702
|
+
*
|
|
3703
|
+
* */
|
|
3240
3704
|
|
|
3241
3705
|
let html = await scope.page.content();
|
|
3242
3706
|
// These are the fields on the current page. Their handles should all exist.
|
|
@@ -3247,8 +3711,11 @@ module.exports = {
|
|
|
3247
3711
|
for ( let field of fields ) {
|
|
3248
3712
|
// fields[n].tag is HTML tag of node, as a string
|
|
3249
3713
|
|
|
3250
|
-
//
|
|
3251
|
-
|
|
3714
|
+
// ⚠️ Keep hidden fields! With some customdatatypes, we set their vals.
|
|
3715
|
+
|
|
3716
|
+
// If the field is a button, save it in a list of buttons that we'll
|
|
3717
|
+
// deal with later
|
|
3718
|
+
if ( scope.is_button_ish( scope, { field }) ) {
|
|
3252
3719
|
buttons.push( field );
|
|
3253
3720
|
} else {
|
|
3254
3721
|
// Try to give a random answer for that element tag
|
|
@@ -3265,15 +3732,24 @@ module.exports = {
|
|
|
3265
3732
|
}, // ends scope.steps
|
|
3266
3733
|
|
|
3267
3734
|
set_random_input_for: {
|
|
3735
|
+
/**
|
|
3736
|
+
* ‼️ We call these methods with `set_random_input_for[ tag ]`. You will not
|
|
3737
|
+
* find them called explicitly.
|
|
3738
|
+
* */
|
|
3268
3739
|
a: async function (scope, { fields }) {
|
|
3269
3740
|
// We don't do anything with links for random input tests
|
|
3270
3741
|
},
|
|
3271
3742
|
buttons: async function (scope, { fields }) {
|
|
3272
3743
|
// Get a random button
|
|
3273
3744
|
let back_btn = null;
|
|
3274
|
-
fields = fields.filter((
|
|
3275
|
-
if (
|
|
3276
|
-
back_btn =
|
|
3745
|
+
fields = fields.filter( async( field ) => {
|
|
3746
|
+
if ( field.selector.includes("daquestionbackbutton") ) {
|
|
3747
|
+
back_btn = field;
|
|
3748
|
+
return false;
|
|
3749
|
+
}
|
|
3750
|
+
let handle = await scope.page.$( field.selector );
|
|
3751
|
+
let bounding_box = await handle?.boundingBox();
|
|
3752
|
+
if ( bounding_box?.height === 0 || bounding_box?.width === 0) {
|
|
3277
3753
|
return false;
|
|
3278
3754
|
}
|
|
3279
3755
|
return true;
|
|
@@ -3330,10 +3806,10 @@ module.exports = {
|
|
|
3330
3806
|
|
|
3331
3807
|
},
|
|
3332
3808
|
canvas: async function (scope, { field }) {
|
|
3333
|
-
|
|
3334
|
-
//
|
|
3335
|
-
//
|
|
3336
|
-
await scope.funnel_the_answer( scope, { field, handle
|
|
3809
|
+
let handle = await scope.page.waitForSelector(`#dasigcanvas`);
|
|
3810
|
+
// We cannot be sure of knowing the name of the signer and so want to
|
|
3811
|
+
// indicate it's random, but we do want something unique
|
|
3812
|
+
await scope.funnel_the_answer( scope, { field, handle, answer: `Random ${ faker.name.lastName() }` });
|
|
3337
3813
|
},
|
|
3338
3814
|
input: async function (scope, { field }) {
|
|
3339
3815
|
/** Handles various `type`s of input element fields, except:
|
|
@@ -3361,8 +3837,7 @@ module.exports = {
|
|
|
3361
3837
|
|
|
3362
3838
|
// If this is a custom datatype
|
|
3363
3839
|
let custom_datatype = await scope.get_custom_datatype( scope, { handle });
|
|
3364
|
-
|
|
3365
|
-
if ( is_custom_datatype ) {
|
|
3840
|
+
if ( scope.get_random_input_for[ custom_datatype ] !== undefined ) {
|
|
3366
3841
|
// And the custom datatype has a hidden field (which will be the original field)
|
|
3367
3842
|
let hidden_field = await scope.get_custom_datatype_hidden_field( scope, { handle });
|
|
3368
3843
|
if ( hidden_field !== null ) {
|