@suffolklitlab/alkiln 5.4.2 → 5.5.0

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.
@@ -0,0 +1,68 @@
1
+ name: GitHub server tests
2
+
3
+ on:
4
+ push:
5
+ workflow_dispatch:
6
+ inputs:
7
+ tags:
8
+ required: False
9
+ description: 'Optional. Use a "tag expression" specify which tagged tests to run (https://cucumber.io/docs/cucumber/api/#tag-expressions)'
10
+ enable_tmate:
11
+ type: boolean
12
+ description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
13
+ required: false
14
+ default: false
15
+
16
+ jobs:
17
+
18
+ github-server-tests:
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ node-version: [18.x]
23
+
24
+ env:
25
+ SECRET_VAR1: secret-var1-value
26
+ SECRET_VAR2: secret-var2-value
27
+ SECRET_FOR_MISSING_FIELD: secret for missing field
28
+
29
+ steps:
30
+ # Place the root directory in this branch
31
+ - uses: actions/checkout@v3
32
+
33
+ - name: Run ALKiln - Start the temporary GitHub server
34
+ id: github_server
35
+ uses: ./action_for_github_server
36
+ with:
37
+ CONFIG_CONTENTS: "${{ secrets.CONFIG_CONTENTS }}"
38
+ - run: echo "ALKiln finished starting the isolated GitHub docassemble server"
39
+
40
+ # Debugging on failure to explore things like config issues
41
+ - name: Run ALKiln - Docker failure tmate session
42
+ if: ${{ failure() }}
43
+ uses: mxschmitt/action-tmate@v3
44
+
45
+ - name: Run ALKiln - Env vars from server output
46
+ run: |
47
+ echo "USER1_EMAIL=${{ steps.github_server.outputs.DA_ADMIN_EMAIL }}" >> $GITHUB_ENV
48
+ echo "USER1_PASSWORD=${{ steps.github_server.outputs.DA_ADMIN_PASSWORD }}" >> $GITHUB_ENV
49
+ ## Example of working with the docker container after it's been started
50
+ ## For example, you can make more users with different permissions and passwords
51
+ #- name: Work with docker
52
+ # run: |
53
+ # container_name=$(docker ps --format '{{.Names}}' | head -n 1)
54
+ # echo "container_name is $container_name"
55
+
56
+ - name: "Run ALKiln tests"
57
+ if: ${{ success() }}
58
+ uses: ./
59
+ with:
60
+ SERVER_URL: "${{ steps.github_server.outputs.SERVER_URL }}"
61
+ DOCASSEMBLE_DEVELOPER_API_KEY: "${{ steps.github_server.outputs.DOCASSEMBLE_DEVELOPER_API_KEY }}"
62
+ INSTALL_METHOD: "server"
63
+ # Temporary while in pre-release
64
+ ALKILN_VERSION: "server"
65
+ # Has been useful for development. Can remove when more stable.
66
+ ALKILN_TAG_EXPRESSION: ""
67
+ - run: echo "ALkiln finished running ALKiln tests"
68
+
package/CHANGELOG.md CHANGED
@@ -45,6 +45,14 @@ Format:
45
45
 
46
46
  <!-- ## [Unreleased] -->
47
47
 
48
+ ## [5.5.0] - 2023-10-27
49
+
50
+ ### Added
51
+ - Added a github action that allows authors to run tests on an isolated server that ALKiln creates on GitHub. They will need to create a new GitHub workflow to run the action. This reduces flakiness that authors' servers can create during server reload caused by updating their config, pulling in a package with a module, and other such things. As with all tests, especially with end to end test, there will always be some flakiness.
52
+
53
+ ### Fixed
54
+ - Continuing development of random input test: Deal with non-existent elements, add a default input `type` of 'text', only press 'Back' 10% of the time instead of potentially 50%, navigate back to the interview from an external link (that goes outside the server) if necessary. Also add more debug logs to help diagnose problems. https://github.com/SuffolkLITLab/ALKiln/pull/633
55
+
48
56
  ## [5.4.2] - 2023-10-22
49
57
 
50
58
  ### Changed
@@ -896,4 +904,4 @@ Format:
896
904
  - `example.feature` - an example test to get a project started
897
905
  - `github/workflows/run_form_tests.yml` - Automated github workflow to run tests when code is pushed and a manual trigger for testing without needing to push
898
906
  - `.gitignore` - ignores files that are generated by the tests and shouldn't be added to the repo
899
- - `package.json` - keep up-to-date version of this test repository and hold scripts that the automated tests run
907
+ - `package.json` - keep up-to-date version of this test repository and hold scripts that the automated tests run
package/action.yml CHANGED
@@ -22,6 +22,15 @@ 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
+ INSTALL_METHOD:
28
+ description: "Default is 'playground'. Other option: 'server'. How to install the package onto the server."
29
+ required: false
30
+ default: "playground"
31
+ ALKILN_TAG_EXPRESSION:
32
+ description: "The tag expression to use to limit which tests to run. Default is no tag expression."
33
+ required: false
25
34
 
26
35
  runs:
27
36
  using: "composite"
@@ -46,29 +55,43 @@ runs:
46
55
  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\nSERVER_RELOAD_TIMEOUT_SECONDS is $SERVER_RELOAD_TIMEOUT_SECONDS\n"
47
56
  shell: bash
48
57
 
49
- # Install node
58
+ # Install
50
59
  - name: "ALKiln: Install node packages"
51
60
  uses: actions/setup-node@v3
52
61
  with:
53
62
  node-version: "18"
54
63
  - name: Install ALKiln directly from npm
55
64
  run: |
56
- npm install -g @suffolklitlab/alkiln
65
+ npm install -g @suffolklitlab/alkiln@$ALKILN_VERSION
57
66
  shell: bash
58
67
 
59
- # Run tests
68
+ # Install on playground
60
69
  - name: "ALKiln: Create a Project and pull the package from GitHub"
70
+ if: ${{ inputs.INSTALL_METHOD == 'playground' }}
61
71
  run: alkiln-setup
62
72
  shell: bash
73
+ - run: echo -e "\n\n====\nALKiln could not create a project on your server's testing account or pull your package into it. Check the messages above this line.\n\n"
74
+ if: ${{ inputs.INSTALL_METHOD == 'playground' && failure() }}
75
+ shell: bash
76
+
77
+ # Install on server
78
+ - name: "ALKiln: Install the GitHub package onto the server"
79
+ if: ${{ inputs.INSTALL_METHOD == 'server' }}
80
+ run: alkiln-server-install
81
+ shell: bash
82
+ - run: echo -e "\n\n====\nALKiln could not install the package on the temporary GitHub docassemble server. Check the steps above this line.\n\n"
83
+ if: ${{ inputs.INSTALL_METHOD == 'server' && failure() }}
84
+ shell: bash
85
+
86
+ # run tests
63
87
  - name: "ALKiln: Run tests"
64
88
  if: ${{ success() }}
65
- run: alkiln-run ${{ github.event.inputs.tags && format('{0}', github.event.inputs.tags) }}
66
- shell: bash
67
- - run: echo -e "\n\n====ALKiln could not create a project on your server's testing account or pull your package into it. Check the messages above this line.\n\n"
68
- if: ${{ failure() }}
89
+ run: alkiln-run ${{ inputs.ALKILN_TAG_EXPRESSION || github.event.inputs.tags && format('{0}', github.event.inputs.tags) }}
69
90
  shell: bash
70
- - name: "ALKiln: Delete project from docassemble test account"
71
- if: ${{ always() }}
91
+
92
+ # on playground, delete project
93
+ - name: "ALKiln: Try to delete the project from the playground of the docassemble test account."
94
+ if: ${{ inputs.INSTALL_METHOD == 'playground' }}
72
95
  run: alkiln-takedown
73
96
  shell: bash
74
97
 
@@ -81,7 +104,7 @@ runs:
81
104
  path: ./alkiln-*
82
105
 
83
106
  # ##################################
84
- # # A developer's workflow should look similar to the below
107
+ # # A developer's workflow should look similar to the below.
85
108
  # # In place of `uses: suffolkLITLab/ALKiln@v5`, they
86
109
  # # might use another version.
87
110
  #
@@ -0,0 +1,141 @@
1
+ name: "ALKiln/github_server: Isolated docassemble tests"
2
+ description: "Download and start up a temporary docassemble server on GitHub's servers, in an isolated environment. Avoids docassemble reload errors and other external factors."
3
+
4
+ # If we want to control the version of the docassemble image, we'd have
5
+ # to make our own image:
6
+ # You can build your own image by doing git clone jhpyle/docassemble and then
7
+ # using git checkout to select a particular version, and then using docker build
8
+ # to build an image. https://docassemble.org/docs/docker.html#downgrading
9
+ # If we just want to use an older version of da (as long as it works with
10
+ # the latest image):
11
+ # If you have an existing container you can use dainstall from the command
12
+ # line to install docassemble.webapp==1.4.68
13
+
14
+ inputs:
15
+ CONFIG_CONTENTS:
16
+ description: "The contents of the config file of the docassemble server"
17
+ required: false
18
+ MAX_SECONDS_FOR_DOCKER:
19
+ description: "Maximum amount of seconds to give the docassemble docker container to serve the site."
20
+ required: false
21
+ default: 600 # 10 min
22
+
23
+ outputs:
24
+ DOCASSEMBLE_DEVELOPER_API_KEY:
25
+ description: "Use this inside `with:` for the ALKiln step. Temporary admin api key. It will be destroyed after the tests are done."
26
+ value: ${{ steps.api_key_output_step.outputs.DOCASSEMBLE_DEVELOPER_API_KEY }}
27
+ SERVER_URL:
28
+ description: "Use this inside `with:` for the ALKiln step. URL of the temporary docassemble server."
29
+ value: "http://localhost"
30
+ DA_ADMIN_EMAIL:
31
+ value: ${{ steps.api_key_output_step.outputs.DA_ADMIN_EMAIL }}
32
+ DA_ADMIN_PASSWORD:
33
+ value: ${{ steps.api_key_output_step.outputs.DA_ADMIN_PASSWORD }}
34
+
35
+
36
+ runs:
37
+ using: "composite"
38
+ steps:
39
+ - uses: actions/checkout@v3
40
+
41
+ - name: "ALKiln github_server: Set environment variables"
42
+ run: |
43
+ echo "DA_ADMIN_API_KEY=abcd1234abcd1234abcd5678abdc5678" >> $GITHUB_ENV
44
+ echo "MAX_SECONDS_FOR_DOCKER=${{ inputs.MAX_SECONDS_FOR_DOCKER }}" >> $GITHUB_ENV
45
+ echo "DA_ADMIN_EMAIL=admin@example.com" >> $GITHUB_ENV
46
+ # Login will fail if this is "password"
47
+ echo "DA_ADMIN_PASSWORD=@123abcdefg" >> $GITHUB_ENV
48
+ shell: bash
49
+ - id: api_key_output_step
50
+ run: |
51
+ echo "DOCASSEMBLE_DEVELOPER_API_KEY=$DA_ADMIN_API_KEY" >> "$GITHUB_OUTPUT"
52
+ echo "DA_ADMIN_EMAIL=$DA_ADMIN_EMAIL" >> "$GITHUB_OUTPUT"
53
+ echo "DA_ADMIN_PASSWORD=$DA_ADMIN_PASSWORD" >> "$GITHUB_OUTPUT"
54
+ shell: bash
55
+
56
+ - name: "ALKiln github_server: Download and start docker"
57
+ run: |
58
+ mkdir /tmp/config
59
+ if [ -n "${{ inputs.CONFIG_CONTENTS }}" ]
60
+ then
61
+ echo "ALKiln github_server: The developer did provided a custom docassemble config file as an input into this action."
62
+ echo "${{ inputs.CONFIG_CONTENTS }}" > /tmp/config/myconfig.yml
63
+ echo "CONFIG_ARGS=--env DA_CONFIG=/tmp/config/myconfig.yml --volume /tmp/config:/tmp/config " >> $GITHUB_ENV
64
+ else
65
+ 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."
66
+ echo "CONFIG_ARGS=" >> $GITHUB_ENV
67
+ fi
68
+ shell: bash
69
+ - run: |
70
+ docker pull jhpyle/docassemble
71
+ docker run --name docassemble_container \
72
+ --env DA_ADMIN_EMAIL="${{ env.DA_ADMIN_EMAIL }}" \
73
+ --env DA_ADMIN_PASSWORD="${{ env.DA_ADMIN_PASSWORD }}" \
74
+ --env DA_ADMIN_API_KEY="${{ env.DA_ADMIN_API_KEY }}" \
75
+ ${{ env.CONFIG_ARGS }} \
76
+ --cap-add SYS_PTRACE \
77
+ --memory="4gb" -d -p 80:80 jhpyle/docassemble
78
+ shell: bash
79
+ - name: "ALKiln github_server: Wait for server to start"
80
+ run: |
81
+ # ALKiln github_server: Wait for server to start
82
+
83
+ # Reset the SECONDS variable
84
+ SECONDS=0
85
+ # Wait 1 minute before pinging the server constantly
86
+ sleep 60
87
+
88
+ response="start"
89
+
90
+ while [[ $response != *"200"* ]]; do
91
+ echo "ALKiln github_server: Time elapsed: $SECONDS seconds"
92
+
93
+ # Check if max seconds till loading has passed
94
+ if [[ $SECONDS -ge $MAX_SECONDS_FOR_DOCKER ]]; then
95
+ echo "ALKiln github_server Error: Timed out waiting for docker container to serve the site. If you want to give your docker container more timem to install, set the MAX_SECONDS_FOR_DOCKER input for this action."
96
+ exit 1
97
+ fi
98
+
99
+ # Set the variable to a new string value in each iteration
100
+ response=$(curl -o /dev/null -s -w "%{http_code}\n" http://localhost:80/health_check?ready=1)
101
+ echo "ALKiln github_server: response is $response"
102
+
103
+ # Check if the string includes the desired text
104
+ if [[ $response == *"200"* ]]; then
105
+ echo "ALKiln github_server: Success! The docker container is ready!"
106
+ else
107
+ echo "ALKiln github_server: The docker container isn't ready yet. Will check again in 30 seconds."
108
+ sleep 30
109
+ fi
110
+
111
+ done
112
+ shell: bash
113
+
114
+ # Don't rely on the environment's version of python
115
+ - uses: actions/setup-python@v4
116
+ with:
117
+ python-version: '3.10'
118
+ - run: pip install docassemblecli
119
+ shell: bash
120
+ # Install the current directory onto the docassemble server
121
+ - run: dainstall . --apiurl http://localhost --apikey $DA_ADMIN_API_KEY
122
+ shell: bash
123
+
124
+ # Upload artifacts that subscribers can download on the Actions summary page
125
+ - name: "ALKiln github_server: Get path names with date and time"
126
+ if: ${{ always() }}
127
+ run: |
128
+ echo "GITHUB_SERVER_LOG_FILE_NAME=/tmp/alkiln_docker_logs-$(date +'%Y-%m-%d at %Hh%Mm%Ss' -u)UTC.txt" >> $GITHUB_ENV
129
+ echo "GITHUB_SERVER_ARTIFACT_NAME=alkiln_github_server-$(date +'%Y-%m-%d at %Hh%Mm%Ss' -u)UTC" >> $GITHUB_ENV
130
+ shell: bash
131
+ - name: "ALKiln github_server: Docker logs"
132
+ if: ${{ always() }}
133
+ run: |
134
+ echo "$(docker logs docassemble_container)" > "${{ env.GITHUB_SERVER_LOG_FILE_NAME }}"
135
+ shell: bash
136
+ - name: "ALKiln github_server: Upload artifacts folder"
137
+ if: ${{ always() }}
138
+ uses: actions/upload-artifact@v3
139
+ with:
140
+ name: ${{ env.GITHUB_SERVER_ARTIFACT_NAME }}
141
+ path: ${{ env.GITHUB_SERVER_LOG_FILE_NAME }}
@@ -79,13 +79,21 @@ da.pull = async function ( timeout ) {
79
79
  }; // Ends da.pull()
80
80
 
81
81
 
82
+ da.has_playground_task_finished = async function ( task_id, timeout ) {
83
+ return await da.has_task_finished( "/api/restart_status", task_id, timeout);
84
+ }
85
+
86
+ da.has_package_task_finished = async function ( task_id, timeout ) {
87
+ return await da.has_task_finished( "/api/package_update_status", task_id, timeout);
88
+ }
89
+
82
90
  // node -e 'require("./lib/docassemble/docassemble_api_REST.js").has_task_finished()'
83
- da.has_task_finished = async function ( task_id, timeout ) {
91
+ da.has_task_finished = async function ( api_endpoint, task_id, timeout ) {
84
92
  /* Check task status based on `task_id`. */
85
93
  let options = {
86
94
  method: 'GET',
87
95
  headers: {'X-API-Key': session_vars.get_dev_api_key()},
88
- url: `${ session_vars.get_da_server_url() }/api/restart_status`,
96
+ url: `${ session_vars.get_da_server_url() }${ api_endpoint }`,
89
97
  params: {
90
98
  task_id: task_id,
91
99
  },
@@ -329,9 +329,8 @@ da_i.pull = async function ( pull_options ) {
329
329
  }
330
330
  }; // Ends da_i.pull()
331
331
 
332
-
333
332
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").has_task_finished()'
334
- da_i.has_task_finished = async function ( task_id, options ) {
333
+ da_i.has_task_finished = async function ( api_endpoint, task_id, options ) {
335
334
  /** Check task status if it exists. If not, return true as task did
336
335
  * not require a server restart. See da_i.pull().
337
336
  * See https://docassemble.org/docs/api.html#restart_status. */
@@ -342,7 +341,7 @@ da_i.has_task_finished = async function ( task_id, options ) {
342
341
  try {
343
342
 
344
343
  // If server did need to restart, check if it's done.
345
- let response = await _da_REST.has_task_finished( task_id, timeout );
344
+ let response = await _da_REST.has_task_finished( api_endpoint, task_id, timeout );
346
345
  log.debug({ pre: `Task status: ${ response.status }. Docassemble status text: ${ response.data.status }. Task id: ${ task_id }` });
347
346
 
348
347
  if ( response.data.status === 'completed' ) {
@@ -353,16 +352,15 @@ da_i.has_task_finished = async function ( task_id, options ) {
353
352
  return false;
354
353
 
355
354
  } else if ( response.data.status === 'unknown' ) {
356
- // Server is working, but status 'unknown'? We shouldn't even
357
- // get here according to the API docs, but it feels weird to have
358
- // no default behavior. What to return? Throw?
355
+ // Server is working, but status 'unknown'? Bad design, but this
356
+ // does get returned when install packages, so return true
359
357
  log.info({
360
358
  pre: `\`task_id\` "${ task_id }" has expired. If it's been `
361
359
  + `less than 1 hour since the task started, the task is `
362
360
  + `complete. See https://docassemble.org/docs/api.html#restart_status.`,
363
- data: response,
361
+ data: response.data,
364
362
  });
365
- return null;
363
+ return true;
366
364
  } // ends status type
367
365
 
368
366
  } catch ( error ) {
@@ -378,8 +376,15 @@ da_i.has_task_finished = async function ( task_id, options ) {
378
376
 
379
377
  }; // Ends da_i.has_task_finished()
380
378
 
379
+ da_i.wait_for_playground_to_restart = async ( task_id, options ) => {
380
+ return await da_i.wait_for_server_to_restart( "/api/restart_status", task_id, options );
381
+ };
382
+
383
+ da_i.wait_for_package_to_restart = async ( task_id, options ) => {
384
+ return await da_i.wait_for_server_to_restart( "/api/package_update_status", task_id, options );
385
+ };
381
386
 
382
- da_i.wait_for_server_to_restart = async ( task_id, options ) => {
387
+ da_i.wait_for_server_to_restart = async ( api_endpoint, task_id, options ) => {
383
388
  /** Wait for the server to finish restarting in case a module needs to load.
384
389
  * This is a polling function because things may be returned
385
390
  * that aren't equivalent to the server being done with a task. */
@@ -410,7 +415,7 @@ da_i.wait_for_server_to_restart = async ( task_id, options ) => {
410
415
  while ( !done_restarting && num_tries <= max_loops ) {
411
416
  num_tries++;
412
417
  // Wait only a short time for the task to finish, not the full default timeout
413
- done_restarting = await da_i.has_task_finished( task_id, { timeout: wait_for_response } );
418
+ done_restarting = await da_i.has_task_finished( api_endpoint, task_id, { timeout: wait_for_response } );
414
419
  if ( !done_restarting ) {
415
420
  if ( num_tries < max_loops ) {
416
421
  // Show a slightly different message after a number of tries to reassure
@@ -10,10 +10,20 @@ let get_base_interview_url = async function () {
10
10
  * When finishing a test, we go to the sign out page (which is supposed to help even
11
11
  * when the user is not logged in.) We can't use `reset=2` because it caused an
12
12
  * 'Unable to locate interview session' error. */
13
+
13
14
  let server_url = session_vars.get_da_server_url();
14
- let dev_id = await docassemble_api_interface.get_dev_id();
15
- let project_name = session_vars.get_project_name();
16
- return `${ server_url }/interview?new_session=1&i=docassemble.playground${ dev_id }${ project_name }%3A`;
15
+
16
+ if (session_vars.get_install_method() === `server`) {
17
+
18
+ return `${ server_url }/interview?new_session=1&i=docassemble.${session_vars.get_repo_folder_name()}%3Adata/questions/`;
19
+
20
+ } else {
21
+
22
+ let dev_id = await docassemble_api_interface.get_dev_id();
23
+ let project_name = session_vars.get_project_name();
24
+ return `${ server_url }/interview?new_session=1&i=docassemble.playground${ dev_id }${ project_name }%3A`;
25
+
26
+ }
17
27
  };
18
28
 
19
29
  module.exports = get_base_interview_url;
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ const session_vars = require('../utils/session_vars');
4
+ const log = require('../utils/log');
5
+ const fg = require('fast-glob');
6
+
7
+ const server_install = async () => {
8
+ session_vars.save_install_method(`server`);
9
+
10
+ let current_dir = process.cwd();
11
+ let path = `${ current_dir }/docassemble/*`;
12
+ let dirs = fg.sync(
13
+ [ path ],
14
+ {
15
+ suppressErrors: true,
16
+ onlyDirectories: true
17
+ }
18
+ );
19
+
20
+ let folders = dirs.map(function (dir) { return dir.split('/').pop(); })
21
+
22
+ let chosen_folder = folders[0];
23
+ if ( folders.length === 0 ) {
24
+ let msg = `There are 0 folders at ${path}. Tests won't run.`;
25
+ log.error({ type: `setup`, data: msg })
26
+ throw new ReferenceError( msg );
27
+ }
28
+
29
+ if ( folders.length > 1 ) {
30
+ log.warn({
31
+ type: `setup`,
32
+ data: `There are ${folders.length} folders at ${path}: "${folders.join('", "')}". ALKiln chose to use the folder "${chosen_folder}".`
33
+ })
34
+ }
35
+
36
+ session_vars.save_repo_folder_name( chosen_folder );
37
+ }; // Ends server_install();
38
+
39
+ server_install();
@@ -39,7 +39,7 @@ const setup = async () => {
39
39
  let task_id = await da_i.pull();
40
40
  // Task id will be null if server did not need to restart
41
41
  if ( task_id ) {
42
- await da_i.wait_for_server_to_restart( task_id );
42
+ await da_i.wait_for_playground_to_restart( task_id );
43
43
  }
44
44
 
45
45
  // Finish
package/lib/scope.js CHANGED
@@ -1246,7 +1246,7 @@ module.exports = {
1246
1246
  + `column, but it is empty. The test might get unexpected results. See `
1247
1247
  + `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/#trigger.`
1248
1248
  + `\nThe row's data:\n${ row_as_story_table }`
1249
- + `\nThis field's data:\n${ JSON.stringify( field )}`;
1249
+ + `\nThis field's data:\n${ JSON.stringify( field.guesses )}`;
1250
1250
  reports.addToReport(scope, { type: `warning`, value: row_trigger_warning });
1251
1251
  rows_without_a_trigger_name.push( row_that_matches_this_field );
1252
1252
  } else {
@@ -1401,8 +1401,7 @@ module.exports = {
1401
1401
  let elem = document.querySelector( selector );
1402
1402
  // If the element has a sibling that's a label, we need to
1403
1403
  // interact with that label instead.
1404
- // `elem` has never been null from what we've seen
1405
- if ( elem.nextSibling && elem.nextSibling.tag === `label` ) {
1404
+ if ( elem && elem.nextSibling && elem.nextSibling.tag === `label` ) {
1406
1405
  return elem.nextSibling;
1407
1406
  } else { return elem; }
1408
1407
  // Pass in the selector as an argument
@@ -1422,14 +1421,15 @@ module.exports = {
1422
1421
  // future we should look into what happens with a `fieldset` for
1423
1422
  // `choices:`, etc.
1424
1423
  let custom_datatype = null, matches = null;
1425
- let group = elem.closest( `.da-form-group` );
1426
- if ( group && group.className ) {
1427
- matches = ` ${ group.className } `.match(/da-field-container-datatype-([^ ]+) /);
1428
- }
1429
- if ( matches ) {
1430
- custom_datatype = matches[1];
1424
+ if (elem) {
1425
+ let group = elem.closest( `.da-form-group` );
1426
+ if ( group && group.className ) {
1427
+ matches = ` ${ group.className } `.match(/da-field-container-datatype-([^ ]+) /);
1428
+ }
1429
+ if ( matches ) {
1430
+ custom_datatype = matches[1];
1431
+ }
1431
1432
  }
1432
-
1433
1433
  return custom_datatype;
1434
1434
  });
1435
1435
 
@@ -1585,7 +1585,13 @@ module.exports = {
1585
1585
  /* Set value of some `input` element to the given value. */
1586
1586
  let row_was_used = true;
1587
1587
 
1588
- let type = await handle.evaluate( elem => { return elem.getAttribute('type'); });
1588
+ let type = await handle.evaluate( elem => {
1589
+ if (elem) {
1590
+ return elem.getAttribute('type');
1591
+ } else {
1592
+ return 'text';
1593
+ }
1594
+ });
1589
1595
  if ( type === `radio` || type === `checkbox` ) {
1590
1596
 
1591
1597
  let [label] = await handle.$x(`following-sibling::label`);
@@ -1596,8 +1602,11 @@ module.exports = {
1596
1602
  let name = await label.evaluate( elem => { return elem.getAttribute('for'); });
1597
1603
  }
1598
1604
 
1599
- if ( type === `radio` ) { await label.evaluate( elem => { return elem.click(); }); }
1600
- else { await scope.setCheckbox( scope, { label, answer }); }
1605
+ if ( type === `radio` ) {
1606
+ await label.evaluate( elem => { return elem.click(); });
1607
+ } else {
1608
+ await scope.setCheckbox( scope, { label, answer });
1609
+ }
1601
1610
 
1602
1611
  await scope.afterStep(scope, { waitForShowIf: true });
1603
1612
 
@@ -2231,9 +2240,14 @@ module.exports = {
2231
2240
  } // ends act depending values
2232
2241
  }, // Ends scope.getThereIsAnotherValue()
2233
2242
 
2243
+ is_on_server_still: async function ( scope ) {
2244
+ let server_url = session_vars.get_da_server_url();
2245
+ return await scope.page.url().startsWith(server_url);
2246
+ },
2247
+
2234
2248
  // throw_if_no_interview_content()?
2235
2249
  has_interview_content: async function ( scope ) {
2236
- /** Return false if ther's no element indicating a da interview question,
2250
+ /** Return false if there's no element indicating a da interview question,
2237
2251
  * like a signature page, other fields, or interview text. Assumes that page has already
2238
2252
  * finished navigating. */
2239
2253
  let elem_da_question = await scope.page.$( `#daMainQuestion` );
@@ -2437,6 +2451,7 @@ module.exports = {
2437
2451
  /** Sets a non-story table variable to a "secret" value (i.e. an env var) */
2438
2452
  // Prevent pictures of a screen with a secret.
2439
2453
  scope.disable_error_screenshot = true;
2454
+
2440
2455
  let var_data = await scope.normalizeTable( scope, {
2441
2456
  var_data: [{
2442
2457
  var: var_name,
@@ -2635,7 +2650,7 @@ module.exports = {
2635
2650
  */
2636
2651
  // Don't take a picture of a failed login in case one of the inputs is correct
2637
2652
  scope.disable_error_screenshot = true;
2638
-
2653
+
2639
2654
  // If there is no browser open, start a new one
2640
2655
  if (!scope.browser) {
2641
2656
  scope.browser = await scope.driver.launch({ headless: !session_vars.DEBUG });
@@ -2737,7 +2752,30 @@ module.exports = {
2737
2752
  },
2738
2753
  buttons: async function (scope, { fields }) {
2739
2754
  // Get a random button
2740
- let field = faker.helpers.arrayElement( fields );
2755
+ let back_btn = null;
2756
+ fields = fields.filter((ff) => {
2757
+ if (ff.selector.includes("daquestionbackbutton")) {
2758
+ back_btn = ff;
2759
+ return false;
2760
+ }
2761
+ return true;
2762
+ });
2763
+ let field = null;
2764
+ if (back_btn != null && Math.random() > 0.9) {
2765
+ // 10% chance of clicking the back button. Back buttons don't undo
2766
+ // external network calls or saving things to Redis, so we make sure
2767
+ // programs can handle (or least don't crash) when users press back
2768
+ field = back_btn;
2769
+ } else {
2770
+ field = faker.helpers.arrayElement( fields );
2771
+ }
2772
+ if (field == null) {
2773
+ // Just skip it? Shouldn't happen but has before. Dig in more
2774
+ if (session_vars.get_debug()) {
2775
+ console.log(`A random button field choice was null. These were the fields to choose from:\n${ JSON.stringify(fields, null, 2) }`);
2776
+ }
2777
+ return;
2778
+ }
2741
2779
  let handle = await scope.get_handle_from_field( scope, { field: field });
2742
2780
  // Send it to be tapped
2743
2781
  await scope.funnel_the_answer( scope, { handle, field, answer: `doesn't matter` });
@@ -2806,7 +2844,6 @@ module.exports = {
2806
2844
  let custom_datatype = await scope.get_custom_datatype( scope, { handle });
2807
2845
  let is_custom_datatype = scope.get_random_input_for[ custom_datatype ] !== undefined;
2808
2846
  if ( is_custom_datatype ) {
2809
-
2810
2847
  // And the custom datatype has a hidden field (which will be the original field)
2811
2848
  let hidden_field = await scope.get_custom_datatype_hidden_field( scope, { handle });
2812
2849
  if ( hidden_field !== null ) {
@@ -2846,7 +2883,7 @@ module.exports = {
2846
2883
  } // ends if field type is one we cover
2847
2884
  } // ends if field type exists
2848
2885
 
2849
- } // ends if is custom datatype
2886
+ } // ends else after is custom datatype
2850
2887
 
2851
2888
  }, // ends scope.set_random_input_for.input
2852
2889
  }, // ends scope.set_random_input_for
package/lib/steps.js CHANGED
@@ -827,6 +827,11 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
827
827
  if ( has_thrown ) { return; }
828
828
  else { has_thrown = true; }
829
829
 
830
+ if (!( await scope.is_on_server_still(scope))) {
831
+ await scope.page.goBack();
832
+ resolve();
833
+ }
834
+
830
835
  let has_content = await scope.has_interview_content( scope );
831
836
 
832
837
  if ( !has_content ) {
@@ -872,6 +877,10 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
872
877
  await scope.continue( scope );
873
878
  } // ends if continue exists
874
879
  } // ends if buttons_exist
880
+ if (!( await scope.is_on_server_still(scope))) {
881
+ await scope.page.goBack();
882
+ resolve();
883
+ }
875
884
 
876
885
  let has_content = await scope.has_interview_content( scope );
877
886
  if ( !has_content ) {
@@ -891,7 +900,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
891
900
 
892
901
  if ( buttons_exist ) {
893
902
  process.stdout.write(`\x1b[36m${ 'v' }\x1b[0m`); // pressed button
894
- }
903
+ }
895
904
 
896
905
  resolve();
897
906
 
@@ -901,6 +910,11 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
901
910
  if ( has_thrown ) { return; }
902
911
  else { has_thrown = true; }
903
912
 
913
+ if (!( await scope.is_on_server_still(scope))) {
914
+ await scope.page.goBack();
915
+ resolve();
916
+ }
917
+
904
918
  // Throw a specific error if there's no interview content
905
919
  let has_content = await scope.has_interview_content( scope );
906
920
  if ( !has_content ) {
@@ -167,14 +167,54 @@ session_vars.delete_project_name = function () {
167
167
  }
168
168
  };
169
169
 
170
- // The file storing the artifacts path contains the name of the current artifacts folder.
170
+ // The filepath storing the artifacts path contains the name of the current artifacts folder.
171
171
  // The directory name it contains will be created or changed with each run of all the tests.
172
172
  // const file_storing_artifact_path = `_alkiln_test_artifacts_dir_name.json`;
173
173
  const artifacts_path_key = `artifacts_path`;
174
174
  session_vars.save_artifacts_path_name = function ( path_name ) {
175
175
  /* Save a key with the name of the current artifacts folder in case it doesn't exist. */
176
+ return session_vars.save_runtime_config_var({ key: artifacts_path_key, value: path_name });
177
+ }; // Ends save_artifacts_path_name()
178
+ session_vars.get_artifacts_path_name = function () {
179
+ /* Get the name of the current artifacts path. */
180
+ return session_vars.get_runtime_config_var( artifacts_path_key );
181
+ }; // Ends get_artifacts_path_name()
182
+
176
183
 
177
- // Ensure artifacts path file exists
184
+ const install_method_key = `da_install_method`;
185
+ session_vars.save_install_method = function ( install_method ) {
186
+ /* Save a key with the name of the current artifacts folder in case it doesn't exist. */
187
+ return session_vars.save_runtime_config_var({ key: install_method_key, value: install_method });
188
+ }; // Ends save_install_method()
189
+ session_vars.get_install_method = function () {
190
+ /* Get the name of the current install method. */
191
+ return session_vars.get_runtime_config_var( install_method_key );
192
+ }; // Ends get_install_method()
193
+
194
+
195
+ const repo_folder_key = `da_repo_folder_name`;
196
+ session_vars.save_repo_folder_name = function (repo_folder_name) {
197
+ /** Save a key with the name of the main package folder in the repository. */
198
+ return session_vars.save_runtime_config_var({ key: repo_folder_key, value: repo_folder_name });
199
+ }; // Ends session_vars.save_repo_folder_name()
200
+ session_vars.get_repo_folder_name = function () {
201
+ /** Get the name of the main folder in the repository. */
202
+ return session_vars.get_runtime_config_var( repo_folder_key );
203
+ }; // Ends session_vars.get_repo_folder_name()
204
+
205
+
206
+ session_vars.save_runtime_config_var = function ({ key, value }) {
207
+ /** Write to the runtime config file by adding a key/value pair to
208
+ * the json object there. The functions calling this are keeping
209
+ * track of the key, so they're useful to keep around.
210
+ *
211
+ * @param key {str} Key to use to store `value` in runtime config file
212
+ * @param value {any} Value to store in runtime config file
213
+ *
214
+ * @returns file {binary?} Runtime config file
215
+ */
216
+
217
+ // Ensure runtime config file exists
178
218
  if ( !fs.existsSync( runtime_config_path ) ) {
179
219
  // If the file doesn't exist, make one because next we need to read it
180
220
  fs.writeFileSync( runtime_config_path, JSON.stringify( {}, null, 2 ) );
@@ -182,27 +222,35 @@ session_vars.save_artifacts_path_name = function ( path_name ) {
182
222
 
183
223
  // Get the json, changing it, and re-write the file from scratch
184
224
  let json = JSON.parse( fs.readFileSync( runtime_config_path ) );
185
- json[ artifacts_path_key ] = path_name;
225
+ json[ key ] = value;
186
226
  let file = fs.writeFileSync( runtime_config_path, JSON.stringify( json, null, 2 ) );
187
227
 
188
228
  if ( session_vars.get_debug() ) {
189
- console.log( `ALKiln debug: Stored name of artifacts directory "${ path_name }" locally.` );
229
+ console.log( `ALKiln debug runtime config var: Stored "${key}" locally as "${value}".` );
190
230
  }
191
231
  return file;
192
- }; // Ends save_artifacts_path_name()
232
+ }; // Ends session_vars.save_runtime_config_var()
193
233
 
194
- session_vars.get_artifacts_path_name = function () {
195
- /* Get the name of the current artifacts path. */
234
+ session_vars.get_runtime_config_var = function ( key ) {
235
+ /** Get the value of the given key from the runtime config file.
236
+ *
237
+ * @param key {str} Key in runtime config file that contains the desired value
238
+ * @returns value || null {any} Value of key in runtime config file. If it
239
+ * doesn't exist, returns null.
240
+ */
196
241
  try {
197
242
  let json = JSON.parse( fs.readFileSync( runtime_config_path ));
198
- let path_name = json[ artifacts_path_key ];
243
+ let value = json[ key ];
244
+ if ( session_vars.get_debug() ) {
245
+ console.log( `ALKiln debug runtime config var: "${key}"" is "${value}".` );
246
+ }
199
247
 
200
- if ( session_vars.get_debug() ) { console.log( `ALKiln debug: Artifacts directory is "${ path_name }".` ); }
201
- return path_name;
248
+ return value;
202
249
  } catch ( error ) {
203
250
  return null;
204
251
  }
205
- }; // Ends get_artifacts_path_name()
252
+ }; // Ends session_vars.get_runtime_config_var()
253
+
206
254
 
207
255
  session_vars.set_sources_paths = function ( paths ) {
208
256
  /** Returns whether at least one of the given path exists. Store
@@ -235,7 +283,7 @@ session_vars.set_sources_paths = function ( paths ) {
235
283
  // Warn about missing paths
236
284
  if ( missing_paths.length > 0 ) {
237
285
  console.warn(
238
- `ALKiln WARNING: ALKiln can't find folders at "${ paths.join(", ") }".`
286
+ `ALKiln WARNING: ALKiln can't find folders at "${ paths.join('", "') }".`
239
287
  );
240
288
  }
241
289
 
@@ -245,10 +293,14 @@ session_vars.set_sources_paths = function ( paths ) {
245
293
  return false;
246
294
  }
247
295
 
296
+ console.info(
297
+ `ALKiln info: Did find "sources" folder(s) at "${ existing_paths.join('", "') }".`
298
+ )
299
+
248
300
  // Otherwise, store valid paths
249
301
  session_vars._sources_paths = existing_paths;
250
302
  return true;
251
- };
303
+ }; // End session_vars.set_sources_paths()
252
304
 
253
305
  session_vars.get_sources_paths = function () {
254
306
  /** Get the "sources" folder path. Should never be undefined during
@@ -259,6 +311,7 @@ session_vars.get_sources_paths = function () {
259
311
  return session_vars._sources_paths;
260
312
  };
261
313
 
314
+
262
315
  session_vars.validateEnvironment = function() {
263
316
  /** Throw a useful error for the developer if any of the env vars
264
317
  * required by absolutely all tests are missing. Gather all
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.4.2",
3
+ "version": "5.5.0",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -9,6 +9,7 @@
9
9
  "local": "npm run setup && npm run cucumber; npm run takedown",
10
10
  "setup": "./lib/docassemble/setup.js",
11
11
  "takedown": "./lib/docassemble/takedown.js",
12
+ "server_install": "./lib/docassemble/server_install.js",
12
13
  "unit": "mocha tests/unit_tests/*.test.*"
13
14
  },
14
15
  "repository": {
@@ -50,6 +51,7 @@
50
51
  "diff": "5.0.0"
51
52
  },
52
53
  "bin": {
54
+ "alkiln-server-install": "./lib/docassemble/server_install.js",
53
55
  "alkiln-setup": "./lib/docassemble/setup.js",
54
56
  "alkiln-takedown": "./lib/docassemble/takedown.js",
55
57
  "alkiln-run": "./lib/run_cucumber.js"
package/setup.py CHANGED
@@ -50,10 +50,10 @@ setup(name='docassemble.ALKilnTests',
50
50
  author='plocket',
51
51
  author_email='example@example.com',
52
52
  license='The MIT License (MIT)',
53
- url='https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing',
53
+ url='https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln',
54
54
  packages=find_packages(),
55
55
  namespace_packages=['docassemble'],
56
- install_requires=['docassemble.ALToolbox>=0.5.1'],
56
+ install_requires=['docassemble.ALToolbox>=0.8.2', 'docassemble.AssemblyLine>=2.24.0'],
57
57
  zip_safe=False,
58
58
  package_data=find_package_data(where='docassemble/ALKilnTests/', package='docassemble.ALKilnTests'),
59
59
  )