@suffolklitlab/alkiln 4.0.0 → 4.2.1-custom-setup-timeout.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test_tests.yml +3 -0
- package/CHANGELOG.md +10 -3
- package/action.yml +6 -0
- package/lib/docassemble/docassemble_api_interface.js +46 -19
- package/lib/scope.js +78 -21
- package/lib/steps.js +21 -14
- package/lib/utils/langs.js +1 -1
- package/lib/utils/session_vars.js +12 -2
- package/package.json +1 -1
- package/tests/features/interactive_steps.feature +18 -0
- package/tests/features/reports.feature +12 -0
|
@@ -31,6 +31,9 @@ jobs:
|
|
|
31
31
|
REPO_URL: https://github.com/plocket/docassemble-ALAutomatedTestingTests
|
|
32
32
|
USER1_EMAIL: ${{ secrets.USER1_EMAIL }}
|
|
33
33
|
USER1_PASSWORD: ${{ secrets.USER1_PASSWORD }}
|
|
34
|
+
SECRET_VAR1: secret-var1-value
|
|
35
|
+
SECRET_VAR2: secret-var2-value
|
|
36
|
+
SECRET_FOR_MISSING_FIELD: secret for missing field
|
|
34
37
|
|
|
35
38
|
name: Run Puppeteer tests
|
|
36
39
|
steps:
|
package/CHANGELOG.md
CHANGED
|
@@ -38,7 +38,13 @@ Format:
|
|
|
38
38
|
### Security
|
|
39
39
|
-
|
|
40
40
|
-->
|
|
41
|
-
|
|
41
|
+
## [Unreleased]
|
|
42
|
+
### Added
|
|
43
|
+
- GitHub environment variable MAX_SECONDS_FOR_SETUP to set a custom maximum time for setup and takedown for packages that take a long time to load.
|
|
44
|
+
|
|
45
|
+
## [4.1.1] - 2022-04-01
|
|
46
|
+
### Added
|
|
47
|
+
- Step to set vars to secret variables. That is, using environment variables while hiding the names of those variables in the report, errors, and console logs.
|
|
42
48
|
|
|
43
49
|
## [4.0.0] - 2022-03-30
|
|
44
50
|
### Added
|
|
@@ -57,10 +63,11 @@ Format:
|
|
|
57
63
|
- BREAKING: Update action.yml node to v17
|
|
58
64
|
- BREAKING: Use alkiln v4 package.json we create during the test run in `action.yml` (as discussed in #489)
|
|
59
65
|
- BREAKING: Use API key to access da server, create projects, pull code, delete projects, and check for server restart.
|
|
66
|
+
- BREAKING: Throw an error for uploading nonexistent file, changed to error instead of warning in reports.
|
|
67
|
+
- Update test setup interview to v4 of ALKiln.
|
|
60
68
|
- Remove and ignore package-lock.json so that our tests will behave more like our users' tests
|
|
61
69
|
- Add warning in steps.js for name input with too many parts. Add test in reports.
|
|
62
|
-
- fix typo in the report
|
|
63
|
-
- Throw an error for uploading nonexistent file, changed to error instead of warning in reports.
|
|
70
|
+
- fix typo in the report
|
|
64
71
|
- Allow a developer to wait as a first Step. See #387.
|
|
65
72
|
|
|
66
73
|
<!-- ## [3.0.1-peer-deps.1] - 2021-12-07 -->
|
package/action.yml
CHANGED
|
@@ -28,6 +28,10 @@ inputs:
|
|
|
28
28
|
description: 'Version of ALKiln to get from npm.'
|
|
29
29
|
required: false
|
|
30
30
|
default: '^4.0.0'
|
|
31
|
+
MAX_SECONDS_FOR_SETUP:
|
|
32
|
+
description: 'Amount of time to give the Project to upload your package's GitHub code.'
|
|
33
|
+
required: false
|
|
34
|
+
default: '120'
|
|
31
35
|
|
|
32
36
|
runs:
|
|
33
37
|
using: "composite"
|
|
@@ -54,9 +58,11 @@ runs:
|
|
|
54
58
|
echo "PLAYGROUND_ID=${{ inputs.PLAYGROUND_ID }}" >> $GITHUB_ENV
|
|
55
59
|
echo "DOCASSEMBLE_DEVELOPER_API_KEY=${{ inputs.DOCASSEMBLE_DEVELOPER_API_KEY }}" >> $GITHUB_ENV
|
|
56
60
|
echo "ALKILN_VERSION=${{ inputs.ALKILN_VERSION }}" >> $GITHUB_ENV
|
|
61
|
+
echo "MAX_SECONDS_FOR_SETUP=${{ inputs.MAX_SECONDS_FOR_SETUP }}" >> $GITHUB_ENV
|
|
57
62
|
shell: bash
|
|
58
63
|
- name: "ALKiln: confirm info"
|
|
59
64
|
run: echo -e "\nRepo is $REPO_URL\nBranch ref is $BRANCH_PATH\nManually added languages are $EXTRA_LANGUAGES\nAll other data is secret\n"
|
|
65
|
+
run: echo -e "\nALKiln version is $ALKILN_VERSION\nRepo is $REPO_URL\nBranch ref is $BRANCH_PATH\nMAX_SECONDS_FOR_SETUP is $MAX_SECONDS_FOR_SETUP\nManually added languages are $EXTRA_LANGUAGES\n"
|
|
60
66
|
shell: bash
|
|
61
67
|
|
|
62
68
|
# This doesn't use an already established package-lock.json and that
|
|
@@ -59,6 +59,10 @@ let da_i = {};
|
|
|
59
59
|
module.exports = da_i;
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
// Constants
|
|
63
|
+
const DEFAULT_MAX_REQUEST_MS = session_vars.get_setup_timeout_ms();
|
|
64
|
+
|
|
65
|
+
|
|
62
66
|
let is_timeout_error = function( error ) {
|
|
63
67
|
/** Return whether the error is an axios timeout error.
|
|
64
68
|
* See:
|
|
@@ -84,7 +88,7 @@ da_i.throw_an_error_if_server_is_not_responding = async function ( options ) {
|
|
|
84
88
|
*/
|
|
85
89
|
|
|
86
90
|
// Defaults
|
|
87
|
-
let { timeout } = options || { timeout:
|
|
91
|
+
let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
88
92
|
log.info({ pre: `Waiting for the docassemble server to respond. Will wait for ${ timeout/1000 } seconds at most.` });
|
|
89
93
|
|
|
90
94
|
try {
|
|
@@ -123,7 +127,7 @@ da_i.get_dev_id = async function ( dev_id_options ) {
|
|
|
123
127
|
* See https://docassemble.org/docs/api.html#user. */
|
|
124
128
|
|
|
125
129
|
// Default timeouts
|
|
126
|
-
let { timeout } = dev_id_options || { timeout:
|
|
130
|
+
let { timeout } = dev_id_options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
127
131
|
let response = await _da_REST.get_dev_id( timeout );
|
|
128
132
|
return response.data.id;
|
|
129
133
|
}; // Ends da_i.get_dev_id();
|
|
@@ -137,7 +141,7 @@ da_i.validate_api_key = async function ( api_key_options ) {
|
|
|
137
141
|
log.info({ pre: `Checking the testing account's API key.` });
|
|
138
142
|
|
|
139
143
|
// Default timeouts
|
|
140
|
-
api_key_options = api_key_options || { timeout:
|
|
144
|
+
api_key_options = api_key_options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
141
145
|
|
|
142
146
|
try {
|
|
143
147
|
|
|
@@ -174,8 +178,13 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
174
178
|
/** Create a docassemble Project on the docassemble server's account.
|
|
175
179
|
* Loop through potential project names until we find one that
|
|
176
180
|
* isn't already taken and then use that name to create the Project. */
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
|
|
182
|
+
// Defaults
|
|
183
|
+
creation_options = creation_options || {};
|
|
184
|
+
let default_opts = { timeout: DEFAULT_MAX_REQUEST_MS, max_tries: 15, wait_seconds: 5 };
|
|
185
|
+
let timeout = creation_options.timeout || default_opts.timeout;
|
|
186
|
+
let max_tries = creation_options.max_tries || default_opts.max_tries;
|
|
187
|
+
let wait_seconds = creation_options.wait_seconds || default_opts.wait_seconds;
|
|
179
188
|
|
|
180
189
|
let project_name = null;
|
|
181
190
|
let name_incrementor = 0;
|
|
@@ -229,6 +238,7 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
229
238
|
} else {
|
|
230
239
|
|
|
231
240
|
// If it's a timeout error, server is busy. Try again after waiting.
|
|
241
|
+
// Why are we using `max_tries` for timeout error as well?
|
|
232
242
|
if ( is_timeout_error( error ) && name_incrementor < max_tries ) {
|
|
233
243
|
log.info({ pre: `The docassemble server was busy when we tried to `
|
|
234
244
|
+ `create the Project. Attempt ${ name_incrementor } of `
|
|
@@ -243,7 +253,7 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
243
253
|
throw error;
|
|
244
254
|
|
|
245
255
|
} // ends type of non-response error
|
|
246
|
-
} // ends type of
|
|
256
|
+
} // ends type of response error
|
|
247
257
|
} // ends try
|
|
248
258
|
} // ends while
|
|
249
259
|
}; // Ends da_i.loop_to_create_project()
|
|
@@ -256,7 +266,7 @@ da_i.pull = async function ( pull_options ) {
|
|
|
256
266
|
log.info({ pre: `Trying to pull code from the "${ session_vars.get_branch_name() }" branch.` });
|
|
257
267
|
|
|
258
268
|
// Default timeouts
|
|
259
|
-
let { timeout } = pull_options || { timeout:
|
|
269
|
+
let { timeout } = pull_options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
260
270
|
|
|
261
271
|
try {
|
|
262
272
|
|
|
@@ -290,7 +300,7 @@ da_i.has_task_finished = async function ( task_id, options ) {
|
|
|
290
300
|
* See https://docassemble.org/docs/api.html#restart_status. */
|
|
291
301
|
|
|
292
302
|
// Defaults
|
|
293
|
-
let { timeout } = options || { timeout:
|
|
303
|
+
let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
294
304
|
|
|
295
305
|
try {
|
|
296
306
|
|
|
@@ -340,8 +350,17 @@ da_i.wait_for_server_to_restart = async ( task_id, options ) => {
|
|
|
340
350
|
+ `"${ task_id }". A task_id is usually created for a package `
|
|
341
351
|
+ `with a module. It can take a few minutes to finish uploading.` });
|
|
342
352
|
|
|
343
|
-
//
|
|
344
|
-
|
|
353
|
+
// Base default
|
|
354
|
+
options = options || {};
|
|
355
|
+
// Default timeout
|
|
356
|
+
let default_timeout = DEFAULT_MAX_REQUEST_MS;
|
|
357
|
+
let timeout = options.timeout || default_timeout;
|
|
358
|
+
// Other defaults
|
|
359
|
+
let default_wait_per_loop = 5 * 1000;
|
|
360
|
+
let default_max_loops = timeout/default_wait_per_loop;
|
|
361
|
+
// Actual values
|
|
362
|
+
let wait_ms_per_loop = options.wait_ms_per_loop || default_wait_per_loop;
|
|
363
|
+
let max_loops = options.max_loops || default_max_loops;
|
|
345
364
|
|
|
346
365
|
// Flags for stopping the `while` loop
|
|
347
366
|
let num_tries = 0;
|
|
@@ -350,22 +369,28 @@ da_i.wait_for_server_to_restart = async ( task_id, options ) => {
|
|
|
350
369
|
// Why loop instead of one long timeout? Because it might not just hang.
|
|
351
370
|
// We might get a return value that's just `waiting`, meaning the
|
|
352
371
|
// task is not done.
|
|
353
|
-
// - [ ] How many tries/time do we give this? Do we allow customization of those values?
|
|
354
372
|
while ( !done_restarting && num_tries <= max_loops ) {
|
|
355
373
|
num_tries++;
|
|
356
|
-
|
|
357
|
-
done_restarting = await da_i.has_task_finished( task_id );
|
|
374
|
+
// Wait only a short time for the task to finish, not the full default timeout
|
|
375
|
+
done_restarting = await da_i.has_task_finished( task_id, { timeout: 1 * 1000 } );
|
|
358
376
|
if ( !done_restarting ) {
|
|
359
377
|
if ( num_tries < max_loops ) {
|
|
360
|
-
|
|
361
|
-
|
|
378
|
+
// Show a slightly different message after a number of tries to reassure
|
|
379
|
+
// users we're still paying attention
|
|
380
|
+
let msg_start = `Not yet complete`;
|
|
381
|
+
if ( num_tries > 3 ) { msg_start = `Still not complete`; }
|
|
382
|
+
|
|
383
|
+
// When the server is busy, the task timeout causes this loop to actually
|
|
384
|
+
// take 6 seconds to complete, not 5, but I'm not sure how to make that clear
|
|
385
|
+
log.info({ pre: `${ msg_start }. Check ${ num_tries } of ${ max_loops }. `
|
|
386
|
+
+ `Will check again in ${ wait_ms_per_loop/1000 } seconds.`
|
|
362
387
|
});
|
|
363
|
-
await time.waitForTimeout(
|
|
388
|
+
await time.waitForTimeout( wait_ms_per_loop );
|
|
364
389
|
|
|
365
390
|
} else {
|
|
366
391
|
// This is our last round, let's wrap it up
|
|
367
392
|
let server_msg = `ALKiln setup: It seems like the docassemble server `
|
|
368
|
-
+ `didn't finish uploading the code after ${ max_loops *
|
|
393
|
+
+ `didn't finish uploading the code after ${ max_loops * (wait_ms_per_loop/1000) } `
|
|
369
394
|
+ `seconds. Some things you can try: check your server is up, try `
|
|
370
395
|
+ `re-running the test, and try pulling the package manually `
|
|
371
396
|
+ `into the playground.`;
|
|
@@ -385,7 +410,7 @@ da_i.delete_project = async function ( delete_options ) {
|
|
|
385
410
|
/** Delete a project on the user's da server. */
|
|
386
411
|
|
|
387
412
|
// Defaults
|
|
388
|
-
let { timeout } = delete_options || { timeout:
|
|
413
|
+
let { timeout } = delete_options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
389
414
|
|
|
390
415
|
let project_name = session_vars.get_project_name()
|
|
391
416
|
log.info({ pre: `Trying to delete Project ${ project_name }` });
|
|
@@ -396,7 +421,9 @@ da_i.delete_project = async function ( delete_options ) {
|
|
|
396
421
|
log.info({ pre: `Deleted Project "${ project_name }"` });
|
|
397
422
|
|
|
398
423
|
} catch ( error ) {
|
|
399
|
-
//
|
|
424
|
+
// TODO: Add more useful error messages, like project doesn't exist. The devs
|
|
425
|
+
// shouldn't be deleting projects by hand, but it's still very possible they would
|
|
426
|
+
// Status 400
|
|
400
427
|
log.error({ pre: `Ran into an error when deleting Project "${ project_name }"` });
|
|
401
428
|
log.debug({ pre: `See https://docassemble.org/docs/api.html#playground_delete_project.` });
|
|
402
429
|
throw error;
|
package/lib/scope.js
CHANGED
|
@@ -22,6 +22,30 @@ let escape_regex = function( to_escape ) {
|
|
|
22
22
|
return to_escape.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
let get_original_row = function (row) {
|
|
26
|
+
/* `row` might be a row, a source row, or an original row. */
|
|
27
|
+
|
|
28
|
+
// Until we flatten row data a litte, we need to find the `orignal` row, which has the flags
|
|
29
|
+
let source_row = row.source || row;
|
|
30
|
+
let original = source_row.original || source_row;
|
|
31
|
+
return original;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let printable_var_value = function(row) {
|
|
35
|
+
/* `row` might be a row, a source row, or an original row. */
|
|
36
|
+
let original = get_original_row(row);
|
|
37
|
+
if ( is_secret_var(original) ) {
|
|
38
|
+
return original.flags.secret_name;
|
|
39
|
+
}
|
|
40
|
+
return row.value;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let is_secret_var = function(row) {
|
|
44
|
+
/* `row` might be a row, a source row, or an original row. */
|
|
45
|
+
let original = get_original_row(row);
|
|
46
|
+
return original.flags !== undefined && original.flags.secret_name !== undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
let proxies_regex = /(\bx\.)|(\bx\[)|(\[[ijklmn]\])/g;
|
|
26
50
|
|
|
27
51
|
module.exports = {
|
|
@@ -63,7 +87,7 @@ module.exports = {
|
|
|
63
87
|
}, // Ends scope.setReportScenarioStatus()
|
|
64
88
|
|
|
65
89
|
convertToOriginalStoryTableRow: async function( scope, { row_data }) {
|
|
66
|
-
/* Returns the original data of row into a string
|
|
90
|
+
/* Returns the original data of row into a string formatted as a story table row.
|
|
67
91
|
* Should this show the actual row data that was used instead? That could
|
|
68
92
|
* be pretty confusing to most devs. */
|
|
69
93
|
let source_row = row_data.source || row_data;
|
|
@@ -113,7 +137,7 @@ module.exports = {
|
|
|
113
137
|
}, // Ends scope.addReportHeading()
|
|
114
138
|
|
|
115
139
|
getPrintableReport: async function( scope ) {
|
|
116
|
-
/* Return a string
|
|
140
|
+
/* Return a string generated from test report data. */
|
|
117
141
|
let report = `Assembly Line Kiln Automated Testing Report - ${ (new Date()).toUTCString() }\n`;
|
|
118
142
|
|
|
119
143
|
// First list failed scenarios, then non-passed scenarios, then passed scenarios
|
|
@@ -137,7 +161,7 @@ module.exports = {
|
|
|
137
161
|
}, // Ends scope.getPrintableReport()
|
|
138
162
|
|
|
139
163
|
getPrintableScenario: async function( scope, { scenario }) {
|
|
140
|
-
/* Return a string
|
|
164
|
+
/* Return a string generated from scenario list of report items. */
|
|
141
165
|
let [ scenario_id, data ] = scenario;
|
|
142
166
|
let report = ``;
|
|
143
167
|
for ( let line of data.lines ) {
|
|
@@ -391,7 +415,7 @@ module.exports = {
|
|
|
391
415
|
scope.page.waitForSelector(`fieldset.da-field-buttons button[type="submit"]`), // other pages (this is the most consistent way)
|
|
392
416
|
scope.page.waitForSelector(`fieldset .dasigsave`), // signature page
|
|
393
417
|
]);
|
|
394
|
-
|
|
418
|
+
await elem.evaluate( el => { return el.className });
|
|
395
419
|
// Waits for navigation or user error
|
|
396
420
|
await scope.tapElement( scope, elem );
|
|
397
421
|
},
|
|
@@ -414,7 +438,7 @@ module.exports = {
|
|
|
414
438
|
await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout })
|
|
415
439
|
|
|
416
440
|
let load_result = await scope.getLoadData( scope );
|
|
417
|
-
// Errors, or reaching timeout, causes an
|
|
441
|
+
// Errors, or reaching timeout, causes an exception and is shown to the developer at the end
|
|
418
442
|
if ( load_result.error ) {
|
|
419
443
|
await scope.addToReport(scope, { type: `error`, value: `On final attempt to load interview, got "${ load_result.error }"` });
|
|
420
444
|
expect( load_result.error ).to.equal( '' );
|
|
@@ -467,19 +491,29 @@ module.exports = {
|
|
|
467
491
|
|
|
468
492
|
normalizeTable: async function ( scope, { var_data, from_story_table=true }) {
|
|
469
493
|
/* Return data derived from cucumber variable-setting data.
|
|
470
|
-
|
|
494
|
+
* Table must have headers. Arrays are not supported. */
|
|
471
495
|
|
|
472
496
|
// Support tables with no 'trigger' column
|
|
473
497
|
let supported_table = [];
|
|
474
498
|
for ( let row of var_data ) {
|
|
475
499
|
// See tests > unit_tests > tables.fixtures.js > tables.old_to_current_formatting
|
|
500
|
+
let actual_var_value = row.value;
|
|
501
|
+
if (is_secret_var(row)) {
|
|
502
|
+
actual_var_value = process.env[row.flags.secret_name];
|
|
503
|
+
if ( actual_var_value === undefined ) {
|
|
504
|
+
let not_in_env_msg = `The GitHub SECRET "${ row.flags.secret_name }" doesn't exist. Check for a typo and check your workflow file.`;
|
|
505
|
+
await scope.addToReport( scope, { type: `error`, value: not_in_env_msg })
|
|
506
|
+
throw ReferenceError(`Could not find "${row.flags.secret_name }": ${not_in_env_msg}.`)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
476
510
|
let result = {
|
|
477
511
|
original: row,
|
|
478
512
|
// Give fake data if needed to prevent some kind of hidden input field from being found
|
|
479
513
|
// See bug at https://github.com/plocket/docassemble-cucumber/issues/79
|
|
480
514
|
// May be able to remove this once converted to `getAllFields`
|
|
481
515
|
var: row.var || 'al_no_var_name',
|
|
482
|
-
value:
|
|
516
|
+
value: actual_var_value,
|
|
483
517
|
trigger: row.trigger || '',
|
|
484
518
|
times_used: 0,
|
|
485
519
|
}
|
|
@@ -487,7 +521,9 @@ module.exports = {
|
|
|
487
521
|
supported_table.push( result );
|
|
488
522
|
}
|
|
489
523
|
|
|
490
|
-
if ( session_vars.get_debug() ) {
|
|
524
|
+
if ( session_vars.get_debug() ) {
|
|
525
|
+
console.log( 'scope.normalizedTable():\n', JSON.stringify( supported_table ));
|
|
526
|
+
}
|
|
491
527
|
|
|
492
528
|
return supported_table;
|
|
493
529
|
}, // Ends scope.normalizeTable()
|
|
@@ -639,7 +675,7 @@ module.exports = {
|
|
|
639
675
|
|
|
640
676
|
// Make sure this selector matches only one node
|
|
641
677
|
let matching_nodes = $( selector );
|
|
642
|
-
// This check is here to ensure
|
|
678
|
+
// This check is here to ensure safety of future development. If there is more
|
|
643
679
|
// or less than one match, this code may have unexpected behavior later.
|
|
644
680
|
// Really need everything to be unique here if at all possible. Shall we resort
|
|
645
681
|
// to parents and n-th child?
|
|
@@ -860,7 +896,7 @@ module.exports = {
|
|
|
860
896
|
let uses_proxies = false; // See "proxies" notes further down
|
|
861
897
|
let trigger_might_be_needed = true;
|
|
862
898
|
|
|
863
|
-
// There are
|
|
899
|
+
// There are multiple guesses because field names are sometimes encoded
|
|
864
900
|
for ( let guess of field.guesses ) {
|
|
865
901
|
for ( let table_row of var_data ) {
|
|
866
902
|
let this_row_matches_this_field = false;
|
|
@@ -1085,7 +1121,7 @@ module.exports = {
|
|
|
1085
1121
|
|
|
1086
1122
|
let { tag, type } = field;
|
|
1087
1123
|
// We use the last match. We wrote a warning for the user if there were
|
|
1088
|
-
//
|
|
1124
|
+
// multiple matches in the story that the last value will be used. In future,
|
|
1089
1125
|
// Steps will be able to set this data gradually and we don't want the developer
|
|
1090
1126
|
// to get confused thinking that an earlier Step was used instead of a later one.
|
|
1091
1127
|
let row = matching_input_rows[ matching_input_rows.length - 1 ];
|
|
@@ -1360,7 +1396,7 @@ module.exports = {
|
|
|
1360
1396
|
},
|
|
1361
1397
|
|
|
1362
1398
|
examinePageID: async function ( scope, question_id = '' ) {
|
|
1363
|
-
/* Looks for a
|
|
1399
|
+
/* Looks for a sanitized version of the question id as it's written
|
|
1364
1400
|
* in the .yml. docassemble's way in python:
|
|
1365
1401
|
* re.sub(r'[^A-Za-z0-9]+', '-', interview_status.question.id.lower()) */
|
|
1366
1402
|
let id_as_class = question_id.toLowerCase().replace(/[^A-Za-z0-9]+/g, '-');
|
|
@@ -1506,9 +1542,10 @@ module.exports = {
|
|
|
1506
1542
|
if ( row.times_used === 0 ) {
|
|
1507
1543
|
num_unused_rows += 1;
|
|
1508
1544
|
let source_row = row;
|
|
1545
|
+
// TODO: Make the row object flatter so we don't need to dig in this way
|
|
1509
1546
|
if ( row.source ) { source_row = row.source; }
|
|
1510
1547
|
let original_row = source_row.original;
|
|
1511
|
-
let msg = `Did not find a field on this page for the variable
|
|
1548
|
+
let msg = `Did not find a field on this page for the variable "${ original_row.var }" that could be set to "${ original_row.value }"`;
|
|
1512
1549
|
await scope.addToReport(scope, { type: `warning`, value: msg });
|
|
1513
1550
|
}
|
|
1514
1551
|
}
|
|
@@ -1605,25 +1642,29 @@ module.exports = {
|
|
|
1605
1642
|
* we believe we've set it up so that people will make it a number.
|
|
1606
1643
|
* Sometimes this will be from `.there_is_another`. We think we've ensured
|
|
1607
1644
|
* that it will only come from Steps and should be `True` or `False`.
|
|
1608
|
-
* This feels precarious, but I can't see another
|
|
1645
|
+
* This feels precarious, but I can't see another permutation.
|
|
1609
1646
|
*
|
|
1610
1647
|
* This may in future be used to normalize all values that need `True`/`False`
|
|
1611
1648
|
* normalization.
|
|
1612
1649
|
*/
|
|
1613
|
-
let value = row.value;
|
|
1650
|
+
let value = row.value.toLowerCase();
|
|
1651
|
+
let val_to_print = printable_var_value(row);
|
|
1614
1652
|
let int_val = parseInt( value );
|
|
1615
1653
|
|
|
1616
1654
|
// Step stuff. Take into account a lot of possible values
|
|
1617
1655
|
if ( value.match( /(false|no|unchecked|uncheck|deselected|deselect)/i )) {
|
|
1656
|
+
// Should this also contain a warning if this is trying to set `target_number`?
|
|
1618
1657
|
return `False`;
|
|
1619
1658
|
|
|
1620
1659
|
} else if ( value.match( /(true|yes|checked|check|selected|select)/i )) {
|
|
1621
1660
|
if ( from_story_table ) {
|
|
1622
|
-
// Just in case, but this should never happen.
|
|
1623
1661
|
if ( row.var.match( /\.there_is_another$/ ) ) {
|
|
1662
|
+
// `.there_is_another` should never get in here, but just in case
|
|
1624
1663
|
await scope.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 )}` });
|
|
1625
1664
|
} else {
|
|
1626
|
-
|
|
1665
|
+
// If `target_number` is set to True instead of a number
|
|
1666
|
+
// Since secret vars can't be passed in a table, this should never be a secret var
|
|
1667
|
+
await scope.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.` });
|
|
1627
1668
|
}
|
|
1628
1669
|
return `False`;
|
|
1629
1670
|
} else { return `True`; }
|
|
@@ -1638,7 +1679,8 @@ module.exports = {
|
|
|
1638
1679
|
return `False`;
|
|
1639
1680
|
|
|
1640
1681
|
} else if ( isNaN( parseInt( value )) ) { // Loop will end
|
|
1641
|
-
|
|
1682
|
+
// I can't figure out how to trigger this warning either
|
|
1683
|
+
await scope.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.` });
|
|
1642
1684
|
return `False`;
|
|
1643
1685
|
|
|
1644
1686
|
} else { // It's a number. Loop will continue.
|
|
@@ -1680,7 +1722,7 @@ module.exports = {
|
|
|
1680
1722
|
// All results are lists to match `$x`
|
|
1681
1723
|
let possible_errors = {
|
|
1682
1724
|
complex_user_error: await scope.page.$$('.alert-danger'),
|
|
1683
|
-
// The below does not work if there's not a space
|
|
1725
|
+
// The below does not work if there's not a space between ':' and 'none'
|
|
1684
1726
|
simple_user_error: await scope.page.$x(`//*[contains(@class, "da-has-error")][not(contains(@style,"display: none"))]`),
|
|
1685
1727
|
system_error: await scope.page.$x( '//meta[@content="docassemble: Error"]' ),
|
|
1686
1728
|
};
|
|
@@ -1703,13 +1745,13 @@ module.exports = {
|
|
|
1703
1745
|
throwPageError: async function ( scope, { error_handlers = {}, id = '' }) {
|
|
1704
1746
|
/* Throw a cucumber error with the error that appeared on the page. */
|
|
1705
1747
|
if ( error_handlers.system_error !== undefined ) {
|
|
1706
|
-
let sys_err_message = `The test tried to continue to the next page, but there was a system error. Check the screenshot.`
|
|
1748
|
+
let sys_err_message = `The test tried to continue to the next page after ${ id }, but there was a system error. Check the screenshot.`
|
|
1707
1749
|
await scope.addToReport(scope, { type: `error`, value: sys_err_message });
|
|
1708
1750
|
expect( was_system_error, sys_err_message ).to.be.false;
|
|
1709
1751
|
|
|
1710
1752
|
} else {
|
|
1711
1753
|
// If it wasn't a system error, it was a user error
|
|
1712
|
-
let user_err_message = `The test tried to continue to the next page, but the user got an error. Check the screenshot.`
|
|
1754
|
+
let user_err_message = `The test tried to continue to the next page after ${ id }, but the user got an error. Check the screenshot.`
|
|
1713
1755
|
await scope.addToReport(scope, { type: `error`, value: user_err_message });
|
|
1714
1756
|
expect( true, user_err_message ).to.be.false;
|
|
1715
1757
|
}
|
|
@@ -1792,6 +1834,21 @@ module.exports = {
|
|
|
1792
1834
|
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
1793
1835
|
}, // Ends set_regular_var()
|
|
1794
1836
|
|
|
1837
|
+
set_secret_var: async ( scope, var_name, set_to_env_name ) => {
|
|
1838
|
+
/** Sets a non-story table variable to a "secret" value (i.e. an env var) */
|
|
1839
|
+
let var_data = await scope.normalizeTable( scope, {
|
|
1840
|
+
var_data: [{
|
|
1841
|
+
var: var_name,
|
|
1842
|
+
value: set_to_env_name,
|
|
1843
|
+
trigger: scope.trigger_not_needed_flag,
|
|
1844
|
+
flags: {secret_name: set_to_env_name}
|
|
1845
|
+
}],
|
|
1846
|
+
});
|
|
1847
|
+
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
1848
|
+
// if this variable cannot be set on this page, trigger an error
|
|
1849
|
+
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
1850
|
+
},
|
|
1851
|
+
|
|
1795
1852
|
tap_term: async ( scope, phrase ) => {
|
|
1796
1853
|
/** Tap a link that doesn't navigate. Depends on the language of the text. */
|
|
1797
1854
|
const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
|
package/lib/steps.js
CHANGED
|
@@ -14,13 +14,11 @@ const wrapPromiseWithTimeout = require('./cucumber_8_shim');
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { expect } = require('chai');
|
|
17
|
-
const puppeteer = require('puppeteer');
|
|
18
17
|
const { v4: uuidv4 } = require('uuid');
|
|
19
18
|
|
|
20
19
|
// Ours
|
|
21
20
|
const scope = require('./scope');
|
|
22
21
|
const session_vars = require('./utils/session_vars');
|
|
23
|
-
|
|
24
22
|
/* Of Note:
|
|
25
23
|
- We're using `*=` because sometimes da text has funny characters in it that are hard to anticipate
|
|
26
24
|
*/
|
|
@@ -33,7 +31,7 @@ require("dotenv").config(); // Where did this go?
|
|
|
33
31
|
1. 'choice' to have a more specific way to access each item. For
|
|
34
32
|
example, for a list collect or other things that have multiple
|
|
35
33
|
things with the same text on the page.
|
|
36
|
-
1. Figure out how to test allowing
|
|
34
|
+
1. Figure out how to test allowing flexibility for coder. For example:
|
|
37
35
|
there is placeholder text for the title of the form and if it's not
|
|
38
36
|
defined, placeholder text should appear (though that behavior may
|
|
39
37
|
bear discussion).
|
|
@@ -180,7 +178,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
180
178
|
|
|
181
179
|
// If response has a file in it that matches the file we are downloading
|
|
182
180
|
if ( response._headers['content-disposition'] && (response._headers['content-disposition']).includes(`attachment;filename=${scope.toDownload}`) ) {
|
|
183
|
-
// We're
|
|
181
|
+
// We're choosing to avoid removing the event listener in case removal
|
|
184
182
|
// would prevent downloading other files. The listener should get
|
|
185
183
|
// destroyed when the page closes anyway.
|
|
186
184
|
|
|
@@ -219,7 +217,7 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
219
217
|
await custom_lang_timeout;
|
|
220
218
|
|
|
221
219
|
|
|
222
|
-
let
|
|
220
|
+
let custom_afterStep_timeout = new Promise(async ( resolve, reject ) => {
|
|
223
221
|
let page_timeout = setTimeout(function() { reject(`\`afterStep()\` took too long.`); }, scope.timeout);
|
|
224
222
|
// self-invoke an anonymous async function so we don't have to
|
|
225
223
|
// abstract the details of resolving and rejecting.
|
|
@@ -230,8 +228,8 @@ Given(/I start the interview at "([^"]+)"(?: in lang "([^"]+)")?/, {timeout: -1}
|
|
|
230
228
|
// prevent temporary hang at end of tests
|
|
231
229
|
finally { clearTimeout( page_timeout ); }
|
|
232
230
|
})();
|
|
233
|
-
}); // ends
|
|
234
|
-
await
|
|
231
|
+
}); // ends custom_afterStep_timeout
|
|
232
|
+
await custom_afterStep_timeout;
|
|
235
233
|
});
|
|
236
234
|
|
|
237
235
|
Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)" and(?: the password)? "([^"]+)"(?: SECRETs)?/i, {timeout: -1}, async ( email, password ) => {
|
|
@@ -252,7 +250,7 @@ Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^
|
|
|
252
250
|
);
|
|
253
251
|
});
|
|
254
252
|
|
|
255
|
-
// I am using a
|
|
253
|
+
// I am using a mobile/pc
|
|
256
254
|
When(/I am using an? (.*)/, async ( device ) => {
|
|
257
255
|
// Let developer pick mobile device if they want to
|
|
258
256
|
// TODO: Add tablet?
|
|
@@ -350,7 +348,7 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
|
|
|
350
348
|
for ( let row of supported_table ) {
|
|
351
349
|
row.report_str = `${ await scope.convertToOriginalStoryTableRow(scope, { row_data: row }) }`;
|
|
352
350
|
}
|
|
353
|
-
// Sort into
|
|
351
|
+
// Sort into alphabetical order, as is recommended for writing story tables
|
|
354
352
|
let sorted_rows = supported_table.sort( function ( row1, row2 ) {
|
|
355
353
|
if( row1.report_str < row2.report_str ) { return -1; }
|
|
356
354
|
if( row1.report_str > row2.report_str ) { return 1; }
|
|
@@ -404,7 +402,7 @@ When(/I wait (\d*\.?\d+) seconds?/, { timeout: -1 }, async ( seconds ) => {
|
|
|
404
402
|
* - Should this really reset the 'navigated' flag? Should the below
|
|
405
403
|
* return succeed or fail? e.g:
|
|
406
404
|
* I tap to continue; I wait 10 seconds; I arrive at the next page;
|
|
407
|
-
* Maybe the '
|
|
405
|
+
* Maybe the 'navigated' flag should only be reset just before trying to
|
|
408
406
|
* navigate (after pressing a button)
|
|
409
407
|
*/
|
|
410
408
|
return wrapPromiseWithTimeout(
|
|
@@ -426,7 +424,7 @@ Then(/I take a screenshot ?(?:named "([^"]+)")?/, { timeout: -1 }, async ( name
|
|
|
426
424
|
When("I do nothing", async () => { await scope.afterStep(scope); }); // √
|
|
427
425
|
|
|
428
426
|
Then('the question id should be {string}', { timeout: -1 }, async ( question_id ) => {
|
|
429
|
-
/* Looks for a
|
|
427
|
+
/* Looks for a sanitized version of the question id as it's written
|
|
430
428
|
* in the .yml. docassemble's way */
|
|
431
429
|
let { question_has_id, id, id_matches } = await scope.examinePageID( scope, question_id );
|
|
432
430
|
|
|
@@ -449,7 +447,7 @@ Then('the question id should be {string}', { timeout: -1 }, async ( question_id
|
|
|
449
447
|
});
|
|
450
448
|
|
|
451
449
|
// Allow emphasis with capital letters
|
|
452
|
-
Then(/I should see the phrase "([^"]+)"/i, async ( phrase ) => { // √
|
|
450
|
+
Then(/I ?(?:should)? see the phrase "([^"]+)"/i, async ( phrase ) => { // √
|
|
453
451
|
/* In Chrome, this `innerText` gets only visible text */
|
|
454
452
|
const bodyText = await scope.page.$eval('body', elem => elem.innerText);
|
|
455
453
|
|
|
@@ -567,7 +565,7 @@ Then('I arrive at the next page', async () => { // √
|
|
|
567
565
|
|
|
568
566
|
// Link observations have the 'Escape' button link in mind
|
|
569
567
|
Then('I should see the link {string}', async ( linkText ) => { // √
|
|
570
|
-
/*
|
|
568
|
+
/* Loosely match link visible text. Should probably deprecate this. */
|
|
571
569
|
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
572
570
|
|
|
573
571
|
let msg = `Cannot find a link with the text "${ linkText }".`;
|
|
@@ -668,7 +666,7 @@ Then(/I get the(?: page's)?(?: JSON)? var(?:iable)?s and val(?:ue)?s/i, { timeou
|
|
|
668
666
|
* to get the variable values on the page of the interview and add
|
|
669
667
|
* them to the report.
|
|
670
668
|
*
|
|
671
|
-
*
|
|
669
|
+
* Variations:
|
|
672
670
|
* I get the page's JSON variables and values
|
|
673
671
|
* I get the json vars and vals
|
|
674
672
|
* I get the page's vars and vals
|
|
@@ -735,6 +733,15 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
|
|
|
735
733
|
);
|
|
736
734
|
});
|
|
737
735
|
|
|
736
|
+
When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
|
|
737
|
+
/* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
|
|
738
|
+
return wrapPromiseWithTimeout(
|
|
739
|
+
scope.steps.set_secret_var( scope, var_name, set_to_env_name ),
|
|
740
|
+
scope.timeout
|
|
741
|
+
);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
|
|
738
745
|
Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_name ) => {
|
|
739
746
|
/* Uploads file (docx, jpg, etc) in same folder to input var. Can actually use
|
|
740
747
|
* 'I set the var "" to ""', but this is more explicit if it's desired. */
|
package/lib/utils/langs.js
CHANGED
|
@@ -20,7 +20,7 @@ for ( let full_path of filepaths ) { // Handle old language files before creati
|
|
|
20
20
|
// Don't apply this to the example test that this package always provides
|
|
21
21
|
if ( full_path.includes( 'example_test.feature' )) { continue; }
|
|
22
22
|
|
|
23
|
-
// Delete
|
|
23
|
+
// Delete already existing non-default language files and skip the rest of this. For example,
|
|
24
24
|
// if 3 languages were tested previously and now only two are being tested.
|
|
25
25
|
if ( full_path.includes( flag )) {
|
|
26
26
|
fs.unlinkSync( full_path );
|
|
@@ -10,7 +10,7 @@ require('dotenv').config();
|
|
|
10
10
|
/* This file provides user info variables. E.g. server address. They're either
|
|
11
11
|
* `process.env` vars or vars that have been built from `process.env` vars.
|
|
12
12
|
* It is meant to avoid duplication, make testing easier, and leave room for
|
|
13
|
-
* future
|
|
13
|
+
* future manipulation. No other file should need to use `process.env`.
|
|
14
14
|
*
|
|
15
15
|
* We have a bunch of `.get_foo()` functions partly because it makes the code
|
|
16
16
|
* much easier to test.
|
|
@@ -20,10 +20,20 @@ const session_vars = {};
|
|
|
20
20
|
|
|
21
21
|
module.exports = session_vars;
|
|
22
22
|
|
|
23
|
+
// Default to null
|
|
23
24
|
session_vars.get_debug = function () { return process.env.DEBUG || null; };
|
|
24
25
|
session_vars.get_dev_api_key = function () { return process.env.DOCASSEMBLE_DEVELOPER_API_KEY || null; };
|
|
25
26
|
session_vars.get_repo_url = function () { return process.env.REPO_URL || null; };
|
|
26
27
|
|
|
28
|
+
// More complex logic
|
|
29
|
+
session_vars.get_setup_timeout_ms = function () {
|
|
30
|
+
let timeout = 120 * 1000;
|
|
31
|
+
if ( process.env.MAX_SECONDS_FOR_SETUP ) {
|
|
32
|
+
timeout = parseFloat( process.env.MAX_SECONDS_FOR_SETUP ) * 1000;
|
|
33
|
+
}
|
|
34
|
+
return timeout;
|
|
35
|
+
};
|
|
36
|
+
|
|
27
37
|
session_vars.get_da_server_url = function () {
|
|
28
38
|
try {
|
|
29
39
|
let url = process.env.SERVER_URL || process.env.BASE_URL;
|
|
@@ -105,7 +115,7 @@ session_vars.get_project_name = function () {
|
|
|
105
115
|
|
|
106
116
|
session_vars.delete_project_name = function () {
|
|
107
117
|
/* Delete the file storing the name of the docassemble project */
|
|
108
|
-
// Avoid race
|
|
118
|
+
// Avoid race conditions possibly caused by checking for existence first
|
|
109
119
|
// Delete file if possible
|
|
110
120
|
try {
|
|
111
121
|
fs.unlinkSync( project_name_file_path );
|
package/package.json
CHANGED
|
@@ -83,3 +83,21 @@ Scenario: I set text-type values
|
|
|
83
83
|
And I tap to continue
|
|
84
84
|
# Next page
|
|
85
85
|
Then the question id should be "the end"
|
|
86
|
+
|
|
87
|
+
@fast @i2 @secret
|
|
88
|
+
Scenario: handles settings from Github secrets
|
|
89
|
+
Given the final Scenario status should be "failed"
|
|
90
|
+
And the Scenario report should include:
|
|
91
|
+
"""
|
|
92
|
+
The GitHub SECRET "SECRET_NOT_THERE" doesn't exist
|
|
93
|
+
"""
|
|
94
|
+
Given I start the interview at "test_secret_vars"
|
|
95
|
+
When I set the var "first_text_entry" to the secret "SECRET_VAR1"
|
|
96
|
+
And I tap to continue
|
|
97
|
+
Then I see the phrase "secret-var1-value"
|
|
98
|
+
When I tap to continue
|
|
99
|
+
And I set the var "second_text_entry" to the github secret "SECRET_VAR2"
|
|
100
|
+
And I tap to continue
|
|
101
|
+
Then I see the phrase "secret-var2-value"
|
|
102
|
+
When I tap to continue
|
|
103
|
+
And I set the variable "third_text_entry" to secret "SECRET_NOT_THERE"
|
|
@@ -386,6 +386,17 @@ Scenario: Fail with could not find files
|
|
|
386
386
|
Given I start the interview at "all_tests"
|
|
387
387
|
And I upload "nonexistant1.pdf, nonexistant2.pdf" to "upload_files_visible"
|
|
388
388
|
|
|
389
|
+
@fast @rf26 @secret @error
|
|
390
|
+
Scenario: Fail to find var while keeping value secret
|
|
391
|
+
Given the final Scenario status should be "failed"
|
|
392
|
+
Given the Scenario report should include:
|
|
393
|
+
"""
|
|
394
|
+
Did not find a field on this page for the variable "missing_var" that could be set to "SECRET_FOR_MISSING_FIELD"
|
|
395
|
+
"""
|
|
396
|
+
And I start the interview at "test_secret_vars"
|
|
397
|
+
And I set the var "missing_var" to the secret "SECRET_FOR_MISSING_FIELD"
|
|
398
|
+
|
|
399
|
+
|
|
389
400
|
# scope.js
|
|
390
401
|
# I upload "___" to "___"
|
|
391
402
|
# No need to test wrong var name
|
|
@@ -409,6 +420,7 @@ Scenario: Warn when there are too many names
|
|
|
409
420
|
And I set the name of "users[0]" to "Uli Udo User Sampson Jr"
|
|
410
421
|
And I tap to continue
|
|
411
422
|
|
|
423
|
+
|
|
412
424
|
# ===============================
|
|
413
425
|
# Reports for "passed" Scenarios
|
|
414
426
|
# ===============================
|