@suffolklitlab/alkiln 5.7.0 → 5.8.1-feat2columns1

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
@@ -174,14 +174,14 @@ module.exports = {
174
174
  if ( showif ) { await scope.page.waitForTimeout( scope.showif_timeout ); }
175
175
  },
176
176
 
177
- waitForTab: async function waitForTab(scope, tab_to_show) {
177
+ waitForTab: async function waitForTab(scope, { tab_id }) {
178
178
  /**
179
- * @param {tab_to_show} the aria-labeledby text of the tab panel, which
179
+ * @param { tab_id } the aria-labeledby text of the tab panel, which
180
180
  * we use to query and wait for that tab panel to be shown.
181
181
  */
182
182
  // Implementation linked to https://github.com/SuffolkLITLab/docassemble-ALToolbox/blob/4aa1069389d2ca9cadb586f309dd73d791f081d1/docassemble/ALToolbox/misc.py#L127
183
183
  // I think `.$()` does not use a timeout
184
- if ( tab_to_show.length == 0) { return }
184
+ if ( tab_id.length == 0) { return }
185
185
  // waiting for the transitionend event: https://stackoverflow.com/a/57955251/11416267
186
186
  await scope.page.evaluate((element_query) => {
187
187
  return new Promise((resolve) => {
@@ -198,13 +198,16 @@ module.exports = {
198
198
  resolve();
199
199
  }
200
200
  });
201
- }, `div[aria-labelledby="${tab_to_show}"]`);
201
+ }, `div[aria-labelledby="${ tab_id }"]`);
202
202
  },
203
203
 
204
- afterStep: async function afterStep(scope, options = {waitForShowIf: false, tabToWaitFor: '', navigated: false, waitForTimeout: 0}) {
205
- /* Common things to do after a step, including take care of errors.
206
- * TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
207
- * TODO: Update to latest cucumberjs, which has `AfterStep` */
204
+ afterStep: async function afterStep(scope, options = {waitForShowIf: false, navigated: false, waitForTimeout: 0}) {
205
+ /** Common things to do after a step, including take care of errors.
206
+ * `waitForShowIf` is automatically `false` for observation/non-interactive steps, etc.
207
+ *
208
+ * TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
209
+ * TODO: Update to latest cucumberjs, which has `AfterStep`
210
+ */
208
211
 
209
212
  if ( scope.page ) {
210
213
  // Use a while loop until the below does not error with `Error: Execution context was destroyed`
@@ -231,7 +234,6 @@ module.exports = {
231
234
 
232
235
  // Wait for elements that might get revealed
233
236
  if ( options.waitForShowIf ) { await scope.waitForShowIf( scope ); }
234
- if ( options.tabToWaitFor ) { await scope.waitForTab( scope, options.tabToWaitFor); }
235
237
  } // ends if scope.page
236
238
 
237
239
  if ( options.navigated ) {
@@ -248,39 +250,56 @@ module.exports = {
248
250
  if ( options.waitForTimeout ) { await time.waitForTimeout( options.waitForTimeout ); }
249
251
  }, // Ends afterStep
250
252
 
251
- tapElement: async function tapElement(scope, elem, tabToWaitFor='', waitForTimeout=0) {
252
- /* Tap a given element, detect navigation, and do appropriate related things. */
253
+ tapElementAndNavigate: async function(scope, { elem, waitForTimeout=0 }) {
254
+ /** Tap a given element, detect navigation, and do appropriate related things.
255
+ * TODO: not quite sure if we'll be using `waitForTimeout`. */
253
256
 
254
- // This check might never be needed now as we try to match fields on the page
255
- // first in user-triggered cases - users used to be able to access this almost
256
- // directly, but they can't anymore. Our functions should know to only pass good things.
257
- let msg = `Cannot find the element to tap.`;
258
- if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
257
+ let start_url = await scope.page.url(); // so we can later check for navigation
259
258
 
260
- expect( elem, msg ).to.exist;
259
+ await scope.tapElementAndStayOnPage(scope, {
260
+ elem,
261
+ click_promise: Promise.all([
262
+ scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
263
+ elem.evaluate( (el) => { return el.click(); })
264
+ ])
265
+ });
261
266
 
262
- // Get the text on the button or link for a potential error message later
263
- let tapperText = await (await elem.getProperty('textContent')).jsonValue();
267
+ // TODO: Not sure how to detect navigation landing at same page, such as
268
+ // with a user input validation error. Does that count as navigating?
269
+ // Maybe url change + trigger var change? Maybe just trigger var?
270
+ let end_url = await scope.page.url();
271
+ let navigated = start_url !== end_url;
264
272
 
265
- let start_url = await scope.page.url(); // so we can later check for navigation
273
+ // Might be able to handle this in `scope.afterStep`
274
+ if ( navigated ) {
266
275
 
267
- // We'll wait for the click itself at the very least
268
- let click_promises = [ elem.evaluate( (el) => { return el.click(); }) ];
269
-
270
- // If we're possibly navigating, wait for navigation as well
271
- if ( !tabToWaitFor ) {
272
- click_promises.push( scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }) );
276
+ // TODO: Can we remove this?
277
+ // Save the id that comes next
278
+ let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
279
+
280
+ // If navigation, wait for 200ms to let things finish loading and moving
281
+ await scope.afterStep(scope, { waitForTimeout: 200, navigated: true });
282
+ } else {
283
+ await scope.afterStep(scope, { waitForTimeout, navigated: false });
273
284
  }
274
- let clickWait = Promise.all( click_promises );
285
+ }, // Ends scope.tapElementAndNavigate()
275
286
 
276
- let winner = null;
287
+ tapElementAndStayOnPage: async function(scope, { elem, click_promise }) {
288
+ /** Tap a given element, detect navigation, and do appropriate related things.
289
+ * `click_promise can be any kind of Promise, including a Promise.all()
290
+ */
291
+ let msg = `Cannot find the element to tap.`;
292
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
293
+ expect( elem, msg ).to.exist;
277
294
 
295
+ click_promise = click_promise || elem.click();
296
+
278
297
  // TODO: implement and use scope.handle_possible_timeout()
279
298
  try {
280
- // Error loads page, so no need to detect to keep things short
281
- // Also, puppeteer will ensure proper timeout.
282
- winner = await Promise.race([
283
- clickWait,
299
+ // Serever error loads page, so keeping things shorter by not looking
300
+ // for that text too. Also, puppeteer will ensure proper timeout.
301
+ await Promise.race([
302
+ click_promise,
284
303
  // This is meant to detect user-visible alerts/errors,
285
304
  // not breaking errors in the testing code.
286
305
  scope.page.waitForSelector('.alert-danger', { visible: true }),
@@ -292,6 +311,9 @@ module.exports = {
292
311
  let non_reload_report_msg = `ALKiln tapped "${ tapperText }", but the interview took to long to finish.`;
293
312
  await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
294
313
  } 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();
295
317
  // Throw any non-timeout error
296
318
  let err_msg = `Error occurred when ALKiln tried to tap "${ tapperText }".`
297
319
  reports.addToReport( scope, { type: `error`, value: err_msg });
@@ -299,30 +321,7 @@ module.exports = {
299
321
  } // ends if error is timeout error
300
322
 
301
323
  } // ends try/catch
302
-
303
- // TODO: Not sure how to detect navigation landing at same page, such as
304
- // with a user input validation error. Does that count as navigating?
305
- // Maybe url change + trigger var change? Maybe just trigger var?
306
- let end_url = await scope.page.url();
307
- let navigated = start_url !== end_url;
308
-
309
- // Might be able to handle this in `scope.afterStep`
310
- if ( navigated ) {
311
- // Save the id that comes next
312
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
313
- // If navigation, wait for 200ms to let things settle down
314
- await scope.afterStep(scope, {waitForTimeout: 200, navigated: navigated});
315
- } else {
316
- // If stay on page, wait for `show if`, the new tab to be loaded, or feedback elements to be move around
317
- options = {waitForShowIf: !tabToWaitFor, tabToWaitFor: tabToWaitFor, navigated: navigated};
318
- if (waitForTimeout > 0) {
319
- options = {waitForTimeout: waitForTimeout, navigated: navigated};
320
- }
321
- await scope.afterStep(scope, options);
322
- }
323
-
324
- return winner;
325
- }, // Ends scope.tapElement()
324
+ }, // Ends scope.tapElementAndStayOnPage()
326
325
 
327
326
  // Handle server reload errors
328
327
  server_statuses: [],
@@ -516,7 +515,7 @@ module.exports = {
516
515
  ]);
517
516
  await elem.evaluate( el => { return el.className });
518
517
  // Waits for navigation or user error
519
- await scope.tapElement( scope, elem );
518
+ await scope.tapElementAndNavigate( scope, { elem });
520
519
  }, // Ends scope.continue()
521
520
 
522
521
  continue_exists: async function ( scope ) {
@@ -545,7 +544,8 @@ module.exports = {
545
544
  reports.addToReport(scope, { type: `error`, value: error_msg });
546
545
  }
547
546
  expect( elem, error_msg ).to.exist;
548
- await scope.tapElement( scope, elem, tab_id );
547
+ await scope.tapElementAndStayOnPage(scope, { elem });
548
+ await scope.waitForTab( scope, { tab_id });
549
549
  },
550
550
 
551
551
  checkForA11y: async function (scope ) {
@@ -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
  }
@@ -586,14 +583,25 @@ module.exports = {
586
583
  };
587
584
  },
588
585
 
589
- tapElementBySelector: async function ( scope, selector, waitForTimeout ) {
590
- let elem = await scope.page.$(`${selector}`);
591
- let error_msg = `Couldn't find "${selector}" on the page, is there a typo?`;
586
+ tapSelectorToNavigate: async function ( scope, selector, waitForTimeout ) {
587
+ let elem = await scope.page.$(`${ selector }`);
588
+ let error_msg = `Couldn't find "${ selector }" on the page, is there a typo?`;
589
+ if ( !elem ) {
590
+ reports.addToReport(scope, { type: `error`, value: error_msg });
591
+ }
592
+ expect( elem, error_msg ).to.exist;
593
+ await scope.tapElementAndNavigate( scope, { elem, waitForTimeout });
594
+ },
595
+
596
+ tapSelectorAndStay: async function ( scope, { selector }) {
597
+ let elem = await scope.page.$(`${ selector }`);
598
+ let error_msg = `Couldn't find "${ selector }" on the page, is there a typo?`;
592
599
  if ( !elem ) {
593
600
  reports.addToReport(scope, { type: `error`, value: error_msg });
594
601
  }
595
602
  expect( elem, error_msg ).to.exist;
596
- await scope.tapElement( scope, elem, '', waitForTimeout );
603
+ await scope.tapElementAndStayOnPage(scope, { elem });
604
+ await scope.afterStep(scope, { waitForShowIf: false, navigated: false, waitForTimeout: 0 });
597
605
  },
598
606
 
599
607
  load: async function ( scope, file_name ) {
@@ -713,13 +721,58 @@ module.exports = {
713
721
  return result;
714
722
  }, // Ends scope.getLoadData()
715
723
 
724
+ // three_column_table_warning: async function ( scope, { var_data=null, raw_var_data=null }) {
725
+
726
+ // },
727
+
728
+ // normalize_table_base: async function ( scope, { var_data=null, raw_var_data=null }) {
729
+ // /* Return data derived from cucumber variable-setting data, from either an object format
730
+ // * (var_data) or an array format (raw_var_data). `var_data` only is used when calling
731
+ // * normalizeTable internally.
732
+ // *
733
+ // * Takes care of the shared funcitonality that needs to happen to all tables.
734
+ // */
735
+
736
+ // if (!var_data && ! raw_var_data) {
737
+ // let no_data_msg = `ALKiln Internal error: normalizeTable called w/o var_data or raw_var_data:` +
738
+ // ` open an issue at https://github.com/SuffolkLITLab/ALKiln/issues/new if you see this!`;
739
+ // reports.addToReport( scope, { type: `error`, value: no_data_msg });
740
+ // throw Error(no_data_msg);
741
+ // }
742
+
743
+ // // Always use the raw var data if present; it has the full first row
744
+ // if (raw_var_data) {
745
+ // var_data = raw_var_data.hashes();
746
+ // if (!('var' in var_data[0]) || !('value' in var_data[0])) {
747
+ // let raw_array = raw_var_data.raw();
748
+ // var_data = [];
749
+ // for (let row_raw of raw_array) {
750
+ // if (row_raw.length < 2) {
751
+ // let bad_table_msg = `Your Story Table has less than 2 columns.`
752
+ // + ` Take another look at Story Table documentation at`
753
+ // + ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#story-tables.`
754
+ // reports.addToReport( scope, { type: `error`, value: bad_table_msg})
755
+ // throw Error(bad_table_msg);
756
+ // } else if (row_raw.length == 2) {
757
+ // var_data.push({var: row_raw[0], value: row_raw[1]});
758
+ // } else {
759
+ // var_data.push({var: row_raw[0], value: row_raw[1], trigger: row_raw[2]});
760
+ // }
761
+ // }
762
+ // }
763
+ // }
764
+
765
+
766
+
767
+ // },
768
+
716
769
  normalizeTable: async function ( scope, { var_data=null, raw_var_data=null }) {
717
770
  /* Return data derived from cucumber variable-setting data, from either an object format
718
771
  * (var_data) or an array format (raw_var_data). `var_data` only is used when calling
719
772
  * normalizeTable internally.
720
773
  */
721
774
 
722
- if (!var_data && ! raw_var_data) {
775
+ if (!var_data && !raw_var_data) {
723
776
  let no_data_msg = `ALKiln Internal error: normalizeTable called w/o var_data or raw_var_data:` +
724
777
  ` open an issue at https://github.com/SuffolkLITLab/ALKiln/issues/new if you see this!`;
725
778
  reports.addToReport( scope, { type: `error`, value: no_data_msg });
@@ -728,39 +781,52 @@ module.exports = {
728
781
 
729
782
  // Always use the raw var data if present; it has the full first row
730
783
  if (raw_var_data) {
784
+
731
785
  var_data = raw_var_data.hashes();
732
- if (!('var' in var_data[0]) || !('value' in var_data[0])) {
786
+ // If this table doesn't have headers
787
+ if (!(`var` in var_data[0]) || !(`value` in var_data[0])) {
733
788
  let raw_array = raw_var_data.raw();
734
789
  var_data = [];
735
790
  for (let row_raw of raw_array) {
736
- if (row_raw.length < 2) {
737
- let bad_table_msg = `Your Story Table definition needs to be changed. The best practice`
738
- + ` is to make your table 3 columns wide with a first row in this format: "| var | value | trigger |".`
739
- + ` The first row of your table is actually "${ raw_array[0] }"`;
791
+
792
+ // Wrong number of columns error
793
+ if (row_raw.length < 2 || row_raw.length > 3) {
794
+ let more_or_less = `more`;
795
+ if ( row_raw.length > 3 ) { more_or_less = `less`; }
796
+ let bad_table_msg = `Your Story Table should have ${ more_or_less } than`
797
+ + ` ${ row_raw.length } columns. Take another look at Story Table documentation at`
798
+ + ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#story-tables.`
740
799
  reports.addToReport( scope, { type: `error`, value: bad_table_msg})
741
800
  throw Error(bad_table_msg);
742
- } else if (row_raw.length == 2) {
801
+ }
802
+
803
+ if (row_raw.length == 2) {
743
804
  var_data.push({var: row_raw[0], value: row_raw[1]});
744
805
  } else {
745
806
  var_data.push({var: row_raw[0], value: row_raw[1], trigger: row_raw[2]});
746
807
  }
747
- }
748
- }
749
- }
808
+ } // ends for every row in table
809
+ } // ends if this table doesn't have headers
810
+
811
+ } // ends if we have access to the full raw data
750
812
 
751
813
  // Support tables with no 'trigger' column
752
814
  let supported_table = [];
815
+
753
816
  for ( let row of var_data ) {
754
817
  // See tests > unit_tests > tables.fixtures.js > tables.old_to_current_formatting
755
818
  let actual_var_value = row.value;
756
819
  if (is_secret_var(row)) {
757
- actual_var_value = process.env[row.flags.secret_name];
820
+ actual_var_value = process.env[ row.flags.secret_name ];
758
821
  if ( actual_var_value === undefined ) {
759
- let not_in_env_msg = `The GitHub SECRET "${ row.flags.secret_name }" doesn't exist. Check for a typo and check your workflow file.`;
760
- reports.addToReport( scope, { type: `error`, value: not_in_env_msg })
761
- throw ReferenceError(`Could not find "${row.flags.secret_name }": ${not_in_env_msg}.`)
822
+ let not_in_env_msg = `ALKiln could not find the GitHub secret "${ row.flags.secret_name }".`
823
+ + ` Check for a typo and check your workflow file.`
824
+ + ` Read more about using GitHub secrets in your workflow file at`
825
+ + ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#github-secrets.`
826
+ reports.addToReport( scope, { type: `error`, value: not_in_env_msg });
827
+ throw ReferenceError(not_in_env_msg);
762
828
  }
763
- }
829
+ } // ends if is secret var
764
830
 
765
831
  let result = {
766
832
  // NOTE: mutating frozen objects will fail silently, so don't mutate this attribute
@@ -769,9 +835,9 @@ module.exports = {
769
835
  // Give fake data if needed to prevent some kind of hidden input field from being found
770
836
  // See bug at https://github.com/plocket/docassemble-cucumber/issues/79
771
837
  // May be able to remove this once converted to `getAllFields`
772
- var: row.var || 'al_no_var_name',
838
+ var: row.var || `alkiln_no_var_name`,
773
839
  value: actual_var_value,
774
- trigger: row.trigger || '',
840
+ trigger: row.trigger || ``,
775
841
  // This will track how many times total the row or its connected
776
842
  // artificial rows are used. Artificial rows are only added for some rows.
777
843
  times_used: 0,
@@ -780,11 +846,12 @@ module.exports = {
780
846
  result.used_for[ result.var ] = 0;
781
847
 
782
848
  supported_table.push( result );
783
- }
849
+ } // ends for every row
784
850
 
785
- if ( session_vars.get_debug() ) {
786
- console.log( 'scope.normalizedTable():\n', JSON.stringify( supported_table ));
787
- }
851
+ log.debug({
852
+ type: `info`,
853
+ pre: `scope.normalizedTable():\n ${JSON.stringify( supported_table )}`
854
+ })
788
855
 
789
856
  return supported_table;
790
857
  }, // Ends scope.normalizeTable()
@@ -804,17 +871,22 @@ module.exports = {
804
871
  // This is the new trigger var element that developers will have to add to their interviews.
805
872
  // AssemblyLine will add it automatically unless someone overrides the values in their own interview.
806
873
  // See the report message to the dev below for a link to an example.
807
- let trigger_elem = $( `#trigger` );
808
- if ( !trigger_elem.length ) { trigger_elem = $( `#sought_variable` ); } // Backwards compatibility
809
- let encoded_trigger_var = $( trigger_elem ).data( `variable` ); // This should come out to 'None' at the very least
810
- if ( !encoded_trigger_var ) {
811
- // TODO: Add documentation and then link to it in this message.
812
- 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.` });
813
- encoded_trigger_var = toBase64( `None` );
814
- }
874
+ let $trigger_elem = $( `#trigger` );
875
+ if ( !$trigger_elem.length ) { $trigger_elem = $( `#sought_variable` ); } // Backwards compatibility
876
+ let encoded_trigger_var = $( $trigger_elem ).data( `variable` ) || ``;
815
877
  let trigger = fromBase64( encoded_trigger_var );
816
878
 
817
-
879
+ let $proxies_node = $( `#alkiln_proxy_var_values` );
880
+ let proxies = {};
881
+ // If possible, get values of `x` and `i` that the dev put on the page
882
+ if ( $proxies_node.length ) {
883
+ // Empty strings are fine since that means that proxy is absent and won't come into play.
884
+ proxies.x = fromBase64( $proxies_node.data( `generic_object` ) );
885
+ for ( let index_var of `ijklmn`.split(``) ) {
886
+ proxies[index_var] = fromBase64( $proxies_node.data( `index_var_${ index_var }` ) );
887
+ }
888
+ }
889
+
818
890
  // Some `show if` fields have `names` like '_field_n' instead of their var name. Ex: _field_1
819
891
  let field_like_names = await scope.getFieldNamesDict( scope, { $: $ });
820
892
 
@@ -834,62 +906,69 @@ module.exports = {
834
906
  };
835
907
  if ( node.attribs.type ) { field_data.type = node.attribs.type; }
836
908
 
909
+ // Often used for the variable name
910
+ let not_decoded_name = $node.attr( `name` );
911
+
912
+ // `<input type="hidden" name="_save_as" value="c2lnbmF0dXJlXzE">`
837
913
  // canvas elements are the only field on a signature page
838
914
  if ( $(`#dasigpage`).length > 0 && $(`#dasigpage`)[0].name ) {
839
915
  field_data.selector = `#daquestion canvas`;
840
916
  field_data.tag = `canvas`;
841
- field_data.guesses = [{
842
- var: trigger,
843
- value: '/sign',
844
- }];
845
- fields.push( field_data );
846
- continue;
917
+ not_decoded_name = $( `input[name="_save_as"]` ).attr( `value` );
918
+
919
+ // field_data.guesses = [{
920
+ // var: trigger,
921
+ // value: '/sign',
922
+ // }];
923
+ // fields.push( field_data );
924
+ // continue;
847
925
  }
848
926
 
849
927
  // The selector we'll use to set the DOM node's value later
850
- field_data.selector = await scope.getFieldSelector( scope, { $, node });
928
+ if ( !field_data.selector ) {
929
+ field_data.selector = await scope.getFieldSelector( scope, { $, node });
930
+ }
851
931
 
852
- // Often used in the variable name
853
- let encoded_name = $node.attr( `name` );
854
932
  // Action buttons keep their var name in a unique place
933
+ // TODO: add examples of the hrefs
855
934
  if ( $( node ).hasClass( 'daquestionactionbutton' )) {
856
935
  let href = node.attribs.href;
857
936
  let match = href.match( /action=([^&]*)/ );
858
937
  if ( match ) {
859
- encoded_name = match[1];
938
+ not_decoded_name = match[1];
860
939
  } else {
861
940
  // Some action buttons just use urls instead of da actions
862
- // TODO: Does this `replace` need the `g` flag?
863
- // TODO: This only replaces the chars at the start of the string. Is that intentional?
864
- encoded_name = href.replace(/^[A-Za-z0-9]*/, '_');
941
+ // This means the first guess of the name should be the not_decoded_name
942
+ // TODO: This only replaces the chars at the start of the string. Is that
943
+ // intentional? Does this `.replace()` need the `g` flag?
944
+ not_decoded_name = href.replace(/^[A-Za-z0-9]*/, '_');
865
945
  }
866
946
  }
867
947
 
868
- if ( !encoded_name ) {
948
+ if ( !not_decoded_name ) {
869
949
  // Some fields, like comboboxes, have a weird var name setup
870
- encoded_name = $node.attr( `id` );
950
+ not_decoded_name = $node.attr( `id` );
871
951
  }
872
952
 
873
953
  // ====================
874
954
  // Table rows
875
955
  // ====================
876
- // Default values
877
- let var_names = await scope.getPossibleVarNames( scope, {
878
- encoded: encoded_name,
879
- field_like_names,
880
- nota: false, // none of the above (detected and overwritten later)
881
- });
882
-
956
+ // "None of the above" checkbox/object radio options don't contain their variable's name
957
+ let is_none_of_the_above = false;
883
958
  if ( $node.hasClass('danota-checkbox') || $node.parents(`.da-field-container-datatype-object_radio`).name ) {
884
- // 'None of the above' fields must find var name info from siblings' attributes
959
+ is_none_of_the_above = true;
885
960
  let first_sib = $(node.parent).find( 'input' )[0];
886
- var_names = await scope.getPossibleVarNames( scope, {
887
- encoded: first_sib.attribs.name,
888
- field_like_names,
889
- nota: true, // none of the above
890
- });
961
+ not_decoded_name = first_sib.attribs.name;
962
+ // TODO: Do we need daaota as well? ("all of the above")
891
963
  }
892
964
 
965
+ let var_names = await scope.getPossibleDecodedNames( scope, {
966
+ not_decoded: not_decoded_name,
967
+ field_like_names,
968
+ is_none_of_the_above: is_none_of_the_above,
969
+ proxies,
970
+ });
971
+
893
972
  // button, radio, checkbox yesno, and maybe others need the value of the `value` attribute
894
973
  let values = [];
895
974
  if ( node.name === `select` ) {
@@ -915,8 +994,6 @@ module.exports = {
915
994
  return await scope.page.evaluate(async function ( elem ) {
916
995
  return await get_interview_variables();
917
996
  });
918
-
919
- // return data;
920
997
  }, // Ends scope.getPageJSON()
921
998
 
922
999
  getFieldSelector: async function ( scope, { $, node }) {
@@ -967,10 +1044,11 @@ module.exports = {
967
1044
  // Ignore blanks, and values that certainly aren't base64 encoded like True and False
968
1045
  // Modified from https://stackoverflow.com/a/8571649/11416267
969
1046
  // DA doesn't use `==` to pad base 64
1047
+ // TODO: How are 'None', 'True', and 'False' being handled?
970
1048
  if (value !== 'None' && value !== 'True' && value !== 'False'
971
1049
  && value.match(da_base64_regex)) {
972
1050
  let value_decoded = fromBase64(value);
973
- let values_decoded = scope.decodeFully( scope, {expr_str: value_decoded });
1051
+ let values_decoded = scope.decodeFully( scope, { expr_str: value_decoded });
974
1052
  for (let possible_val of values_decoded) {
975
1053
  possible_table_rows.push({var: var_name, value: possible_val, });
976
1054
  }
@@ -997,12 +1075,20 @@ module.exports = {
997
1075
  */
998
1076
  if ( !$('input[name="_varnames"]')[0] ) { return {}; }
999
1077
 
1078
+ // TODO: Why are we docoding the field names here? We could decode them later, right?
1079
+ // In `getPossibleDecodedNames` we could do:
1080
+ // if ( field_like_original_names[ not_decoded ] ) {
1081
+ // still_encoded = field_like_names[ not_decoded ];
1082
+ // decode( still_encoded )
1083
+ // }
1084
+
1000
1085
  let field_like_names = {};
1001
1086
  let field_like_names_encoded = $('input[name="_varnames"]')[0].attribs.value;
1002
1087
  let field_like_names_str = fromBase64( field_like_names_encoded );
1003
1088
  let field_like_names_json = JSON.parse( field_like_names_str );
1004
1089
  for ( let field_like_key in field_like_names_json ) {
1005
1090
  let field_like_DOM_name = fromBase64( field_like_key );
1091
+ // Even with `multiselect`, field names are only encoded once
1006
1092
  let field_like_var_name = fromBase64( field_like_names_json[ field_like_key ] );
1007
1093
  field_like_names[ field_like_DOM_name ] = field_like_var_name;
1008
1094
  }
@@ -1010,8 +1096,13 @@ module.exports = {
1010
1096
  return field_like_names;
1011
1097
  }, // Ends scope.getFieldNamesDict()
1012
1098
 
1013
- getPossibleVarNames: async function ( scope, { encoded, field_like_names, nota }) {
1014
- /* Given a base64 encoded string and a map of `_field_n` to var names, return
1099
+ getPossibleDecodedNames: async function ( scope, {
1100
+ not_decoded,
1101
+ field_like_names={},
1102
+ is_none_of_the_above=false,
1103
+ proxies={}
1104
+ }) {
1105
+ /** Given a potentially base64 encoded string and a map of `_field_n` to var names, return
1015
1106
  * the decoded possible var names and, sometimes, values that the
1016
1107
  * encoded string could be. Only some node names actually use `_field_n`.
1017
1108
  *
@@ -1029,41 +1120,69 @@ module.exports = {
1029
1120
  * TODO: We'll probably eventually have to search for `[B'encoded']` and such
1030
1121
  * iteratively to get all of them. Hasn't cropped up yet, though.
1031
1122
  *
1032
- * @param encoded {str} - base64 encoded string of the node
1123
+ * @param not_decoded {str} - potentially base64 encoded string from the node
1033
1124
  * @param field_like_names {obj} - e.g. `{ _field_0: 'some_checkbox_var_name' }`
1034
- * @param nota {bool} - is the field a 'none of the above' field? */
1125
+ * @param is_none_of_the_above {bool} - is the field a 'none of the above' field?
1126
+ * @param proxies {obj} - e.g. `{ x: str, i: str, j: str, ... n: str }`. Empty strings
1127
+ * are fine since that means that proxy is absent and won't come into play.
1128
+ */
1129
+
1035
1130
  // E.g. continue buttons that don't set a variable
1036
- if ( encoded === undefined ) { return []; }
1131
+ if ( not_decoded === undefined ) { return []; }
1132
+ // Not sure it's necessary to have both with and without proxies here
1133
+ let guesses = scope.with_and_without_proxies( scope, {
1134
+ var_name: `${ not_decoded }`,
1135
+ proxies,
1136
+ });
1037
1137
 
1038
1138
  // Names can be encoded multiple times, so start peeling the layers
1039
- let layer_1_decoded = fromBase64( encoded );
1040
- // If this is of the format `_field_n`, we need to get the actual matching var name.
1041
- if ( field_like_names[ layer_1_decoded ] ) {
1042
- layer_1_decoded = field_like_names[ layer_1_decoded ];
1139
+ let decoded_once = fromBase64( not_decoded );
1140
+ // If this is of the format `_field_n`, we need to get the actual matching var name
1141
+ if ( field_like_names[ decoded_once ] ) {
1142
+ decoded_once = field_like_names[ decoded_once ];
1043
1143
  }
1044
1144
 
1045
1145
  try {
1046
- let action_button_json = JSON.parse( layer_1_decoded );
1047
- if ( action_button_json.action ) { layer_1_decoded = action_button_json.action }
1146
+ // We think this never has proxy vars
1147
+ let action_button_json = JSON.parse( decoded_once );
1148
+ if ( action_button_json.action ) { decoded_once = action_button_json.action }
1048
1149
  } catch ( error ) {
1049
1150
  // Things that are not action buttons can't be parsed this way.
1050
1151
  // A special error for action buttons requires passing the node in here too, which is a lot to pass in.
1051
1152
  // TODO: Should we move this back into `getAllFields()`?
1052
1153
  }
1053
1154
 
1054
- return scope.decodeFully(scope, { expr_str: layer_1_decoded, field_like_names, nota });
1055
- }, // Ends scope.getPossibleVarNames()
1056
-
1057
- decodeFully: function ( scope, { expr_str, field_like_names = {}, nota} ) {
1058
- /*
1155
+ // Include `not_decoded` since the name might not be encoded
1156
+ // TODO: Should we try to replace proxies in `not_decoded`. I can't
1157
+ // think of an occasion where that would be necessary.
1158
+ guesses = guesses.concat( scope.decodeFully( scope, {
1159
+ expr_str: decoded_once,
1160
+ field_like_names,
1161
+ is_none_of_the_above,
1162
+ proxies
1163
+ })
1164
+ );
1165
+
1166
+ return guesses;
1167
+ }, // Ends scope.getPossibleDecodedNames()
1168
+
1169
+ decodeFully: function ( scope, {
1170
+ expr_str,
1171
+ field_like_names = {},
1172
+ is_none_of_the_above=false,
1173
+ proxies={}
1174
+ }) {
1175
+ /**
1059
1176
  * Given a DA variable or value name, make sure it's fully decoded from base64.
1060
- * @param variable_str {str} - a python variable string, potentially partially base64 encoded
1177
+ * @param expr_str {str} - a python variable string, potentially partially base64 encoded
1061
1178
  * Some examples:
1062
1179
  * - my_variable
1063
1180
  * - checkboxes_choices[B'Y2hlY2tib3hfb3RoZXJfb3B0XzI']
1064
1181
  * - foo[bar][B'Y2hlY2tib3hfb3RoZXJfb3B0XzI']
1065
1182
  * @param field_like_names {obj} - e.g. `{ _field_0: 'some_checkbox_var_name' }`
1066
- * @param nota {bool} - is the field a 'none of the above' field?
1183
+ * @param is_none_of_the_above {bool} - is the field a 'none of the above' field?
1184
+ * @param proxies {obj} - e.g. `{ x: str, i: str, j: str, ... n: str }`. Empty strings
1185
+ * are fine since that means that proxy is absent and won't come into play.
1067
1186
  */
1068
1187
  // Some fields have a key hidden inside them.
1069
1188
  // This gets the variable name before the final brackets, and the likely
@@ -1083,22 +1202,37 @@ module.exports = {
1083
1202
  if ( before_and_in_brackets ) {
1084
1203
  let key_or_name = before_and_in_brackets[1];
1085
1204
  // If this is of the format `_field_n`, we need to get the actual matching var name.
1086
- let var_name = field_like_names [ key_or_name ] || key_or_name;
1205
+ let var_name = field_like_names[ key_or_name ] || key_or_name;
1087
1206
 
1088
1207
  let dictionary_key = before_and_in_brackets[2];
1089
1208
 
1090
1209
  // Our own marker for a 'none of the above' checkbox choice
1091
- if ( nota ) {
1092
- possible.push( `${ var_name }['None']` );
1210
+ if ( is_none_of_the_above ) {
1211
+ // possible.push( `${ var_name }['None']` );
1212
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1213
+ var_name: `${ var_name }['None']`,
1214
+ proxies,
1215
+ }));
1093
1216
  } else {
1217
+
1218
+ // TODO: Just use try/catches?
1219
+
1094
1220
  if (dictionary_key.match(da_base64_regex)) {
1095
1221
  // Get the choice name. E.g. option_1, option_2
1096
1222
  let dictionary_key_decoded = fromBase64( dictionary_key );
1097
- possible.push( `${ var_name }['${ dictionary_key_decoded }']` );
1223
+ // possible.push( `${ var_name }['${ dictionary_key_decoded }']` );
1224
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1225
+ var_name: `${ var_name }['${ dictionary_key_decoded }']`,
1226
+ proxies,
1227
+ }));
1098
1228
  if (dictionary_key_decoded.match(da_base64_regex)) {
1099
1229
  try {
1100
- // The part inside `[B'encoded']` could need one more decoding.
1101
- possible.push(`${ var_name }['${ fromBase64( dictionary_key_decoded ) }']`);
1230
+ // The part inside `[B|R|O'encoded']` could need one more decoding.
1231
+ // possible.push(`${ var_name }['${ fromBase64( dictionary_key_decoded ) }']`);
1232
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1233
+ var_name: `${ var_name }['${ fromBase64( dictionary_key_decoded ) }']`,
1234
+ proxies,
1235
+ }));
1102
1236
  } catch ( err ) {} // It wasn't double encoded. Do nothing.
1103
1237
  }
1104
1238
  }
@@ -1107,18 +1241,65 @@ module.exports = {
1107
1241
  // =====================
1108
1242
  // If bracket regex didn't match, just use var name
1109
1243
  } else {
1110
- possible.push(`${ expr_str }`);
1244
+ possible = possible.concat( scope.with_and_without_proxies( scope, {
1245
+ var_name: `${ expr_str }`,
1246
+ proxies,
1247
+ }) );
1111
1248
  }
1112
1249
  return possible;
1113
1250
  },
1114
1251
 
1252
+ with_and_without_proxies: function ( scope, { var_name, proxies={} }) {
1253
+ /** Given a variable or value's possible name and a map of values for
1254
+ * each proxy variable, return a list of either one or two strings.
1255
+ * It always includes the given variable name. If proxies were found
1256
+ * and replaced, the list also includes a version of the variable
1257
+ * name in which the proxies have been replaced by their values.
1258
+ *
1259
+ * Proxies are generic objects (x) or index variables (i, j, k, l, m, n).
1260
+ *
1261
+ * @param { string } var_name - The possible name of a variable or value
1262
+ * @param { Object } proxies - A map of proxy variables to their values on
1263
+ * the HTML page, if they're there.
1264
+ *
1265
+ * @returns {Array<string>} - An array of at least one string - var_name
1266
+ * itself. Possibly a second value where proxy variables in var_name
1267
+ * are replaced with their values (if the proxy variables are present
1268
+ * and if their values are passed in).
1269
+ * */
1270
+ // We could combine these regexes, but this may be easier to read
1271
+ let generic_obj_regex = /^(x)\b/;
1272
+ let index_var_regex = /(?<=\[)([ijklmn])(?=\])/g;
1273
+ // If proxies exist in the HTML, replace any proxies in the variables
1274
+ // with those values.
1275
+ let new_name = var_name;
1276
+ if ( proxies.x !== '' && proxies.x !== undefined ) {
1277
+ new_name = new_name.replace( generic_obj_regex, proxies.x );
1278
+ }
1279
+ new_name = new_name.replace( index_var_regex, function(match) {
1280
+ if ( proxies[ match ] === '' || proxies[ match ] === undefined ) {
1281
+ return match;
1282
+ } else {
1283
+ return proxies[ match ];
1284
+ }
1285
+ });
1286
+
1287
+ // The older versions of Story Table need to see "x", "i", etc.,
1288
+ // so keep that old name around.
1289
+ let unique_names = [ var_name ];
1290
+ // If there were proxies, add the new version of the name
1291
+ if ( new_name !== var_name ) { unique_names.push( new_name ); }
1292
+
1293
+ return unique_names;
1294
+ }, // Ends scope.with_and_without_proxies()
1295
+
1115
1296
  getOptionValues: async function ( scope, { $select }) {
1116
1297
  /* Returns all the option node values of the select node. */
1117
1298
  let options = [];
1118
1299
  let option_nodes = $select.find( `option` );
1119
1300
  for ( let opt of option_nodes ) {
1120
1301
  options.push( opt.attribs.value );
1121
- // Dropdowns created w/ objects are base64 encoded; but it's handled in getPossibleTableRowMatches
1302
+ // Dropdowns created w/ objects are base64 encoded; but it's handled in getPossibleTableRowMatchers
1122
1303
  }
1123
1304
  return options;
1124
1305
  }, // Ends scope.getOptionValues()
@@ -1126,11 +1307,11 @@ module.exports = {
1126
1307
 
1127
1308
  // TODO: Maybe add `from_story_table` to the var data table (array...)
1128
1309
  getMatchingRows: async function ( scope, { field, var_data }) {
1129
- /* Given var_data, a list of objects with variable data for
1310
+ /** Given var_data, a list of objects with variable data for
1130
1311
  * setting fields, and an object for a DOM field, returns list of the
1131
1312
  * rows that match the DOM field. That list of story table rows may be empty.
1132
1313
  *
1133
- * For notes on trigger var, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger
1314
+ * For notes on trigger var, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger
1134
1315
  *
1135
1316
  * @param var_data {arr} - fields and the values they can be set to
1136
1317
  * @param var_data[n].var {str} - name of variable to set
@@ -1194,6 +1375,7 @@ module.exports = {
1194
1375
  // If the field uses proxy variables - x, i, j, etc, flag it. See "proxies" note.
1195
1376
  uses_proxies = uses_proxies || guess.var.match( proxies_regex );
1196
1377
 
1378
+ // TODO: can we just use `uses_proxies` in here?
1197
1379
  if ( table_row.trigger === scope.trigger_not_needed_flag ) {
1198
1380
  trigger_might_be_needed = false;
1199
1381
  }
@@ -1224,9 +1406,9 @@ module.exports = {
1224
1406
  // field/page trigger name is needed but is missing.
1225
1407
  if ( !page_trigger_name ) {
1226
1408
  let page_trigger_warning = `A field on this page uses an index `
1227
- + `variable or a generic object, but the page does not `
1228
- + `contain the trigger variable's name in its HTML. See `
1229
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#a-missing-trigger-variable.`;
1409
+ + `variable or a generic object, but the page is missing `
1410
+ + `the ALKiln "trigger" variable's name. See `
1411
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#a-missing-trigger-variable.`;
1230
1412
  reports.addToReport(scope, { type: `warning`, value: page_trigger_warning });
1231
1413
  }
1232
1414
 
@@ -1236,18 +1418,18 @@ module.exports = {
1236
1418
 
1237
1419
  if ( !table_row_trigger_name ) {
1238
1420
  // var/row trigger name is (probably) needed but is missing. Caveat: If there's
1239
- // only one page in the proxy var loop and there's only one row, trigger name is
1240
- // not needed, but I'm not sure how we figure out all those things here and log
1241
- // a useful message.
1421
+ // only one page in the proxy var loop and there's only one row (like the "I set the var"
1422
+ // Step), trigger name is not needed, but I'm not sure how we figure out all those
1423
+ // things here and log a useful message.
1242
1424
 
1243
1425
  // First, make the row data more readable to humans.
1244
1426
  let row_as_story_table = reports.convertToOriginalStoryTableRow( scope, { row_data: row_that_matches_this_field });
1245
- let row_trigger_warning = `This story table row needs a value in the "trigger" `
1427
+ let row_trigger_warning = `This Story Table row needs a value in the "trigger" `
1246
1428
  + `column, but it is empty. The test might get unexpected results. See `
1247
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1248
- + `\nThe row's data:\n${ row_as_story_table }`
1249
- + `\nThis field's data:\n${ JSON.stringify( field.guesses )}`;
1429
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
1430
+ + `\nThe rows:\n${ row_as_story_table }`;
1250
1431
  reports.addToReport(scope, { type: `warning`, value: row_trigger_warning });
1432
+
1251
1433
  rows_without_a_trigger_name.push( row_that_matches_this_field );
1252
1434
  } else {
1253
1435
  let triggers_match = await scope.matchNeutralizedQuotes( scope, { value1: page_trigger_name, value2: table_row_trigger_name });
@@ -1267,7 +1449,7 @@ module.exports = {
1267
1449
  }
1268
1450
 
1269
1451
  // Warn the user if all matching rows had trigger names that conflicted.
1270
- // Missing trigger names don't count as conflicting.
1452
+ // Missing trigger names don't count as conflicting. [Did this mean missing in the table or in the page?]
1271
1453
  if ( final_matching_rows.length === 0 ) {
1272
1454
 
1273
1455
  // First, make the row data more readable to humans.
@@ -1277,12 +1459,12 @@ module.exports = {
1277
1459
  conflicts_as_story_table_rows.push( table_str );
1278
1460
  }
1279
1461
 
1280
- let all_trigger_names_conflict_msg = `Is this story table missing a row? `
1281
- + `${ rows_that_match_field_names.length } story table row(s) matched a field name, `
1282
- + `but none of their trigger variable name(s) matched the trigger variable `
1283
- + `name in the screen's HTML. The test will have to ignore this field. See `
1284
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1285
- + `\nThe data of row(s) with conflicting trigger name(s):\n${ conflicts_as_story_table_rows.join( `\n` )}`
1462
+ let all_trigger_names_conflict_msg = `${ rows_that_match_field_names.length } `
1463
+ + `Story Table row(s) matched a field name, but didn't match a trigger `
1464
+ + `variable. The Story Table might be missng a row or a trigger variable. `
1465
+ + `The test will have to ignore this field. See `
1466
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
1467
+ + `\nThe row(s) data:\n${ conflicts_as_story_table_rows.join( `\n` )}`
1286
1468
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1287
1469
  reports.addToReport(scope, {
1288
1470
  type: `warning`,
@@ -1308,7 +1490,7 @@ module.exports = {
1308
1490
  let multiple_matches_msg = `Do you have a duplicate story table row? `
1309
1491
  + `Multiple story table rows match this field's variable name. `
1310
1492
  + `The test will use the last row that matched. See `
1311
- + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1493
+ + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
1312
1494
  + `\nThe matching rows:\n${ rows_as_story_table.join(`\n`) }`
1313
1495
  + `\nThis field's data:\n${ JSON.stringify( field )}`;
1314
1496
  reports.addToReport(scope, { type: `warning`, value: multiple_matches_msg });
@@ -1363,7 +1545,9 @@ module.exports = {
1363
1545
  let disabled = await handle.evaluate(( elem )=> {
1364
1546
  // da's header covers the top of the section containing the fields which means
1365
1547
  // sometimes it blocks nodes from clicks. Scroll item to the bottom instead.
1366
- if ( !elem.disabled ) { elem.scrollIntoView(false); }
1548
+ // Note: I'm not sure how we'd get in here if `elem` didn't exist, but
1549
+ // I once got an error here that I couldn't read `.disabled` of `null`.
1550
+ if ( elem && !elem.disabled ) { elem.scrollIntoView(false); }
1367
1551
  return elem.disabled;
1368
1552
  });
1369
1553
 
@@ -1539,7 +1723,7 @@ module.exports = {
1539
1723
 
1540
1724
  enter_answer: {
1541
1725
  button: async function ( scope, { handle, answer }) {
1542
- await scope.tapElement( scope, handle );
1726
+ await scope.tapElementAndNavigate( scope, { elem: handle });
1543
1727
  return true;
1544
1728
  },
1545
1729
  textarea: async function ( scope, { handle, answer }) {
@@ -2180,7 +2364,7 @@ module.exports = {
2180
2364
  value: 'The attribute `.there_is_another` is invalid in story table tests. Replace it with '
2181
2365
  + '`.target_number` in your `var` and `trigger` columns. Set the `value` to the number of items in '
2182
2366
  + `that list. This test will now set this row's \`value\` to \`False\`. See `
2183
- + `https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing#there_is_another. `
2367
+ + `https://suffolklitlab.github.io/docassemble-AssemblyLine-documentation/docs/alkiln#there_is_another. `
2184
2368
  + `The row data is\n${ JSON.stringify( var_datum.original )}` });
2185
2369
  } // ends if it's a `target_number` row
2186
2370
  } // ends for each row
@@ -2210,7 +2394,7 @@ module.exports = {
2210
2394
  } else if ( value.match( /(true|yes|checked|check|selected|select)/i )) {
2211
2395
  if ( from_story_table && !row.artificial ) {
2212
2396
  if ( row.var.match( /\.there_is_another$/ ) ) {
2213
- 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 )}` });
2397
+ 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 )}` });
2214
2398
  } else {
2215
2399
  // If `target_number` is set to True instead of a number
2216
2400
  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.` });
@@ -2466,16 +2650,16 @@ module.exports = {
2466
2650
  }, // Ends scope.steps.set_secret_var
2467
2651
 
2468
2652
  tap_term: async ( scope, phrase ) => {
2469
- /** Tap a link that doesn't navigate. Depends on the language of the text. */
2653
+ /** Tap a link that doesn't navigate. Depends on the language of the text.
2654
+ * TODO: This may be broken at this point.
2655
+ * */
2470
2656
  const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
2471
2657
 
2472
2658
  let msg = `The term "${ phrase }" seems to be missing.`;
2473
2659
  if ( !link ) { reports.addToReport(scope, { type: `error`, value: msg }); }
2474
2660
  expect( link, msg ).to.exist;
2475
-
2476
- await scope.tapElement( scope, link );
2477
-
2478
- await scope.afterStep(scope, {waitForShowIf: true});
2661
+ await scope.tapElementAndStayOnPage(scope, { elem: link });
2662
+ await scope.page.waitForTimeout(10000);
2479
2663
  }, // Ends scope.steps.tap_term()
2480
2664
 
2481
2665
  sign: async ( scope, name ) => {
@@ -2529,7 +2713,7 @@ module.exports = {
2529
2713
  if (failed_to_download) {
2530
2714
  reports.addToReport(scope, { type: `warning`, value: `Could not download file using fetch (${ err_msg }). ALKiln will now fallback to the click download method` });
2531
2715
  scope.toDownload = filename;
2532
- // Should this be using `scope.tapElement()`?
2716
+ // Should this be using `scope.tapElement`?
2533
2717
  await elem.evaluate( elem => { return elem.click(); });
2534
2718
  await scope.detectDownloadComplete( scope );
2535
2719
  }
@@ -2572,6 +2756,7 @@ module.exports = {
2572
2756
  * 1. Four words - a first, middle, last, and suffix (ex: "Tal Tay Taylor III")
2573
2757
  *
2574
2758
  * The string 'the var'/'the variable' part of the step is optional, as you can see below.
2759
+ * This is a linear step, so no trigger var is needed.
2575
2760
  */
2576
2761
  let name_patterns = [
2577
2762
  [ `text` ],
@@ -2617,6 +2802,7 @@ module.exports = {
2617
2802
  *
2618
2803
  * The string 'the var'/'the variable' part of the step is optional,
2619
2804
  * as you can see below.
2805
+ * This is a linear step only, so it doesn't need to have a trigger var.
2620
2806
  */
2621
2807
 
2622
2808
  // Get all the parts of the address properly separated
@@ -2702,7 +2888,7 @@ module.exports = {
2702
2888
  await scope.page.type( `#email`, email );
2703
2889
  await scope.page.type( `#password`, password );
2704
2890
  let elem = await scope.page.$( `button[name="submit"]` );
2705
- await scope.tapElement( scope, elem );
2891
+ await scope.tapElementAndNavigate( scope, { elem });
2706
2892
 
2707
2893
  // Wait to see the success message.
2708
2894
  // This is a bit of an assumption and needs success and failure testing