@suffolklitlab/alkiln 5.13.4 → 5.14.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.
@@ -1,101 +1,60 @@
1
- # Constrained random input tests
1
+ # Prioritize constrained random answers tests over pure random answers tests
2
2
 
3
- I do 10 constrained random tests
3
+ ## Context and Problem Statement
4
4
 
5
- ## Why do people want purely random input tests?
5
+ We could develop constrained randomness first or pure randomness first. Is there a reason to prioritize one over another?
6
6
 
7
- 1. They don't have time to write individual specific tests
8
- 2. They are worried they haven't covered all eventualities
7
+ ## Considered Options
9
8
 
10
- ## Problems for purely random tests
9
+ - Purely random answers. They fill in fields without any guidance.
10
+ - Constrained random answers tests. They make the test author list all the variable names and all the choices of answers they want to allow for those variables.
11
+ - Hybrid approach. Allow an author to specify some values while letting other values stay completely random.
11
12
 
12
- - Non-specific input. E.g. a name field might get a sentence. Can we do our best? That would be more work.
13
+ ## Decision Outcome
13
14
 
14
- ## Why would people prefer constrained random input tests?
15
+ Prioritize constrained random answers tests over pure random answers tests. Strangely, they seem to be the more flexible of the two options.
15
16
 
16
- - They want to make the tests more focused on certain scenarios and avoid wasting time on unimportant tests. For example, sometimes several options actually give the same answer. A purely random test would give more preference to the repeated answer.
17
- - **Some fields might require very specific answers, like a real lawyer's bar number or a specific file name for uploading exhibits.**
18
- - Narrow down the paths at some points, like always choosing that you have minor children
19
- - **Even if the user doesn't know it, this is the most flexible option**
17
+ ## Pros and Cons of the Options
20
18
 
21
- ## Note
19
+ The discussion about the general pros and cons of random answers tests will be in the `constrained_random_tests` doc for now.
22
20
 
23
- ~~If in the future we want to add the option to not specify, then we can and it won't be a breaking change.~~
21
+ ### Purely random answers
24
22
 
25
- We have to decide what it means to allow a person to not specify. E.g if excluding a row means don't give an answer to that variable at all, then it can't in future mean "give a random answer to this field". Options:
23
+ Pros:
26
24
 
27
- - (bad) Excluding a row means don't give an answer. Including a row with a blank answer means give a random answer.
28
- - (bad) Including a row with a blank answer means don't give an answer. Give unlisted variables random answers.
29
- - Or should we require a string (e.g. ALK_NO_ANSWER) for fields that should not get random inputs?
30
- - (most easy purely random input method to implement without changing current code?) Or specify randomness (e.g. ALK_RANDOM_ANSWER)?
31
- - **(backwards compatible because we can become more flexible later) Require both of the above?**
25
+ - Very little work for test authors to write.
26
+ - Because they're so free-form, they may do a better job finding unexpected behavior that authors didn't think to test for.
32
27
 
