@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/.github/workflows/github_server.yml +2 -2
- package/.github/workflows/playground.yml +2 -0
- package/CHANGELOG.md +22 -0
- package/action.yml +29 -14
- package/action_for_github_server/action.yml +37 -21
- package/lib/scope.js +370 -184
- package/lib/steps.js +147 -13
- package/lib/utils/log.js +16 -5
- package/package.json +1 -1
- package/tests/unit_tests/fields.fixtures.js +368 -86
- package/tests/unit_tests/getAllFields.test.js +13 -10
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,
|
|
177
|
+
waitForTab: async function waitForTab(scope, { tab_id }) {
|
|
178
178
|
/**
|
|
179
|
-
* @param {
|
|
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 (
|
|
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="${
|
|
201
|
+
}, `div[aria-labelledby="${ tab_id }"]`);
|
|
202
202
|
},
|
|
203
203
|
|
|
204
|
-
afterStep: async function afterStep(scope, options = {waitForShowIf: false,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
263
|
-
|
|
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
|
-
|
|
273
|
+
// Might be able to handle this in `scope.afterStep`
|
|
274
|
+
if ( navigated ) {
|
|
266
275
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
285
|
+
}, // Ends scope.tapElementAndNavigate()
|
|
275
286
|
|
|
276
|
-
|
|
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
|
-
//
|
|
281
|
-
// Also, puppeteer will ensure proper timeout.
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 && !
|
|
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
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
}
|
|
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 = `
|
|
760
|
-
|
|
761
|
-
|
|
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 ||
|
|
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
|
-
|
|
786
|
-
|
|
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 (
|
|
809
|
-
let encoded_trigger_var = $( trigger_elem ).data( `variable` )
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
|
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
|
-
|
|
938
|
+
not_decoded_name = match[1];
|
|
860
939
|
} else {
|
|
861
940
|
// Some action buttons just use urls instead of da actions
|
|
862
|
-
//
|
|
863
|
-
// TODO: This only replaces the chars at the start of the string. Is that
|
|
864
|
-
|
|
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 ( !
|
|
948
|
+
if ( !not_decoded_name ) {
|
|
869
949
|
// Some fields, like comboboxes, have a weird var name setup
|
|
870
|
-
|
|
950
|
+
not_decoded_name = $node.attr( `id` );
|
|
871
951
|
}
|
|
872
952
|
|
|
873
953
|
// ====================
|
|
874
954
|
// Table rows
|
|
875
955
|
// ====================
|
|
876
|
-
//
|
|
877
|
-
let
|
|
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
|
-
|
|
959
|
+
is_none_of_the_above = true;
|
|
885
960
|
let first_sib = $(node.parent).find( 'input' )[0];
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
1040
|
-
// If this is of the format `_field_n`, we need to get the actual matching var name
|
|
1041
|
-
if ( field_like_names[
|
|
1042
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
|
1228
|
-
+ `
|
|
1229
|
-
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/
|
|
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
|
|
1240
|
-
// not needed, but I'm not sure how we figure out all those
|
|
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
|
|
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/
|
|
1248
|
-
+ `\nThe
|
|
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 =
|
|
1281
|
-
+
|
|
1282
|
-
+ `
|
|
1283
|
-
+ `
|
|
1284
|
-
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/
|
|
1285
|
-
+ `\nThe
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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.
|
|
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
|