@suffolklitlab/alkiln 5.8.1 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/scope.js CHANGED
@@ -292,6 +292,9 @@ module.exports = {
292
292
  if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
293
293
  expect( elem, msg ).to.exist;
294
294
 
295
+ // Get the text on the button or link for a potential error message later
296
+ let tapperText = await (await elem.getProperty('textContent')).jsonValue();
297
+
295
298
  click_promise = click_promise || elem.click();
296
299
 
297
300
  // TODO: implement and use scope.handle_possible_timeout()
@@ -308,14 +311,11 @@ module.exports = {
308
311
  } catch ( error ) {
309
312
 
310
313
  if ( error.name === `TimeoutError` ) {
311
- let non_reload_report_msg = `ALKiln tapped "${ tapperText }", but the interview took to long to finish.`;
314
+ let non_reload_report_msg = `After ALKiln tapped "${ tapperText }" it took too long to load the next page.`;
312
315
  await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
313
316
  } else {
314
-
315
- // Get the text on the button or link for a potential error message later
316
- let tapperText = await (await elem.getProperty('textContent')).jsonValue();
317
317
  // Throw any non-timeout error
318
- let err_msg = `Error occurred when ALKiln tried to tap "${ tapperText }".`
318
+ let err_msg = `An error occurred when ALKiln tried to tap "${ tapperText }".`
319
319
  reports.addToReport( scope, { type: `error`, value: err_msg });
320
320
  throw error;
321
321
  } // ends if error is timeout error
@@ -464,9 +464,9 @@ module.exports = {
464
464
 
465
465
  throw_server_was_reloading_error: async function( scope ) {//, { server_timeout_id, timeout_message } ) {
466
466
  /* Throw the server reload error with appropriate side effects. */
467
- let reload_failure_msg = `The test was unable to continue because the `
468
- + `server was reloading. ALKiln will try this Scenario a total of 2 times. An `
469
- + `error will still be printed for this attempt.`;
467
+ let reload_failure_msg = `The test was unable to continue because it `
468
+ + `took too long to reach the server. ALKiln will try this Scenario a total of `
469
+ + `2 times. A full error will be printed for the second attempt.`;
470
470
  reports.addToReport( scope, { type: `error`, value: reload_failure_msg });
471
471
  // reports.addToReport( scope, { type: `error`, value: timeout_message });
472
472
  // clearTimeout( server_timeout_id );
@@ -573,10 +573,7 @@ module.exports = {
573
573
  axe_path = await scope.getA11yPath( scope );
574
574
  fs.writeFileSync( axe_path, JSON.stringify( axe_result, null, 2 ));
575
575
  let msg = `Found potential accessibility issues on the ${ scope.page_id } screen. Details in ${ axe_path }.`;
576
- reports.addToReport( scope, {
577
- type: `error`,
578
- value: msg
579
- });
576
+ reports.addToReport( scope, { type: `error`, value: msg });
580
577
  } else {
581
578
  reports.addToReport(scope, { type: `info`, value: `Accessibility on ${ scope.page_id } passed!`})
582
579
  }
@@ -730,7 +727,7 @@ module.exports = {
730
727
  * normalizeTable internally.
731
728
  */
732
729
 
733
- if (!var_data && ! raw_var_data) {
730
+ if (!var_data && !raw_var_data) {
734
731
  let no_data_msg = `ALKiln Internal error: normalizeTable called w/o var_data or raw_var_data:` +
735
732
  ` open an issue at https://github.com/SuffolkLITLab/ALKiln/issues/new if you see this!`;
736
733
  reports.addToReport( scope, { type: `error`, value: no_data_msg });
@@ -739,39 +736,52 @@ module.exports = {
739
736
 
740
737
  // Always use the raw var data if present; it has the full first row
741
738
  if (raw_var_data) {
739
+
742
740
  var_data = raw_var_data.hashes();
743
- if (!('var' in var_data[0]) || !('value' in var_data[0])) {
741
+ // If this table doesn't have headers
742
+ if (!(`var` in var_data[0]) || !(`value` in var_data[0])) {
744
743
  let raw_array = raw_var_data.raw();
745
744
  var_data = [];
746
745
  for (let row_raw of raw_array) {
747
- if (row_raw.length < 2) {
748
- let bad_table_msg = `Your Story Table definition needs to be changed. The best practice`
749
- + ` is to make your table 3 columns wide with a first row in this format: "| var | value | trigger |".`
750
- + ` The first row of your table is actually "${ raw_array[0] }"`;
746
+
747
+ // Wrong number of columns error
748
+ if (row_raw.length < 2 || row_raw.length > 3) {
749
+ let more_or_less = `more`;
750
+ if ( row_raw.length > 3 ) { more_or_less = `less`; }
751
+ let bad_table_msg = `Your Story Table should have ${ more_or_less } than`
752
+ + ` ${ row_raw.length } columns. Take another look at Story Table documentation at`
753
+ + ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#story-tables.`
751
754
  reports.addToReport( scope, { type: `error`, value: bad_table_msg})
752
755
  throw Error(bad_table_msg);
753
- } else if (row_raw.length == 2) {
756
+ }
757
+
758
+ if (row_raw.length == 2) {
754
759
  var_data.push({var: row_raw[0], value: row_raw[1]});
755
760
  } else {
756
761
  var_data.push({var: row_raw[0], value: row_raw[1], trigger: row_raw[2]});
757
762
  }
758
- }
759
- }
760
- }
763
+ } // ends for every row in table
764
+ } // ends if this table doesn't have headers
765
+
766
+ } // ends if we have access to the full raw data
761
767
 
762
768
  // Support tables with no 'trigger' column
763
769
  let supported_table = [];
770
+
764
771
  for ( let row of var_data ) {
765
772
  // See tests > unit_tests > tables.fixtures.js > tables.old_to_current_formatting
766
773
  let actual_var_value = row.value;
767
774
  if (is_secret_var(row)) {
768
- actual_var_value = process.env[row.flags.secret_name];
775
+ actual_var_value = process.env[ row.flags.secret_name ];
769
776
  if ( actual_var_value === undefined ) {
770
- let not_in_env_msg = `The GitHub SECRET "${ row.flags.secret_name }" doesn't exist. Check for a typo and check your workflow file.`;
771
- reports.addToReport( scope, { type: `error`, value: not_in_env_msg })
772
- throw ReferenceError(`Could not find "${row.flags.secret_name }": ${not_in_env_msg}.`)
777
+ let not_in_env_msg = `ALKiln could not find the GitHub secret "${ row.flags.secret_name }".`
778
+ + ` Check for a typo and check your workflow file.`
779
+ + ` Read more about using GitHub secrets in your workflow file at`
780
+ + ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#github-secrets.`
781
+ reports.addToReport( scope, { type: `error`, value: not_in_env_msg });
782
+ throw ReferenceError(not_in_env_msg);
773
783
  }
774
- }
784
+ } // ends if is secret var
775
785
 
776
786
  let result = {
777
787
  // NOTE: mutating frozen objects will fail silently, so don't mutate this attribute
@@ -780,9 +790,9 @@ module.exports = {
780
790
  // Give fake data if needed to prevent some kind of hidden input field from being found
781
791
  // See bug at https://github.com/plocket/docassemble-cucumber/issues/79
782
792
  // May be able to remove this once converted to `getAllFields`
783
- var: row.var || 'al_no_var_name',
793
+ var: row.var || `alkiln_no_var_name`,
784
794
  value: actual_var_value,
785
- trigger: row.trigger || '',
795
+ trigger: row.trigger || ``,
786
796
  // This will track how many times total the row or its connected
787
797
  // artificial rows are used. Artificial rows are only added for some rows.
788
798
  times_used: 0,
@@ -791,11 +801,12 @@ module.exports = {
791
801
  result.used_for[ result.var ] = 0;
792
802
 
793
803
  supported_table.push( result );
794
- }
804
+ } // ends for every row
795
805
 
796
- if ( session_vars.get_debug() ) {
797
- console.log( 'scope.normalizedTable():\n', JSON.stringify( supported_table ));
798
- }
806
+ log.debug({
807
+ type: `info`,
808
+ pre: `scope.normalizedTable():\n ${JSON.stringify( supported_table )}`
809
+ })
799
810
 
800
811
  return supported_table;
801
812
  }, // Ends scope.normalizeTable()
@@ -815,17 +826,22 @@ module.exports = {
815
826
  // This is the new trigger var element that developers will have to add to their interviews.
816
827
  // AssemblyLine will add it automatically unless someone overrides the values in their own interview.
817
828
  // See the report message to the dev below for a link to an example.
818
- let trigger_elem = $( `#trigger` );
819
- if ( !trigger_elem.length ) { trigger_elem = $( `#sought_variable` ); } // Backwards compatibility
820
- let encoded_trigger_var = $( trigger_elem ).data( `variable` ); // This should come out to 'None' at the very least
821
- if ( !encoded_trigger_var ) {
822
- // TODO: Add documentation and then link to it in this message.
823
- reports.addToReport(scope, { type: `warning`, value: `You are missing an element containing the data for the page's trigger variable. Some fields, like loops with index variables, may not work correctly in these tests. See https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#a-missing-trigger-variable.` });
824
- encoded_trigger_var = toBase64( `None` );
825
- }
829
+ let $trigger_elem = $( `#trigger` );
830
+ if ( !$trigger_elem.length ) { $trigger_elem = $( `#sought_variable` ); } // Backwards compatibility
831
+ let encoded_trigger_var = $( $trigger_elem ).data( `variable` ) || ``;
826
832
  let trigger = fromBase64( encoded_trigger_var );
827
833
 
828
-
834
+ let $proxies_node = $( `#alkiln_proxy_var_values` );
835
+ let proxies = {};
836
+ // If possible, get values of `x` and `i` that the dev put on the page
837
+ if ( $proxies_node.length ) {
838
+ // Empty strings are fine since that means that proxy is absent and won't come into play.
839
+ proxies.x = fromBase64( $proxies_node.data( `generic_object` ) );
840
+ for ( let index_var of `ijklmn`.split(``) ) {
841
+ proxies[index_var] = fromBase64( $proxies_node.data( `index_var_${ index_var }` ) );
842
+ }
843
+ }
844
+
829
845
  // Some `show if` fields have `names` like '_field_n' instead of their var name. Ex: _field_1
830
846
  let field_like_names = await scope.getFieldNamesDict( scope, { $: $ });
831
847
 
@@ -845,62 +861,62 @@ module.exports = {
845
861
  };
846
862
  if ( node.attribs.type ) { field_data.type = node.attribs.type; }
847
863
 
864
+ // Often used for the variable name
865
+ let not_decoded_name = $node.attr( `name` );
866
+
867
+ // `<input type="hidden" name="_save_as" value="c2lnbmF0dXJlXzE">`
848
868
  // canvas elements are the only field on a signature page
849
869
  if ( $(`#dasigpage`).length > 0 && $(`#dasigpage`)[0].name ) {
850
870
  field_data.selector = `#daquestion canvas`;
851
871
  field_data.tag = `canvas`;
852
- field_data.guesses = [{
853
- var: trigger,
854
- value: '/sign',
855
- }];
856
- fields.push( field_data );
857
- continue;
872
+ not_decoded_name = $( `input[name="_save_as"]` ).attr( `value` );
858
873
  }
859
874
 
860
875
  // The selector we'll use to set the DOM node's value later
861
- field_data.selector = await scope.getFieldSelector( scope, { $, node });
876
+ if ( !field_data.selector ) {
877
+ field_data.selector = await scope.getFieldSelector( scope, { $, node });
878
+ }
862
879
 
863
- // Often used in the variable name
864
- let encoded_name = $node.attr( `name` );
865
880
  // Action buttons keep their var name in a unique place
881
+ // TODO: add examples of the hrefs
866
882
  if ( $( node ).hasClass( 'daquestionactionbutton' )) {
867
883
  let href = node.attribs.href;
868
884
  let match = href.match( /action=([^&]*)/ );
869
885
  if ( match ) {
870
- encoded_name = match[1];
886
+ not_decoded_name = match[1];
871
887
  } else {
872
888
  // Some action buttons just use urls instead of da actions
873
- // TODO: Does this `replace` need the `g` flag?
874
- // TODO: This only replaces the chars at the start of the string. Is that intentional?
875
- encoded_name = href.replace(/^[A-Za-z0-9]*/, '_');
889
+ // This means the first guess of the name should be the not_decoded_name
890
+ // TODO: This only replaces the chars at the start of the string. Is that
891
+ // intentional? Does this `.replace()` need the `g` flag?
892
+ not_decoded_name = href.replace(/^[A-Za-z0-9]*/, '_');
876
893
  }
877
894
  }
878
895
 
879
- if ( !encoded_name ) {
896
+ if ( !not_decoded_name ) {
880
897
  // Some fields, like comboboxes, have a weird var name setup
881
- encoded_name = $node.attr( `id` );
898
+ not_decoded_name = $node.attr( `id` );
882
899
  }
883
900
 
884
901
  // ====================
885
902
  // Table rows
886
903
  // ====================
887
- // Default values
888
- let var_names = await scope.getPossibleVarNames( scope, {
889
- encoded: encoded_name,
890
- field_like_names,
891
- nota: false, // none of the above (detected and overwritten later)
892
- });
893
-
904
+ // "None of the above" checkbox/object radio options don't contain their variable's name
905
+ let is_none_of_the_above = false;
894
906
  if ( $node.hasClass('danota-checkbox') || $node.parents(`.da-field-container-datatype-object_radio`).name ) {
895
- // 'None of the above' fields must find var name info from siblings' attributes
907
+ is_none_of_the_above = true;
896
908
  let first_sib = $(node.parent).find( 'input' )[0];
897
- var_names = await scope.getPossibleVarNames( scope, {
898
- encoded: first_sib.attribs.name,
899
- field_like_names,
900
- nota: true, // none of the above
901
- });
909
+ not_decoded_name = first_sib.attribs.name;
910
+ // TODO: Do we need daaota as well? ("all of the above")
902
911
  }
903
912
 
913
+ let var_names = await scope.getPossibleDecodedNames( scope, {
914
+ not_decoded: not_decoded_name,
915
+ field_like_names,
916
+ is_none_of_the_above: is_none_of_the_above,
917
+ proxies,
918
+ });
919
+
904
920
  // button, radio, checkbox yesno, and maybe others need the value of the `value` attribute
905
921
  let values = [];
906
922
  if ( node.name === `select` ) {
@@ -926,8 +942,6 @@ module.exports = {
926
942
  return await scope.page.evaluate(async function ( elem ) {
927
943
  return await get_interview_variables();
928
944
  });
929
-
930
- // return data;
931
945
  }, // Ends scope.getPageJSON()
932
946
 
933
947
  getFieldSelector: async function ( scope, { $, node }) {
@@ -978,10 +992,11 @@ module.exports = {
978
992
  // Ignore blanks, and values that certainly aren't base64 encoded like True and False
979
993
  // Modified from https://stackoverflow.com/a/8571649/11416267
980
994
  // DA doesn't use `==` to pad base 64
995
+ // TODO: How are 'None', 'True', and 'False' being handled?
981
996
  if (value !== 'None' && value !== 'True' && value !== 'False'
982
997
  && value.match(da_base64_regex)) {
983
998
  let value_decoded = fromBase64(value);
984
- let values_decoded = scope.decodeFully( scope, {expr_str: value_decoded });
999
+ let values_decoded = scope.decodeFully( scope, { expr_str: value_decoded });
985
1000
  for (let possible_val of values_decoded) {
986
1001
  possible_table_rows.push({var: var_name, value: possible_val, });
987
1002
  }
@@ -1008,12 +1023,20 @@ module.exports = {
1008
1023
  */
1009
1024
  if ( !$('input[name="_varnames"]')[0] ) { return {}; }
1010
1025
 
1026
+ // TODO: Why are we docoding the field names here? We could decode them later, right?
1027
+ // In `getPossibleDecodedNames` we could do:
1028
+ // if ( field_like_original_names[ not_decoded ] ) {
1029
+ // still_encoded = field_like_names[ not_decoded ];
1030
+ // decode( still_encoded )
1031
+ // }
1032
+
1011
1033
  let field_like_names = {};
1012
1034
  let field_like_names_encoded = $('input[name="_varnames"]')[0].attribs.value;
1013
1035
  let field_like_names_str = fromBase64( field_like_names_encoded );
1014
1036
  let field_like_names_json = JSON.parse( field_like_names_str );
1015
1037
  for ( let field_like_key in field_like_names_json ) {
1016
1038
  let field_like_DOM_name = fromBase64( field_like_key );
1039
+ // Even with `multiselect`, field names are only encoded once
1017
1040
  let field_like_var_name = fromBase64( field_like_names_json[ field_like_key ] );
1018
1041
  field_like_names[ field_like_DOM_name ] = field_like_var_name;
1019
1042
  }
@@ -1021,8 +1044,13 @@ module.exports = {
1021
1044
  return field_like_names;
1022
1045
  }, // Ends scope.getFieldNamesDict()
1023
1046
 
1024
- getPossibleVarNames: async function ( scope, { encoded, field_like_names, nota }) {
1025
- /* Given a base64 encoded string and a map of `_field_n` to var names, return
1047
+ getPossibleDecodedNames: async function ( scope, {
1048
+ not_decoded,
1049
+ field_like_names={},
1050
+ is_none_of_the_above=false,
1051
+ proxies={}
1052
+ }) {
1053
+ /** Given a potentially base64 encoded string and a map of `_field_n` to var names, return
1026
1054
  * the decoded possible var names and, sometimes, values that the
1027
1055
  * encoded string could be. Only some node names actually use `_field_n`.
1028
1056
  *
@@ -1040,41 +1068,69 @@ module.exports = {
1040
1068
  * TODO: We'll probably eventually have to search for `[B'encoded']` and such
1041
1069
  * iteratively to get all of them. Hasn't cropped up yet, though.
1042
1070
  *
1043
- * @param encoded {str} - base64 encoded string of the node
1071
+ * @param not_decoded {str} - potentially base64 encoded string from the node
1044
1072
  * @param field_like_names {obj} - e.g. `{ _field_0: 'some_checkbox_var_name' }`
1045
- * @param nota {bool} - is the field a 'none of the above' field? */
1073
+ * @param is_none_of_the_above {bool} - is the field a 'none of the above' field?
1074
+ * @param proxies {obj} - e.g. `{ x: str, i: str, j: str, ... n: str }`. Empty strings
1075
+ * are fine since that means that proxy is absent and won't come into play.
1076
+ */
1077
+
1046
1078
  // E.g. continue buttons that don't set a variable
1047
- if ( encoded === undefined ) { return []; }
1079
+ if ( not_decoded === undefined ) { return []; }
1080
+ // Not sure it's necessary to have both with and without proxies here
1081
+ let guesses = scope.with_and_without_proxies( scope, {
1082
+ var_name: `${ not_decoded }`,
1083
+ proxies,
1084
+ });
1048
1085
 
1049
1086
  // Names can be encoded multiple times, so start peeling the layers
1050
- let layer_1_decoded = fromBase64( encoded );
1051
- // If this is of the format `_field_n`, we need to get the actual matching var name.
1052
- if ( field_like_names[ layer_1_decoded ] ) {
1053
- layer_1_decoded = field_like_names[ layer_1_decoded ];
1087
+ let decoded_once = fromBase64( not_decoded );
1088
+ // If this is of the format `_field_n`, we need to get the actual matching var name
1089
+ if ( field_like_names[ decoded_once ] ) {
1090
+ decoded_once = field_like_names[ decoded_once ];
1054
1091
  }
1055
1092
 
1056
1093
  try {
1057
- let action_button_json = JSON.parse( layer_1_decoded );
1058
- if ( action_button_json.action ) { layer_1_decoded = action_button_json.action }
1094
+ // We think this never has proxy vars
1095
+ let action_button_json = JSON.parse( decoded_once );
1096
+ if ( action_button_json.action ) { decoded_once = action_button_json.action }
1059
1097
  } catch ( error ) {
1060
1098
  // Things that are not action buttons can't be parsed this way.
1061
1099
  // A special error for action buttons requires passing the node in here too, which is a lot to pass in.
1062
1100
  // TODO: Should we move this back into `getAllFields()`?
1063
1101
  }
1064
1102
 
1065
- return scope.decodeFully(scope, { expr_str: layer_1_decoded, field_like_names, nota });
1066
- }, // Ends scope.getPossibleVarNames()
1067
-
1068
- decodeFully: function ( scope, { expr_str, field_like_names = {}, nota} ) {
1069
- /*
1103
+ // Include `not_decoded` since the name might not be encoded
1104
+ // TODO: Should we try to replace proxies in `not_decoded`. I can't
1105
+ // think of an occasion where that would be necessary.
1106
+ guesses = guesses.concat( scope.decodeFully( scope, {
1107
+ expr_str: decoded_once,
1108
+ field_like_names,
1109
+ is_none_of_the_above,
1110
+ proxies
1111
+ })
1112
+ );
1113
+
1114
+ return guesses;
1115
+ }, // Ends scope.getPossibleDecodedNames()
1116
+
1117
+ decodeFully: function ( scope, {
1118
+ expr_str,
1119
+ field_like_names = {},
1120
+ is_none_of_the_above=false,
1121
+ proxies={}
1122
+ }) {
1123
+ /**
1070
1124
  * Given a DA variable or value name, make sure it's fully decoded from base64.
1071
- * @param variable_str {str} - a python variable string, potentially partially base64 encoded
1125
+ * @param expr_str {str} - a python variable string, potentially partially base64 encoded
1072
1126
  * Some examples:
1073
1127
  * - my_variable
1074
1128
  * - checkboxes_choices[B'Y2hlY2tib3hfb3RoZXJfb3B0XzI']
1075
1129
  * - foo[bar][B'Y2hlY2tib3hfb3RoZXJfb3B0XzI']
1076
1130
  * @param field_like_names {obj} - e.g. `{ _field_0: 'some_checkbox_var_name' }`
1077
- * @param nota {bool} - is the field a 'none of the above' field?
1131
+ * @param is_none_of_the_above {bool} - is the field a 'none of the above' field?
1132
+ * @param proxies {obj} - e.g. `{ x: str, i: str, j: str, ... n: str }`. Empty strings
1133
+ * are fine since that means that proxy is absent and won't come into play.
1078
1134
  */
1079
1135
  // Some fields have a key hidden inside them.
1080
1136
  // This gets the variable name before the final brackets, and the likely
@@ -1094,22 +1150,37 @@ module.exports = {
1094
1150
  if ( before_and_in_brackets ) {
1095
1151
  let key_or_name = before_and_in_brackets[1];
1096
1152
  // If this is of the format `_field_n`, we need to get the actual matching var name.
1097
- let var_name = field_like_names [ key_or_name ] || key_or_name;
1153
+ let var_name = field_like_names[ key_or_name ] || key_or_name;
1098
1154
 
1099
1155
  let dictionary_key = before_and_in_brackets[2];
1100
1156
 
1101
1157
  // Our own marker for a 'none of the above' checkbox choice
1102
- if ( nota ) {
1103
- possible.push( `${ var_name }['None']` );
1158
+ if ( is_none_of_the_above ) {
1159
+ // possible.push( `${ var_name }['None']` );
1160
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1161
+ var_name: `${ var_name }['None']`,
1162
+ proxies,
1163
+ }));
1104
1164
  } else {
1165
+
1166
+ // TODO: Just use try/catches?
1167
+
1105
1168
  if (dictionary_key.match(da_base64_regex)) {
1106
1169
  // Get the choice name. E.g. option_1, option_2
1107
1170
  let dictionary_key_decoded = fromBase64( dictionary_key );
1108
- possible.push( `${ var_name }['${ dictionary_key_decoded }']` );
1171
+ // possible.push( `${ var_name }['${ dictionary_key_decoded }']` );
1172
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1173
+ var_name: `${ var_name }['${ dictionary_key_decoded }']`,
1174
+ proxies,
1175
+ }));
1109
1176
  if (dictionary_key_decoded.match(da_base64_regex)) {
1110
1177
  try {
1111
- // The part inside `[B'encoded']` could need one more decoding.
1112
- possible.push(`${ var_name }['${ fromBase64( dictionary_key_decoded ) }']`);
1178
+ // The part inside `[B|R|O'encoded']` could need one more decoding.
1179
+ // possible.push(`${ var_name }['${ fromBase64( dictionary_key_decoded ) }']`);
1180
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1181
+ var_name: `${ var_name }['${ fromBase64( dictionary_key_decoded ) }']`,
1182
+ proxies,
1183
+ }));
1113
1184
  } catch ( err ) {} // It wasn't double encoded. Do nothing.
1114
1185
  }
1115
1186
  }
@@ -1118,18 +1189,65 @@ module.exports = {
1118
1189
  // =====================
1119
1190
  // If bracket regex didn't match, just use var name
1120
1191
  } else {
1121
- possible.push(`${ expr_str }`);
1192
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1193
+ var_name: `${ expr_str }`,
1194
+ proxies,
1195
+ }) );
1122
1196
  }
1123
1197
  return possible;
1124
1198
  },
1125
1199
 
1200
+ with_and_without_proxies: function ( scope, { var_name, proxies={} }) {
1201
+ /** Given a variable or value's possible name and a map of values for
1202
+ * each proxy variable, return a list of either one or two strings.
1203
+ * It always includes the given variable name. If proxies were found
1204
+ * and replaced, the list also includes a version of the variable
1205
+ * name in which the proxies have been replaced by their values.
1206
+ *
1207
+ * Proxies are generic objects (x) or index variables (i, j, k, l, m, n).
1208
+ *
1209
+ * @param { string } var_name - The possible name of a variable or value
1210
+ * @param { Object } proxies - A map of proxy variables to their values on
1211
+ * the HTML page, if they're there.
1212
+ *
1213
+ * @returns {Array<string>} - An array of at least one string - var_name
1214
+ * itself. Possibly a second value where proxy variables in var_name
1215
+ * are replaced with their values (if the proxy variables are present
1216
+ * and if their values are passed in).
1217
+ * */
1218
+ // We could combine these regexes, but this may be easier to read
1219
+ let generic_obj_regex = /^(x)\b/;
1220
+ let index_var_regex = /(?<=\[)([ijklmn])(?=\])/g;
1221
+ // If proxies exist in the HTML, replace any proxies in the variables
1222
+ // with those values.
1223
+ let new_name = var_name;
1224
+ if ( proxies.x !== '' && proxies.x !== undefined ) {
1225
+ new_name = new_name.replace( generic_obj_regex, proxies.x );
1226
+ }
1227
+ new_name = new_name.replace( index_var_regex, function(match) {
1228
+ if ( proxies[ match ] === '' || proxies[ match ] === undefined ) {
1229
+ return match;
1230
+ } else {
1231
+ return proxies[ match ];
1232
+ }
1233
+ });
1234
+
1235
+ // The older versions of Story Table need to see "x", "i", etc.,
1236
+ // so keep that old name around.
1237
+ let unique_names = [ var_name ];
1238
+ // If there were proxies, add the new version of the name
1239
+ if ( new_name !== var_name ) { unique_names.push( new_name ); }
1240
+
1241
+ return unique_names;
1242
+ }, // Ends scope.with_and_without_proxies()
1243
+
1126
1244
  getOptionValues: async function ( scope, { $select }) {
1127
1245
  /* Returns all the option node values of the select node. */
1128
1246
  let options = [];
1129
1247
  let option_nodes = $select.find( `option` );
1130
1248
  for ( let opt of option_nodes ) {
1131
1249
  options.push( opt.attribs.value );
1132
- // Dropdowns created w/ objects are base64 encoded; but it's handled in getPossibleTableRowMatches
1250
+ // Dropdowns created w/ objects are base64 encoded; but it's handled in getPossibleTableRowMatchers
1133
1251
  }
1134
1252
  return options;
1135
1253
  }, // Ends scope.getOptionValues()
@@ -1137,11 +1255,11 @@ module.exports = {
1137
1255
 
1138
1256
  // TODO: Maybe add `from_story_table` to the var data table (array...)
1139
1257
  getMatchingRows: async function ( scope, { field, var_data }) {
1140
- /* Given var_data, a list of objects with variable data for
1258
+ /** Given var_data, a list of objects with variable data for
1141
1259
  * setting fields, and an object for a DOM field, returns list of the
1142
1260
  * rows that match the DOM field. That list of story table rows may be empty.
1143
1261
  *
1144
- * For notes on trigger var, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger
1262
+ * For notes on trigger var, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger
1145
1263
  *
1146
1264
  * @param var_data {arr} - fields and the values they can be set to
1147
1265
  * @param var_data[n].var {str} - name of variable to set
@@ -1205,6 +1323,7 @@ module.exports = {
1205
1323
  // If the field uses proxy variables - x, i, j, etc, flag it. See "proxies" note.
1206
1324
  uses_proxies = uses_proxies || guess.var.match( proxies_regex );
1207
1325
 
1326
+ // TODO: can we just use `uses_proxies` in here?
1208
1327
  if ( table_row.trigger === scope.trigger_not_needed_flag ) {
1209
1328
  trigger_might_be_needed = false;
1210
1329
  }
@@ -1235,9 +1354,9 @@ module.exports = {
1235
1354
  // field/page trigger name is needed but is missing.
1236
1355
  if ( !page_trigger_name ) {
1237
1356
  let page_trigger_warning = `A field on this page uses an index `
1238
- + `variable or a generic object, but the page does not `
1239
- + `contain the trigger variable's name in its HTML. See `
1240
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#a-missing-trigger-variable.`;
1357
+ + `variable or a generic object, but the page is missing `
1358
+ + `the ALKiln "trigger" variable's name. See `
1359
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#a-missing-trigger-variable.`;
1241
1360
  reports.addToReport(scope, { type: `warning`, value: page_trigger_warning });
1242
1361
  }
1243
1362
 
@@ -1247,18 +1366,18 @@ module.exports = {
1247
1366
 
1248
1367
  if ( !table_row_trigger_name ) {
1249
1368
  // var/row trigger name is (probably) needed but is missing. Caveat: If there's
1250
- // only one page in the proxy var loop and there's only one row, trigger name is
1251
- // not needed, but I'm not sure how we figure out all those things here and log
1252
- // a useful message.
1369
+ // only one page in the proxy var loop and there's only one row (like the "I set the var"
1370
+ // Step), trigger name is not needed, but I'm not sure how we figure out all those
1371
+ // things here and log a useful message.
1253
1372
 
1254
1373
  // First, make the row data more readable to humans.
1255
1374
  let row_as_story_table = reports.convertToOriginalStoryTableRow( scope, { row_data: row_that_matches_this_field });
1256
- let row_trigger_warning = `This story table row needs a value in the "trigger" `
1375
+ let row_trigger_warning = `This Story Table row needs a value in the "trigger" `
1257
1376
  + `column, but it is empty. The test might get unexpected results. See `
1258
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1259
- + `\nThe row's data:\n${ row_as_story_table }`
1260
- + `\nThis field's data:\n${ JSON.stringify( field.guesses )}`;
1377
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
1378
+ + `\nThe rows:\n${ row_as_story_table }`;
1261
1379
  reports.addToReport(scope, { type: `warning`, value: row_trigger_warning });
1380
+
1262
1381
  rows_without_a_trigger_name.push( row_that_matches_this_field );
1263
1382
  } else {
1264
1383
  let triggers_match = await scope.matchNeutralizedQuotes( scope, { value1: page_trigger_name, value2: table_row_trigger_name });
@@ -1278,7 +1397,7 @@ module.exports = {
1278
1397
  }
1279
1398
 
1280
1399
  // Warn the user if all matching rows had trigger names that conflicted.
1281
- // Missing trigger names don't count as conflicting.
1400
+ // Missing trigger names don't count as conflicting. [Did this mean missing in the table or in the page?]
1282
1401
  if ( final_matching_rows.length === 0 ) {
1283
1402
 
1284
1403
  // First, make the row data more readable to humans.
@@ -1288,12 +1407,12 @@ module.exports = {
1288
1407
  conflicts_as_story_table_rows.push( table_str );
1289
1408
  }
1290
1409
 
1291
- let all_trigger_names_conflict_msg = `Is this story table missing a row? `
1292
- + `${ rows_that_match_field_names.length } story table row(s) matched a field name, `
1293
- + `but none of their trigger variable name(s) matched the trigger variable `
1294
- + `name in the screen's HTML. The test will have to ignore this field. See `
1295
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1296
- + `\nThe data of row(s) with conflicting trigger name(s):\n${ conflicts_as_story_table_rows.join( `\n` )}`
1410
+ let all_trigger_names_conflict_msg = `${ rows_that_match_field_names.length } `
1411
+ + `Story Table row(s) matched a field name, but didn't match a trigger `
1412
+ + `variable. The Story Table might be missng a row or a trigger variable. `
1413
+ + `The test will have to ignore this field. See `
1414
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
1415
+ + `\nThe row(s) data:\n${ conflicts_as_story_table_rows.join( `\n` )}`
1297
1416
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1298
1417
  reports.addToReport(scope, {
1299
1418
  type: `warning`,
@@ -1318,9 +1437,12 @@ module.exports = {
1318
1437
 
1319
1438
  let multiple_matches_msg = `Do you have a duplicate story table row? `
1320
1439
  + `Multiple story table rows match this field's variable name. `
1321
- + `The test will use the last row that matched. See `
1322
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1323
- + `\nThe matching rows:\n${ rows_as_story_table.join(`\n`) }`
1440
+ + `The test will use the last row that matched.`
1441
+
1442
+ if ( trigger_might_be_needed ) {
1443
+ multiple_matches_msg += ` See https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
1444
+ }
1445
+ multiple_matches_msg += `\nThe matching rows:\n${ rows_as_story_table.join(`\n`) }`
1324
1446
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1325
1447
  reports.addToReport(scope, { type: `warning`, value: multiple_matches_msg });
1326
1448
  }
@@ -1374,7 +1496,9 @@ module.exports = {
1374
1496
  let disabled = await handle.evaluate(( elem )=> {
1375
1497
  // da's header covers the top of the section containing the fields which means
1376
1498
  // sometimes it blocks nodes from clicks. Scroll item to the bottom instead.
1377
- if ( !elem.disabled ) { elem.scrollIntoView(false); }
1499
+ // Note: I'm not sure how we'd get in here if `elem` didn't exist, but
1500
+ // I once got an error here that I couldn't read `.disabled` of `null`.
1501
+ if ( elem && !elem.disabled ) { elem.scrollIntoView(false); }
1378
1502
  return elem.disabled;
1379
1503
  });
1380
1504
 
@@ -1982,24 +2106,21 @@ module.exports = {
1982
2106
 
1983
2107
  // If all table rows must be used and any table row was not, error
1984
2108
  if ( ensure_all_vars_used ) {
1985
- let num_unused_rows = 0;
1986
2109
  for ( let row of var_data ) {
1987
2110
  if ( row.times_used === 0 ) {
1988
- num_unused_rows += 1;
2111
+ let values_used_status = 'The page is missing a value';
1989
2112
  let source_row = row;
1990
2113
  // TODO: Make the row object flatter so we don't need to dig in this way
1991
2114
  if ( row.source ) { source_row = row.source; }
1992
2115
  let original_row = source_row.original;
1993
- let msg = `Did not find a field on this page for the variable "${ original_row.var }" that could be set to "${ original_row.value }"`;
1994
- reports.addToReport(scope, { type: `warning`, value: msg });
2116
+ let { id } = await scope.examinePageID( scope, 'none to match' );
2117
+ let msg = `In a linear Step all the values you give MUST be filled in, but ALKiln was unable `
2118
+ + `to find a field on the page "${id}" for the variable "${ original_row.var }" that `
2119
+ + `could be set to "${ original_row.value }". See this Scenario's HTML file for details.`;
2120
+ reports.addToReport(scope, { type: `error`, value: msg });
2121
+ expect( values_used_status, msg ).to.equal( '' );
1995
2122
  }
1996
2123
  }
1997
- if ( num_unused_rows > 0 ) {
1998
- let { id } = await scope.examinePageID( scope, 'none to match' );
1999
- let msg = `Error on page "${id}". See screenshot and see text above for more details.`;
2000
- reports.addToReport(scope, { type: `error`, value: msg });
2001
- expect( num_unused_rows, msg ).to.equal( 0 );
2002
- }
2003
2124
  }
2004
2125
 
2005
2126
  // If didn't already press a continue button, press one now.
@@ -2191,7 +2312,7 @@ module.exports = {
2191
2312
  value: 'The attribute `.there_is_another` is invalid in story table tests. Replace it with '
2192
2313
  + '`.target_number` in your `var` and `trigger` columns. Set the `value` to the number of items in '
2193
2314
  + `that list. This test will now set this row's \`value\` to \`False\`. See `
2194
- + `https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing#there_is_another. `
2315
+ + `https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/alkiln#there_is_another. `
2195
2316
  + `The row data is\n${ JSON.stringify( var_datum.original )}` });
2196
2317
  } // ends if it's a `target_number` row
2197
2318
  } // ends for each row
@@ -2221,7 +2342,7 @@ module.exports = {
2221
2342
  } else if ( value.match( /(true|yes|checked|check|selected|select)/i )) {
2222
2343
  if ( from_story_table && !row.artificial ) {
2223
2344
  if ( row.var.match( /\.there_is_another$/ ) ) {
2224
- reports.addToReport( scope, { type: `warning`, value: `The attribute \`.there_is_another\` is invalid in story table tests. Replace it with \`.target_number\` in your \`var\` column. Set the \`value\` to the number of items in that list. This test will now set this row's \`value\` to \`False\`. See https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing#there_is_another-loop. The row data is\n${ JSON.stringify( row.original )}` });
2345
+ reports.addToReport( scope, { type: `warning`, value: `The attribute \`.there_is_another\` is invalid in story table tests. Replace it with \`.target_number\` in your \`var\` column. Set the \`value\` to the number of items in that list. This test will now set this row's \`value\` to \`False\`. See https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/alkiln#there_is_another-loop. The row data is\n${ JSON.stringify( row.original )}` });
2225
2346
  } else {
2226
2347
  // If `target_number` is set to True instead of a number
2227
2348
  reports.addToReport( scope, { type: `warning`, value: `${ val_to_print } value is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
@@ -2583,6 +2704,7 @@ module.exports = {
2583
2704
  * 1. Four words - a first, middle, last, and suffix (ex: "Tal Tay Taylor III")
2584
2705
  *
2585
2706
  * The string 'the var'/'the variable' part of the step is optional, as you can see below.
2707
+ * This is a linear step, so no trigger var is needed.
2586
2708
  */
2587
2709
  let name_patterns = [
2588
2710
  [ `text` ],
@@ -2628,6 +2750,7 @@ module.exports = {
2628
2750
  *
2629
2751
  * The string 'the var'/'the variable' part of the step is optional,
2630
2752
  * as you can see below.
2753
+ * This is a linear step only, so it doesn't need to have a trigger var.
2631
2754
  */
2632
2755
 
2633
2756
  // Get all the parts of the address properly separated