@suffolklitlab/alkiln 5.13.3-feat-random-4 → 5.13.3-feat-random-6

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/CHANGELOG.md CHANGED
@@ -72,6 +72,17 @@ Format:
72
72
  - Fixed: log codes checker obob.
73
73
  - Fixed: updated `actions/upload-artifact@v3` to `actions/upload-artifact@v4`
74
74
 
75
+ ## [5.14.0] - 2025-05-15
76
+
77
+ ### Added
78
+
79
+ - Adds a linear Step to properly set multi-line values for `textarea`s with a doc string (closes [#1003](https://github.com/SuffolkLITLab/ALKiln/issues/1003)). We are unable to fix the current "set variable" linear Step for this. See [this comment on #655](https://github.com/SuffolkLITLab/ALKiln/issues/655#issuecomment-2876491571).
80
+
81
+ ### Internal
82
+
83
+ - Changes how Log joins logs.
84
+ - Allows reports to accept log messages as an argument without duplicating metadata text. Debug logs will still save duplicate messages.
85
+
75
86
  ## [5.13.4] - 2025-01-11
76
87
 
77
88
  ### Changed
@@ -223,7 +223,7 @@ This design is working within systems that limit how we can do what we need to d
223
223
 
224
224
  Alternative: Get rid of our support for some of these environments. The temporary GitHub docassemble server environment hasn't been used by a lot of authors. Other than that, the other environments have gotten a lot of use. Even though authors have not yet used the temporary GitHub environment much, they say they want that functionality. The trouble there seems to be complexity with initial set up.
225
225
 
226
- Alternative: Instead of dealing with separate runtimes, have one command and script that wraps all these currently separate processes in one runtime and handles which ones to trigger under what circumstances. If we included `./action_for_github/action.yml` in that command, we would prevent authors from customizing the temporary GitHub docassemble server. That would not be acceptable. To avoid closing off access to server customization, we can only partially integrate these different processes. Also, GitHub actions in general will still have to deal with some core business sometimes. For example, they have to create the artifacts folder name so they can use it later to upload the artifacts. This combined runtime also feels more coupled/entangled and therefore harder to maintain. The need for more command line arguments could make local development more complicated. It is still worth considering.
226
+ Alternative: Instead of dealing with separate runtimes, have one command and script that wraps all these currently separate processes in one runtime and handles which ones to trigger under what circumstances. If we included `./action_for_github/action.yml` in that command, we would prevent authors from customizing the temporary GitHub docassemble server. That would not be acceptable. To avoid closing off access to server customization, we can only partially integrate these different processes. Also, GitHub actions in general will still sometimes have to deal with some core business. For example, they have to create the artifacts folder name so they can use it later to upload the artifacts. This combined runtime also feels more coupled/entangled and therefore harder to maintain. The need for more command line arguments could make local development more complicated. It is still worth considering.
227
227
 
228
228
  <!-- Discuss: How to articulate the entanglement with more specifics and examples. -->
229
229
 
@@ -234,7 +234,7 @@ Alternative script names: `install_package`, `save_server`, `delete_package`.
234
234
  _Security, privacy, observability, etc._
235
235
 
236
236
  - Some tests need to use sensitive information. For example, some tests need a logged-in user and thus ALKiln needs the login info for a user. Different environments store this information differently and there is documentation about how to use sensitive information securely and privately.
237
- - Core ALKiln code[^2] ends up needing to handle some of the complexity of the 4 environments. Example - the `Log` class `path` takes an optional initial argument to set the artifacts path at the beginning of that runtime process. Other parts of the process have to assume that this path already exists. We have tried to abstract environment complexity out to `run_cucumber.js`, the script that wraps the core code, but there is still logic that the core code handles.
237
+ - Core ALKiln code[^2] ends up needing to handle some of the complexity of the 4 environments. Example - the `Log` class `path` takes an optional initial argument to set the artifacts path before the core code runs. The process in the core code (and elsewhere) have to assume that this path already exists and use config values. We have tried to abstract environment complexity out to `run_cucumber.js`, the script that wraps the core code, but there is still logic that the core code handles.
238
238
 
239
239
  ---
240
240
 
@@ -21,115 +21,165 @@ Our 4 environments can be condensed into 3 environments for the purpose of manag
21
21
  - ALKilnInThePlayground
22
22
  - The command line for internal development
23
23
 
24
- The GitHub action creates an artifact folder name and sends it to various scripts that use an instance of the `Log` class to create the folder and save the name. In other environments, an instance of the `Log` class both creates the name, creates the folder, and saves the name. The instance of the `Log` saves the name in `runtime_config.json`. That instance of `Log` and other ALKiln code uses that name to create and store files in that artifacts folder.
24
+ Every environment runs npm commands. Those commands are sometimes run separately and thus trigger separate processes. We must find a way to persist information, like the name of the artifacts folder, across these processes.
25
25
 
26
- The artifact folder name generation code is in 2 places - the GitHub action and ALKiln's core code[^1]. In future, we may reduce that to just one location - the core code.
26
+ The GitHub action creates an artifact folder name and sends it to a script that use an instance of the `Log` class to create the folder and save the name. In other environments, an instance of the `Log` class is what creates the name.
27
27
 
28
- <!-- More complex description that I don't think we need now, but want to double check:
29
- The scripts that GitHub action uses need to store its output in the same artifacts folder. The GitHub action therefore builds the name of the artifact folder and passes that around to the different scripts. The scripts pass the folder name to the `Log` class every time, which saves the folder name in `runtime_config.json`. After that, the core code uses that `json` value to put other artifacts in the same folder. We also have folder-naming code in the core code which we use for other purposes. The GitHub action duplicates that folder-naming code, which is unfortunate.
28
+ The instance of the `Log` saves the name in `runtime_config.json`. That instance of `Log` and other ALKiln code uses that name to create and store files in that artifacts folder.
30
29
 
31
- At the same time, tests that run in ALKilnInThePlayground don't have GitHub to manage the artifact folder name. It only uses the `run_cucumber` script, not any of the others...
30
+ The artifact folder name generation code is in 2 places - the GitHub action and ALKiln's core code[^1]. In future, we may be able to reduce that to just one location - the core code.
32
31
 
33
- Adding to that confusion, when people (like internal ALKiln developers) run tests from their command line, they do use the `setup` script once at first, but then run test suites repeatedly...
32
+ Below are more detailed descriptions of how each environment interacts with the folder name. Where the folder name is created, gets passed, gets saved, and gets used. These descriptions still gloss over many details.
34
33
 
35
- Neither of the last 2 can depend on the `runtime_config.json` folder name that `setup` creates. If they did, all their multiple test runs would save to that first folder. Since both those environments use `run_cucumber.js` alone repeatedly, and since we don't want to duplicate folder-naming behavior between even more scripts, we decided that those non-GitHub environments should be able to let `Log` create a folder name from scratch every time. The `Log` constructor has to be able to accept a `path` argument with a folder name and also to accept an undefined `path` argument.
36
- -->
34
+ The diagrams highlight `Log` to help visualize a common point in all the flows.
37
35
 
38
- A future goal is to remove the need to store the name in the runtime configuration file to reduce that complexity a bit. These flows are up for discussion.
39
36
 
40
- Below are more detailed descriptions of the different paths the folder name takes in the different environments. Where the folder name is created, gets passed, gets saved, and gets used. Note that there's a little glossing over the details. For example, everything saves to `runtime_config.json`, but only the core code[^1] ever uses the value there.
37
+ ### Shared behavior
38
+
39
+ For each environment, the start and end of how they manage the artifacts folder name is different. In the middle, though - once the artifacts folder name is already stored in the `runtime_config` file - the flow is the same.
40
+
41
+ Npm commands that come in the middle have this flow to save logs:
42
+
43
+ ```mermaid
44
+ flowchart LR
45
+
46
+ environment --triggers--> commands["middle</br>commands"]
47
+ commands --trigger--> scripts["package</br>code"]
48
+ scripts --make--> log2["#96;Log#96;"]:::log
49
+ log2 --"gets name</br>from"--> runtime2["runtime config</br>file"]
50
+ log2 --"adds to"--> folder2[folder</br>& files]
51
+
52
+ classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
53
+ ```
54
+
55
+ The core code also needs to use the artifacts folder name in order to save both logs and other artifacts:
56
+
57
+ ```mermaid
58
+ flowchart LR
59
+
60
+ core["core</br>code"] --"makes"--> log4["#96;Log#96;"]:::log
61
+ core --"adds to"--> folder3[folder</br>& files]
62
+ core --"gets name</br>from"--> runtime3["runtime config</br>file"]
63
+ log4 --"gets name</br>from"--> runtime3
64
+ log4 --"adds to"--> folder3
65
+
66
+ classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
67
+ ```
68
+
69
+ Those are the shared flows. Each environment, though, has its own way to store the name at the start and each environment has its own way to give the author the final artifacts.
41
70
 
42
- The diagrams highlight `Log` to help visualize a common point in all the flows.
43
71
 
44
72
  ### GitHub action
45
73
 
46
- 1. GitHub action creates a new folder name, passes that name to `setup`, which passes that name to `Log`, which creates the artifacts folder and saves the name in `runtime_config.json`. The setup `Log` uses the name to store logs in the folder. The setup uses its `Log`.
47
- 2. GitHub action uses the same folder name, passes that same name to `run_cucumber`, which passes that same name to `Log`. The `runtime_cucumber` process uses that `Log`. The `run_cucumber` process also triggers the core code, which uses the folder name in `runtime_config.json` to create its own `Log`. The core code also uses that name to store other artifact files in the folder.
48
- 3. GitHub action creates the folder name, passes that same name to `takedown`, which passes that same name when instantiating its `Log`. The takedown `Log` uses the name to store logs in the folder.
74
+ The GitHub action starts off with an empty `runtime_config` file. The action creates a new folder name and passes that name to the `artifacts` script, which passes that name to a `Log`. The `Log` creates the artifacts folder and saves the name in `runtime_config.json`.
49
75
 
50
76
  ```mermaid
51
- %% "pass name too" has 2 o's at the end because mermaid makes one of them disappear
77
+ flowchart LR
78
+
79
+ action["GitHub action"] --creates--> name["artifacts</br>folder name"]
80
+ action --"passes</br>name "--> command["'artifacts'</br>command"]
81
+ command --"passes</br>name"--> artifacts["artifacts.js"]
82
+ artifacts --"passes</br>name"--> log1["#96;Log#96;"]:::log
83
+ log1 --"stores name in</br>& uses"--> runtime1["runtime config</br>file"]
84
+ log1 --"creates</br>with name"--> folder1["folder</br>& files"]
85
+ log1 --"adds to"--> folder1
52
86
 
87
+ classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
88
+ ```
89
+
90
+ Then other processes use the name from the config to store logs and other files in the artifacts folder (as we've already shown).
91
+
92
+ Finally, the action uses the name in the action context to download the artifacts folder so the user can have access to it after the action is done.
93
+
94
+ ```mermaid
53
95
  flowchart LR
54
- action[action part 1] --create/pass name too--> setup
55
- setup["setup"] --pass name too--> log1["#96;Log#96;"]:::log
56
- log1 --store name in/ignore--> runtime1["runtime config file"]
57
- log1 --create with name--> folder1[folder]
58
-
59
- action2[action part 2] --pass name too--> run["run"]
60
- run --pass name too--> log2["#96;Log#96;"]:::log
61
- log2 --store name in/ignore--> runtime2["runtime config file"]
62
- log2 --use with name--> folder2[folder]
63
- run --> core["core code"]
64
- core --pass name too--> log3["#96;Log#96;"]:::log
65
- core --use with name--> folder2
66
- core --get name from--> runtime2
67
- log3 --use with name--> folder2
68
- log3 --store name in/ignore--> runtime2
69
-
70
- action3[action part 3] --pass name too--> takedown["takedown"]
71
- takedown --pass name too--> log4["#96;Log#96;"]:::log
72
- log4 --store name in/ignore--> runtime3["runtime config file"]
73
- log4 --create with name--> folder5[folder]
96
+
97
+ action["GitHub action"] --previously created--> name["artifacts folder name"]
98
+ action --uses name to get--> folder["folder & files"]
74
99
 
75
100
  classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
76
101
  ```
77
102
 
103
+
78
104
  ### ALKilnInThePlayground
79
105
 
80
- 1. ALKilnInThePlayground triggers `run_cucumber` without a folder name which triggers `Log` without a folder name. `Log` creates a new folder name and saves it in `runtime_config.json`. It also creates the artifacts folder. The `run_cucumber` process also triggers the core code, which uses the folder name in `runtime_config.json` to create its own `Log`. The core code also uses that name to store other artifact files in the folder.
106
+ _Note: This should be implemented by the time anyone actually reads this. Here's hoping._
107
+
108
+ ALKilnInThePlayground starts off with an empty `runtime_config` file. ALKilnInThePlayground triggers the `artifacts` command, but without a folder name. The code still creates a `Log` (also without a folder name). The `Log` itself creates the name.
81
109
 
82
110
  ```mermaid
83
111
  flowchart LR
84
- ALKilnInThePlayground --> run["run"]
85
- run --> log2["#96;Log#96;"]:::log
86
- log2 --store name in/ignore--> runtime2["runtime config file"]
87
- log2 --create with name--> folder2[folder]
88
- run --> core["core code"]
89
- core --> log3["#96;Log#96;"]:::log
90
- core --use with name--> folder2
91
- core --get name from--> runtime2
92
- log3 --use with name--> folder2
93
- log3 --store name in/ignore--> runtime2
112
+
113
+ ALKilnInThePlayground --> command["'artifacts'</br>command"]
114
+ command --> artifacts["artifacts.js"]
115
+ artifacts --> log1["#96;Log#96;"]:::log
116
+ log1 --creates--> name["artifacts</br>folder name"]
117
+ log1 --stores name--> runtime1["runtime config file"]
118
+ log1 --"gets name</br>from"--> runtime1["runtime config</br>file"]
119
+ log1 --creates</br>with name--> folder1["folder</br>& files"]
94
120
 
95
121
  classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
96
122
  ```
97
123
 
124
+ Then other processes use the name from the config to store logs and other files in the artifacts folder (as we've already shown).
125
+
126
+ Finally, ALKilnInThePlayground itself uses the runtime config file to get the name so it can give the artifacts to the user.
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+
131
+ ALKilnInThePlayground --"gets name from"--> runtime1["runtime config file"]
132
+ ALKilnInThePlayground --"uses name to get"--> folder["folder & files"]
133
+
134
+ classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
135
+ ```
136
+
137
+
98
138
  ### Command line
99
139
 
100
- 1. The developer uses the command line to trigger setup without a folder name which triggers `Log` without a folder name. `Log` creates a name and creates the artifacts folder, then stores files in that folder.
101
- 2. The developer uses the command line to trigger `run_cucumber` without a folder name which triggers `Log` without a folder name. `Log` creates and saves a new folder name in `runtime_config.json`, and creates the artifacts folder. The `run_cucumber` process also triggers the core code, which uses the folder name in `runtime_config.json` to create its own `Log`. The core code also uses that name to store other artifact files in the folder.
102
- 3. The developer uses the command line to trigger `takedown` without a folder name which triggers `Log` without a folder name. `Log` creates a name and the artifacts folder, then stores files in that folder.
140
+ The command line flow is similar to ALKilnInThePlayground, but has to deal with a pre-existing runtime config file[^2]. The command line flow overwrites the artifacts folder name on each run.
141
+
142
+ An internal developer uses the command line to trigger the `artifacts` command, but without a folder name. The code still creates a `Log` (also without a folder name). The `Log` itself creates a name and overwrites the current name in the runtime config file.
103
143
 
104
144
  ```mermaid
105
145
  flowchart LR
106
- cmd1[command line] --> setup
107
- setup["setup"] --> log1["#96;Log#96;"]:::log
108
- log1 --store name in/ignore--> runtime1["runtime config file"]
109
- log1 --create with/use with name--> folder1[folder]
110
-
111
- cmd2[command line] --> run["run"]
112
- run --> log2["#96;Log#96;"]:::log
113
- log2 --store name in/ignore--> runtime2["runtime config file"]
114
- log2 --create with name--> folder2[folder]
115
- run --> core["core code"]
116
- core --> log3["#96;Log#96;"]:::log
117
- core --use with name--> folder2
118
- core --get name from--> runtime2
119
- log3 --use with name--> folder2
120
- log3 --store name in/ignore--> runtime2
121
-
122
- cmd3[command line] --> takedown["takedown"]
123
- takedown --> log4["#96;Log#96;"]:::log
124
- log4 --store name in/ignore--> runtime3["runtime config file"]
125
- log4 --create with name--> folder5[folder]
146
+
147
+ dev --> command["'artifacts'</br>command"]
148
+ command --> artifacts["artifacts.js"]
149
+ artifacts --> log1["#96;Log#96;"]:::log
150
+ log1 --creates--> name["artifacts</br>folder name"]
151
+ log1 --"overwrites</br>previous name"--> runtime1["runtime config file"]
152
+ log1 --"gets name</br>from"--> runtime1["runtime config</br>file"]
153
+ log1 --creates</br>with name--> folder1["folder</br>& files"]
126
154
 
127
155
  classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
128
156
  ```
129
157
 
158
+ Then other processes use the name from the config to store logs and other files in the artifacts folder (as we've already shown).
159
+
160
+ ALKiln stores the artifacts folder in the root of the local repository folder. The developer has direct access to the file system and can see every new artifacts folder. In fact, they can also see any previous artifacts folders. We avoid adding those artifacts folders to the repo by including the folder name pattern in the `.gitignore` and `.npmignore` files.
161
+
162
+ ```mermaid
163
+ flowchart LR
164
+
165
+ git --šŸ™ˆ--> folder["folders & files"]
166
+ dev --šŸ‘ļø--> folder
167
+ npm --šŸ™‰--> folder
168
+
169
+ classDef log fill:#d6fd88,stroke:#000,stroke-width:2px,color:#000
170
+ ```
171
+
172
+
130
173
  ## Alternatives considered
131
174
 
132
- - One script for creating the artifacts folder name: Make a script that saves the folder name to the ALKiln `runtime_config.json`. Then all the other code, including instances of `Log`, would always use `runtime_config.json`. We would want `package.json` scripts for the command line environment that would include getting the name of the folder. That potentially complicates the code and process for internal development. We will see how our current design behaves first.
133
- - Use `Log`'s path name instead of the one in `runtime_config.json`. We thought we would want to save in previous artifacts folders, but it now seems potentially unnecessary. This seems the most viable option for the future, but it is unclear how we can integrate `Log` with all the other files. I wouldn't consider PDFs and screenshots to be logs.
175
+ - Each originator (GitHub, ALKilnInThePlayground, the dev) could always create the name from the outside. This would duplicate work, but would give more fine-grained control.
176
+ - Have `Log` make a different folder name each time and, at the very end, combine the contents of those folders into one folder. Hey, all ideas are welcome here.
177
+
178
+ For name creation:
179
+
180
+ - Avoid GitHub actions creating names. That is, create names only in `artifacts.js` and pass that out again. Needs research.
181
+
134
182
 
135
183
  [^1]: **Core framework code/core code:** the code that implements the Steps in the `.feature` test files.
184
+
185
+ [^2] Keeping the `runtime_config` file around helps greatly improves the local development experience. You can read more in the document that talks about the 4 environments ALKiln supports.
@@ -56,8 +56,6 @@ const server_install = async () => {
56
56
  session_vars.validateEnvironment();
57
57
 
58
58
  // 2. Prepare logs
59
- // Local devs: This will be separate from your test paths
60
- // unless you pass a `path` argument into the cucumber run command
61
59
  const argv = require(`minimist`)(process.argv.slice(2));
62
60
  const log = new Log({ path: argv.path, context, });
63
61
 
@@ -54,7 +54,7 @@ const setup = async () => {
54
54
  + `dainstall this package on your server. See above for more details.`;
55
55
  log.throw({ code: `ALK0047`, context,
56
56
  before: `ā˜ļø ā˜ļø ā˜ļø Error data ā˜ļø ā˜ļø ā˜ļø\n`,
57
- error: new Error(`Error while installing project on your docassemble server`) },
57
+ error: new Error(`Error while installing project on your docassemble server during GitHub+Youā„¢ tests`) },
58
58
  install_err_msg
59
59
  );
60
60
  }
@@ -68,8 +68,6 @@ const setup = async () => {
68
68
  session_vars.validateEnvironment();
69
69
 
70
70
  // 2. Prepare logs
71
- // Local devs: This will be separate from your test paths unless you pass a
72
- // `path` argument into the cucumber run command
73
71
  const argv = require(`minimist`)(process.argv.slice(2));
74
72
  const log = new Log({ path: argv.path, context, });
75
73
 
package/lib/globals.js CHANGED
@@ -6,5 +6,5 @@ let globals = {
6
6
 
7
7
  module.exports = globals;
8
8
 
9
- // Would this be clearer as a config file instead?
10
- // If not, change name to `constants`?
9
+ // Discuss: Would this be clearer as a config file instead? If not, change name
10
+ // to `constants`?
package/lib/scope.js CHANGED
@@ -888,7 +888,7 @@ module.exports = {
888
888
  }, // Ends scope.normalizeTable()
889
889
 
890
890
  getAllFields: async function ( scope, { html }) {
891
- /* Given HTML string of a page, returns a list of all the fields in it
891
+ /** Given HTML string of a page, returns a list of all the fields in it
892
892
  * as some kind of sophisticated data? With their selectors, for example.
893
893
  * We will choose selectors that will continue to identify the node, eve
894
894
  * after a hidden field is revealed, etc.
@@ -1019,12 +1019,15 @@ module.exports = {
1019
1019
  // button, radio, checkbox yesno, and maybe others need the value of the `value` attribute
1020
1020
  let values = [];
1021
1021
  if ( node.name === `select` ) {
1022
+ // Get possible values
1022
1023
  values = await scope.getOptionValues( scope, { $select: $node });
1023
1024
  }
1024
1025
  if ( values.length === 0 && node.attribs.value ) {
1025
1026
  // TODO: Add links to docs about da YAML values that devs never fill in themselves and
1026
1027
  // so don't know what they are. E.g. yesnomabye -> True/False/None.
1027
1028
  // TODO: Text inputs and textareas must ignore the value, etc. How?
1029
+
1030
+ // Get the current value for not `select` fields
1028
1031
  values.push( node.attribs.value );
1029
1032
  }
1030
1033
 
@@ -1387,6 +1390,14 @@ module.exports = {
1387
1390
  * @param <field[n].guesses[n].trigger> {str} - var name that triggered this page
1388
1391
  *
1389
1392
  * @returns [ var_data[n] ]
1393
+ * @returns [{
1394
+ * table_row: var_data[n],
1395
+ * field: {
1396
+ * selector: str,
1397
+ * guesses: [{ values: [str] }]
1398
+ * }
1399
+ * }]
1400
+ * What does it return when there's no match?
1390
1401
  */
1391
1402
 
1392
1403
  let rows_that_match_field_names = [];
@@ -746,7 +746,7 @@ function get_number_of_Scenarios_to_generate2({ step, prev_num_to_make }) {
746
746
  /** Too many numbers */
747
747
  if ( number_matcher && number_matcher.length > 1 ) {
748
748
  // Some feedback indicated using the last number makes more sense to people
749
- // than the first
749
+ // than the first. Also, matches Story Table behavior.
750
750
  user_num = parseInt( number_matcher[ number_matcher.length - 1 ]);
751
751
  warnings.push(
752
752
  log.warn({ code: `ALK0240`, context: `generating files`, },
package/lib/steps.js CHANGED
@@ -420,11 +420,11 @@ When(/I? ?gets? to ?.*?(".*").*?with.*?(?:this data:)?/i, {timeout: -1}, async (
420
420
  // Some errors happen in there to be closer to the relevant code. We'd have to try/catch anyway.
421
421
  let error = await scope.setFields(scope, { var_data: ensured_var_data, id, ensure_navigation: true });
422
422
 
423
- // TODO: use Log class to throw this error?
423
+ // Discuss: use Log class to throw this error?
424
424
  if ( error.was_found ) { await scope.throwPageError( scope, { id: id }); }
425
425
  resolve();
426
426
  } catch ( err ) {
427
- let err_msg = `ALKiln ran into an error it tried to answer fields on the page with the question id "${ id }". The text above might have more information.`
427
+ let err_msg = `ALKiln ran into an error when it tried to answer fields on the page with the question id "${ id }". The text above might have more information.`
428
428
  reports.addToReport( scope, { type: `error`, code: `ALK0062`, value: err_msg });
429
429
  // Throw error instead?
430
430
  reject( err );
@@ -721,39 +721,48 @@ Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkTe
721
721
  await scope.afterStep(scope);
722
722
  });
723
723
 
724
- Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)" should be/i, async (var_name, expected_value) => {
725
- /* Check that the text string of the page's JSON var value matches
726
- * the given text string. Does not currently accept nested values
727
- * like "foo.bar". The word "text" is required.
728
- *
729
- * The format for this step is something like this:
730
- * ```
731
- * Then the text in the JSON variable "var_name" should be
732
- * """
733
- * A single or
734
- * multiline string
735
- * """
736
- * ```
737
- *
738
- * Sentence combinations that will work:
739
- * `the text in the JSON variable "variable_with_text_value" should be`
740
- * `the text in the JSON var "variable_with_text_value" should be`
741
- * `the text in "variable_with_text_value" should be`
742
- * `text var "variable_with_text_value" should be`
743
- */
724
+ Then(/(?:the )?text(?: in)?(?: for)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)" should be/i, async (var_name, expected_value) => {
725
+ /** Check that the text string of the page's JSON var value matches
726
+ * the given text string. Does not currently accept nested values
727
+ * like "foo.bar". The word "text" is required.
728
+ *
729
+ * The format for this step is something like this:
730
+ * ```
731
+ * Then the text in the JSON variable "var_name" should be
732
+ * """
733
+ * A single or
734
+ * multiline string
735
+ * """
736
+ * ```
737
+ *
738
+ * Note: ALKiln removes `\r` from both the expected and actual values.
739
+ *
740
+ * Sentence combinations that will work:
741
+ * the text in the JSON variable "variable_with_text_value" should be
742
+ * the text in the JSON var "variable_with_text_value" should be
743
+ * the text for "variable_with_text_value" should be
744
+ * text "variable_with_text_value" should be
745
+ * */
744
746
 
745
747
  // Get JSON variable value
746
748
  let { json_path, json } = await scope.savePageJSONData( scope );
749
+
750
+ // docassemble (at least sometimes) makes new lines with `\r\n`
747
751
  let actual_value = json.variables[ var_name ];
752
+ actual_value = actual_value.replace(/\r/, ``);
753
+ // Normalize `expected_value` as well for backwards compatibility in case any
754
+ // authors are already compensating for docassemble
755
+ expected_value = expected_value.replace(/\r/, ``);
748
756
 
749
757
  // Add warning to report if value is not as expected
750
758
  if ( actual_value !== expected_value ) {
751
759
  reports.addToReport( scope, {
752
760
  type: `error`,
753
761
  code: `ALK0083`,
754
- value: `The variable "${ var_name }" was not equal to the expected `
755
- + `value on the "${ scope.page_id }" screen. Check `
756
- + `${ json_path } to see the page's JSON variables.`
762
+ value: `The variable "${ var_name }" was different from the expected `
763
+ + `value on the "${ scope.page_id }" screen.`
764
+ + `The expected values was\n` + expected_value
765
+ + `\nwhile the actual value was\n` + actual_value
757
766
  });
758
767
  }
759
768
 
@@ -1058,6 +1067,22 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
1058
1067
 
1059
1068
  });
1060
1069
 
1070
+ When(/I set the var(?:iable)? "([^"]+)" to\s*$/, { timeout: -1 }, async ( var_name, answer ) => {
1071
+ /** Set a non-story table variable (with or without a choice associated with
1072
+ * it) to a value that can have new lines in it. Most useful for
1073
+ * `textarea`s.
1074
+ *
1075
+ * Example:
1076
+ * I set the var "benefits['SSI']" to
1077
+ * """
1078
+ * Some multline
1079
+ * value
1080
+ * """
1081
+ * */
1082
+ // `page` timeout will deal with custom timeout
1083
+ await scope.steps.set_regular_var( scope, var_name, answer )
1084
+ });
1085
+
1061
1086
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
1062
1087
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
1063
1088
  // This could theoretically tap an element, so
package/lib/utils/Log.js CHANGED
@@ -624,31 +624,28 @@ class Log {
624
624
  } // Ends Log._stringify_logs()
625
625
 
626
626
  _stringify_inline({ prev_line_type, inline_log }) {
627
- /** Formats the start of an inline log based on the previous log and returns
628
- * that combined string.
627
+ /** Formats the log to add a space or a new line separator. May end up with
628
+ * a leading space for now.
629
629
  *
630
630
  * @param { "inline"|"block"|null } prev_line_type - Required. The type of
631
631
  * the previous log.
632
632
  * @param { null|non-obj } inline_log - Required. The log.
633
633
  *
634
- * @returns { string } The formatted log.
634
+ * @returns { string } The formatted line.
635
635
  *
636
636
  * @example
637
637
  * // Backticks are to make the surrounding white space more clear
638
638
  * log._stringify_inline({ prev_line_type: null, inline_log: "apple" });
639
- * // `apple`
639
+ * // "apple "
640
640
  * log._stringify_inline({ prev_line_type: 'inline', inline_log: "apple" });
641
- * // ` apple`
641
+ * // "apple "
642
642
  * log._stringify_inline({ prev_line_type: 'block', inline_log: "apple" });
643
- * // `
644
- * // apple`
643
+ * // "
644
+ * // apple"
645
645
  * */
646
- // An inline log's string starts with nothing if it's the first string...
647
646
  let start = ``;
648
- // ...starts after a space if its next to an inline string...
649
- if ( prev_line_type === `inline` ) { start = ` `; }
650
- // ...or starts on a new line if it's after a block of text, like an error
651
- // or object.
647
+ if ( prev_line_type === `inline` && !inline_log.startsWith(`\n`) ) { start = ` `; }
648
+ // This is actually the end of the block-type log
652
649
  else if ( prev_line_type === `block` ) { start = `\n`; }
653
650
  return `${ start }${ inline_log }`;
654
651
  }
@@ -658,6 +655,8 @@ class Log {
658
655
  * a "block"-type separator at the start. Can handle circular references.
659
656
  * Catch ANY error.
660
657
  *
658
+ * @param { "inline"|"block"|null } prev_line_type - Required. The type of
659
+ * the previous log.
661
660
  * @param { Object } obj_log - Required. The object to format.
662
661
  *
663
662
  * @returns { string } The obj as a string plus a separator
@@ -669,15 +668,15 @@ class Log {
669
668
  * // { fruit: 'bananas' }
670
669
  * */
671
670
  let start = ``;
672
- if ( prev_line_type === `inline` || prev_line_type === `block` ) {
673
- start = `\n`;
674
- }
671
+ // Avoid adding a new line to the start of the first line
672
+ let is_first_line = prev_line_type === null;
673
+ if ( !is_first_line ) { start = `\n`; }
675
674
 
676
675
  try {
677
676
  // Try to stringify the object.
678
677
  return `${ start }${ this._inspect({ value: obj_log }) }`;
679
678
 
680
- // Discuss: record these errors?
679
+ // Discuss: record the outer error?
681
680
  } catch ( outer_error ) {
682
681
  // If that fails, see if it has its own way of stringifying
683
682
  try {
@@ -263,25 +263,28 @@ reports.getPrintableScenario = function( scenario ) {
263
263
  return report;
264
264
  }; // Ends reports.getPrintableScenario()
265
265
 
266
- let getLineText = function ({ type=`no type`, code=`ALK00rl`, value=``, indent=`` }) {
266
+ let getLineText = function ({ from_log, type=`no type`, code=`ALK00rl`, value=``, indent=`` }) {
267
267
 
268
- value = `${ value.replace( /\n/g, "\n" + indent ) }`;
268
+ value = `${ value.replace( /\n/g, `\n` + indent ) }`;
269
269
 
270
270
  if ( type === `as_is` ) { return `${ indent }${ value }`; }
271
271
 
272
272
  let start = indent;
273
- if ( type.includes(`outcome`) ) { start = `\n`; }
273
+ if ( type.includes(`outcome`) ) { start = `\n`; } // extra new line
274
274
 
275
275
  if ( type.includes(`page_id`) ) { start += `screen id: `; }
276
- else if ( type.includes(`warning`) ) { start += `šŸ”Ž ${ code } WARNING: `; }
277
- else if ( type.includes(`error`) ) { start += `\nšŸ¤• ${ code } ERROR: `; } // extra new line
278
- let result = `${ start }${ value }`;
279
- return result;
280
- }; // Ends getLine()
276
+ else if ( type.includes(`warning`) && !from_log ) { start += `šŸ”Ž ${ code } WARNING: `; }
277
+ else if ( type.includes(`error`) && !from_log ) { start += `šŸ¤• ${ code } ERROR: `; } // extra new line
278
+
279
+ return `${ start }${ value }`;
280
+
281
+ }; // Ends getLine()
281
282
 
282
- let getDebugLine = function ({ type=``, code=`ALK0278`, value=``, indent=`` }) {
283
+ let getDebugLine = function ({ from_log, type=``, code=`ALK00rd`, value=``, indent=`` }) {
283
284
  /** In debug log lines, include all the information possible. */
284
285
 
286
+ // Discuss: How to prevent saving in the debug log twice
287
+ if ( from_log ) { return value; }
285
288
  // `plain` is usually used for visual clarity and shouldn't have
286
289
  // codes and such associated with it. Return right away.
287
290
  if ( type.includes(`plain`) ) { return `${ indent }${ value }`; }
@@ -310,7 +313,7 @@ let getDebugLine = function ({ type=``, code=`ALK0278`, value=``, indent=`` }) {
310
313
  if ( type.includes(`warning`) ) { description_parts.push( `WARNING` ); }
311
314
  if ( type.includes(`error`) ) { description_parts.push( `ERROR` ); }
312
315
  // Default
313
- if ( description_parts.length <= 0 ) { description_parts.push( `UNKNOWN` ); }
316
+ if ( description_parts.length <= 0 ) { description_parts.push( `UNKNOWN LOG TYPE` ); }
314
317
 
315
318
  start = [ start, ...description_parts ].join(` `);
316
319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.13.3-feat-random-4",
3
+ "version": "5.13.3-feat-random-6",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -9,7 +9,8 @@ module.exports = fixts = {};
9
9
  *
10
10
  * TODO
11
11
  * Research: Can we get back null or undefined without syntax errors? (unlikely)
12
- * Research: Can feature.children[0] exist without a `.scenario` prop? (unlikely)
12
+ * Research: Can a feature.children item exist without having a `.scenario`
13
+ * prop? (unlikely)
13
14
  * */
14
15
 
15
16
  fixts.generator_missing_Feature = {};
@@ -51,22 +51,6 @@ simple.exact_log_codes = [
51
51
  simple.num_Scenarios = 1;
52
52
 
53
53
 
54
- /** TODO: 2 generator Steps in one Scenario */
55
-
56
- `Feature: I generate constrained random tests
57
-
58
- Scenario: I fail with 2 generator Steps in 1 Scenario
59
- Given ALKiln makes 2 constrained answers tests
60
- Given I start the interview at "form_entrypoint_file_name"
61
- And I generate 2 constrained random tests that get to "end" when I pick from:
62
- | var | possible_values |
63
- | other_var | val1;; val2;; val3 |
64
- And I generate 1 constrained random test that gets to "end" when I pick from:
65
- | var | possible_values |
66
- | user_choice | correct;; wrong |
67
- `;
68
-
69
-
70
54
  /**
71
55
  * ===========
72
56
  * Minimal required Step text: