@suffolklitlab/alkiln 5.12.0 → 5.13.0-logs1
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/.env.example +29 -0
- package/.github/workflows/github_server.yml +59 -12
- package/.github/workflows/playground.yml +11 -4
- package/CHANGELOG.md +41 -1
- package/CONTRIBUTING.md +98 -83
- package/README.md +1 -1
- package/action.yml +67 -29
- package/action_for_github_server/action.yml +59 -31
- package/docs/decisions/006_which_log_level_to_pick_2024_09_5.md +46 -0
- package/docs/decisions/adopt_architectural_decision_records.md +58 -0
- package/docs/decisions/detect_sign_in.md +124 -0
- package/docs/decisions/pdfjs_version_2024_08_08.md +18 -0
- package/docs/decisions/reports_and_logs_2024_08_11.md +141 -0
- package/docs/decisions/update_puppeteer_2024_07_28.md +62 -0
- package/docs/story_tables.md +1 -1
- package/lib/docassemble/docassemble_api_REST.js +67 -1
- package/lib/docassemble/docassemble_api_interface.js +225 -18
- package/lib/docassemble/server_install.js +23 -9
- package/lib/docassemble/setup.js +33 -20
- package/lib/docassemble/takedown.js +27 -7
- package/lib/run_cucumber.js +83 -33
- package/lib/scope.js +222 -82
- package/lib/steps.js +206 -62
- package/lib/utils/files.js +8 -6
- package/lib/utils/log.js +477 -84
- package/lib/utils/reports.js +14 -5
- package/lib/utils/session_vars.js +25 -18
- package/lib/utils/time.js +44 -0
- package/npm-shrinkwrap.json +635 -193
- package/package.json +8 -8
- package/setup.py +1 -1
- package/tests/log_codes/check_codes.sh +163 -108
- package/tests/log_codes/log_code_expected_instances.txt +213 -0
- package/tests/unit_tests/getAllFields.test.js +5 -0
- package/tests/unit_tests/getMatchingRows.test.js +5 -0
- package/tests/unit_tests/log.test.js +20 -0
- package/tests/unit_tests/normalizeTable.test.js +5 -0
- package/tests/log_codes/deleted_codes.json +0 -8
package/lib/scope.js
CHANGED
|
@@ -13,14 +13,13 @@ const pdf_reader = require('./utils/pdf_reader');
|
|
|
13
13
|
const session_vars = require('./utils/session_vars');
|
|
14
14
|
const get_base_interview_url = require(`./docassemble/get_base_interview_url`);
|
|
15
15
|
const time = require('./utils/time');
|
|
16
|
-
const log = require('./utils/log');
|
|
16
|
+
// const log = require('./utils/log');
|
|
17
17
|
const { waitForTimeout } = require('./utils/time');
|
|
18
18
|
const files = require('./utils/files' );
|
|
19
|
-
const
|
|
19
|
+
const da_api = require('./docassemble/docassemble_api_interface');
|
|
20
20
|
const wrapPromiseWithTimeout = require('./cucumber_8_shim');
|
|
21
21
|
const reports = require(`./utils/reports`);
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
let ordinal_to_integer = {
|
|
25
24
|
only: 0, first: 0, second: 1, third: 2, fourth: 3, fifth: 4,
|
|
26
25
|
sixth: 5, seventh: 6, eighth: 7, ninth: 8, tenth: 9,
|
|
@@ -84,10 +83,16 @@ let toBase64 = function ( utf8_str, exclude_padding=true) {
|
|
|
84
83
|
|
|
85
84
|
let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
|
|
86
85
|
let da_base64_regex = /^[A-Za-z0-9+/]+$/;
|
|
86
|
+
let log = null;
|
|
87
87
|
|
|
88
88
|
module.exports = {
|
|
89
89
|
trigger_not_needed_flag: `ALKiln: no trigger variable needed`,
|
|
90
90
|
|
|
91
|
+
|
|
92
|
+
set_log: function ({ log_obj }) {
|
|
93
|
+
log = log_obj;
|
|
94
|
+
},
|
|
95
|
+
|
|
91
96
|
getSafeScenarioBaseFilename: async function( scope, { scenario }) {
|
|
92
97
|
/** Return a string that's safe to be a filename. Include the Scenario
|
|
93
98
|
* description and tags.
|
|
@@ -171,7 +176,7 @@ module.exports = {
|
|
|
171
176
|
// If something might be hidden or shown, wait extra time to let it hide or show
|
|
172
177
|
// I think `.$()` does not use a timeout
|
|
173
178
|
let showif = await scope.page.$('.dashowif');
|
|
174
|
-
if ( showif ) { await
|
|
179
|
+
if ( showif ) { await time.waitForTimeout( scope.showif_timeout ); }
|
|
175
180
|
},
|
|
176
181
|
|
|
177
182
|
waitForTab: async function waitForTab(scope, { tab_id }) {
|
|
@@ -288,9 +293,7 @@ module.exports = {
|
|
|
288
293
|
/** Tap a given element, detect navigation, and do appropriate related things.
|
|
289
294
|
* `click_promise can be any kind of Promise, including a Promise.all()
|
|
290
295
|
*/
|
|
291
|
-
|
|
292
|
-
if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0102`, value: msg }); }
|
|
293
|
-
expect( elem, msg ).to.exist;
|
|
296
|
+
await scope.guard_against_missing_tap_element(scope, { elem });
|
|
294
297
|
|
|
295
298
|
// Get the text on the button or link for a potential error message later
|
|
296
299
|
let tapperText = await (await elem.getProperty('textContent')).jsonValue();
|
|
@@ -326,6 +329,14 @@ module.exports = {
|
|
|
326
329
|
} // ends try/catch
|
|
327
330
|
}, // Ends scope.tapElementAndStayOnPage()
|
|
328
331
|
|
|
332
|
+
guard_against_missing_tap_element: async function(scope, { elem }) {
|
|
333
|
+
/** Handle the error message for a missing tap element, if it's missing. */
|
|
334
|
+
let msg = `Cannot find the element to tap.`;
|
|
335
|
+
if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0102`, value: msg }); }
|
|
336
|
+
expect( elem, msg ).to.exist;
|
|
337
|
+
return elem;
|
|
338
|
+
},
|
|
339
|
+
|
|
329
340
|
// Handle server reload errors
|
|
330
341
|
server_statuses: [],
|
|
331
342
|
server_check_interval_ms: 1 * 1000,
|
|
@@ -339,7 +350,7 @@ module.exports = {
|
|
|
339
350
|
// This code assumes a reasonable response time would be n seconds if the
|
|
340
351
|
// server is up and that a server reload/restart won't take less than n seconds.
|
|
341
352
|
// Also, it's non-blocking, so it doesn't slow down tests
|
|
342
|
-
let is_up = await
|
|
353
|
+
let is_up = await da_api.is_server_responding({ timeout: scope.server_check_interval_ms });
|
|
343
354
|
// Add status to the beginning of the list to make truncating easier
|
|
344
355
|
scope.server_statuses.unshift( is_up );
|
|
345
356
|
|
|
@@ -501,7 +512,7 @@ module.exports = {
|
|
|
501
512
|
if ( diff <= 0 ) { return false; }
|
|
502
513
|
|
|
503
514
|
// If time hasn't expired, wait and then check again
|
|
504
|
-
await
|
|
515
|
+
await time.waitForTimeout( 100 );
|
|
505
516
|
await scope.detectDownloadComplete(scope, endTime);
|
|
506
517
|
|
|
507
518
|
return false;
|
|
@@ -627,7 +638,7 @@ module.exports = {
|
|
|
627
638
|
interview_url = `${ base_url }${ file_name }`;
|
|
628
639
|
}
|
|
629
640
|
|
|
630
|
-
reports.addToReport(scope,{
|
|
641
|
+
reports.addToReport(scope, {
|
|
631
642
|
code: `ALK0113`,
|
|
632
643
|
type: `row info`,
|
|
633
644
|
value: `Trying to load the interview at "${ interview_url }"`
|
|
@@ -638,7 +649,17 @@ module.exports = {
|
|
|
638
649
|
// TODO: implement and use scope.handle_possible_timeout()
|
|
639
650
|
try {
|
|
640
651
|
|
|
641
|
-
await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout })
|
|
652
|
+
await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout });
|
|
653
|
+
// Get interview session values, if possible, so we can delete the session later
|
|
654
|
+
let interview_vars = await scope.page.evaluate(async function() {
|
|
655
|
+
try {
|
|
656
|
+
// won't exist on non-da interview pages
|
|
657
|
+
return await get_interview_variables();
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
scope.set_interview_session_data( scope, { vars: interview_vars });
|
|
642
663
|
|
|
643
664
|
} catch ( error ) {
|
|
644
665
|
|
|
@@ -676,12 +697,42 @@ module.exports = {
|
|
|
676
697
|
});
|
|
677
698
|
expect( load_result.error ).to.equal( '' );
|
|
678
699
|
} else {
|
|
679
|
-
reports.addToReport(scope, { type: `row info`, code: `ALK0117`, value: `Successfully found the interview
|
|
700
|
+
reports.addToReport(scope, { type: `row info`, code: `ALK0117`, value: `Successfully found the interview` });
|
|
680
701
|
}
|
|
681
702
|
|
|
682
703
|
return scope.page;
|
|
683
704
|
}, // Ends scope.load()
|
|
684
705
|
|
|
706
|
+
set_interview_session_data: function ( scope, { vars = null }) {
|
|
707
|
+
/** Sets this interview's session data for this scenario if it doesn't
|
|
708
|
+
* already exist.
|
|
709
|
+
*
|
|
710
|
+
* @returns final_vars { object | null }
|
|
711
|
+
* @returns final_vars.i { str } File path of interview
|
|
712
|
+
* @returns final_vars.uid { str } UID of interview
|
|
713
|
+
* */
|
|
714
|
+
if ( vars && vars.i ) {
|
|
715
|
+
let scenario = scope.scenarios.get( scope.scenario_id );
|
|
716
|
+
if ( !scenario.interviews ) { scenario.interviews = {}; }
|
|
717
|
+
if ( !scenario.interviews[ vars.uid ] ) {
|
|
718
|
+
scenario.interviews[ vars.uid ] = vars;
|
|
719
|
+
log.debug({ code: `ALK0185`, context: `session_id_etc` },
|
|
720
|
+
`Saved new interview data. File path: ${ vars.i }. Session id: ${ vars.uid }.`
|
|
721
|
+
);
|
|
722
|
+
} else {
|
|
723
|
+
log.debug({ code: `ALK0186`, context: `session_id_etc` },
|
|
724
|
+
`Interview data already existed. File path: ${ vars.i }. Session id: ${ vars.uid }.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return vars;
|
|
728
|
+
} else {
|
|
729
|
+
log.debug({ code: `ALK0187`, context: `session_id_etc` },
|
|
730
|
+
`Found no interview data.`
|
|
731
|
+
);
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}, // Ends scope.set_interview_session_data()
|
|
735
|
+
|
|
685
736
|
getLoadData: async function ( scope ) {
|
|
686
737
|
/* Return elements that could load on the page, like #daMainQuestion
|
|
687
738
|
* and #da-retry. If there's a system error on the page, return its main text.
|
|
@@ -771,7 +822,7 @@ module.exports = {
|
|
|
771
822
|
if ( row_raw.length > 3 ) { more_or_less = `less`; }
|
|
772
823
|
let bad_table_msg = `Your Story Table should have ${ more_or_less } than`
|
|
773
824
|
+ ` ${ row_raw.length } columns. Take another look at Story Table documentation at`
|
|
774
|
-
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln
|
|
825
|
+
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/writing#story-table.`
|
|
775
826
|
reports.addToReport( scope, { type: `error`, code: `ALK0120`, value: bad_table_msg})
|
|
776
827
|
throw Error(bad_table_msg);
|
|
777
828
|
}
|
|
@@ -798,7 +849,7 @@ module.exports = {
|
|
|
798
849
|
let not_in_env_msg = `ALKiln could not find the GitHub secret "${ row.flags.secret_name }".`
|
|
799
850
|
+ ` Check for a typo and check your workflow file.`
|
|
800
851
|
+ ` Read more about using GitHub secrets in your workflow file at`
|
|
801
|
-
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln
|
|
852
|
+
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/writing#secrets.`
|
|
802
853
|
reports.addToReport( scope, { type: `error`, code: `ALK0121`, value: not_in_env_msg });
|
|
803
854
|
throw ReferenceError(not_in_env_msg);
|
|
804
855
|
}
|
|
@@ -824,11 +875,9 @@ module.exports = {
|
|
|
824
875
|
supported_table.push( result );
|
|
825
876
|
} // ends for every row
|
|
826
877
|
|
|
827
|
-
log.debug({
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
pre: `scope.normalizedTable():\n ${JSON.stringify( supported_table )}`
|
|
831
|
-
});
|
|
878
|
+
log.debug({ code: `ALK0122`, context: `table`, },
|
|
879
|
+
`scope.normalizedTable():`, supported_table
|
|
880
|
+
);
|
|
832
881
|
|
|
833
882
|
return supported_table;
|
|
834
883
|
}, // Ends scope.normalizeTable()
|
|
@@ -978,11 +1027,9 @@ module.exports = {
|
|
|
978
1027
|
fields.push( field_data );
|
|
979
1028
|
} // ends for every node
|
|
980
1029
|
|
|
981
|
-
log.debug({
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
pre: `page fields:\n${ JSON.stringify( fields )}`
|
|
985
|
-
});
|
|
1030
|
+
log.debug({ code: `ALK0124`, context: `page fields`, },
|
|
1031
|
+
fields
|
|
1032
|
+
);
|
|
986
1033
|
return fields;
|
|
987
1034
|
}, // Ends scope.getAllFields()
|
|
988
1035
|
|
|
@@ -1032,11 +1079,9 @@ module.exports = {
|
|
|
1032
1079
|
/* Return variable names and values combined in ways that can
|
|
1033
1080
|
* try to match table rows. Tables can have two properties:
|
|
1034
1081
|
* variable name and value. */
|
|
1035
|
-
log.debug({
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
pre: `scope.getPossibleTableRowMatchers() var_names: ${JSON.stringify(var_names)} values: ${JSON.stringify(values)}`
|
|
1039
|
-
});
|
|
1082
|
+
log.debug({ code: `ALK0126`, context: `matching`, },
|
|
1083
|
+
`scope.getPossibleTableRowMatchers() var_names:`, var_names, `values:`, values
|
|
1084
|
+
);
|
|
1040
1085
|
let possible_table_rows = [];
|
|
1041
1086
|
for ( let var_name of var_names ) {
|
|
1042
1087
|
// Inputs like textareas don't have values
|
|
@@ -1317,6 +1362,7 @@ module.exports = {
|
|
|
1317
1362
|
* setting fields, and an object for a DOM field, returns list of the
|
|
1318
1363
|
* rows that match the DOM field. That list of story table rows may be empty.
|
|
1319
1364
|
*
|
|
1365
|
+
* Using a `trigger` column is deprecated, but still used in many tests in the wild.
|
|
1320
1366
|
* For notes on trigger var, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger
|
|
1321
1367
|
*
|
|
1322
1368
|
* @param var_data {arr} - fields and the values they can be set to
|
|
@@ -1411,9 +1457,13 @@ module.exports = {
|
|
|
1411
1457
|
|
|
1412
1458
|
// field/page trigger name is needed but is missing.
|
|
1413
1459
|
if ( !page_trigger_name ) {
|
|
1414
|
-
let page_trigger_warning = `
|
|
1415
|
-
+ `
|
|
1416
|
-
+ `
|
|
1460
|
+
let page_trigger_warning = `You are using a 3-column Story Table. If possible, you should `
|
|
1461
|
+
+ `think about switching to the newer, easier, 2-column Story Table. That will probably `
|
|
1462
|
+
+ `get rid of this warning. Read more at `
|
|
1463
|
+
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/trouble#deprecated-story-tables. `
|
|
1464
|
+
+ `If you still want to use this 3-column format, here is more info about the warning: `
|
|
1465
|
+
+ `A field on this page uses an index variable or a generic object in a 3-column `
|
|
1466
|
+
+ `Story Table, but the page is missing the ALKiln "trigger" variable's name. See `
|
|
1417
1467
|
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#a-missing-trigger-variable.`;
|
|
1418
1468
|
reports.addToReport(scope, { type: `warning`, code: `ALK0127`, value: page_trigger_warning });
|
|
1419
1469
|
}
|
|
@@ -1430,7 +1480,7 @@ module.exports = {
|
|
|
1430
1480
|
|
|
1431
1481
|
// First, make the row data more readable to humans.
|
|
1432
1482
|
let row_as_story_table = reports.convertToOriginalStoryTableRow( scope, { row_data: row_that_matches_this_field });
|
|
1433
|
-
let row_trigger_warning = `This Story Table row needs a value in the "trigger" `
|
|
1483
|
+
let row_trigger_warning = `This 3-column Story Table row needs a value in the "trigger" `
|
|
1434
1484
|
+ `column, but it is empty. The test might get unexpected results. See `
|
|
1435
1485
|
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
|
|
1436
1486
|
+ `\nThe rows:\n${ row_as_story_table }`;
|
|
@@ -1467,7 +1517,7 @@ module.exports = {
|
|
|
1467
1517
|
|
|
1468
1518
|
let all_trigger_names_conflict_msg = `${ rows_that_match_field_names.length } `
|
|
1469
1519
|
+ `Story Table row(s) matched a field name, but didn't match a trigger `
|
|
1470
|
-
+ `variable.
|
|
1520
|
+
+ `variable. This 3-column Story Table might be missing a row or a trigger variable. `
|
|
1471
1521
|
+ `The test will have to ignore this field. See `
|
|
1472
1522
|
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
|
|
1473
1523
|
+ `\nThe row(s) data:\n${ conflicts_as_story_table_rows.join( `\n` )}`
|
|
@@ -1506,17 +1556,13 @@ module.exports = {
|
|
|
1506
1556
|
reports.addToReport(scope, { type: `warning`, code: `ALK0130`, value: multiple_matches_msg });
|
|
1507
1557
|
}
|
|
1508
1558
|
|
|
1509
|
-
log.debug({
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
pre: `final_matching_rows:\n${JSON.stringify( final_matching_rows )}`
|
|
1513
|
-
});
|
|
1559
|
+
log.debug({ code: `ALK0131`, context: `matching`, },
|
|
1560
|
+
`final_matching_rows:`, final_matching_rows
|
|
1561
|
+
);
|
|
1514
1562
|
if ( final_matching_rows.length == 0) {
|
|
1515
|
-
log.debug({
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
pre: `field:\n${JSON.stringify( field )}`
|
|
1519
|
-
});
|
|
1563
|
+
log.debug({ code: `ALK0132`, context: `matching`, },
|
|
1564
|
+
`0 matching rows. Field:`, field
|
|
1565
|
+
);
|
|
1520
1566
|
}
|
|
1521
1567
|
|
|
1522
1568
|
return final_matching_rows;
|
|
@@ -1582,7 +1628,7 @@ module.exports = {
|
|
|
1582
1628
|
catch(err){ elem.style.background = 'Khaki'; }
|
|
1583
1629
|
return elem.getAttribute('for') || elem.getAttribute('name');
|
|
1584
1630
|
});
|
|
1585
|
-
await
|
|
1631
|
+
await time.waitForTimeout( 400 );
|
|
1586
1632
|
}
|
|
1587
1633
|
|
|
1588
1634
|
// We use the last match. We wrote a warning for the user if there were
|
|
@@ -1602,11 +1648,9 @@ module.exports = {
|
|
|
1602
1648
|
|
|
1603
1649
|
get_handle_from_field: async function ( scope, { field }) {
|
|
1604
1650
|
/** Get the element's puppeteer handle using the field's selector. */
|
|
1605
|
-
log.debug({
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
pre: `scope.get_handle_from_field() field.selector: ${JSON.stringify(field.selector)}`
|
|
1609
|
-
});
|
|
1651
|
+
log.debug({ code: `ALK0133`, },
|
|
1652
|
+
`scope.get_handle_from_field() field.selector:`, field.selector
|
|
1653
|
+
);
|
|
1610
1654
|
let handle = await scope.page.evaluateHandle(( selector ) => {
|
|
1611
1655
|
let elem = document.querySelector( selector );
|
|
1612
1656
|
// If the element has a sibling that's a label, we need to
|
|
@@ -1805,7 +1849,7 @@ module.exports = {
|
|
|
1805
1849
|
});
|
|
1806
1850
|
if ( type === `radio` || type === `checkbox` ) {
|
|
1807
1851
|
|
|
1808
|
-
let [label] = await handle
|
|
1852
|
+
let [label] = await handle.$$(`xpath/.//following-sibling::label`);
|
|
1809
1853
|
|
|
1810
1854
|
if ( session_vars.get_debug() ) {
|
|
1811
1855
|
if ( answer.toLowerCase() === 'false' ) { await label.evaluate( elem => { elem.style.background = 'tomato'; }); }
|
|
@@ -2118,7 +2162,9 @@ module.exports = {
|
|
|
2118
2162
|
* @param ensure_all_vars_used {bool} [false] = Default false. If true, errors if any
|
|
2119
2163
|
* object in var_data is not used during running of this function.
|
|
2120
2164
|
*/
|
|
2121
|
-
log.debug({
|
|
2165
|
+
log.debug({ code: `ALK0221`, before: `~~~~~~~~~~~~~~\n`, },
|
|
2166
|
+
`scope.setFields()`
|
|
2167
|
+
);
|
|
2122
2168
|
|
|
2123
2169
|
// Only record the page id once for each page to which we navigate
|
|
2124
2170
|
let { id } = await scope.examinePageID( scope, 'none to match' );
|
|
@@ -2162,13 +2208,12 @@ module.exports = {
|
|
|
2162
2208
|
// Accumulate number of times this row was used. Esp important for `.target_number`
|
|
2163
2209
|
await scope.increment_row_use( scope, { row: row_used });
|
|
2164
2210
|
|
|
2165
|
-
|
|
2166
|
-
|
|
2211
|
+
log.stdout({}, `\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
|
|
2167
2212
|
}
|
|
2168
2213
|
} // ends for every non-submit-button field on the page
|
|
2169
2214
|
} // ends while fields are still being set
|
|
2170
2215
|
|
|
2171
|
-
if ( session_vars.get_debug() ) { await
|
|
2216
|
+
if ( session_vars.get_debug() ) { await time.waitForTimeout(2000); }
|
|
2172
2217
|
|
|
2173
2218
|
// Press a continue button with a story table variable name if it exists
|
|
2174
2219
|
let did_navigate = false;
|
|
@@ -2208,7 +2253,7 @@ module.exports = {
|
|
|
2208
2253
|
// }
|
|
2209
2254
|
|
|
2210
2255
|
did_navigate = true;
|
|
2211
|
-
|
|
2256
|
+
log.stdout({}, `\x1b[36m${ 'v' }\x1b[0m`); // pressed button successfully
|
|
2212
2257
|
break;
|
|
2213
2258
|
|
|
2214
2259
|
} // ends if row_used
|
|
@@ -2255,7 +2300,7 @@ module.exports = {
|
|
|
2255
2300
|
// }
|
|
2256
2301
|
|
|
2257
2302
|
did_navigate = true;
|
|
2258
|
-
|
|
2303
|
+
log.stdout({}, `\x1b[36m${ '>' }\x1b[0m`); // continued successfully
|
|
2259
2304
|
} // ends if !did_navigate && ensure_navigation
|
|
2260
2305
|
|
|
2261
2306
|
if ( did_navigate && scope.check_all_for_a11y ) {
|
|
@@ -2516,7 +2561,7 @@ module.exports = {
|
|
|
2516
2561
|
|
|
2517
2562
|
// Attempt to solve weird race condition issues where page has not properly continued
|
|
2518
2563
|
while ( curr_url === prev_url && !error.was_found && !timedOut ) {
|
|
2519
|
-
await
|
|
2564
|
+
await time.waitForTimeout( 50 );
|
|
2520
2565
|
curr_url = await scope.page.url();
|
|
2521
2566
|
error = await scope.checkForError( scope );
|
|
2522
2567
|
}
|
|
@@ -2537,10 +2582,10 @@ module.exports = {
|
|
|
2537
2582
|
|
|
2538
2583
|
// All results are lists to match `$x`
|
|
2539
2584
|
let possible_errors = {
|
|
2540
|
-
complex_user_error: await scope.page.$$(
|
|
2585
|
+
complex_user_error: await scope.page.$$(`.alert-danger`),
|
|
2541
2586
|
// The below does not work if there's not a space between ':' and 'none'
|
|
2542
|
-
simple_user_error: await scope.page
|
|
2543
|
-
system_error: await scope.page
|
|
2587
|
+
simple_user_error: await scope.page.$$(`xpath/.//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`),
|
|
2588
|
+
system_error: await scope.page.$$(`xpath/.//meta[@content="docassemble: Error"]` ),
|
|
2544
2589
|
};
|
|
2545
2590
|
|
|
2546
2591
|
let error_handlers = {};
|
|
@@ -2552,7 +2597,7 @@ module.exports = {
|
|
|
2552
2597
|
// Also, if you're debugging and you close the window early, the process will still
|
|
2553
2598
|
// take this long to exit after the tests finish. Not sure how to account for that.
|
|
2554
2599
|
if ( session_vars.get_debug() ) {
|
|
2555
|
-
await
|
|
2600
|
+
await time.waitForTimeout( ( scope.timeout - (1 * 1000)) );
|
|
2556
2601
|
}
|
|
2557
2602
|
was_found = true;
|
|
2558
2603
|
error_handlers[ error_key ] = possible_errors[ error_key ][0];
|
|
@@ -2657,7 +2702,7 @@ module.exports = {
|
|
|
2657
2702
|
}, // Ends scope.steps.add_json_vars_to_report()
|
|
2658
2703
|
|
|
2659
2704
|
link_works: async ( scope, linkText ) => {
|
|
2660
|
-
let [link] = await scope.page
|
|
2705
|
+
let [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${ linkText }")]`);
|
|
2661
2706
|
let prop_obj = await link.getProperty( 'href' );
|
|
2662
2707
|
let actual_url = await prop_obj.jsonValue();
|
|
2663
2708
|
|
|
@@ -2715,13 +2760,13 @@ module.exports = {
|
|
|
2715
2760
|
/** Tap a link that doesn't navigate. Depends on the language of the text.
|
|
2716
2761
|
* TODO: This may be broken at this point.
|
|
2717
2762
|
* */
|
|
2718
|
-
const [link] = await scope.page
|
|
2763
|
+
const [link] = await scope.page.$$(`xpath/.//a[contains(text(), "${phrase}")]`);
|
|
2719
2764
|
|
|
2720
2765
|
let msg = `The term "${ phrase }" seems to be missing.`;
|
|
2721
2766
|
if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0151`, value: msg }); }
|
|
2722
2767
|
expect( link, msg ).to.exist;
|
|
2723
2768
|
await scope.tapElementAndStayOnPage(scope, { elem: link });
|
|
2724
|
-
await
|
|
2769
|
+
await time.waitForTimeout(10000);
|
|
2725
2770
|
}, // Ends scope.steps.tap_term()
|
|
2726
2771
|
|
|
2727
2772
|
sign: async ( scope, name ) => {
|
|
@@ -2740,7 +2785,7 @@ module.exports = {
|
|
|
2740
2785
|
* TODO: Properly wait for download to complete. See notes in
|
|
2741
2786
|
* scope.js scope.detectDownloadComplete()
|
|
2742
2787
|
*/
|
|
2743
|
-
let [elem] = await scope.page
|
|
2788
|
+
let [elem] = await scope.page.$$(`xpath/.//a[contains(@href, "${ filename }")]`);
|
|
2744
2789
|
|
|
2745
2790
|
let msg = `"${ filename }" seems to be missing. Cannot find a link to that document.`;
|
|
2746
2791
|
if ( !elem ) { reports.addToReport(scope, { type: `error`, code: `ALK0152`, value: msg }); }
|
|
@@ -2893,7 +2938,9 @@ module.exports = {
|
|
|
2893
2938
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2894
2939
|
}, // Ends scope.steps.set_al_address()
|
|
2895
2940
|
|
|
2896
|
-
sign_in: async function ( scope, {
|
|
2941
|
+
sign_in: async function ( scope, {
|
|
2942
|
+
email_secret_name, password_secret_name, api_key_secret_name
|
|
2943
|
+
}) {
|
|
2897
2944
|
/** Allow the developer to log a user into their server using GitHub
|
|
2898
2945
|
* secrets to authenticate.
|
|
2899
2946
|
*/
|
|
@@ -2946,24 +2993,118 @@ module.exports = {
|
|
|
2946
2993
|
if ( password === undefined ) {
|
|
2947
2994
|
reports.addToReport( scope, { type: `error`, code: `ALK0162`, value: password_msg })
|
|
2948
2995
|
}
|
|
2996
|
+
// Save API key to clean up interviews later. Nice to have, not critical
|
|
2997
|
+
if ( api_key_secret_name ) {
|
|
2998
|
+
let api_key = process.env[ api_key_secret_name ];
|
|
2999
|
+
let api_key_msg = `The API key in the variable "${ api_key_secret_name }" is missing. Check for a typo in your test, workflow file, GitHub secret, or server config.`
|
|
3000
|
+
if ( api_key === undefined ) {
|
|
3001
|
+
reports.addToReport( scope, { type: `warning`, code: `ALK0199`, value: api_key_msg });
|
|
3002
|
+
} else if ( api_key === `` ) {
|
|
3003
|
+
reports.addToReport( scope, { type: `warning`, code: `ALK0200`,
|
|
3004
|
+
value: `ALKiln found the API key in the variable "${ api_key_secret_name }", but it was empty. It had no text in it. Edit the value of the GitHub secret or server config variable called "${ api_key_secret_name }".` });
|
|
3005
|
+
} else {
|
|
3006
|
+
let access_info = await da_api.get_api_key_access_status({ api_key });
|
|
3007
|
+
if ( !access_info.has_auth ) {
|
|
3008
|
+
reports.addToReport( scope, { type: `warning`, code: `ALK0201`,
|
|
3009
|
+
value: `The server rejected the API key in the variable "${ api_key_secret_name }". Does it exist on this server? Was it deleted somehow? You can always make a new one.` });
|
|
3010
|
+
}
|
|
3011
|
+
// Even if it doesn't have access, give it a shot anyway.
|
|
3012
|
+
// Maybe we got something wrong.
|
|
3013
|
+
scope.scenarios.get( scope.scenario_id ).api_keys.push( api_key );
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
2949
3016
|
|
|
2950
3017
|
expect( email, email_msg ).to.not.equal( undefined );
|
|
2951
3018
|
expect( password, password_msg ).to.not.equal( undefined );
|
|
2952
3019
|
|
|
2953
|
-
// Try to
|
|
3020
|
+
// Try to enter the information
|
|
2954
3021
|
await scope.page.type( `#email`, email );
|
|
2955
3022
|
await scope.page.type( `#password`, password );
|
|
2956
|
-
let elem = await scope.page.$( `button[
|
|
2957
|
-
await scope.
|
|
3023
|
+
let elem = await scope.page.$( `button[type="submit"]` );
|
|
3024
|
+
await scope.guard_against_missing_tap_element(scope, { elem });
|
|
2958
3025
|
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3026
|
+
// Submit and see what happens
|
|
3027
|
+
let winner = await scope.steps.race_sign_in_navigation( scope, { elem });
|
|
3028
|
+
// Add the result to the report and possibly throw errors
|
|
3029
|
+
if ( winner[0] === `success` ) {
|
|
3030
|
+
reports.addToReport( scope, {
|
|
3031
|
+
type: `row info`, code: `ALK0163`,
|
|
3032
|
+
value: `Successfully signed into ${ login_url }`
|
|
3033
|
+
});
|
|
3034
|
+
await scope.afterStep(scope, {waitForShowIf: false});
|
|
3035
|
+
} else if ( winner[0] === `failure` ) {
|
|
3036
|
+
let error_msg = reports.addToReport( scope, {
|
|
3037
|
+
type: `error`, code: `ALK0209`,
|
|
3038
|
+
value: `Failed to sign into ${ login_url }. Make sure you followed the instructions at https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#sign-in.`
|
|
3039
|
+
});
|
|
3040
|
+
throw new Error( error_msg );
|
|
3041
|
+
} else if ( winner[0] === `error` ) {
|
|
3042
|
+
let error_msg = reports.addToReport( scope, {
|
|
3043
|
+
type: `error`, code: `ALK0210`,
|
|
3044
|
+
value: `Error on sign in page at ${ login_url }. Try this by hand and then run the test again.`
|
|
3045
|
+
});
|
|
3046
|
+
throw new Error( error_msg );
|
|
3047
|
+
}
|
|
3048
|
+
}, // Ends scope.steps.sign_in()
|
|
3049
|
+
|
|
3050
|
+
race_sign_in_navigation: async function ( scope, { elem }) {
|
|
3051
|
+
/** Wait for sign in navigation success or failure, or system error. */
|
|
3052
|
+
|
|
3053
|
+
// After everything, clean up incomplete promises
|
|
3054
|
+
const controller = new AbortController;
|
|
3055
|
+
|
|
3056
|
+
// Redirect
|
|
3057
|
+
let redirect_promise = scope.page.waitForResponse(function ( response ) {
|
|
3058
|
+
return response.status() === 302;
|
|
3059
|
+
}, { signal: controller.signal })
|
|
3060
|
+
.catch(( catch_info ) => {
|
|
3061
|
+
log.debug({ code: `ALK0204`, context: `nav`, },
|
|
3062
|
+
`302 sign-in wait catch error:`, catch_info.name, catch_info,
|
|
3063
|
+
);
|
|
2963
3064
|
});
|
|
2964
3065
|
|
|
2965
|
-
|
|
2966
|
-
|
|
3066
|
+
// Invalid credentials, no navigation, no sign in
|
|
3067
|
+
let wrong_sign_in_promise = scope.page.waitForResponse(function ( response ) {
|
|
3068
|
+
return response.status() >= 200 && response.status() < 300 && response.url().includes(`/sign-in`);
|
|
3069
|
+
}, { signal: controller.signal })
|
|
3070
|
+
.catch(( catch_info ) => {
|
|
3071
|
+
log.debug({ code: `ALK0205`, context: `nav`, },
|
|
3072
|
+
`200s sign-in wait catch error:`, catch_info.name, catch_info,
|
|
3073
|
+
);
|
|
3074
|
+
});
|
|
3075
|
+
|
|
3076
|
+
// System error
|
|
3077
|
+
let error_promise = scope.page.waitForResponse(function ( response ) {
|
|
3078
|
+
return response.status() >= 500 && response.status() < 600;
|
|
3079
|
+
}, { signal: controller.signal })
|
|
3080
|
+
.catch(( catch_info ) => {
|
|
3081
|
+
log.debug({ code: `ALK0206`, context: `nav`, },
|
|
3082
|
+
`500s sign-in wait catch error:`, catch_info.name, catch_info,
|
|
3083
|
+
);
|
|
3084
|
+
});
|
|
3085
|
+
|
|
3086
|
+
let click_promise = elem.click(); // MUST complete
|
|
3087
|
+
const winner = await Promise.race([
|
|
3088
|
+
Promise.all([ `success`, click_promise, redirect_promise ]),
|
|
3089
|
+
Promise.all([ `failure`, click_promise, wrong_sign_in_promise ]),
|
|
3090
|
+
Promise.all([ `error`, click_promise, error_promise ]),
|
|
3091
|
+
]).catch(function ( error ) {
|
|
3092
|
+
let error_msg = reports.addToReport( scope, {
|
|
3093
|
+
type: `error`, code: `ALK0207`,
|
|
3094
|
+
value: `Unknown error waiting for results during sign in at ${ login_url }.`
|
|
3095
|
+
});
|
|
3096
|
+
throw new Error( error );
|
|
3097
|
+
});
|
|
3098
|
+
|
|
3099
|
+
// Clean up unresolved promises
|
|
3100
|
+
controller.abort();
|
|
3101
|
+
|
|
3102
|
+
log.debug({ code: `ALK0208`, context: `nav` },
|
|
3103
|
+
`Sign-in winner:`, winner
|
|
3104
|
+
);
|
|
3105
|
+
|
|
3106
|
+
return winner;
|
|
3107
|
+
},
|
|
2967
3108
|
|
|
2968
3109
|
set_random_page_vars: async ( scope ) => {
|
|
2969
3110
|
/** Answer inputs randomly. */
|
|
@@ -2983,7 +3124,7 @@ module.exports = {
|
|
|
2983
3124
|
} else {
|
|
2984
3125
|
// Try to give a random answer for that element tag
|
|
2985
3126
|
await scope.set_random_input_for[ field.tag ]( scope, { field: field });
|
|
2986
|
-
|
|
3127
|
+
log.stdout({}, `\x1b[36m${ '*' }\x1b[0m`); // assumes var was set if no error occurred
|
|
2987
3128
|
}
|
|
2988
3129
|
|
|
2989
3130
|
} // End for all page fields
|
|
@@ -3019,11 +3160,10 @@ module.exports = {
|
|
|
3019
3160
|
}
|
|
3020
3161
|
if (field == null) {
|
|
3021
3162
|
// Just skip it? Shouldn't happen but has before. Dig in more
|
|
3022
|
-
log.debug({
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
});
|
|
3163
|
+
log.debug({ code: `ALK0164`, context: `random`, },
|
|
3164
|
+
`A random button field choice was null. These were the fields to choose from:`,
|
|
3165
|
+
fields
|
|
3166
|
+
);
|
|
3027
3167
|
return;
|
|
3028
3168
|
}
|
|
3029
3169
|
let handle = await scope.get_handle_from_field( scope, { field: field });
|