@suffolklitlab/alkiln 5.7.0 → 5.8.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.
@@ -2,7 +2,7 @@ name: GitHub server installation
2
2
 
3
3
  on:
4
4
  push:
5
- # Default opened, reopened, synchronize. "edited" is something else.
5
+ # Default "opened", "reopened", "synchronize". "edited" fires when the PR title gets edited, etc.
6
6
  pull_request_target:
7
7
  workflow_dispatch:
8
8
  inputs:
@@ -44,7 +44,7 @@ jobs:
44
44
  id: github_server
45
45
  #### Developer note: you'll need to replace `.` with the path to
46
46
  #### the repo and branch. For example:
47
- #### suffolkLITLab/ALKiln@v5/action_for_github_server
47
+ #### suffolkLITLab/ALKiln/action_for_github_server@v5
48
48
  uses: ./action_for_github_server
49
49
  with:
50
50
  CONFIG_CONTENTS: "${{ secrets.CONFIG_CONTENTS }}"
@@ -54,3 +54,5 @@ jobs:
54
54
  #### Developer note: Useful for temporarily shortening ALKiln tests
55
55
  #### to debug the docker install step
56
56
  # ALKILN_TAG_EXPRESSION: @a_tag_expression
57
+ #### Developer note: you probalby don't need this
58
+ # ALKILN_VERSION: 5.7.0-feature-tap1
package/CHANGELOG.md CHANGED
@@ -45,6 +45,28 @@ Format:
45
45
 
46
46
  <!-- ## [Unreleased] -->
47
47
 