33
- Which of these is worse? Mind reading can be bad. On the other hand, specifying everything may be somewhat antithetical to the 2 goals of people who want purely random input tests. (added to the interview an unrequired value, but forgot to add it to the test, they won't test all eventualities).
28
+ Cons:
34
29
 
35
- Possible solutions for people who want purely random tests or bits of tests:
36
- - Add a Step for answering randomly for a whole test.
37
- - Add a Step for answering randomly up to various page ids (list is allowed).
38
- - Add Step to answer randomly for one page.
30
+ - Unless we can find better ways of generating purely random test files for the authors, copying a random answers test to keep permanently might be very challenging.
31
+ - Fields might get nonsensical answers. For example, a very large number for age or a very small number for income.
32
+ - Impossible to use for fields that need very specific answers, like a real lawyer's bar number or a specific file name for uploading exhibits.
39
33
 
40
- ## Flexible constraints?
34
+ ### Constrained random answers
41
35
 
42
- Post MVP
36
+ Pros:
43
37
 
44
- - Allow number range "ALKiln: [1, 5)"? Or they have to do "1, 2, 3, 4"
45
- - "ALKiln: year 1965 - 1999"
46
- - "ALKiln: date [today, today - 10)"
47
- - "ALKiln: max length 3" (of 5 checkbox answers)
48
- - "ALKiln: name"
49
- - "ALKiln: number" (and other types like "currency") (we can probably figure those out)
38
+ - Useful for fields that need very specific answers, like a real lawyer's bar number or a specific file name for uploading exhibits.
39
+ - An author can focus the tests' answers on important user stories. For example, they might want to focus some tests on users that have children while keeping other answers more random.
40
+ - An author might be able to make more efficient random tests. For example, a multiple choice question may have multiple answers that, behind the scenes, set the same value. Authors might do this to help users who are confused about their options give more accurate answers. A purely random test would pick that repeated value too often and waste testing time.
50
41
 
51
- ## Note
42
+ Cons:
52
43
 
53
- MVP of random input tests
44
+ - They take more time to write.
45
+ - They take more work to maintain.
54
46
 
55
- ## no seeds
47
+ <!-- TODO: Add to docs - each `possible_values` cell should contain 1 or more possible answers. -->
56
48
 
57
- seeds can break more easily. instead we save the spec of the test(s)
49
+ ### Hybrid
58
50
 
59
- ## MVP
51
+ In future, we believe we can figure out how to let users have partially constrained tests where unspecified fields get purely random answers while constrained fields choose from a set of answers. It would need to be its own Step.
60
52
 
61
- save the spec of the test...somewhere. Probably using special name to say
62
- "rerun" and then change the name at the end of the run.
63
- retry the test somehow (if needed)...
64
- present the test and results to the user
53
+ Pros:
65
54
 
66
- ## Better
55
+ - Offers the most flexibility and control.
67
56
 
68
- Avoid repeating previous random tests
69
- Avoid repeating tests of users (which is much(?) harder)
57
+ Cons:
70
58
 
71
- ## Specifics
72
-
73
- Create a file with a certain # of Scenarios and run it
74
- - Does cucumber decide the tests it'll run at runtime or before? I think the former
75
- - Is it better to split tests up into separate files if there are a lot?
76
- - Is it better or worse to help the developer feel the magnitude of the #
77
- of tests they're running (using Example) [Answer: worse]
78
- - How to handle separate constrained random tables?
79
-
80
- ## right now
81
- [r1 opt 1, r1 opt 2, r1 opt 3, name input, date input, description input]
82
-
83
- ## need for random
84
-
85
- detect r1 while looping through the fields
86
- [r1 opt 1, r1 opt 2, r1 opt 3, name input, date input, description input]
87
- Con: as we loop through, we don't know the number of options at each particular instance
88
-
89
- or make grouped fields into groups. Basically group r1:
90
- [[r1 opt 1, r1 opt 2, r1 opt 3], name input, date input, description input]
91
-
92
- ## why not generate a test that uses all the constraints and thus is fully random?
93
-
94
- Because we'd have to parse docassembled *compiled* files, which is very hard
95
-
96
- Alternative:
97
-
98
- When the developer runs a test, they can use a Step to have it collect all possible fields that ALKiln sees during that test run.
99
- Q: How effectively can we decode the fields? Or can we make a tool that will help a developer decode their fields? (Include an input that just decodes one encoded string (`atob()`))
100
- Q: If the fields are multiple choice and the developer's test picks at least one choice from that group of fields, can we use that to properly decode all the names of the fields? Hesitant to do this because some values may be encoded differently from other values (I think).
101
- Q: Is this more work than just refactoring how we group fields?
59
+ - Might be hard to avoid confusion about test behavior.
60
+ - Means developing both methods of testing. That's out of scope for one PR.
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 = [];
package/lib/steps.js CHANGED
@@ -707,39 +707,48 @@ Then(/the "([^"]+)" link opens in (a new window|the same window)/, async (linkTe
707
707
  await scope.afterStep(scope);
708
708
  });
709
709
 
710
- Then(/(?:the )?text(?: in)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)" should be/i, async (var_name, expected_value) => {
711
- /* Check that the text string of the page's JSON var value matches
712
- * the given text string. Does not currently accept nested values
713
- * like "foo.bar". The word "text" is required.
714
- *
715
- * The format for this step is something like this:
716
- * ```
717
- * Then the text in the JSON variable "var_name" should be
718
- * """
719
- * A single or
720
- * multiline string
721
- * """
722
- * ```
723
- *
724
- * Sentence combinations that will work:
725
- * `the text in the JSON variable "variable_with_text_value" should be`
726
- * `the text in the JSON var "variable_with_text_value" should be`
727
- * `the text in "variable_with_text_value" should be`
728
- * `text var "variable_with_text_value" should be`
729
- */
710
+ Then(/(?:the )?text(?: in)?(?: for)?(?: the)?(?: JSON)?(?: var)?(?:iable)?(?: )?"([^"]+)" should be/i, async (var_name, expected_value) => {
711
+ /** Check that the text string of the page's JSON var value matches
712
+ * the given text string. Does not currently accept nested values
713
+ * like "foo.bar". The word "text" is required.
714
+ *
715
+ * The format for this step is something like this:
716
+ * ```
717
+ * Then the text in the JSON variable "var_name" should be
718
+ * """
719
+ * A single or
720
+ * multiline string
721
+ * """
722
+ * ```
723
+ *
724
+ * Note: ALKiln removes `\r` from both the expected and actual values.
725
+ *
726
+ * Sentence combinations that will work:
727
+ * the text in the JSON variable "variable_with_text_value" should be
728
+ * the text in the JSON var "variable_with_text_value" should be
729
+ * the text for "variable_with_text_value" should be
730
+ * text "variable_with_text_value" should be
731
+ * */
730
732
 
731
733
  // Get JSON variable value
732
734
  let { json_path, json } = await scope.savePageJSONData( scope );
735
+
736
+ // docassemble (at least sometimes) makes new lines with `\r\n`
733
737
  let actual_value = json.variables[ var_name ];
738
+ actual_value = actual_value.replace(/\r/, ``);
739
+ // Normalize `expected_value` as well for backwards compatibility in case any
740
+ // authors are already compensating for docassemble
741
+ expected_value = expected_value.replace(/\r/, ``);
734
742
 
735
743
  // Add warning to report if value is not as expected
736
744
  if ( actual_value !== expected_value ) {
737
745
  reports.addToReport( scope, {
738
746
  type: `error`,
739
747
  code: `ALK0083`,
740
- value: `The variable "${ var_name }" was not equal to the expected `
741
- + `value on the "${ scope.page_id }" screen. Check `
742
- + `${ json_path } to see the page's JSON variables.`
748
+ value: `The variable "${ var_name }" was different from the expected `
749
+ + `value on the "${ scope.page_id }" screen.`
750
+ + `The expected values was\n` + expected_value
751
+ + `\nwhile the actual value was\n` + actual_value
743
752
  });
744
753
  }
745
754
 
@@ -1044,6 +1053,22 @@ When(/I set the var(?:iable)? "([^"]+)" to "([^"]+)"/, { timeout: -1 }, async (
1044
1053
 
1045
1054
  });
1046
1055
 
1056
+ When(/I set the var(?:iable)? "([^"]+)" to\s*$/, { timeout: -1 }, async ( var_name, answer ) => {
1057
+ /** Set a non-story table variable (with or without a choice associated with
1058
+ * it) to a value that can have new lines in it. Most useful for
1059
+ * `textarea`s.
1060
+ *
1061
+ * Example:
1062
+ * I set the var "benefits['SSI']" to
1063
+ * """
1064
+ * Some multline
1065
+ * value
1066
+ * """
1067
+ * */
1068
+ // `page` timeout will deal with custom timeout
1069
+ await scope.steps.set_regular_var( scope, var_name, answer )
1070
+ });
1071
+
1047
1072
  When(/I set the var(?:iable)? "([^"]+)" to ?(?:the)? ?(?:GitHub)? secret "([^"]+)"/i, { timeout: -1 }, async ( var_name, set_to_env_name ) => {
1048
1073
  /* Set a non-story table variable to a secrete value. Same as the above, but reads the value from `process.env` */
1049
1074
  // This could theoretically tap an element, so
package/lib/utils/Log.js CHANGED
@@ -491,31 +491,28 @@ class Log {
491
491
  } // Ends Log._stringify_logs()
492
492
 
493
493
  _stringify_inline({ prev_line_type, inline_log }) {
494
- /** Formats the start of an inline log based on the previous log and returns
495
- * that combined string.
494
+ /** Formats the log to add a space or a new line separator. May end up with
495
+ * a leading space for now.
496
496
  *
497
497
  * @param { "inline"|"block"|null } prev_line_type - Required. The type of
498
498
  * the previous log.
499
499
  * @param { null|non-obj } inline_log - Required. The log.
500
500
  *
501
- * @returns { string } The formatted log.
501
+ * @returns { string } The formatted line.
502
502
  *
503
503
  * @example
504
504
  * // Backticks are to make the surrounding white space more clear
505
505
  * log._stringify_inline({ prev_line_type: null, inline_log: "apple" });
506
- * // `apple`
506
+ * // "apple "
507
507
  * log._stringify_inline({ prev_line_type: 'inline', inline_log: "apple" });
508
- * // ` apple`
508
+ * // "apple "
509
509
  * log._stringify_inline({ prev_line_type: 'block', inline_log: "apple" });
510
- * // `
511
- * // apple`
510
+ * // "
511
+ * // apple"
512
512
  * */
513
- // An inline log's string starts with nothing if it's the first string...
514
513
  let start = ``;
515
- // ...starts after a space if its next to an inline string...
516
- if ( prev_line_type === `inline` ) { start = ` `; }
517
- // ...or starts on a new line if it's after a block of text, like an error
518
- // or object.
514
+ if ( prev_line_type === `inline` && !inline_log.startsWith(`\n`) ) { start = ` `; }
515
+ // This is actually the end of the block-type log
519
516
  else if ( prev_line_type === `block` ) { start = `\n`; }
520
517
  return `${ start }${ inline_log }`;
521
518
  }
@@ -525,6 +522,8 @@ class Log {
525
522
  * a "block"-type separator at the start. Can handle circular references.
526
523
  * Catch ANY error.
527
524
  *
525
+ * @param { "inline"|"block"|null } prev_line_type - Required. The type of
526
+ * the previous log.
528
527
  * @param { Object } obj_log - Required. The object to format.
529
528
  *
530
529
  * @returns { string } The obj as a string plus a separator
@@ -536,15 +535,15 @@ class Log {
536
535
  * // { fruit: 'bananas' }
537
536
  * */
538
537
  let start = ``;
539
- if ( prev_line_type === `inline` || prev_line_type === `block` ) {
540
- start = `\n`;
541
- }
538
+ // Avoid adding a new line to the start of the first line
539
+ let is_first_line = prev_line_type === null;
540
+ if ( !is_first_line ) { start = `\n`; }
542
541
 
543
542
  try {
544
543
  // Try to stringify the object.
545
544
  return `${ start }${ this._inspect({ value: obj_log }) }`;
546
545
 
547
- // Discuss: record these errors?
546
+ // Discuss: record the outer error?
548
547
  } catch ( outer_error ) {
549
548
  // If that fails, see if it has its own way of stringifying
550
549
  try {