@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.
@@ -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
- <!-- ## [Unreleased] -->
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: 5 * 1000 };
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: 75 * 1000 };
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: 75 * 1000 };
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
- let default_opts = { timeout: 3 * 1000, max_tries: 15, wait_seconds: 5 };
178
- let { timeout, max_tries, wait_seconds } = creation_options || default_opts;
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 reponse error
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: 75 * 1000 };
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: 3 * 1000 };
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
- // Defaults
344
- let { wait_seconds_per_loop, max_loops } = options || { wait_seconds_per_loop: 5, max_loops: 15 };
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
- log.info({ pre: `Not yet complete. Check ${ num_tries } of ${ max_loops }. `
361
- + `Will check again in ${ wait_seconds_per_loop } seconds.`
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( wait_seconds_per_loop * 1000 );
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 * wait_seconds_per_loop } `
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: 75 * 1000 };
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
- // Should we throw in here?
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 formated as a story table row.
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 genereated from test report data. */
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 genereated from scenario list of report items. */
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
- let classname = await elem.evaluate( el => { return el.className });
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 excpetion and is shown to the developer at the end
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
- * Table must have headers. Arrays are not supported. */
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: row.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() ) { console.log( 'scope.normalizedTable():\n', JSON.stringify( supported_table )); }
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 saftey of future development. If there is more
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 mutliple guesses because field names are sometimes encoded
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
- // mutliple matches in the story that the last value will be used. In future,
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 santized version of the question id as it's written
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 \`${ original_row.var }\` that could be set to \`${ original_row.value }\``;
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 permuation.
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
- await scope.addToReport( scope, { type: `warning`, value: `${ value } is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
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
- await scope.addToReport( scope, { type: `warning`, value: `${ value } is not a valid value for ${ row.var } here. This test will default to \`False\` to avoid problems.` });
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 betweein ':' and 'none'
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 felxibility for coder. For example:
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 chosing avoid removing the event listener in case removal
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 custom_aferStep_timeout = new Promise(async ( resolve, reject ) => {
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 custom_aferStep_timeout
234
- await custom_aferStep_timeout;
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 moble/pc
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 aphabetical order, as is recommended for writing story tables
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 'navitaged' flag should only be reset just before trying to
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 santized version of the question id as it's written
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
- /* Loosley match link visible text. Should probably deprecate this. */
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
- * Varations:
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. */
@@ -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 alredy existing non-default language files and skip the rest of this. For example,
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 maninpulation. No other file should need to use `process.env`.
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 coditions possibly caused by checking for existence first
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "4.0.0",
3
+ "version": "4.2.1-custom-setup-timeout.1",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -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
  # ===============================