48
+ ## [5.8.1] - 2023-12-28
49
+
50
+ ### Fixed
51
+ - Restored previous behavior - Projects are now deleted even when a workflow fails. See [#831](https://github.com/SuffolkLITLab/ALKiln/issues/831).
52
+ - Fixed typo of comment explaining how to reference the isolated GitHub server action.
53
+
54
+ ### Security
55
+ - Harden security for action input handling. That said, the source of the problem would come from author workflows, so there's a lot more authors can do to help this situation than we can. Workflows triggered by pull requests are a specific place to watch. GitHub already has [some default safeguards](https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks) to prevent strangers, specifically first-time contributors, from triggering workflows like that. They can also set their [org](https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization#configuring-required-approval-for-workflows-from-public-forks) or [repo](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#controlling-changes-from-forks-to-workflows-in-public-repositories) to be more strict about pull requests from outside collaborators.
56
+
57
+ ### Internal
58
+ - Freeze `docassemblecli` version at 0.0.17. See [#827](https://github.com/SuffolkLITLab/ALKiln/issues/827).
59
+
60
+ ## [5.8.0] - 2023-12-18
61
+
62
+ ### Added
63
+ - Step to tap an element without navigating. See [#834](https://github.com/SuffolkLITLab/ALKiln/issues/834).
64
+
65
+ ### Internal
66
+ - Use puppeteer `.click()`` again
67
+ - Re-activate looking for terms on the page.
68
+
69
+
48
70
  ## [5.7.0] - 2023-11-14
49
71
 
50
72
  ### Added
package/action.yml CHANGED
@@ -22,8 +22,6 @@ inputs:
22
22
  description: "Max amount of time to give the server to reload if it needs to"
23
23
  required: false
24
24
  # no default to let the custom Scenario timeouts create the reload timeout
25
- # INSTALL_LOCATION? Will that get confused with the location passed into the command line?
26
- # Maybe `INSTALL_LOCATION` could be "playground", "server", or a custom path and we could handle all those.
27
25
  INSTALL_METHOD:
28
26
  description: "Default is 'playground'. Other option: 'server'. How to install the package onto the server."
29
27
  required: false
@@ -31,6 +29,10 @@ inputs:
31
29
  ALKILN_TAG_EXPRESSION:
32
30
  description: "The tag expression to use to limit which tests to run. Default is no tag expression."
33
31
  required: false
32
+ DOCASSEMBLECLI_VERSION:
33
+ description: "The version of docassemblecli to install."
34
+ required: false
35
+ default: "0.0.17"
34
36
 
35
37
  runs:
36
38
  using: "composite"
@@ -40,18 +42,29 @@ runs:
40
42
 
41
43
  # Set environment variables
42
44
  - name: "ALKiln setup info: Set environment variables"
43
- # Human-readable date/time:
44
- # https://www.shell-tips.com/linux/how-to-format-date-and-time-in-linux-macos-and-bash/#how-to-format-a-date-in-bash
45
+ # Safe input handling. Avoid evaluating in bash.
46
+ env:
47
+ BRANCH_PATH: ${{ github.ref }}
48
+ BASE_URL: ${{ inputs.SERVER_URL }}
49
+ DOCASSEMBLE_DEVELOPER_API_KEY: ${{ inputs.DOCASSEMBLE_DEVELOPER_API_KEY }}
50
+ ALKILN_VERSION: ${{ inputs.ALKILN_VERSION }}
51
+ MAX_SECONDS_FOR_SETUP: ${{ inputs.MAX_SECONDS_FOR_SETUP }}
52
+ SERVER_RELOAD_TIMEOUT_SECONDS: ${{ inputs.SERVER_RELOAD_TIMEOUT_SECONDS }}
53
+ DOCASSEMBLECLI_VERSION: ${{ inputs.DOCASSEMBLECLI_VERSION }}
45
54
  run: |
46
55
  # ALKiln setup info: Set environment variables
56
+ # Human-readable date/time: https://www.shell-tips.com/linux/how-to-format-date-and-time-in-linux-macos-and-bash/#how-to-format-a-date-in-bash
47
57
  echo "ARTIFACT_NAME=alkiln-$(date +'%Y-%m-%d at %Hh%Mm%Ss' -u)UTC" >> $GITHUB_ENV
48
58
  echo "REPO_URL=${{ github.server_url }}/${{ github.repository }}" >> $GITHUB_ENV
49
- echo "BRANCH_PATH=${{ github.ref }}" >> $GITHUB_ENV
50
- echo "BASE_URL=${{ inputs.SERVER_URL }}" >> $GITHUB_ENV
51
- echo "DOCASSEMBLE_DEVELOPER_API_KEY=${{ inputs.DOCASSEMBLE_DEVELOPER_API_KEY }}" >> $GITHUB_ENV
52
- echo "ALKILN_VERSION=${{ inputs.ALKILN_VERSION }}" >> $GITHUB_ENV
53
- echo "MAX_SECONDS_FOR_SETUP=${{ inputs.MAX_SECONDS_FOR_SETUP }}" >> $GITHUB_ENV
54
- echo "SERVER_RELOAD_TIMEOUT_SECONDS=${{ inputs.SERVER_RELOAD_TIMEOUT_SECONDS }}" >> $GITHUB_ENV
59
+ echo "BRANCH_PATH=$BRANCH_PATH" >> $GITHUB_ENV
60
+ # Workflow inputs
61
+ echo "BASE_URL=$BASE_URL" >> $GITHUB_ENV
62
+ echo "DOCASSEMBLE_DEVELOPER_API_KEY=$DOCASSEMBLE_DEVELOPER_API_KEY" >> $GITHUB_ENV
63
+ echo "ALKILN_VERSION=$ALKILN_VERSION" >> $GITHUB_ENV
64
+ echo "MAX_SECONDS_FOR_SETUP=$MAX_SECONDS_FOR_SETUP" >> $GITHUB_ENV
65
+ echo "SERVER_RELOAD_TIMEOUT_SECONDS=$SERVER_RELOAD_TIMEOUT_SECONDS" >> $GITHUB_ENV
66
+ echo "DOCASSEMBLECLI_VERSION=$DOCASSEMBLECLI_VERSION" >> $GITHUB_ENV
67
+ # Hard-coded internal ALKiln vars
55
68
  echo "_ORIGIN=github" >> $GITHUB_ENV
56
69
  shell: bash
57
70
  - name: "ALKiln setup info: confirm environment variables"
@@ -68,7 +81,7 @@ runs:
68
81
  - name: Install ALKiln directly from npm
69
82
  run: |
70
83
  # ALKiln setup info: Install ALKiln directly from npm
71
- npm install -g @suffolklitlab/alkiln@$ALKILN_VERSION
84
+ npm install -g "@suffolklitlab/alkiln@$ALKILN_VERSION"
72
85
  shell: bash
73
86
 
74
87
  # Install on playground
@@ -82,7 +95,7 @@ runs:
82
95
  if: ${{ inputs.INSTALL_METHOD == 'playground' }}
83
96
  run: |
84
97
  # ALKiln setup info: Create a Project and install the package from GitHub
85
- pip3 install docassemblecli
98
+ pip install "docassemblecli==$DOCASSEMBLECLI_VERSION"
86
99
  alkiln-setup
87
100
  shell: bash
88
101
  - name: "ALKiln setup ERROR"
@@ -103,12 +116,14 @@ runs:
103
116
  # run tests
104
117
  - name: "ALKiln info: Run tests"
105
118
  if: ${{ success() }}
106
- run: alkiln-run ${{ inputs.ALKILN_TAG_EXPRESSION || github.event.inputs.tags && format('{0}', github.event.inputs.tags) }}
119
+ env:
120
+ ALKILN_TAG_EXPRESSION: ${{ inputs.ALKILN_TAG_EXPRESSION || github.event.inputs.tags && format('{0}', github.event.inputs.tags) }}
121
+ run: alkiln-run $ALKILN_TAG_EXPRESSION
107
122
  shell: bash
108
123
 
109
124
  # on playground, delete project
110
125
  - name: "ALKiln info: Try to delete the project from the playground of the docassemble test account."
111
- if: ${{ inputs.INSTALL_METHOD == 'playground' }}
126
+ if: ${{ always() && inputs.INSTALL_METHOD == 'playground' }}
112
127
  run: alkiln-takedown
113
128
  shell: bash
114
129
 
@@ -23,6 +23,10 @@ inputs:
23
23
  description: "Default is 600 seconds. Maximum amount of seconds to give the docassemble docker container to serve the site."
24
24
  required: false
25
25
  default: 600 # 10 min
26
+ DOCASSEMBLECLI_VERSION:
27
+ description: "The version of docassemblecli to install."
28
+ required: false
29
+ default: "0.0.17"
26
30
 
27
31
  outputs:
28
32
  DOCASSEMBLE_DEVELOPER_API_KEY:
@@ -44,11 +48,16 @@ runs:
44
48
  - uses: actions/checkout@v3
45
49
 
46
50
  - name: "ALKiln GitHub server - Set environment variables"
51
+ env:
52
+ MAX_SECONDS_FOR_DOCKER: ${{ inputs.MAX_SECONDS_FOR_DOCKER }}
53
+ DOCASSEMBLECLI_VERSION: ${{ inputs.DOCASSEMBLECLI_VERSION }}
47
54
  run: |
55
+ # ALKiln GitHub server info: Set environment variables
56
+ echo "MAX_SECONDS_FOR_DOCKER=$MAX_SECONDS_FOR_DOCKER" >> $GITHUB_ENV
57
+ echo "DOCASSEMBLECLI_VERSION=$DOCASSEMBLECLI_VERSION" >> $GITHUB_ENV
48
58
  echo "DA_ADMIN_API_KEY=abcd1234abcd1234abcd5678abdc5678" >> $GITHUB_ENV
49
- echo "MAX_SECONDS_FOR_DOCKER=${{ inputs.MAX_SECONDS_FOR_DOCKER }}" >> $GITHUB_ENV
50
59
  echo "DA_ADMIN_EMAIL=admin@example.com" >> $GITHUB_ENV
51
- # Login will fail if this is "password"
60
+ # Login will fail if this value is "password"
52
61
  echo "DA_ADMIN_PASSWORD=@123abcdefg" >> $GITHUB_ENV
53
62
  shell: bash
54
63
  - id: api_key_output_step
@@ -59,14 +68,16 @@ runs:
59
68
  shell: bash
60
69
 
61
70
  - name: "ALKiln GitHub server - Get config"
71
+ env:
72
+ CONFIG: ${{ inputs.CONFIG_CONTENTS }}
62
73
  run: |
63
74
  # ALKiln GitHub server info: config values
64
75
  mkdir /tmp/config
65
76
  # Worth making this silent by defining a variable with > /dev/null...?
66
- if [ -n "${{ inputs.CONFIG_CONTENTS }}" ]
77
+ if [ -n "$CONFIG" ]
67
78
  then
68
79
  echo "ALKiln GitHub server - The developer did provide a custom docassemble config file as an input into this action."
69
- echo "${{ inputs.CONFIG_CONTENTS }}" > /tmp/config/myconfig.yml
80
+ echo "$CONFIG" > /tmp/config/myconfig.yml
70
81
  echo "CONFIG_ARGS=--env DA_CONFIG=/tmp/config/myconfig.yml --volume /tmp/config:/tmp/config " >> $GITHUB_ENV
71
82
  else
72
83
  echo "ALKiln GitHub server - The developer did NOT provide a custom docassemble config file as an input into this action. Docassemble will use its default config."
@@ -74,20 +85,20 @@ runs:
74
85
  fi
75
86
  shell: bash
76
87
 
77
- # GitHub masks secret values like the config, so we don't really
78
- # need to stop that yet, but users may start using variables, so
79
- # a question: If in future we allow variables instead of just secrets,
88
+ # Question: If in future we allow GitHub variables instead of just secrets,
80
89
  # should we let those be visible or prevent them along with the docker logs?
81
-
82
- # Also, does this leave GitHub job environment output While just
83
- # hiding the output from the job console?
90
+ # Also, does GitHub hide job environment output of secrets as well as
91
+ # hiding the secrets' output from the job console?
84
92
 
85
93
  # Define the var to prevent/show docker output
94
+ # TODO: Why are we only doing this for manually triggered workflows?
86
95
  - name: Env var for showing docker output
96
+ env:
97
+ SHOW_DOCKER_OUTPUT: ${{ inputs.SHOW_DOCKER_OUTPUT }}
87
98
  run: |
88
99
  # ALKiln GitHub server info: Docker logs visibility decision
89
100
  if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
90
- echo "OUTPUT_DOCKER=${{ inputs.SHOW_DOCKER_OUTPUT }}" >> $GITHUB_ENV
101
+ echo "OUTPUT_DOCKER=$SHOW_DOCKER_OUTPUT" >> $GITHUB_ENV
91
102
  else
92
103
  echo "OUTPUT_DOCKER=false" >> $GITHUB_ENV
93
104
  fi
@@ -105,31 +116,36 @@ runs:
105
116
  printf '===\nALKiln GitHub server - GitHub will now show the output of docker creation. ALKiln cannot control this output. If you want to prevent output, use "with:" and set the input "SHOW_DOCKER_OUTPUT" to "false". This output will only be visible to those who can already see the action output, like people with admin or write permissions.\n==='
106
117
  shell: bash
107
118
 
119
+ # TODO: Reduce these to 1 block by putting ` > /dev/null 2>&1` into an env var
108
120
  # No output
109
121
  - name: "ALKiln GitHub server - Download and start docker silently"
110
122
  if: ${{ env.OUTPUT_DOCKER == 'false' }}
123
+ env:
124
+ CONFIG_ARGS: ${{ env.CONFIG_ARGS }}
111
125
  run: |
112
126
  # ALKiln GitHub server info: Silent docker pull/run
113
127
  docker pull jhpyle/docassemble > /dev/null 2>&1
114
128
  docker run --name docassemble_container \
115
- --env DA_ADMIN_EMAIL="${{ env.DA_ADMIN_EMAIL }}" \
116
- --env DA_ADMIN_PASSWORD="${{ env.DA_ADMIN_PASSWORD }}" \
117
- --env DA_ADMIN_API_KEY="${{ env.DA_ADMIN_API_KEY }}" \
118
- ${{ env.CONFIG_ARGS }} \
129
+ --env DA_ADMIN_EMAIL="$DA_ADMIN_EMAIL" \
130
+ --env DA_ADMIN_PASSWORD="$DA_ADMIN_PASSWORD" \
131
+ --env DA_ADMIN_API_KEY="$DA_ADMIN_API_KEY" \
132
+ $CONFIG_ARGS \
119
133
  --cap-add SYS_PTRACE \
120
134
  --memory="4gb" -d -p 80:80 jhpyle/docassemble > /dev/null 2>&1
121
135
  shell: bash
122
136
  # With output
123
137
  - name: "ALKiln GitHub server - Download and start docker"
124
138
  if: ${{ env.OUTPUT_DOCKER == 'true' }}
139
+ env:
140
+ CONFIG_ARGS: ${{ env.CONFIG_ARGS }}
125
141
  run: |
126
142
  # ALKiln GitHub server info: Docker pull/run
127
143
  docker pull jhpyle/docassemble
128
144
  docker run --name docassemble_container \
129
- --env DA_ADMIN_EMAIL="${{ env.DA_ADMIN_EMAIL }}" \
130
- --env DA_ADMIN_PASSWORD="${{ env.DA_ADMIN_PASSWORD }}" \
131
- --env DA_ADMIN_API_KEY="${{ env.DA_ADMIN_API_KEY }}" \
132
- ${{ env.CONFIG_ARGS }} \
145
+ --env DA_ADMIN_EMAIL="$DA_ADMIN_EMAIL" \
146
+ --env DA_ADMIN_PASSWORD="$DA_ADMIN_PASSWORD" \
147
+ --env DA_ADMIN_API_KEY="$DA_ADMIN_API_KEY" \
148
+ $CONFIG_ARGS \
133
149
  --cap-add SYS_PTRACE \
134
150
  --memory="4gb" -d -p 80:80 jhpyle/docassemble
135
151
  shell: bash
@@ -188,7 +204,7 @@ runs:
188
204
  with:
189
205
  python-version: '3.10'
190
206
  # No output
191
- - run: pip install docassemblecli > /dev/null 2>&1
207
+ - run: pip install "docassemblecli==$DOCASSEMBLECLI_VERSION" > /dev/null 2>&1
192
208
  if: ${{ env.OUTPUT_DOCKER == 'false' }}
193
209
  shell: bash
194
210
  # Install the package, the current directory (.), onto the docassemble server
@@ -197,7 +213,7 @@ runs:
197
213
  shell: bash
198
214
 
199
215
  # With output
200
- - run: pip install docassemblecli
216
+ - run: pip install "docassemblecli==$DOCASSEMBLECLI_VERSION"
201
217
  if: ${{ env.OUTPUT_DOCKER == 'true' }}
202
218
  shell: bash
203
219
  # Install the package, the current directory (.), onto the docassemble server
package/lib/scope.js CHANGED
@@ -174,14 +174,14 @@ module.exports = {
174
174
  if ( showif ) { await scope.page.waitForTimeout( scope.showif_timeout ); }
175
175
  },
176
176
 
177
- waitForTab: async function waitForTab(scope, tab_to_show) {
177
+ waitForTab: async function waitForTab(scope, { tab_id }) {
178
178
  /**
179
- * @param {tab_to_show} the aria-labeledby text of the tab panel, which
179
+ * @param { tab_id } the aria-labeledby text of the tab panel, which
180
180
  * we use to query and wait for that tab panel to be shown.
181
181
  */
182
182
  // Implementation linked to https://github.com/SuffolkLITLab/docassemble-ALToolbox/blob/4aa1069389d2ca9cadb586f309dd73d791f081d1/docassemble/ALToolbox/misc.py#L127
183
183
  // I think `.$()` does not use a timeout
184
- if ( tab_to_show.length == 0) { return }
184
+ if ( tab_id.length == 0) { return }
185
185
  // waiting for the transitionend event: https://stackoverflow.com/a/57955251/11416267
186
186
  await scope.page.evaluate((element_query) => {
187
187
  return new Promise((resolve) => {
@@ -198,13 +198,16 @@ module.exports = {
198
198
  resolve();
199
199
  }
200
200
  });
201
- }, `div[aria-labelledby="${tab_to_show}"]`);
201
+ }, `div[aria-labelledby="${ tab_id }"]`);
202
202
  },
203
203
 
204
- afterStep: async function afterStep(scope, options = {waitForShowIf: false, tabToWaitFor: '', navigated: false, waitForTimeout: 0}) {
205
- /* Common things to do after a step, including take care of errors.
206
- * TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
207
- * TODO: Update to latest cucumberjs, which has `AfterStep` */
204
+ afterStep: async function afterStep(scope, options = {waitForShowIf: false, navigated: false, waitForTimeout: 0}) {
205
+ /** Common things to do after a step, including take care of errors.
206
+ * `waitForShowIf` is automatically `false` for observation/non-interactive steps, etc.
207
+ *
208
+ * TODO: discuss splitting into `afterField` and `afterStep` or something. Or just change name
209
+ * TODO: Update to latest cucumberjs, which has `AfterStep`
210
+ */
208
211
 
209
212
  if ( scope.page ) {
210
213
  // Use a while loop until the below does not error with `Error: Execution context was destroyed`
@@ -231,7 +234,6 @@ module.exports = {
231
234
 
232
235
  // Wait for elements that might get revealed
233
236
  if ( options.waitForShowIf ) { await scope.waitForShowIf( scope ); }
234
- if ( options.tabToWaitFor ) { await scope.waitForTab( scope, options.tabToWaitFor); }
235
237
  } // ends if scope.page
236
238
 
237
239
  if ( options.navigated ) {
@@ -248,39 +250,56 @@ module.exports = {
248
250
  if ( options.waitForTimeout ) { await time.waitForTimeout( options.waitForTimeout ); }
249
251
  }, // Ends afterStep
250
252
 
251
- tapElement: async function tapElement(scope, elem, tabToWaitFor='', waitForTimeout=0) {
252
- /* Tap a given element, detect navigation, and do appropriate related things. */
253
+ tapElementAndNavigate: async function(scope, { elem, waitForTimeout=0 }) {
254
+ /** Tap a given element, detect navigation, and do appropriate related things.
255
+ * TODO: not quite sure if we'll be using `waitForTimeout`. */
253
256
 
254
- // This check might never be needed now as we try to match fields on the page
255
- // first in user-triggered cases - users used to be able to access this almost
256
- // directly, but they can't anymore. Our functions should know to only pass good things.
257
- let msg = `Cannot find the element to tap.`;
258
- if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
257
+ let start_url = await scope.page.url(); // so we can later check for navigation
259
258
 
260
- expect( elem, msg ).to.exist;
259
+ await scope.tapElementAndStayOnPage(scope, {
260
+ elem,
261
+ click_promise: Promise.all([
262
+ scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
263
+ elem.evaluate( (el) => { return el.click(); })
264
+ ])
265
+ });
261
266
 
262
- // Get the text on the button or link for a potential error message later
263
- let tapperText = await (await elem.getProperty('textContent')).jsonValue();
267
+ // TODO: Not sure how to detect navigation landing at same page, such as
268
+ // with a user input validation error. Does that count as navigating?
269
+ // Maybe url change + trigger var change? Maybe just trigger var?
270
+ let end_url = await scope.page.url();
271
+ let navigated = start_url !== end_url;
264
272
 
265
- let start_url = await scope.page.url(); // so we can later check for navigation
273
+ // Might be able to handle this in `scope.afterStep`
274
+ if ( navigated ) {
266
275
 
267
- // We'll wait for the click itself at the very least
268
- let click_promises = [ elem.evaluate( (el) => { return el.click(); }) ];
269
-
270
- // If we're possibly navigating, wait for navigation as well
271
- if ( !tabToWaitFor ) {
272
- click_promises.push( scope.page.waitForNavigation({ waitUntil: 'domcontentloaded' }) );
276
+ // TODO: Can we remove this?
277
+ // Save the id that comes next
278
+ let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
279
+
280
+ // If navigation, wait for 200ms to let things finish loading and moving
281
+ await scope.afterStep(scope, { waitForTimeout: 200, navigated: true });
282
+ } else {
283
+ await scope.afterStep(scope, { waitForTimeout, navigated: false });
273
284
  }
274
- let clickWait = Promise.all( click_promises );
285
+ }, // Ends scope.tapElementAndNavigate()
275
286
 
276
- let winner = null;
287
+ tapElementAndStayOnPage: async function(scope, { elem, click_promise }) {
288
+ /** Tap a given element, detect navigation, and do appropriate related things.
289
+ * `click_promise can be any kind of Promise, including a Promise.all()
290
+ */
291
+ let msg = `Cannot find the element to tap.`;
292
+ if ( !elem ) { reports.addToReport(scope, { type: `error`, value: msg }); }
293
+ expect( elem, msg ).to.exist;
277
294
 
295
+ click_promise = click_promise || elem.click();
296
+
278
297
  // TODO: implement and use scope.handle_possible_timeout()
279
298
  try {
280
- // Error loads page, so no need to detect to keep things short
281
- // Also, puppeteer will ensure proper timeout.
282
- winner = await Promise.race([
283
- clickWait,
299
+ // Serever error loads page, so keeping things shorter by not looking
300
+ // for that text too. Also, puppeteer will ensure proper timeout.
301
+ await Promise.race([
302
+ click_promise,
284
303
  // This is meant to detect user-visible alerts/errors,
285
304
  // not breaking errors in the testing code.
286
305
  scope.page.waitForSelector('.alert-danger', { visible: true }),
@@ -292,6 +311,9 @@ module.exports = {
292
311
  let non_reload_report_msg = `ALKiln tapped "${ tapperText }", but the interview took to long to finish.`;
293
312
  await scope.handle_page_timeout_error( scope, { non_reload_report_msg, error });
294
313
  } else {
314
+
315
+ // Get the text on the button or link for a potential error message later
316
+ let tapperText = await (await elem.getProperty('textContent')).jsonValue();
295
317
  // Throw any non-timeout error
296
318
  let err_msg = `Error occurred when ALKiln tried to tap "${ tapperText }".`
297
319
  reports.addToReport( scope, { type: `error`, value: err_msg });
@@ -299,30 +321,7 @@ module.exports = {
299
321
  } // ends if error is timeout error
300
322
 
301
323
  } // ends try/catch
302
-
303
- // TODO: Not sure how to detect navigation landing at same page, such as
304
- // with a user input validation error. Does that count as navigating?
305
- // Maybe url change + trigger var change? Maybe just trigger var?
306
- let end_url = await scope.page.url();
307
- let navigated = start_url !== end_url;
308
-
309
- // Might be able to handle this in `scope.afterStep`
310
- if ( navigated ) {
311
- // Save the id that comes next
312
- let { question_has_id, id, id_matches } = await scope.examinePageID( scope, 'none to match' );
313
- // If navigation, wait for 200ms to let things settle down
314
- await scope.afterStep(scope, {waitForTimeout: 200, navigated: navigated});
315
- } else {
316
- // If stay on page, wait for `show if`, the new tab to be loaded, or feedback elements to be move around
317
- options = {waitForShowIf: !tabToWaitFor, tabToWaitFor: tabToWaitFor, navigated: navigated};
318
- if (waitForTimeout > 0) {
319
- options = {waitForTimeout: waitForTimeout, navigated: navigated};
320
- }
321
- await scope.afterStep(scope, options);
322
- }
323
-
324
- return winner;
325
- }, // Ends scope.tapElement()
324
+ }, // Ends scope.tapElementAndStayOnPage()
326
325
 
327
326
  // Handle server reload errors
328
327
  server_statuses: [],
@@ -516,7 +515,7 @@ module.exports = {
516
515
  ]);
517
516
  await elem.evaluate( el => { return el.className });
518
517
  // Waits for navigation or user error
519
- await scope.tapElement( scope, elem );
518
+ await scope.tapElementAndNavigate( scope, { elem });
520
519
  }, // Ends scope.continue()
521
520
 
522
521
  continue_exists: async function ( scope ) {
@@ -545,7 +544,8 @@ module.exports = {
545
544
  reports.addToReport(scope, { type: `error`, value: error_msg });
546
545
  }
547
546
  expect( elem, error_msg ).to.exist;
548
- await scope.tapElement( scope, elem, tab_id );
547
+ await scope.tapElementAndStayOnPage(scope, { elem });
548
+ await scope.waitForTab( scope, { tab_id });
549
549
  },
550
550
 
551
551
  checkForA11y: async function (scope ) {
@@ -586,14 +586,25 @@ module.exports = {
586
586
  };
587
587
  },
588
588
 
589
- tapElementBySelector: async function ( scope, selector, waitForTimeout ) {
590
- let elem = await scope.page.$(`${selector}`);
591
- let error_msg = `Couldn't find "${selector}" on the page, is there a typo?`;
589
+ tapSelectorToNavigate: async function ( scope, selector, waitForTimeout ) {
590
+ let elem = await scope.page.$(`${ selector }`);
591
+ let error_msg = `Couldn't find "${ selector }" on the page, is there a typo?`;
592
+ if ( !elem ) {
593
+ reports.addToReport(scope, { type: `error`, value: error_msg });
594
+ }
595
+ expect( elem, error_msg ).to.exist;
596
+ await scope.tapElementAndNavigate( scope, { elem, waitForTimeout });
597
+ },
598
+
599
+ tapSelectorAndStay: async function ( scope, { selector }) {
600
+ let elem = await scope.page.$(`${ selector }`);
601
+ let error_msg = `Couldn't find "${ selector }" on the page, is there a typo?`;
592
602
  if ( !elem ) {
593
603
  reports.addToReport(scope, { type: `error`, value: error_msg });
594
604
  }
595
605
  expect( elem, error_msg ).to.exist;
596
- await scope.tapElement( scope, elem, '', waitForTimeout );
606
+ await scope.tapElementAndStayOnPage(scope, { elem });
607
+ await scope.afterStep(scope, { waitForShowIf: false, navigated: false, waitForTimeout: 0 });
597
608
  },
598
609
 
599
610
  load: async function ( scope, file_name ) {
@@ -1539,7 +1550,7 @@ module.exports = {
1539
1550
 
1540
1551
  enter_answer: {
1541
1552
  button: async function ( scope, { handle, answer }) {
1542
- await scope.tapElement( scope, handle );
1553
+ await scope.tapElementAndNavigate( scope, { elem: handle });
1543
1554
  return true;
1544
1555
  },
1545
1556
  textarea: async function ( scope, { handle, answer }) {
@@ -2466,16 +2477,16 @@ module.exports = {
2466
2477
  }, // Ends scope.steps.set_secret_var
2467
2478
 
2468
2479
  tap_term: async ( scope, phrase ) => {
2469
- /** Tap a link that doesn't navigate. Depends on the language of the text. */
2480
+ /** Tap a link that doesn't navigate. Depends on the language of the text.
2481
+ * TODO: This may be broken at this point.
2482
+ * */
2470
2483
  const [link] = await scope.page.$x(`//a[contains(text(), "${phrase}")]`);
2471
2484
 
2472
2485
  let msg = `The term "${ phrase }" seems to be missing.`;
2473
2486
  if ( !link ) { reports.addToReport(scope, { type: `error`, value: msg }); }
2474
2487
  expect( link, msg ).to.exist;
2475
-
2476
- await scope.tapElement( scope, link );
2477
-
2478
- await scope.afterStep(scope, {waitForShowIf: true});
2488
+ await scope.tapElementAndStayOnPage(scope, { elem: link });
2489
+ await scope.page.waitForTimeout(10000);
2479
2490
  }, // Ends scope.steps.tap_term()
2480
2491
 
2481
2492
  sign: async ( scope, name ) => {
@@ -2529,7 +2540,7 @@ module.exports = {
2529
2540
  if (failed_to_download) {
2530
2541
  reports.addToReport(scope, { type: `warning`, value: `Could not download file using fetch (${ err_msg }). ALKiln will now fallback to the click download method` });
2531
2542
  scope.toDownload = filename;
2532
- // Should this be using `scope.tapElement()`?
2543
+ // Should this be using `scope.tapElement`?
2533
2544
  await elem.evaluate( elem => { return elem.click(); });
2534
2545
  await scope.detectDownloadComplete( scope );
2535
2546
  }
@@ -2702,7 +2713,7 @@ module.exports = {
2702
2713
  await scope.page.type( `#email`, email );
2703
2714
  await scope.page.type( `#password`, password );
2704
2715
  let elem = await scope.page.$( `button[name="submit"]` );
2705
- await scope.tapElement( scope, elem );
2716
+ await scope.tapElementAndNavigate( scope, { elem });
2706
2717
 
2707
2718
  // Wait to see the success message.
2708
2719
  // This is a bit of an assumption and needs success and failure testing
package/lib/steps.js CHANGED
@@ -254,7 +254,7 @@ Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^
254
254
  * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
255
255
  * I log in with "USER1_EMAIL" and "USER1_PASSWORD"
256
256
  */
257
- // Exiting the custom timeout happens in scope.tapElement() if needed
257
+ // `page` timeout will deal with custom timeout
258
258
  // Couldn't test the timeout for tapping the button because there's not
259
259
  // enough of a pause between initial navigation and pressing button.
260
260
  await scope.steps.sign_in( scope, {
@@ -745,24 +745,32 @@ When(/I tap to continue/i, { timeout: -1 }, async () => {
745
745
  // Any selectors for this seem somewhat precarious
746
746
  // No data will cause an automatic continue when `ensure_navigation` is true
747
747
 
748
- // Exiting the custom timeout happens in scope.tapElement() where we can
748
+ // `page` timeout will deal with custom timeout
749
749
  // check if the server is reloading
750
750
  await scope.setFields( scope, { var_data: [], ensure_navigation: true });
751
751
  });
752
752
 
753
753
  When(/I tap the "([^"]+)" tab/i, { timeout: -1 }, async ( tabId ) => {
754
- // Exiting the custom timeout happens in scope.tapElement() if needed
754
+ // `page` timeout will deal with custom timeout
755
755
  await scope.tapTab( scope, tabId );
756
756
  });
757
757
 
758
- When(/I tap the "([^"]+)" element$/i, { timeout: -1 }, async ( elemSelector ) => {
759
- // Exiting the custom timeout happens in scope.tapElement() if needed
760
- await scope.tapElementBySelector( scope, elemSelector, 2000 );
758
+ When(/I tap the "([^"]+)" element(?: and go to (?:a new|the next) page)?$/i, { timeout: -1 }, async ( elemSelector ) => {
759
+ // `page` timeout will deal with custom timeout
760
+ await scope.tapSelectorToNavigate( scope, elemSelector, 2000 );
761
761
  });
762
762
 
763
- When(/I tap the "([^"]+)" element and wait ([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
764
- // Exiting the custom timeout happens in scope.tapElement() if needed
765
- await scope.tapElementBySelector( scope, elemSelector, waitTime * 1000 )
763
+ When(/I tap the "([^"]+)" element and stay(?: on the same page)?$/i, { timeout: -1 }, async ( selector ) => {
764
+ // `page` timeout will deal with custom timeout
765
+ await scope.tapSelectorAndStay( scope, { selector });
766
+ });
767
+
768
+ When(/I tap the "([^"]+)" element and wait (?:for )?([0-9]+) second(?:s)?/i, { timeout: -1 }, async ( elemSelector, waitTime ) => {
769
+ // TODO: Why is it the developer can't just wait after the step?
770
+ // Also, this is always supposed to navigate, and that handles its own waiting.
771
+
772
+ // `page` timeout will deal with custom timeout
773
+ await scope.tapSelectorToNavigate( scope, elemSelector, waitTime * 1000 )
766
774
  });
