@suffolklitlab/alkiln 4.10.2 → 4.10.3-feat-debug-cache
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +1 -0
- package/CHANGELOG.md +8 -2
- package/CONTRIBUTING.md +205 -0
- package/LICENSE +21 -0
- package/README.md +51 -11
- package/lib/docassemble/docassemble_api_interface.js +11 -9
- package/lib/scope.js +48 -70
- package/lib/steps.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @plocket @BryceStevenWilley
|
package/CHANGELOG.md
CHANGED
|
@@ -38,9 +38,15 @@ Format:
|
|
|
38
38
|
### Security
|
|
39
39
|
-
|
|
40
40
|
-->
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
## [Unreleased]
|
|
42
|
+
## Added
|
|
43
|
+
- Contribution docs.
|
|
44
|
+
- Story table can take screenshots after each visited page.
|
|
45
|
+
- Projects created in da each have a unique name. We assume for now this will not build up test packages in the server.
|
|
43
46
|
|
|
47
|
+
## [4.10.3] - 2023-01-11
|
|
48
|
+
### Removed
|
|
49
|
+
- Internal - ignore local test output files. Part 2 of the process out of 2.
|
|
44
50
|
|
|
45
51
|
## [4.10.2] - 2023-01-11
|
|
46
52
|
### Removed
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
For documentation changes, you can just edit and make a pull request. Ignore the instructions below.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
If you are not already in contact with us, you can join the #assembly-line-test channel in the [docassemble Slack](https://docassemble.org/docs/support.html#tocAnchor-1-1) or [make a new issue](https://github.com/SuffolkLITLab/ALKiln/issues/new) and mention @niharikasingh to let us know you're interested.
|
|
8
|
+
|
|
9
|
+
1. Asking to join as a contributor is simplest. You can fork, but you will have to change lines in your `.env` file and may have to set GitHub secrets in your repository. You have to talk to us either way.
|
|
10
|
+
1. Clone the repository locally: `git clone git@github.com:SuffolkLITLab/ALKiln.git`
|
|
11
|
+
1. Run `npm install`
|
|
12
|
+
1. Create [your `.env` file](#env) at the root of the project.
|
|
13
|
+
1. Contact @niharikasingh to get important information for your `.env` file.
|
|
14
|
+
1. Run `npm run setup`
|
|
15
|
+
|
|
16
|
+
Advanced contributors: If you're going to work on our docassemble internal testing interviews, do so as a contributor on this repository. Avoid forking if you can. The reasons are complex. If you feel a need to fork, reach out to us.
|
|
17
|
+
|
|
18
|
+
## PRs
|
|
19
|
+
|
|
20
|
+
Before making a pull request
|
|
21
|
+
|
|
22
|
+
1. Search issues to see if what you are thinking of has been discussed yet.
|
|
23
|
+
1. Make an issue about the change you are thinking of.
|
|
24
|
+
1. Assign yourself to an issue.
|
|
25
|
+
1. Make a new branch.
|
|
26
|
+
1. Make your edits. Feel free to [run tests](#running-internal-tests) as you go.
|
|
27
|
+
1. When you're ready to push
|
|
28
|
+
1. Run `npm run test` to make sure your code passes all our tests.
|
|
29
|
+
1. Make an entry in the `CHANGELOG.md` describing your change.
|
|
30
|
+
1. AVOID changing the version number. We'll do that after merging your changes.
|
|
31
|
+
1. If needed, update the `README.md` (e.g. the cheat sheet section) and/or `CONTRIBUTING.md` docs. Changes to our architecture might require editing `CONTRIBUTING.md`.
|
|
32
|
+
1. Push to GitHub.
|
|
33
|
+
1. Wait for the GitHub tests to pass. You may have to re-run failing tests—server restarts or race conditions may interfere with them. We have reduced them greatly, but some are impossible to avoid.
|
|
34
|
+
1. Make your pull request. Work in progress pull requests are great too. A pull request must be reviewed before it can be merged.
|
|
35
|
+
|
|
36
|
+
## .env
|
|
37
|
+
|
|
38
|
+
You need to make your own `.env` file. It contains sensitive variables, like passwords. We use them for our internal tests. The file should look something like the below:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# For everyone
|
|
42
|
+
|
|
43
|
+
DOCASSEMBLE_DEVELOPER_API_KEY=<ask @niharikasingh>
|
|
44
|
+
SERVER_URL=https://apps-dev.suffolklitlab.org
|
|
45
|
+
REPO_URL=https://github.com/SuffolkLITLab/alkiln
|
|
46
|
+
SERVER_RELOAD_TIMEOUT_SECONDS=
|
|
47
|
+
MAX_SECONDS_FOR_SETUP=
|
|
48
|
+
|
|
49
|
+
# For internal tests
|
|
50
|
+
|
|
51
|
+
# Set DEBUG to 1 to run in non-headless mode and see the test run live
|
|
52
|
+
DEBUG=
|
|
53
|
+
# If you edit interview files (.yml files), you should push to a new
|
|
54
|
+
# branch and update BRANCH_PATH to that branch name
|
|
55
|
+
BRANCH_PATH=v4
|
|
56
|
+
USER1_EMAIL=<ask @niharikasingh>
|
|
57
|
+
USER1_PASSWORD=<ask @niharikasingh>
|
|
58
|
+
SECRET_VAR1=secret-var1-value
|
|
59
|
+
SECRET_VAR2=secret-var2-value
|
|
60
|
+
SECRET_FOR_MISSING_FIELD=secret for missing field
|
|
61
|
+
SECRET_INVALID_THERE_IS_ANOTHER=invalid value for there_is_another
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Running internal tests
|
|
65
|
+
|
|
66
|
+
Only run this the first time you run tests in this repository to make the code available to test on the docassemble server:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run setup
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you run `takedown`, you will then have to run `setup` again.
|
|
73
|
+
|
|
74
|
+
Use the syntax below to trigger all tests (cucumber and unit tests):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run only cucumber tests:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm run cucumber
|
|
84
|
+
|
|
85
|
+
# To test Features or Scenarios specific tags:
|
|
86
|
+
npm run cucumber -- "--tags" "@tagname"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To run the unit tests in isolation:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm run unit
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If you or someone else changes the interview code in `./docassemble`, you have to clean up the old data on the server before running `setup` again:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm run takedown
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## How the tests run
|
|
102
|
+
|
|
103
|
+
Our tests require the user/developer to have a docassemble server on which they host these interviews (online forms) and at least one developer account. With our help, our users set up the tests to run when they commit new code or to run manually. When GitHub runs the tests, it does the following:
|
|
104
|
+
|
|
105
|
+
1. Sets up on the docassemble server. In a given developer account:
|
|
106
|
+
1. Creates a new interview for this particular branch of the repository.
|
|
107
|
+
1. Pulls in the code from that branch into the docassemble server.
|
|
108
|
+
1. Runs the tests
|
|
109
|
+
1. Pretends to be an anonymous account that comes to the form and inputs answers.
|
|
110
|
+
1. Writes up reports and possibly takes screenshots to show to the developer.
|
|
111
|
+
1. Cleans up by deleting the interview it created
|
|
112
|
+
|
|
113
|
+
When the developer commits code to GitHub, their account triggers our code, passing along variables that our code needs, like their GitHub secret of the API key for their docassemble server developer account.
|
|
114
|
+
|
|
115
|
+
## Very general architecture of files and folders
|
|
116
|
+
|
|
117
|
+
An honest look at our current project architecture—some of our files and folders, what they do, and how they interact.
|
|
118
|
+
|
|
119
|
+
### Logic architecture
|
|
120
|
+
|
|
121
|
+
ALKiln uses [cucumber](https://cucumber.io/docs/installation/javascript/)[^1] with [Gherkin](https://cucumber.io/docs/gherkin/reference/) syntax, [puppeteer](https://pptr.dev/), and [chai](https://www.chaijs.com/).
|
|
122
|
+
|
|
123
|
+
#### .feature files
|
|
124
|
+
|
|
125
|
+
The `.feature` files are written in Gherkin, a syntax cucumber uses. The "code" in there relies on the functions set up in `./lib/steps.js`.
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
// The test_something.feature file step
|
|
129
|
+
Then I sign
|
|
130
|
+
|
|
131
|
+
// relies on the code in steps.js
|
|
132
|
+
Then('I sign', { timeout: -1 }, async () => {
|
|
133
|
+
return wrapPromiseWithTimeout(
|
|
134
|
+
scope.steps.sign( scope ),
|
|
135
|
+
scope.timeout
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### steps.js
|
|
141
|
+
|
|
142
|
+
`./lib/steps.js` defines all the Gherkin sentences that our users (the developers) can write. See [cucumber documentation on defining steps](https://cucumber.io/docs/cucumber/step-definitions/?lang=javascript). We sometimes use "cucumber expressions" and, but often use regular expressions as we often want to be more flexible about the user's input.
|
|
143
|
+
|
|
144
|
+
The file handles things like:
|
|
145
|
+
|
|
146
|
+
* Creating state
|
|
147
|
+
* Defining steps for the users/developers
|
|
148
|
+
* Defining steps for internal testing, like testing for appropriate errors and error messages
|
|
149
|
+
* Finishing a Scenario
|
|
150
|
+
* Finishing all tests
|
|
151
|
+
|
|
152
|
+
#### scope.js
|
|
153
|
+
|
|
154
|
+
`./lib/steps.js` makes heavy use of `./lib/scope.js`, where most of the action happens. We have decided to mostly keep it in one file to avoid bouncing back and forth between files.
|
|
155
|
+
|
|
156
|
+
Many `./lib/steps.js` steps make use of the `scope.steps` object. It is there to avoid duplicating code between steps.
|
|
157
|
+
|
|
158
|
+
The file handles things like:
|
|
159
|
+
|
|
160
|
+
* Finding form fields on a page
|
|
161
|
+
* Filling out form fields
|
|
162
|
+
* Generating random data for form fields
|
|
163
|
+
* Waiting for pages to load
|
|
164
|
+
* Checking for server restarts
|
|
165
|
+
* Generating test report content
|
|
166
|
+
|
|
167
|
+
### Logging
|
|
168
|
+
|
|
169
|
+
`./lib/utils/log.js` takes care of logging information of various kinds (normal, debug, error) to the command line in ways we hope are useful. We should probably find a logging library instead, but this is what we have for now. An important feature is that it ensures the messages clearly belong to ALKiln and not to some other process.
|
|
170
|
+
|
|
171
|
+
### session_vars.js
|
|
172
|
+
|
|
173
|
+
The `./lib/utils/session_var.js` file keeps track of what you might otherwise think of as environment variables. We like to think of them as constants, but some of them do need to go through functions, so for consistency we get them all through functions. Also because it was easier to test them when they are functions.
|
|
174
|
+
|
|
175
|
+
### Setup and takedown logic
|
|
176
|
+
|
|
177
|
+
All of the setup and takedown logic is in the `./lib/docassemble` directory. It is named after the docassemble API code that is in there. Also, setup and takedown interact very closely with docassemble and the docassemble server.
|
|
178
|
+
|
|
179
|
+
Other files also use the docassemble API functions, so this folder is in a bit of a messy position.
|
|
180
|
+
|
|
181
|
+
### Bash commands
|
|
182
|
+
|
|
183
|
+
When you `npm run` the scripts in our `./package.json` you can setup, run, or takedown tests. We are working on converting this to a command line tool to avoid some complexity in GitHub actions, increase security, and align more with what this framework has grown into. (TODO: Define this better)
|
|
184
|
+
|
|
185
|
+
### GitHub composite action
|
|
186
|
+
|
|
187
|
+
Our `./action.yml` is a [composite action](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action). The users/developers let it do most of the work for them. It installs npm, installs our repo, then uses our repo to set up the tests, run the tests, generate and save reports, errors, and screenshots, and clean up the tests.
|
|
188
|
+
|
|
189
|
+
### Internal tests
|
|
190
|
+
|
|
191
|
+
We have our own docassemble package in our repo to test our own end-to-end tests. Most of what the package needs is in `./docassemble`. That docassemble package is what the test setup pulls into the docassemble server. Our internal end-to-end cucumber tests are `.feature` files that are stored in `./docassemble/ALKilnTests/data/sources`. The interview (online form) `.yml` files that those `.feature` files use are also deep in the `./docassemble` package. You probably won't be touching the interview files. If you get curious, feel free to ask us.
|
|
192
|
+
|
|
193
|
+
There are other files the docassemble package needs, like `./setup.py`, so if you see them around, don't worry about them.
|
|
194
|
+
|
|
195
|
+
Our unit tests are in `./tests/unit_tests` and their filenames end in `.test.js`. Like the cucumber tests, they use chai for assertions. The fixtures for the tests are also contained in that folder and end in `.fixtures.js`.
|
|
196
|
+
|
|
197
|
+
If you want to know the commands, you can go to the [instructions for running tests](#running-internal-tests).
|
|
198
|
+
|
|
199
|
+
### Files that you don't need to look at
|
|
200
|
+
|
|
201
|
+
`index.js` and `world.js` are short files that cucumber needs. `./lib/utils/langs.js` used to work, but we're not sure it does anymore. No one has used it yet, so we're waiting to experiment until someone expresses a need or we have some extra time.
|
|
202
|
+
|
|
203
|
+
## Footnotes
|
|
204
|
+
|
|
205
|
+
[1] To be clear, our framework is a misuse of cucumberjs. cucumberjs is geared towards behavior driven development. We try to make BDD available to our developers, but it's not always possible and not necessarily our goal.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 plocket
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
1
|
# ALKiln
|
|
2
|
-
Integrated automated end-to-end testing with docassemble, puppeteer, cucumber, and chai.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
dependent on it.
|
|
3
|
+
ALKiln, or Assembly Line Kiln, is an open source project that helps [docassemble](https://docassemble.org/) developers create automated tests for their interviews (online forms). It works well with the [Document Assembly Line Project](https://suffolklitlab.org/docassemble-AssemblyLine-documentation) but isn't dependent on it.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
<!--
|
|
6
|
+
https://github.com/18F/open-source-guide/blob/18f-pages/pages/making-readmes-readable.md
|
|
7
|
+
√ What is this repo or project? (You can reuse the repo description you used earlier because this section doesn’t have to be long.)
|
|
8
|
+
√ How does it work?
|
|
9
|
+
√ Who will use this repo or project?
|
|
10
|
+
√ What is the goal of this project?
|
|
11
|
+
-->
|
|
8
12
|
|
|
9
|
-
##
|
|
10
|
-
Read about how to use this framework at https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/.
|
|
13
|
+
## Use ALKiln
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
Read about how to use this framework in [Assembly Line's documentation](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/automated_integrated_testing/).
|
|
16
|
+
|
|
17
|
+
If you have questions or need to report a bug, [start a new issue](https://github.com/SuffolkLITLab/ALKiln/issues/new).
|
|
18
|
+
|
|
19
|
+
The following documentation is for internal developers—those working on ALKiln itself.
|
|
20
|
+
|
|
21
|
+
## Our code
|
|
22
|
+
|
|
23
|
+
Our code pretends to be a human interacting with online forms (interviews) that have been made with the [docassemble](https://docassemble.org) open source platform. It looks at the HTML DOM, manipulates it, and interacts with it.
|
|
24
|
+
|
|
25
|
+
It uses [cucumber](https://cucumber.io/docs/installation/javascript/), [puppeteer](https://pptr.dev/), and [chai](https://www.chaijs.com/).
|
|
26
|
+
|
|
27
|
+
## Our users
|
|
28
|
+
|
|
29
|
+
Our users are developers of online forms (interviews). Usually they're very new to coding and are unfamiliar with GitHub and with a lot of coding concepts. If they know how to code, it tends to be in Python because that's what docassemble uses. They usually don't have the resource or funding to put into learning these skills.
|
|
30
|
+
|
|
31
|
+
For example, a pro bono law firm is trying to write a form that people can use to create a restraining order. They're coding it using docassemble. Their users are already going through a hard time, and the forms make them outline their heart wrenching experiences in detail. The developers need to make sure their users don't get system errors right in the middle of that. They use our framework to run the end-to-end tests.
|
|
32
|
+
|
|
33
|
+
They do good work and we hope to give them a less code-focused way to automate their testing. They avoid having to learn Javascript, GitHub actions, and other such things.
|
|
34
|
+
|
|
35
|
+
To do that, we use [cucumberjs](https://cucumber.io/docs/installation/javascript/), which uses [Gherkin](https://cucumber.io/docs/gherkin/reference/) syntax to give a user/developer a more human-like syntax to instruct alkiln on what answers to give to the user's online form.
|
|
36
|
+
|
|
37
|
+
## Contributing
|
|
38
|
+
|
|
39
|
+
Read about contributing in our [CONTRIBUTING.md document](CONTRIBUTING.md). Here's a quick cheat sheet for some commands once you're up and running:
|
|
40
|
+
|
|
41
|
+
## Cheat sheet
|
|
42
|
+
|
|
43
|
+
*Once you've already read the contributing documentation, you can use these as quick reminders for running our internal tests.*
|
|
44
|
+
To set up for the integration tests, create the project on the server:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
15
47
|
npm run setup
|
|
16
48
|
```
|
|
17
49
|
|
|
18
50
|
Use the syntax below to trigger specific tags:
|
|
19
|
-
|
|
51
|
+
|
|
52
|
+
```bash
|
|
20
53
|
npm run cucumber -- "--tags" "@tagname"
|
|
21
54
|
```
|
|
22
55
|
|
|
23
56
|
To run the unit tests in isolation:
|
|
24
|
-
|
|
57
|
+
|
|
58
|
+
```bash
|
|
25
59
|
npm run unit
|
|
26
60
|
```
|
|
61
|
+
|
|
62
|
+
If you or someone else changes the interview code in `./docassemble`, you have to clean up the old data on the server before running `setup` again:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run takedown
|
|
66
|
+
```
|
|
@@ -204,16 +204,16 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
204
204
|
let wait_seconds = creation_options.wait_seconds || 5;
|
|
205
205
|
|
|
206
206
|
let project_name = null;
|
|
207
|
-
let
|
|
207
|
+
let num_tries = 0;
|
|
208
208
|
|
|
209
209
|
// Try to create a project. If the project name is already taken, increment and loop again.
|
|
210
|
-
while (
|
|
211
|
-
|
|
210
|
+
while ( num_tries <= max_tries ) {
|
|
211
|
+
num_tries++;
|
|
212
212
|
try {
|
|
213
213
|
|
|
214
|
-
// Try
|
|
214
|
+
// Try adding a new timestamp to the name. Must remove all non-valid characters
|
|
215
215
|
// TODO: This sanitization is already done in session_vars, so remove it from here
|
|
216
|
-
project_name = session_vars.get_project_base_name().replace(/[^A-Za-z0-9]/g, '') +
|
|
216
|
+
project_name = session_vars.get_project_base_name().replace(/[^A-Za-z0-9]/g, '') + `${Date.now()}`;
|
|
217
217
|
log.info({ pre: `Trying to create a docassemble Project with the name ${ project_name }` });
|
|
218
218
|
let response = await _da_REST.create_project( project_name, timeout );
|
|
219
219
|
log.info({ pre: `The docassemble server is ready. Created a Project named ${ project_name }` });
|
|
@@ -226,7 +226,7 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
226
226
|
|
|
227
227
|
if ( response && response.data ) {
|
|
228
228
|
if ( response.data.includes( `Invalid project` ) ) {
|
|
229
|
-
if (
|
|
229
|
+
if ( num_tries < max_tries ) {
|
|
230
230
|
// A non-unique project name. Try the next project name.
|
|
231
231
|
continue;
|
|
232
232
|
|
|
@@ -256,11 +256,11 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
256
256
|
|
|
257
257
|
// If it's a timeout error, server is busy. Try again after waiting.
|
|
258
258
|
// Why are we using `max_tries` for timeout error as well?
|
|
259
|
-
if ( is_timeout_error( error ) &&
|
|
259
|
+
if ( is_timeout_error( error ) && num_tries < max_tries ) {
|
|
260
260
|
log.info({ pre: `The docassemble server was busy when we tried to `
|
|
261
|
-
+ `create the Project. Attempt ${
|
|
261
|
+
+ `create the Project. Attempt ${ num_tries } of `
|
|
262
262
|
+ `${ max_tries }. Will try again in ${ wait_seconds } seconds.` });
|
|
263
|
-
|
|
263
|
+
num_tries--; // Try this name again next time
|
|
264
264
|
await time.waitForTimeout( wait_seconds * 1000 );
|
|
265
265
|
continue;
|
|
266
266
|
|
|
@@ -272,6 +272,8 @@ da_i.loop_to_create_project = async function( creation_options ) {
|
|
|
272
272
|
} // ends type of non-response error
|
|
273
273
|
} // ends type of response error
|
|
274
274
|
} // ends try
|
|
275
|
+
|
|
276
|
+
// TODO: Add a message that we're done trying to create a project, successful or not
|
|
275
277
|
} // ends while
|
|
276
278
|
}; // Ends da_i.loop_to_create_project()
|
|
277
279
|
|
package/lib/scope.js
CHANGED
|
@@ -52,7 +52,7 @@ let is_secret_var = function(row) {
|
|
|
52
52
|
/* `row` might be a row, a source row, or an original row. */
|
|
53
53
|
let original = get_original_row(row);
|
|
54
54
|
return original.flags !== undefined && original.flags.secret_name !== undefined;
|
|
55
|
-
}
|
|
55
|
+
};
|
|
56
56
|
|
|
57
57
|
let fromBase64 = function(base64_str) {
|
|
58
58
|
/* Takes a base64 encoded string and returns it decoded.
|
|
@@ -636,54 +636,6 @@ module.exports = {
|
|
|
636
636
|
|
|
637
637
|
return false;
|
|
638
638
|
},
|
|
639
|
-
|
|
640
|
-
setVar: async function ( scope, var_name, value ) {
|
|
641
|
-
/* Use variable names and values to interact with buttons that
|
|
642
|
-
* have values, text input elements, textareas, and dropdowns.
|
|
643
|
-
* Returns the element that was found.
|
|
644
|
-
*
|
|
645
|
-
* TODO:
|
|
646
|
-
* - Birthdate fields that will be showing up relatively soon
|
|
647
|
-
* - Time fields?
|
|
648
|
-
*/
|
|
649
|
-
let base64_var_name = toBase64( var_name );
|
|
650
|
-
|
|
651
|
-
// Will catch some radio and checkbox inputs too, which will then be rejected
|
|
652
|
-
// Note: I think `showif`s with `code` don't use `data-saveas`
|
|
653
|
-
let to_manipulate = await Promise.race([
|
|
654
|
-
scope.page.waitForSelector(`button[name="${base64_var_name}"][value="${value}"]`), // button
|
|
655
|
-
scope.page.waitForSelector(`:not(button)[name="${base64_var_name}"]`), // others (not showif)
|
|
656
|
-
scope.page.waitForSelector(`*[data-saveas="${base64_var_name}"] input`), // text input showif
|
|
657
|
-
scope.page.waitForSelector(`*[data-saveas="${base64_var_name}"] textarea`), // textarea showif
|
|
658
|
-
scope.page.waitForSelector(`*[data-saveas="${base64_var_name}"] select`), // select showif/hideif
|
|
659
|
-
]);
|
|
660
|
-
|
|
661
|
-
// Throw error for checkboxes and radios
|
|
662
|
-
let type = await to_manipulate.evaluate( elem => { return elem.getAttribute('type') });
|
|
663
|
-
if ( type === `checkbox` || type === `radio` ) {
|
|
664
|
-
throw new TypeError(`You need the ${type} step, e.g. 'I tap the ${type} variable "some_dict" with the value "choice_value"'`);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Actions based on element
|
|
668
|
-
let tagName = await to_manipulate.evaluate( elem => { return elem.tagName });
|
|
669
|
-
tagName = tagName.toLowerCase();
|
|
670
|
-
|
|
671
|
-
if ( tagName === `button` ) { // Tap buttons
|
|
672
|
-
await scope.tapElement( scope, to_manipulate );
|
|
673
|
-
|
|
674
|
-
} else if ( tagName === `select` ) { // Choose drop-down option
|
|
675
|
-
await to_manipulate.select(value);
|
|
676
|
-
|
|
677
|
-
} else if ( tagName === `input` || tagName === `textarea` ) {
|
|
678
|
-
// Set text field values (includes any other input fields)
|
|
679
|
-
await to_manipulate.evaluate( el => { el.value = '' });
|
|
680
|
-
await to_manipulate.type( value );
|
|
681
|
-
// TODO: Add hideif to this too
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Do your own waiting if you need to in the step
|
|
685
|
-
return to_manipulate;
|
|
686
|
-
}, // Ends scope.setVar()
|
|
687
639
|
|
|
688
640
|
continue_button_selector: `fieldset.da-field-buttons button[type="submit"].btn-primary`,
|
|
689
641
|
signature_selector: `fieldset .dasigsave`,
|
|
@@ -1736,7 +1688,7 @@ module.exports = {
|
|
|
1736
1688
|
await scope.afterStep(scope, { waitForShowIf: true });
|
|
1737
1689
|
},
|
|
1738
1690
|
canvas: async function ( scope, { handle, answer }) {
|
|
1739
|
-
await scope.
|
|
1691
|
+
await scope.draw_signature( scope );
|
|
1740
1692
|
await scope.afterStep( scope );
|
|
1741
1693
|
},
|
|
1742
1694
|
input: async function ( scope, { handle, answer }) {
|
|
@@ -1919,7 +1871,7 @@ module.exports = {
|
|
|
1919
1871
|
}, 500)
|
|
1920
1872
|
},
|
|
1921
1873
|
|
|
1922
|
-
|
|
1874
|
+
draw_signature: async function ( scope ) {
|
|
1923
1875
|
/* On a signature page, there should only be a single signature canvas
|
|
1924
1876
|
* and a continue button. Draw in the #dasigcanvas element. `<canvas>`
|
|
1925
1877
|
* takes a while to load. */
|
|
@@ -2050,6 +2002,13 @@ module.exports = {
|
|
|
2050
2002
|
if ( row_used.source ) { row_used.source.times_used += 1 }
|
|
2051
2003
|
row_used.times_used += 1;
|
|
2052
2004
|
|
|
2005
|
+
let { id } = scope.examinePageID( scope );
|
|
2006
|
+
await scope.page.screenshot({
|
|
2007
|
+
path: `./${ scope.paths.scenario }/pic-${Date.now()}-${ id }.jpg`,
|
|
2008
|
+
type: 'jpeg',
|
|
2009
|
+
fullPage: true
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2053
2012
|
// TODO: special continue/navigation button function that handles url and all?
|
|
2054
2013
|
|
|
2055
2014
|
// Have to catch errors right now or may get stuck in an infinite loop?
|
|
@@ -2063,6 +2022,7 @@ module.exports = {
|
|
|
2063
2022
|
// Discuss moving this out of here. Having to pass in `id` just for
|
|
2064
2023
|
// these messages smells bad. Con: Already a lot of code out there.
|
|
2065
2024
|
error = await scope.waitUntilContinued( scope, { url: page_url, id });
|
|
2025
|
+
|
|
2066
2026
|
// if ( error.was_found ) {
|
|
2067
2027
|
// await scope.throwPageError( scope, { id: id });
|
|
2068
2028
|
// }
|
|
@@ -2101,6 +2061,14 @@ module.exports = {
|
|
|
2101
2061
|
if ( !did_navigate && ensure_navigation ) {
|
|
2102
2062
|
// All fields on this page should be taken care of
|
|
2103
2063
|
let page_url = await scope.page.url();
|
|
2064
|
+
|
|
2065
|
+
let { id } = scope.examinePageID( scope );
|
|
2066
|
+
await scope.page.screenshot({
|
|
2067
|
+
path: `./${ scope.paths.scenario }/pic-${Date.now()}-${ id }.jpg`,
|
|
2068
|
+
type: 'jpeg',
|
|
2069
|
+
fullPage: true
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2104
2072
|
await scope.continue( scope );
|
|
2105
2073
|
// Have to catch errors right now or may get stuck in an infinite loop?
|
|
2106
2074
|
// TODO: Don't catch errors - maybe the developer expected/wanted an error
|
|
@@ -2356,7 +2324,7 @@ module.exports = {
|
|
|
2356
2324
|
});
|
|
2357
2325
|
|
|
2358
2326
|
await scope.afterStep( scope );
|
|
2359
|
-
}, // Ends screenshot()
|
|
2327
|
+
}, // Ends scope.steps.screenshot()
|
|
2360
2328
|
|
|
2361
2329
|
add_json_vars_to_report: async ( scope ) => {
|
|
2362
2330
|
/** Uses https://docassemble.org/docs/functions.html#js_get_interview_variables
|
|
@@ -2371,7 +2339,7 @@ module.exports = {
|
|
|
2371
2339
|
type: `json`,
|
|
2372
2340
|
value: JSON.stringify( data, null, 2 )
|
|
2373
2341
|
});
|
|
2374
|
-
}, // Ends add_json_vars_to_report()
|
|
2342
|
+
}, // Ends scope.steps.add_json_vars_to_report()
|
|
2375
2343
|
|
|
2376
2344
|
link_works: async ( scope, linkText ) => {
|
|
2377
2345
|
let [link] = await scope.page.$x(`//a[contains(text(), "${ linkText }")]`);
|
|
@@ -2390,7 +2358,7 @@ module.exports = {
|
|
|
2390
2358
|
linkPage.close()
|
|
2391
2359
|
|
|
2392
2360
|
await scope.afterStep(scope);
|
|
2393
|
-
}, // Ends link_works()
|
|
2361
|
+
}, // Ends scope.steps.link_works()
|
|
2394
2362
|
|
|
2395
2363
|
set_regular_var: async ( scope, var_name, answer ) => {
|
|
2396
2364
|
/** Set a non-story table variable (with or without a choice associated with it) to a value. Set:
|
|
@@ -2408,7 +2376,7 @@ module.exports = {
|
|
|
2408
2376
|
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
2409
2377
|
// if this variable cannot be set on this page, trigger an error
|
|
2410
2378
|
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
2411
|
-
}, // Ends set_regular_var()
|
|
2379
|
+
}, // Ends scope.steps.set_regular_var()
|
|
2412
2380
|
|
|
2413
2381
|
set_secret_var: async ( scope, var_name, answer_env_name ) => {
|
|
2414
2382
|
/** Sets a non-story table variable to a "secret" value (i.e. an env var) */
|
|
@@ -2425,7 +2393,7 @@ module.exports = {
|
|
|
2425
2393
|
// Don't continue if the variable doesn't cause navigation itself. Also,
|
|
2426
2394
|
// if this variable cannot be set on this page, trigger an error
|
|
2427
2395
|
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
2428
|
-
},
|
|
2396
|
+
}, // Ends scope.steps.set_secret_var
|
|
2429
2397
|
|
|
2430
2398
|
tap_term: async ( scope, phrase ) => {
|
|
2431
2399
|
/** Tap a link that doesn't navigate. Depends on the language of the text. */
|
|
@@ -2438,13 +2406,13 @@ module.exports = {
|
|
|
2438
2406
|
await scope.tapElement( scope, link );
|
|
2439
2407
|
|
|
2440
2408
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2441
|
-
}, // Ends tap_term()
|
|
2409
|
+
}, // Ends scope.steps.tap_term()
|
|
2442
2410
|
|
|
2443
2411
|
sign: async ( scope ) => {
|
|
2444
2412
|
/** Sign on a signature page. */
|
|
2445
|
-
await scope.
|
|
2413
|
+
await scope.draw_signature( scope );
|
|
2446
2414
|
await scope.afterStep(scope);
|
|
2447
|
-
}, // Ends sign()
|
|
2415
|
+
}, // Ends scope.steps.sign()
|
|
2448
2416
|
|
|
2449
2417
|
download: async ( scope, filename ) => {
|
|
2450
2418
|
/* Taps the link that leads to the given filename to trigger downloading.
|
|
@@ -2466,7 +2434,7 @@ module.exports = {
|
|
|
2466
2434
|
// Should this be using `scope.tapElement()`?
|
|
2467
2435
|
await elem.evaluate( elem => { return elem.click(); });
|
|
2468
2436
|
await scope.detectDownloadComplete( scope );
|
|
2469
|
-
}, // Ends download()
|
|
2437
|
+
}, // Ends scope.steps.download()
|
|
2470
2438
|
|
|
2471
2439
|
set_al_name: async ( scope, var_base, name_str ) => {
|
|
2472
2440
|
/** Given a variable name and a space-separated string, fills the
|
|
@@ -2503,15 +2471,20 @@ module.exports = {
|
|
|
2503
2471
|
|
|
2504
2472
|
let desired_pattern = name_patterns[( name_parts.length - 1 )];
|
|
2505
2473
|
|
|
2474
|
+
let raw_data = [];
|
|
2506
2475
|
for ( let parti = 0; parti < name_parts.length; parti++ ) {
|
|
2507
2476
|
let var_name = `${ var_base }.name.${ desired_pattern[ parti ] }`
|
|
2508
2477
|
let value = name_parts[ parti ];
|
|
2509
|
-
|
|
2478
|
+
raw_data.push({ var: var_name, value: value, trigger: scope.trigger_not_needed_flag });
|
|
2510
2479
|
}
|
|
2511
2480
|
|
|
2481
|
+
let var_data = await scope.normalizeTable( scope, { var_data: raw_data, });
|
|
2482
|
+
// Avoid continuing automatically. If this var cannot be set on this page, error
|
|
2483
|
+
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
2484
|
+
|
|
2512
2485
|
// Continue to next page...?
|
|
2513
2486
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2514
|
-
}, // Ends set_al_name()
|
|
2487
|
+
}, // Ends scope.steps.set_al_name()
|
|
2515
2488
|
|
|
2516
2489
|
set_al_address: async ( scope, var_base, address_str ) => {
|
|
2517
2490
|
/** Given a variable name and a four-part address formatted as below,
|
|
@@ -2530,16 +2503,21 @@ module.exports = {
|
|
|
2530
2503
|
let address_parts = split.concat( last_parts );
|
|
2531
2504
|
let start = `${ var_base }.address.`;
|
|
2532
2505
|
|
|
2533
|
-
let
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2506
|
+
let no_trigger = scope.trigger_not_needed_flag; // just making the lines shorter
|
|
2507
|
+
let raw_data = [
|
|
2508
|
+
{ var: `${start}address`, value: address_parts[0], trigger: no_trigger },
|
|
2509
|
+
// Remove accidental 'Unit' or 'unit ' text in case dev included it accidentally
|
|
2510
|
+
{ var: `${start}unit`, value: (address_parts[1].toLowerCase()).replace(/unit\s*/, ''), trigger: no_trigger },
|
|
2511
|
+
{ var: `${start}city`, value: address_parts[2], trigger: no_trigger },
|
|
2512
|
+
{ var: `${start}state`, value: address_parts[3], trigger: no_trigger },
|
|
2513
|
+
{ var: `${start}zip`, value: address_parts[4], trigger: no_trigger },
|
|
2514
|
+
];
|
|
2515
|
+
let var_data = await scope.normalizeTable( scope, { var_data: raw_data, });
|
|
2516
|
+
// Avoid continuing automatically. If this var cannot be set on this page, error
|
|
2517
|
+
await scope.setFields( scope, { var_data, ensure_navigation: false, ensure_all_vars_used: true });
|
|
2540
2518
|
|
|
2541
2519
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2542
|
-
}, // Ends set_al_address()
|
|
2520
|
+
}, // Ends scope.steps.set_al_address()
|
|
2543
2521
|
|
|
2544
2522
|
sign_in: async function ( scope, { email_secret_name, password_secret_name }) {
|
|
2545
2523
|
/** Allow the developer to log a user into their server using GitHub
|
package/lib/steps.js
CHANGED
|
@@ -386,6 +386,12 @@ When(/^(?:the user gets)?(?:I get)? to ?(?:the)?(?: ?question)?(?: id)? "([^"]+)
|
|
|
386
386
|
if( row1.report_str > row2.report_str ) { return 1; }
|
|
387
387
|
return 0;
|
|
388
388
|
});
|
|
389
|
+
|
|
390
|
+
await scope.page.screenshot({
|
|
391
|
+
path: `./${ scope.paths.scenario }/pic-${Date.now()}-${ id }.jpg`,
|
|
392
|
+
type: 'jpeg',
|
|
393
|
+
fullPage: true
|
|
394
|
+
});
|
|
389
395
|
|
|
390
396
|
// Add full used table to report so it can be copied if needed.
|
|
391
397
|
// TODO: Should we/How do we get this in the final scenario report instead of here?
|
package/package.json
CHANGED