767
775
 
768
776
  When(/I check the page for (?:accessibility|a11y) issues/i, {timeout: -1}, async () => {
@@ -967,7 +975,7 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
967
975
  * 1. I set the var "benefits['SSI']" to "false"
968
976
  * 1. I set the variable "rent_interval" to "weekly"
969
977
  */
970
- // Exiting the custom timeout happens in scope.tapElement() if needed
978
+ // `page` timeout will deal with custom timeout
971
979
  await scope.steps.set_regular_var( scope, var_name, answer )
972
980
 
973
981
  });
@@ -975,7 +983,7 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
975
983
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
976
984
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
977
985
  // This could theoretically tap an element, so
978
- // exiting the custom timeout happens in scope.tapElement() if needed
986
+ // `page` timeout will deal with custom timeout
979
987
  await scope.steps.set_secret_var( scope, var_name, set_to_env_name )
980
988
  });
981
989
 
@@ -995,7 +1003,7 @@ Then(/I upload "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async ( filenames, var_
995
1003
 
996
1004
  When('I tap the defined text link {string}', { timeout: -1 }, async ( phrase ) => {
997
1005
  /** Tap a link that doesn't navigate. Depends on the language of the text. */
998
- // exiting the custom timeout happens in scope.tapElement() if needed
1006
+ // `page` timeout will deal with custom timeout
999
1007
  await scope.steps.tap_term( scope, phrase );
1000
1008
  });
1001
1009
 
@@ -1022,7 +1030,7 @@ When(/I download "([^"]+)"$/, { timeout: -1 }, async ( filename ) => {
1022
1030
  * page load. Just stick to the name of the file.
1023
1031
  */
1024
1032
  // TODO:
1025
- // This should be using `scope.tapElement()`? When someone clicks on a link to
1033
+ // This should be using `scope.tapElementAndStayOnPage()`? When someone clicks on a link to
1026
1034
  // download a document, it sends a request to the server, which could be reloading
1027
1035
  return wrapPromiseWithTimeout(
1028
1036
  scope.steps.download( scope, filename ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.7.0",
3
+ "version": "5.8.1",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {