@terreno/blocks 57.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/biome.jsonc +18 -0
  4. package/bunfig.toml +6 -0
  5. package/dist/asks/errors.d.ts +77 -0
  6. package/dist/asks/errors.js +364 -0
  7. package/dist/asks/files.d.ts +53 -0
  8. package/dist/asks/files.js +171 -0
  9. package/dist/asks/formValues.d.ts +32 -0
  10. package/dist/asks/formValues.js +229 -0
  11. package/dist/asks/headless.d.ts +227 -0
  12. package/dist/asks/headless.js +94 -0
  13. package/dist/asks/jsonSchema.d.ts +6 -0
  14. package/dist/asks/jsonSchema.js +62 -0
  15. package/dist/asks/limits.d.ts +58 -0
  16. package/dist/asks/limits.js +61 -0
  17. package/dist/asks/prompt.d.ts +10 -0
  18. package/dist/asks/prompt.js +144 -0
  19. package/dist/asks/schema.d.ts +826 -0
  20. package/dist/asks/schema.js +864 -0
  21. package/dist/asks/simpleCard.d.ts +85 -0
  22. package/dist/asks/simpleCard.js +279 -0
  23. package/dist/asks/validateInput.d.ts +13 -0
  24. package/dist/asks/validateInput.js +19 -0
  25. package/dist/asks/validateResponse.d.ts +13 -0
  26. package/dist/asks/validateResponse.js +278 -0
  27. package/dist/cli.d.ts +11 -0
  28. package/dist/cli.js +58 -0
  29. package/dist/errors.d.ts +51 -0
  30. package/dist/errors.js +106 -0
  31. package/dist/index.d.ts +31 -0
  32. package/dist/index.js +111 -0
  33. package/dist/jsonSchema.d.ts +5 -0
  34. package/dist/jsonSchema.js +12 -0
  35. package/dist/limits.d.ts +27 -0
  36. package/dist/limits.js +30 -0
  37. package/dist/lint.d.ts +21 -0
  38. package/dist/lint.js +388 -0
  39. package/dist/parse.d.ts +16 -0
  40. package/dist/parse.js +95 -0
  41. package/dist/parsePartial.d.ts +12 -0
  42. package/dist/parsePartial.js +105 -0
  43. package/dist/prompt.d.ts +9 -0
  44. package/dist/prompt.js +95 -0
  45. package/dist/schema.d.ts +199 -0
  46. package/dist/schema.js +277 -0
  47. package/dist/validate.d.ts +17 -0
  48. package/dist/validate.js +331 -0
  49. package/package.json +60 -0
  50. package/schemas/pendingAsksResponse.schema.json +195 -0
  51. package/schemas/simpleCard.schema.json +135 -0
  52. package/schemas/turnRequest.schema.json +108 -0
  53. package/schemas/turnResponse.schema.json +204 -0
  54. package/scripts/writeJsonSchemas.ts +11 -0
  55. package/src/asks/docParity.test.ts +65 -0
  56. package/src/asks/errors.test.ts +250 -0
  57. package/src/asks/errors.ts +449 -0
  58. package/src/asks/files.test.ts +294 -0
  59. package/src/asks/files.ts +216 -0
  60. package/src/asks/fixtures/invalid/choice-blank-text.json +20 -0
  61. package/src/asks/fixtures/invalid/choice-default-not-in-options.json +13 -0
  62. package/src/asks/fixtures/invalid/choice-default-selects-two.json +16 -0
  63. package/src/asks/fixtures/invalid/choice-description-too-long.json +24 -0
  64. package/src/asks/fixtures/invalid/choice-duplicate-ids.json +17 -0
  65. package/src/asks/fixtures/invalid/choice-label-too-long.json +23 -0
  66. package/src/asks/fixtures/invalid/choice-many-bounds-not-whole.json +17 -0
  67. package/src/asks/fixtures/invalid/choice-many-bounds-out-of-range.json +17 -0
  68. package/src/asks/fixtures/invalid/choice-many-default-duplicate.json +17 -0
  69. package/src/asks/fixtures/invalid/choice-many-default-too-many.json +15 -0
  70. package/src/asks/fixtures/invalid/choice-many-max-above-options.json +14 -0
  71. package/src/asks/fixtures/invalid/choice-many-min-above-choices.json +13 -0
  72. package/src/asks/fixtures/invalid/choice-many-min-above-max.json +15 -0
  73. package/src/asks/fixtures/invalid/choice-missing-required.json +14 -0
  74. package/src/asks/fixtures/invalid/choice-mixed-errors.json +19 -0
  75. package/src/asks/fixtures/invalid/choice-not-an-object.json +5 -0
  76. package/src/asks/fixtures/invalid/choice-one-with-many-fields.json +19 -0
  77. package/src/asks/fixtures/invalid/choice-option-id-format.json +35 -0
  78. package/src/asks/fixtures/invalid/choice-options-not-objects.json +12 -0
  79. package/src/asks/fixtures/invalid/choice-other-label-too-long.json +25 -0
  80. package/src/asks/fixtures/invalid/choice-other-label-without-allow-other.json +14 -0
  81. package/src/asks/fixtures/invalid/choice-prompt-empty.json +12 -0
  82. package/src/asks/fixtures/invalid/choice-prompt-too-long.json +23 -0
  83. package/src/asks/fixtures/invalid/choice-select-unknown.json +12 -0
  84. package/src/asks/fixtures/invalid/choice-submit-label-too-long.json +13 -0
  85. package/src/asks/fixtures/invalid/choice-title-too-long.json +24 -0
  86. package/src/asks/fixtures/invalid/choice-too-few-options.json +9 -0
  87. package/src/asks/fixtures/invalid/choice-too-many-options.json +219 -0
  88. package/src/asks/fixtures/invalid/choice-unknown-key-option.json +15 -0
  89. package/src/asks/fixtures/invalid/choice-unknown-key-root.json +13 -0
  90. package/src/asks/fixtures/invalid/choice-wrong-types.json +16 -0
  91. package/src/asks/fixtures/invalid/confirm-emoji-label-too-long.json +13 -0
  92. package/src/asks/fixtures/invalid/confirm-label-too-long.json +14 -0
  93. package/src/asks/fixtures/invalid/confirm-labels-alike-after-trim.json +14 -0
  94. package/src/asks/fixtures/invalid/confirm-labels-alike.json +13 -0
  95. package/src/asks/fixtures/invalid/confirm-missing-prompt-blank-label.json +16 -0
  96. package/src/asks/fixtures/invalid/confirm-unknown-keys.json +23 -0
  97. package/src/asks/fixtures/invalid/confirm-wrong-types.json +18 -0
  98. package/src/asks/fixtures/invalid/files-accept-duplicate.json +13 -0
  99. package/src/asks/fixtures/invalid/files-accept-empty.json +13 -0
  100. package/src/asks/fixtures/invalid/files-accept-unknown.json +13 -0
  101. package/src/asks/fixtures/invalid/files-counts-out-of-range.json +19 -0
  102. package/src/asks/fixtures/invalid/files-min-above-max.json +15 -0
  103. package/src/asks/fixtures/invalid/files-missing-accept-unknown-key.json +17 -0
  104. package/src/asks/fixtures/invalid/form-default-bad-format.json +40 -0
  105. package/src/asks/fixtures/invalid/form-default-invalid-date.json +40 -0
  106. package/src/asks/fixtures/invalid/form-default-not-in-options.json +58 -0
  107. package/src/asks/fixtures/invalid/form-default-out-of-range.json +33 -0
  108. package/src/asks/fixtures/invalid/form-default-text-bounds.json +42 -0
  109. package/src/asks/fixtures/invalid/form-default-wrong-type.json +30 -0
  110. package/src/asks/fixtures/invalid/form-duplicate-field-ids.json +24 -0
  111. package/src/asks/fixtures/invalid/form-field-id-format.json +19 -0
  112. package/src/asks/fixtures/invalid/form-field-text-limits.json +33 -0
  113. package/src/asks/fixtures/invalid/form-keys-of-another-type.json +39 -0
  114. package/src/asks/fixtures/invalid/form-length-above-cap.json +30 -0
  115. package/src/asks/fixtures/invalid/form-min-above-max.json +32 -0
  116. package/src/asks/fixtures/invalid/form-no-fields.json +13 -0
  117. package/src/asks/fixtures/invalid/form-select-options.json +44 -0
  118. package/src/asks/fixtures/invalid/form-too-many-fields.json +59 -0
  119. package/src/asks/fixtures/invalid/form-unknown-field-type.json +27 -0
  120. package/src/asks/fixtures/invalid/markdown-bounds-out-of-range.json +18 -0
  121. package/src/asks/fixtures/invalid/markdown-initial-above-cap.json +13 -0
  122. package/src/asks/fixtures/invalid/markdown-max-above-cap.json +13 -0
  123. package/src/asks/fixtures/invalid/markdown-min-above-cap.json +13 -0
  124. package/src/asks/fixtures/invalid/markdown-min-above-max.json +14 -0
  125. package/src/asks/fixtures/invalid/markdown-placeholder-too-long.json +13 -0
  126. package/src/asks/fixtures/invalid/markdown-wrong-types-unknown-key.json +27 -0
  127. package/src/asks/fixtures/valid/choice-emoji-label-cut.json +32 -0
  128. package/src/asks/fixtures/valid/choice-labels-collide-after-cut.json +21 -0
  129. package/src/asks/fixtures/valid/choice-long-text-cut.json +41 -0
  130. package/src/asks/fixtures/valid/choice-many-default-below-min.json +24 -0
  131. package/src/asks/fixtures/valid/choice-many-every-choice-with-other.json +24 -0
  132. package/src/asks/fixtures/valid/choice-many-no-default.json +21 -0
  133. package/src/asks/fixtures/valid/choice-many-optional-no-buttons.json +20 -0
  134. package/src/asks/fixtures/valid/choice-many-options-default-label-collides.json +23 -0
  135. package/src/asks/fixtures/valid/choice-many-options-no-buttons.json +21 -0
  136. package/src/asks/fixtures/valid/choice-many-options-no-default.json +22 -0
  137. package/src/asks/fixtures/valid/choice-many-options-with-default.json +30 -0
  138. package/src/asks/fixtures/valid/choice-many-required-with-default.json +29 -0
  139. package/src/asks/fixtures/valid/choice-many-single-pick-or-other.json +29 -0
  140. package/src/asks/fixtures/valid/choice-many-toppings-with-default.json +36 -0
  141. package/src/asks/fixtures/valid/choice-max-limits.json +245 -0
  142. package/src/asks/fixtures/valid/choice-plan-with-default.json +42 -0
  143. package/src/asks/fixtures/valid/choice-two-options-no-decline.json +32 -0
  144. package/src/asks/fixtures/valid/choice-two-options.json +32 -0
  145. package/src/asks/fixtures/valid/confirm-allow-decline.json +40 -0
  146. package/src/asks/fixtures/valid/confirm-default-labels.json +36 -0
  147. package/src/asks/fixtures/valid/confirm-destructive-delete.json +41 -0
  148. package/src/asks/fixtures/valid/confirm-emoji-labels.json +38 -0
  149. package/src/asks/fixtures/valid/confirm-long-text-cut.json +40 -0
  150. package/src/asks/fixtures/valid/files-every-type.json +28 -0
  151. package/src/asks/fixtures/valid/files-one-csv-no-decline.json +16 -0
  152. package/src/asks/fixtures/valid/files-receipt-photos.json +24 -0
  153. package/src/asks/fixtures/valid/form-boolean-newsletter.json +30 -0
  154. package/src/asks/fixtures/valid/form-date-due.json +44 -0
  155. package/src/asks/fixtures/valid/form-datetime-meeting.json +44 -0
  156. package/src/asks/fixtures/valid/form-datetime-minutes-offset.json +44 -0
  157. package/src/asks/fixtures/valid/form-eight-fields.json +71 -0
  158. package/src/asks/fixtures/valid/form-email-contact.json +44 -0
  159. package/src/asks/fixtures/valid/form-invoice-details.json +68 -0
  160. package/src/asks/fixtures/valid/form-multiselect-channels.json +64 -0
  161. package/src/asks/fixtures/valid/form-number-budget.json +45 -0
  162. package/src/asks/fixtures/valid/form-phone-callback.json +44 -0
  163. package/src/asks/fixtures/valid/form-select-shipping.json +58 -0
  164. package/src/asks/fixtures/valid/form-text-required-default.json +47 -0
  165. package/src/asks/fixtures/valid/form-textarea-notes.json +23 -0
  166. package/src/asks/fixtures/valid/form-time-reminder.json +44 -0
  167. package/src/asks/fixtures/valid/form-url-website.json +30 -0
  168. package/src/asks/fixtures/valid/markdown-announcement-draft.json +39 -0
  169. package/src/asks/fixtures/valid/markdown-empty-draft.json +34 -0
  170. package/src/asks/fixtures/valid/markdown-initial-blank-below-min.json +24 -0
  171. package/src/asks/fixtures/valid/markdown-initial-over-max.json +24 -0
  172. package/src/asks/fixtures/valid/markdown-long-text-cut.json +37 -0
  173. package/src/asks/fixtures/valid/markdown-no-decline.json +28 -0
  174. package/src/asks/fixtures/valid/markdown-no-initial-with-min.json +25 -0
  175. package/src/asks/formValues.ts +320 -0
  176. package/src/asks/headless.test.ts +146 -0
  177. package/src/asks/headless.ts +130 -0
  178. package/src/asks/jsonSchema.test.ts +171 -0
  179. package/src/asks/jsonSchema.ts +89 -0
  180. package/src/asks/limits.ts +58 -0
  181. package/src/asks/prompt.test.ts +200 -0
  182. package/src/asks/prompt.ts +173 -0
  183. package/src/asks/schema.test.ts +198 -0
  184. package/src/asks/schema.ts +1203 -0
  185. package/src/asks/simpleCard.test.ts +998 -0
  186. package/src/asks/simpleCard.ts +341 -0
  187. package/src/asks/validateInput.test.ts +875 -0
  188. package/src/asks/validateInput.ts +24 -0
  189. package/src/asks/validateResponse.test.ts +740 -0
  190. package/src/asks/validateResponse.ts +341 -0
  191. package/src/blocksDocParity.test.ts +21 -0
  192. package/src/cli.test.ts +134 -0
  193. package/src/cli.ts +65 -0
  194. package/src/display.test.ts +68 -0
  195. package/src/errors.test.ts +29 -0
  196. package/src/errors.ts +125 -0
  197. package/src/fixtures/invalid/column-not-found.yaml +14 -0
  198. package/src/fixtures/invalid/column-type.yaml +14 -0
  199. package/src/fixtures/invalid/dataset-not-found.yaml +7 -0
  200. package/src/fixtures/invalid/dataset-too-large.yaml +20 -0
  201. package/src/fixtures/invalid/depth-exceeded.yaml +10 -0
  202. package/src/fixtures/invalid/duplicate-id.yaml +6 -0
  203. package/src/fixtures/invalid/invalid-enum.yaml +5 -0
  204. package/src/fixtures/invalid/key-order.yaml +4 -0
  205. package/src/fixtures/invalid/missing-required.yaml +3 -0
  206. package/src/fixtures/invalid/not-a-document.txt +1 -0
  207. package/src/fixtures/invalid/row-arity.yaml +11 -0
  208. package/src/fixtures/invalid/select-target.yaml +12 -0
  209. package/src/fixtures/invalid/table-too-wide.yaml +9 -0
  210. package/src/fixtures/invalid/too-many-points.yaml +8 -0
  211. package/src/fixtures/invalid/unknown-key.yaml +5 -0
  212. package/src/fixtures/invalid/unsupported-version.yaml +4 -0
  213. package/src/fixtures/invalid/yaml-anchor.yaml +6 -0
  214. package/src/fixtures/partial/cut-after-divider.yaml +5 -0
  215. package/src/fixtures/valid/actions.yaml +32 -0
  216. package/src/fixtures/valid/chart.yaml +20 -0
  217. package/src/fixtures/valid/layout.yaml +26 -0
  218. package/src/fixtures/valid/prose.yaml +4 -0
  219. package/src/html.test.ts +52 -0
  220. package/src/index.test.ts +114 -0
  221. package/src/index.ts +175 -0
  222. package/src/jsonSchema.test.ts +33 -0
  223. package/src/jsonSchema.ts +11 -0
  224. package/src/limits.ts +27 -0
  225. package/src/lint.test.ts +289 -0
  226. package/src/lint.ts +484 -0
  227. package/src/parse.test.ts +281 -0
  228. package/src/parse.ts +104 -0
  229. package/src/parsePartial.test.ts +53 -0
  230. package/src/parsePartial.ts +116 -0
  231. package/src/prompt.test.ts +24 -0
  232. package/src/prompt.ts +104 -0
  233. package/src/schema.ts +509 -0
  234. package/src/tests/askFixtures.ts +40 -0
  235. package/src/validate.perf.test.ts +43 -0
  236. package/src/validate.ts +369 -0
  237. package/tsconfig.json +18 -0
@@ -0,0 +1,875 @@
1
+ import {describe, expect, it} from "bun:test";
2
+ import {invalidAskFixtures, validAskFixtures} from "../tests/askFixtures";
3
+ import {type AskKind, type AskSurface, isCompactAskKind} from "./schema";
4
+ import {validateAskInput} from "./validateInput";
5
+
6
+ const NO_OPTION = {id: "no", label: "No"};
7
+
8
+ const TWO_OPTIONS = [{id: "yes", label: "Yes"}, NO_OPTION];
9
+
10
+ /** What the compact surface returns for each valid fixture, read off the compact rules by hand. */
11
+ const COMPACT_ERRORS_BY_FIXTURE: Record<string, {code: string; path: string}[]> = {
12
+ "choice-emoji-label-cut": [{code: "TOO_LONG", path: "options[0].label"}],
13
+ "choice-labels-collide-after-cut": [
14
+ {code: "TOO_LONG", path: "options[0].label"},
15
+ {code: "TOO_LONG", path: "options[1].label"},
16
+ ],
17
+ "choice-long-text-cut": [
18
+ {code: "TOO_LONG", path: "options[0].label"},
19
+ {code: "TOO_LONG", path: "options[1].label"},
20
+ ],
21
+ "choice-many-default-below-min": [
22
+ {code: "UNKNOWN_KEY", path: "maxSelected"},
23
+ {code: "UNKNOWN_KEY", path: "minSelected"},
24
+ {code: "INVALID_ENUM", path: "select"},
25
+ ],
26
+ "choice-many-every-choice-with-other": [
27
+ {code: "UNKNOWN_KEY", path: "allowOther"},
28
+ {code: "UNKNOWN_KEY", path: "maxSelected"},
29
+ {code: "UNKNOWN_KEY", path: "minSelected"},
30
+ {code: "UNKNOWN_KEY", path: "otherLabel"},
31
+ {code: "INVALID_ENUM", path: "select"},
32
+ ],
33
+ "choice-many-no-default": [{code: "INVALID_ENUM", path: "select"}],
34
+ "choice-many-optional-no-buttons": [
35
+ {code: "UNKNOWN_KEY", path: "minSelected"},
36
+ {code: "INVALID_ENUM", path: "select"},
37
+ ],
38
+ "choice-many-options-default-label-collides": [
39
+ {code: "TOO_MANY", path: "options"},
40
+ {code: "TOO_LONG", path: "options[0].label"},
41
+ {code: "TOO_LONG", path: "options[1].label"},
42
+ ],
43
+ "choice-many-options-no-buttons": [{code: "TOO_MANY", path: "options"}],
44
+ "choice-many-options-no-default": [{code: "TOO_MANY", path: "options"}],
45
+ "choice-many-options-with-default": [{code: "TOO_MANY", path: "options"}],
46
+ "choice-many-required-with-default": [
47
+ {code: "TOO_MANY", path: "options"},
48
+ {code: "INVALID_ENUM", path: "select"},
49
+ ],
50
+ "choice-many-single-pick-or-other": [
51
+ {code: "UNKNOWN_KEY", path: "allowOther"},
52
+ {code: "UNKNOWN_KEY", path: "maxSelected"},
53
+ {code: "INVALID_ENUM", path: "select"},
54
+ ],
55
+ "choice-many-toppings-with-default": [
56
+ {code: "UNKNOWN_KEY", path: "allowOther"},
57
+ {code: "UNKNOWN_KEY", path: "maxSelected"},
58
+ {code: "TOO_MANY", path: "options"},
59
+ {code: "UNKNOWN_KEY", path: "otherLabel"},
60
+ {code: "INVALID_ENUM", path: "select"},
61
+ ],
62
+ "choice-max-limits": [
63
+ {code: "TOO_MANY", path: "options"},
64
+ {code: "TOO_LONG", path: "options[0].label"},
65
+ ],
66
+ "choice-plan-with-default": [],
67
+ "choice-two-options": [],
68
+ "choice-two-options-no-decline": [],
69
+ "confirm-allow-decline": [],
70
+ "confirm-default-labels": [],
71
+ "confirm-destructive-delete": [],
72
+ "confirm-emoji-labels": [],
73
+ "confirm-long-text-cut": [],
74
+ };
75
+
76
+ const compactErrors = (options: {id: string; label: string}[]) =>
77
+ validateAskInput({
78
+ input: {options, prompt: "Pick one.", select: "one"},
79
+ kind: "choice",
80
+ surface: "compact",
81
+ });
82
+
83
+ describe("validateAskInput golden fixtures", () => {
84
+ it("has fixtures in both folders", () => {
85
+ expect(validAskFixtures().length).toBeGreaterThanOrEqual(10);
86
+ expect(invalidAskFixtures().length).toBeGreaterThanOrEqual(20);
87
+ });
88
+
89
+ for (const fixture of validAskFixtures()) {
90
+ it(`accepts valid/${fixture.name}`, () => {
91
+ expect(validateAskInput({input: fixture.input, kind: fixture.kind})).toEqual([]);
92
+ });
93
+ }
94
+
95
+ for (const fixture of invalidAskFixtures()) {
96
+ it(`rejects invalid/${fixture.name} with exactly the expected errors`, () => {
97
+ const errors = validateAskInput({input: fixture.input, kind: fixture.kind});
98
+ expect(errors.map(({code, path}) => ({code, path}))).toEqual(fixture.errors);
99
+ });
100
+ }
101
+ });
102
+
103
+ describe("validateAskInput messages", () => {
104
+ it("explains an unknown key and how to remove it", () => {
105
+ expect(
106
+ validateAskInput({
107
+ input: {icon: "star", options: TWO_OPTIONS, prompt: "Continue?", select: "one"},
108
+ kind: "choice",
109
+ })
110
+ ).toEqual([
111
+ {
112
+ code: "UNKNOWN_KEY",
113
+ fix: 'Remove "icon".',
114
+ message: '"icon" is not a field of this object.',
115
+ path: "icon",
116
+ },
117
+ ]);
118
+ });
119
+
120
+ it("names the first option that used a duplicated id", () => {
121
+ expect(
122
+ validateAskInput({
123
+ input: {
124
+ options: [
125
+ {id: "yes", label: "Yes"},
126
+ {id: "yes", label: "Yes please"},
127
+ ],
128
+ prompt: "Continue?",
129
+ select: "one",
130
+ },
131
+ kind: "choice",
132
+ })
133
+ ).toEqual([
134
+ {
135
+ code: "DUPLICATE_ID",
136
+ fix: "Give every option a unique id.",
137
+ message: 'Option id "yes" is already used by options[0].',
138
+ path: "options[1].id",
139
+ },
140
+ ]);
141
+ });
142
+
143
+ it("gives the length limit in TOO_LONG", () => {
144
+ expect(
145
+ validateAskInput({
146
+ input: {options: TWO_OPTIONS, prompt: "x".repeat(501), select: "one"},
147
+ kind: "choice",
148
+ })
149
+ ).toEqual([
150
+ {
151
+ code: "TOO_LONG",
152
+ fix: "Shorten prompt to 500 characters or fewer.",
153
+ message: "prompt is longer than 500 characters.",
154
+ path: "prompt",
155
+ },
156
+ ]);
157
+ });
158
+
159
+ it("describes the expected and received types", () => {
160
+ expect(
161
+ validateAskInput({
162
+ input: {allowDecline: "no", options: TWO_OPTIONS, prompt: "Continue?", select: "one"},
163
+ kind: "choice",
164
+ })
165
+ ).toEqual([
166
+ {
167
+ code: "INVALID_TYPE",
168
+ fix: "Make allowDecline true or false.",
169
+ message: "allowDecline must be true or false, not a string.",
170
+ path: "allowDecline",
171
+ },
172
+ ]);
173
+ });
174
+
175
+ it("says where a missing field belongs", () => {
176
+ expect(
177
+ validateAskInput({
178
+ input: {
179
+ options: [{id: "yes"}, {id: "no", label: "No"}],
180
+ prompt: "Continue?",
181
+ select: "one",
182
+ },
183
+ kind: "choice",
184
+ })
185
+ ).toEqual([
186
+ {
187
+ code: "MISSING_REQUIRED",
188
+ fix: 'Add "label" to options[0].',
189
+ message: "options[0].label is required.",
190
+ path: "options[0].label",
191
+ },
192
+ ]);
193
+ });
194
+
195
+ it("quotes the rejected value for INVALID_ENUM and INVALID_FORMAT", () => {
196
+ expect(
197
+ validateAskInput({
198
+ input: {
199
+ options: [
200
+ {id: "Yes", label: "Yes"},
201
+ {id: "no", label: "No"},
202
+ ],
203
+ prompt: "Continue?",
204
+ select: "all",
205
+ },
206
+ kind: "choice",
207
+ })
208
+ ).toEqual([
209
+ {
210
+ code: "INVALID_FORMAT",
211
+ fix: 'Use 1-64 lowercase letters, digits, "_", or "-", starting with a letter or digit.',
212
+ message: 'options[0].id "Yes" is not a valid option id.',
213
+ path: "options[0].id",
214
+ },
215
+ {
216
+ code: "INVALID_ENUM",
217
+ fix: 'Use one of "one", "many".',
218
+ message: 'select must be one of "one", "many", not "all".',
219
+ path: "select",
220
+ },
221
+ ]);
222
+ });
223
+
224
+ it("explains select many bounds that are not whole numbers or are out of range", () => {
225
+ expect(
226
+ validateAskInput({
227
+ input: {
228
+ maxSelected: 0,
229
+ minSelected: 1.5,
230
+ options: TWO_OPTIONS,
231
+ prompt: "Pick.",
232
+ select: "many",
233
+ },
234
+ kind: "choice",
235
+ })
236
+ ).toEqual([
237
+ {
238
+ code: "RANGE_INVALID",
239
+ fix: "Make maxSelected at least 1.",
240
+ message: "maxSelected must be at least 1.",
241
+ path: "maxSelected",
242
+ },
243
+ {
244
+ code: "INVALID_TYPE",
245
+ fix: "Make minSelected a whole number.",
246
+ message: "minSelected must be a whole number, not 1.5.",
247
+ path: "minSelected",
248
+ },
249
+ ]);
250
+ });
251
+
252
+ it("names the choice count when maxSelected is more than the ask offers", () => {
253
+ expect(
254
+ validateAskInput({
255
+ input: {maxSelected: 3, options: TWO_OPTIONS, prompt: "Pick.", select: "many"},
256
+ kind: "choice",
257
+ })
258
+ ).toEqual([
259
+ {
260
+ code: "RANGE_INVALID",
261
+ fix: "Set maxSelected to 2 or fewer, or add options.",
262
+ message: "maxSelected is 3, but the ask offers 2 choices.",
263
+ path: "maxSelected",
264
+ },
265
+ ]);
266
+ });
267
+
268
+ it("counts Other as a choice for maxSelected", () => {
269
+ expect(
270
+ validateAskInput({
271
+ input: {
272
+ allowOther: true,
273
+ maxSelected: 3,
274
+ options: TWO_OPTIONS,
275
+ prompt: "Pick.",
276
+ select: "many",
277
+ },
278
+ kind: "choice",
279
+ })
280
+ ).toEqual([]);
281
+ });
282
+
283
+ it("explains minSelected above maxSelected", () => {
284
+ expect(
285
+ validateAskInput({
286
+ input: {
287
+ maxSelected: 1,
288
+ minSelected: 2,
289
+ options: TWO_OPTIONS,
290
+ prompt: "Pick.",
291
+ select: "many",
292
+ },
293
+ kind: "choice",
294
+ })
295
+ ).toEqual([
296
+ {
297
+ code: "RANGE_INVALID",
298
+ fix: "Lower minSelected, or raise maxSelected and add options.",
299
+ message: "minSelected (2) is more than maxSelected (1).",
300
+ path: "minSelected",
301
+ },
302
+ ]);
303
+ });
304
+
305
+ it("names the offered choices as the limit when minSelected and maxSelected are both above them", () => {
306
+ expect(
307
+ validateAskInput({
308
+ input: {
309
+ allowOther: true,
310
+ maxSelected: 5,
311
+ minSelected: 4,
312
+ options: TWO_OPTIONS,
313
+ prompt: "Pick.",
314
+ select: "many",
315
+ },
316
+ kind: "choice",
317
+ })
318
+ ).toEqual([
319
+ {
320
+ code: "RANGE_INVALID",
321
+ fix: "Set maxSelected to 3 or fewer, or add options.",
322
+ message: "maxSelected is 5, but the ask offers 3 choices.",
323
+ path: "maxSelected",
324
+ },
325
+ {
326
+ code: "RANGE_INVALID",
327
+ fix: "Lower minSelected, or raise maxSelected and add options.",
328
+ message: "minSelected (4) is more than the 3 choices the ask offers.",
329
+ path: "minSelected",
330
+ },
331
+ ]);
332
+ });
333
+
334
+ it("names the offered choices as the limit when minSelected is above them without maxSelected", () => {
335
+ expect(
336
+ validateAskInput({
337
+ input: {minSelected: 3, options: TWO_OPTIONS, prompt: "Pick.", select: "many"},
338
+ kind: "choice",
339
+ })
340
+ ).toEqual([
341
+ {
342
+ code: "RANGE_INVALID",
343
+ fix: "Lower minSelected, or raise maxSelected and add options.",
344
+ message: "minSelected (3) is more than the 2 choices the ask offers.",
345
+ path: "minSelected",
346
+ },
347
+ ]);
348
+ });
349
+
350
+ it("points select one asks with many fields to select many", () => {
351
+ expect(
352
+ validateAskInput({
353
+ input: {
354
+ allowOther: true,
355
+ minSelected: 2,
356
+ options: TWO_OPTIONS,
357
+ prompt: "Pick.",
358
+ select: "one",
359
+ },
360
+ kind: "choice",
361
+ })
362
+ ).toEqual([
363
+ {
364
+ code: "OTHER_NOT_ALLOWED",
365
+ fix: 'Use select "many" (with maxSelected 1 for a single pick), or remove allowOther.',
366
+ message: 'allowOther needs select "many".',
367
+ path: "allowOther",
368
+ },
369
+ {
370
+ code: "RANGE_INVALID",
371
+ fix: 'Remove minSelected, or use select "many".',
372
+ message: 'select "one" picks exactly one option, so minSelected must be 1, not 2.',
373
+ path: "minSelected",
374
+ },
375
+ ]);
376
+ });
377
+
378
+ it("explains too many or repeated defaults and otherLabel without allowOther", () => {
379
+ expect(
380
+ validateAskInput({
381
+ input: {
382
+ default: ["yes", "yes", "no"],
383
+ maxSelected: 2,
384
+ options: TWO_OPTIONS,
385
+ otherLabel: "Something else",
386
+ prompt: "Pick.",
387
+ select: "many",
388
+ },
389
+ kind: "choice",
390
+ })
391
+ ).toEqual([
392
+ {
393
+ code: "SELECTION_COUNT",
394
+ fix: "Keep at most 2 option ids in default.",
395
+ message: "default lists 3 option ids, but the ask allows at most 2.",
396
+ path: "default",
397
+ },
398
+ {
399
+ code: "DUPLICATE_ID",
400
+ fix: "List each option id in default once.",
401
+ message: 'Default "yes" is already listed at default[0].',
402
+ path: "default[1]",
403
+ },
404
+ {
405
+ code: "OTHER_NOT_ALLOWED",
406
+ fix: "Set allowOther to true, or remove otherLabel.",
407
+ message: "otherLabel is set, but allowOther is not true.",
408
+ path: "otherLabel",
409
+ },
410
+ ]);
411
+ });
412
+
413
+ it("rejects select many on the compact surface", () => {
414
+ expect(
415
+ validateAskInput({
416
+ input: {options: TWO_OPTIONS, prompt: "Pick.", select: "many"},
417
+ kind: "choice",
418
+ surface: "compact",
419
+ })
420
+ ).toEqual([
421
+ {
422
+ code: "INVALID_ENUM",
423
+ fix: 'Use "one".',
424
+ message: 'select must be "one", not "many".',
425
+ path: "select",
426
+ },
427
+ ]);
428
+ });
429
+
430
+ it("sorts array indexes numerically", () => {
431
+ const options = Array.from({length: 12}, (_, index) => ({
432
+ id: `o${index}`,
433
+ label: `Option ${index}`,
434
+ }));
435
+ options[2] = {id: "o0", label: "Again"};
436
+ options[10] = {id: "o0", label: "Once more"};
437
+ const errors = validateAskInput({
438
+ input: {options, prompt: "Pick.", select: "one"},
439
+ kind: "choice",
440
+ });
441
+ expect(errors.map((error) => error.path)).toEqual(["options[2].id", "options[10].id"]);
442
+ });
443
+ });
444
+
445
+ describe("validateAskInput option id limits", () => {
446
+ it("accepts a 64-character id and rejects a 65-character id", () => {
447
+ const withId = (id: string): unknown => ({
448
+ options: [
449
+ {id, label: "Long"},
450
+ {id: "short", label: "Short"},
451
+ ],
452
+ prompt: "Pick.",
453
+ select: "one",
454
+ });
455
+ expect(validateAskInput({input: withId("a".repeat(64)), kind: "choice"})).toEqual([]);
456
+ expect(
457
+ validateAskInput({input: withId("a".repeat(65)), kind: "choice"}).map((error) => error.code)
458
+ ).toEqual(["INVALID_FORMAT"]);
459
+ });
460
+
461
+ it("accepts digits, underscores, and dashes after the first character", () => {
462
+ expect(
463
+ validateAskInput({
464
+ input: {
465
+ options: [
466
+ {id: "9to5", label: "Nine to five"},
467
+ {id: "night_shift-2", label: "Night shift"},
468
+ ],
469
+ prompt: "Pick a shift.",
470
+ select: "one",
471
+ },
472
+ kind: "choice",
473
+ })
474
+ ).toEqual([]);
475
+ });
476
+ });
477
+
478
+ describe("validateAskInput kinds", () => {
479
+ it("throws for a kind outside the catalog", () => {
480
+ expect(() => validateAskInput({input: {}, kind: "signature" as unknown as AskKind})).toThrow(
481
+ 'Unknown ask kind "signature".'
482
+ );
483
+ });
484
+
485
+ it("throws for a surface outside ASK_SURFACES", () => {
486
+ expect(() =>
487
+ validateAskInput({input: {}, kind: "choice", surface: "watch" as unknown as AskSurface})
488
+ ).toThrow('Unknown ask surface "watch".');
489
+ });
490
+ });
491
+
492
+ const compactKindFixtures = <Fixture extends {kind: AskKind}>(fixtures: Fixture[]): Fixture[] =>
493
+ fixtures.filter((fixture) => isCompactAskKind(fixture.kind));
494
+
495
+ describe("validateAskInput on the compact surface", () => {
496
+ it("covers exactly the valid fixtures of the compact kinds", () => {
497
+ expect(Object.keys(COMPACT_ERRORS_BY_FIXTURE).sort()).toEqual(
498
+ compactKindFixtures(validAskFixtures())
499
+ .map((fixture) => fixture.name)
500
+ .sort()
501
+ );
502
+ });
503
+
504
+ it("throws for a kind the compact surface does not offer", () => {
505
+ expect(() =>
506
+ validateAskInput({input: {prompt: "Edit it."}, kind: "markdown", surface: "compact"})
507
+ ).toThrow('The compact surface does not offer ask kind "markdown".');
508
+ });
509
+
510
+ for (const fixture of compactKindFixtures(validAskFixtures())) {
511
+ it(`returns the compact errors of valid/${fixture.name}`, () => {
512
+ const errors = validateAskInput({
513
+ input: fixture.input,
514
+ kind: fixture.kind,
515
+ surface: "compact",
516
+ });
517
+ expect(errors.map(({code, path}) => ({code, path}))).toEqual(
518
+ COMPACT_ERRORS_BY_FIXTURE[fixture.name] ?? []
519
+ );
520
+ });
521
+ }
522
+
523
+ for (const fixture of compactKindFixtures(invalidAskFixtures())) {
524
+ it(`still rejects invalid/${fixture.name}`, () => {
525
+ expect(
526
+ validateAskInput({input: fixture.input, kind: fixture.kind, surface: "compact"}).length
527
+ ).toBeGreaterThan(0);
528
+ });
529
+ }
530
+
531
+ it("accepts 2 or 3 options with distinct labels of at most 20 characters", () => {
532
+ expect(compactErrors(TWO_OPTIONS)).toEqual([]);
533
+ expect(
534
+ compactErrors([
535
+ {id: "starter", label: "Twenty characters!!!"},
536
+ {id: "team", label: "Exactly twenty chars"},
537
+ {id: "enterprise", label: "Party 🎉🎉🎉🎉🎉🎉🎉"},
538
+ ])
539
+ ).toEqual([]);
540
+ });
541
+
542
+ it("gives the limit of 3 options in TOO_MANY", () => {
543
+ expect(
544
+ compactErrors([...TWO_OPTIONS, {id: "maybe", label: "Maybe"}, {id: "later", label: "Later"}])
545
+ ).toEqual([
546
+ {
547
+ code: "TOO_MANY",
548
+ fix: "Remove items from options until it has 3 or fewer.",
549
+ message: "options has more than 3 items.",
550
+ path: "options",
551
+ },
552
+ ]);
553
+ });
554
+
555
+ it("gives the limit of 20 characters in TOO_LONG", () => {
556
+ expect(compactErrors([{id: "yes", label: "x".repeat(21)}, NO_OPTION])).toEqual([
557
+ {
558
+ code: "TOO_LONG",
559
+ fix: "Shorten options[0].label to 20 characters or fewer.",
560
+ message: "options[0].label is longer than 20 characters.",
561
+ path: "options[0].label",
562
+ },
563
+ ]);
564
+ });
565
+
566
+ it("counts each emoji as 2 or more characters, as the button cut does", () => {
567
+ expect(compactErrors([{id: "party", label: "Party 🎉🎉🎉🎉🎉🎉🎉🎉"}, NO_OPTION])).toEqual([
568
+ {
569
+ code: "TOO_LONG",
570
+ fix: "Shorten options[0].label to 20 characters or fewer, or use fewer emoji.",
571
+ message: "options[0].label is longer than 20 characters, counting each emoji as 2 or more.",
572
+ path: "options[0].label",
573
+ },
574
+ ]);
575
+ });
576
+
577
+ it("names the first option that used a duplicated label", () => {
578
+ expect(
579
+ compactErrors([
580
+ {id: "red", label: "Red"},
581
+ {id: "crimson", label: "Red"},
582
+ ])
583
+ ).toEqual([
584
+ {
585
+ code: "DUPLICATE_LABEL",
586
+ fix: "Give every option a different label.",
587
+ message: 'Option label "Red" is already used by options[0].',
588
+ path: "options[1].label",
589
+ },
590
+ ]);
591
+ });
592
+
593
+ it("counts labels that differ only in spaces at either end as the same", () => {
594
+ expect(
595
+ compactErrors([
596
+ {id: "go", label: "Go"},
597
+ {id: "go_now", label: " Go "},
598
+ ])
599
+ ).toEqual([
600
+ {
601
+ code: "DUPLICATE_LABEL",
602
+ fix: "Give every option a different label.",
603
+ message:
604
+ 'Option label " Go " is already used by options[0], ignoring spaces at either end.',
605
+ path: "options[1].label",
606
+ },
607
+ ]);
608
+ });
609
+
610
+ it("still rejects a label that is only spaces", () => {
611
+ expect(
612
+ compactErrors([{id: "go", label: " "}, NO_OPTION]).map(({code, path}) => ({code, path}))
613
+ ).toEqual([{code: "TOO_SHORT", path: "options[0].label"}]);
614
+ });
615
+
616
+ it("allows duplicated labels on the full surface", () => {
617
+ expect(
618
+ validateAskInput({
619
+ input: {
620
+ options: [
621
+ {id: "red", label: "Red"},
622
+ {id: "crimson", label: "Red"},
623
+ ],
624
+ prompt: "Pick one.",
625
+ select: "one",
626
+ },
627
+ kind: "choice",
628
+ })
629
+ ).toEqual([]);
630
+ });
631
+
632
+ it("keeps the full rules: unique ids and a default among the options", () => {
633
+ expect(
634
+ validateAskInput({
635
+ input: {
636
+ default: ["green"],
637
+ options: [
638
+ {id: "red", label: "Red"},
639
+ {id: "red", label: "Crimson"},
640
+ ],
641
+ prompt: "Pick one.",
642
+ select: "one",
643
+ },
644
+ kind: "choice",
645
+ surface: "compact",
646
+ }).map(({code, path}) => ({code, path}))
647
+ ).toEqual([
648
+ {code: "DEFAULT_NOT_IN_OPTIONS", path: "default[0]"},
649
+ {code: "DUPLICATE_ID", path: "options[1].id"},
650
+ ]);
651
+ });
652
+ });
653
+
654
+ describe("validateAskInput confirm", () => {
655
+ it("accepts a prompt alone, taking the default labels", () => {
656
+ expect(validateAskInput({input: {prompt: "Send it?"}, kind: "confirm"})).toEqual([]);
657
+ });
658
+
659
+ it("gives the 20-character limit of a button label in TOO_LONG", () => {
660
+ expect(
661
+ validateAskInput({input: {confirmLabel: "x".repeat(21), prompt: "Go?"}, kind: "confirm"})
662
+ ).toEqual([
663
+ {
664
+ code: "TOO_LONG",
665
+ fix: "Shorten confirmLabel to 20 characters or fewer.",
666
+ message: "confirmLabel is longer than 20 characters.",
667
+ path: "confirmLabel",
668
+ },
669
+ ]);
670
+ });
671
+
672
+ it("counts each emoji as 2 or more characters, as the button cut does", () => {
673
+ expect(
674
+ validateAskInput({
675
+ input: {denyLabel: "Wait 🕒🕒🕒🕒🕒🕒🕒🕒", prompt: "Go?"},
676
+ kind: "confirm",
677
+ })
678
+ ).toEqual([
679
+ {
680
+ code: "TOO_LONG",
681
+ fix: "Shorten denyLabel to 20 characters or fewer, or use fewer emoji.",
682
+ message: "denyLabel is longer than 20 characters, counting each emoji as 2 or more.",
683
+ path: "denyLabel",
684
+ },
685
+ ]);
686
+ });
687
+
688
+ it("rejects two buttons with the same label, counting the default labels", () => {
689
+ expect(
690
+ validateAskInput({
691
+ input: {confirmLabel: "Cancel", prompt: "Cancel the order?"},
692
+ kind: "confirm",
693
+ })
694
+ ).toEqual([
695
+ {
696
+ code: "DUPLICATE_LABEL",
697
+ fix: 'Give confirmLabel a label other than "Cancel", the deny button\'s label.',
698
+ message: 'confirmLabel "Cancel" is the same as the deny button\'s label.',
699
+ path: "confirmLabel",
700
+ },
701
+ ]);
702
+ expect(
703
+ validateAskInput({
704
+ input: {confirmLabel: "OK", denyLabel: "OK", prompt: "Go?"},
705
+ kind: "confirm",
706
+ }).map(({code, path}) => ({code, path}))
707
+ ).toEqual([{code: "DUPLICATE_LABEL", path: "denyLabel"}]);
708
+ });
709
+
710
+ it("counts labels that differ only in spaces at either end as the same", () => {
711
+ expect(
712
+ validateAskInput({
713
+ input: {confirmLabel: "Send ", denyLabel: " Send", prompt: "Go?"},
714
+ kind: "confirm",
715
+ })
716
+ ).toEqual([
717
+ {
718
+ code: "DUPLICATE_LABEL",
719
+ fix: 'Give denyLabel a label other than "Send ", the approve button\'s label.',
720
+ message:
721
+ 'denyLabel " Send" is the same as the approve button\'s label, ignoring spaces at either end.',
722
+ path: "denyLabel",
723
+ },
724
+ ]);
725
+ expect(
726
+ validateAskInput({input: {confirmLabel: "Cancel ", prompt: "Go?"}, kind: "confirm"}).map(
727
+ ({code, path}) => ({code, path})
728
+ )
729
+ ).toEqual([{code: "DUPLICATE_LABEL", path: "confirmLabel"}]);
730
+ expect(
731
+ validateAskInput({input: {denyLabel: " ", prompt: "Go?"}, kind: "confirm"}).map(
732
+ ({code, path}) => ({code, path})
733
+ )
734
+ ).toEqual([{code: "TOO_SHORT", path: "denyLabel"}]);
735
+ });
736
+
737
+ it("names confirmLabel and denyLabel in place of submitLabel", () => {
738
+ expect(
739
+ validateAskInput({input: {prompt: "Send it?", submitLabel: "Send"}, kind: "confirm"})
740
+ ).toEqual([
741
+ {
742
+ code: "UNKNOWN_KEY",
743
+ fix: 'Remove "submitLabel".',
744
+ message: '"submitLabel" is not a field of this object.',
745
+ path: "submitLabel",
746
+ },
747
+ ]);
748
+ });
749
+
750
+ it("applies the same rules on the compact surface", () => {
751
+ for (const input of [
752
+ {prompt: "Send it?"},
753
+ {confirmLabel: "Delete 14 todos", denyLabel: "Keep them", destructive: true, prompt: "Go?"},
754
+ {confirmLabel: "Cancel", prompt: "Go?"},
755
+ {confirmLabel: "x".repeat(21), prompt: "Go?"},
756
+ ]) {
757
+ expect(validateAskInput({input, kind: "confirm", surface: "compact"})).toEqual(
758
+ validateAskInput({input, kind: "confirm"})
759
+ );
760
+ }
761
+ });
762
+ });
763
+
764
+ describe("validateAskInput form", () => {
765
+ const form = (fields: unknown[]) => ({fields, prompt: "A few details."});
766
+
767
+ it("accepts a form with a single field", () => {
768
+ expect(
769
+ validateAskInput({
770
+ input: form([{id: "company", label: "Company", type: "text"}]),
771
+ kind: "form",
772
+ })
773
+ ).toEqual([]);
774
+ });
775
+
776
+ it("names a field id that breaks the id format", () => {
777
+ expect(
778
+ validateAskInput({
779
+ input: form([{id: "Full Name", label: "Name", type: "text"}]),
780
+ kind: "form",
781
+ })
782
+ ).toEqual([
783
+ {
784
+ code: "INVALID_FORMAT",
785
+ fix: 'Use 1-64 lowercase letters, digits, "_", or "-", starting with a letter or digit.',
786
+ message: 'fields[0].id "Full Name" is not a valid field id.',
787
+ path: "fields[0].id",
788
+ },
789
+ ]);
790
+ });
791
+
792
+ it("names the first field that used a duplicated id", () => {
793
+ expect(
794
+ validateAskInput({
795
+ input: form([
796
+ {id: "name", label: "Name", type: "text"},
797
+ {id: "name", label: "Email", type: "email"},
798
+ ]),
799
+ kind: "form",
800
+ })
801
+ ).toEqual([
802
+ {
803
+ code: "DUPLICATE_ID",
804
+ fix: "Give every field a unique id.",
805
+ message: 'Field id "name" is already used by fields[0].',
806
+ path: "fields[1].id",
807
+ },
808
+ ]);
809
+ });
810
+
811
+ it("lists the field types for an unknown type, so a password field is refused", () => {
812
+ const [error] = validateAskInput({
813
+ input: form([{id: "secret", label: "Password", type: "password"}]),
814
+ kind: "form",
815
+ });
816
+ expect(error.code).toBe("INVALID_ENUM");
817
+ expect(error.path).toBe("fields[0].type");
818
+ expect(error.fix).toBe(
819
+ 'Use one of "text", "textarea", "email", "url", "phone", "number", "date", "time", "datetime", "boolean", "select", "multiselect".'
820
+ );
821
+ });
822
+
823
+ it("checks a default with the same rules as an answer, naming the default", () => {
824
+ expect(
825
+ validateAskInput({
826
+ input: form([
827
+ {default: 0, id: "seats", label: "Seats", max: 500, min: 1, type: "number"},
828
+ {
829
+ default: "apac",
830
+ id: "region",
831
+ label: "Region",
832
+ options: [
833
+ {id: "us", label: "US"},
834
+ {id: "eu", label: "EU"},
835
+ ],
836
+ type: "select",
837
+ },
838
+ ]),
839
+ kind: "form",
840
+ })
841
+ ).toEqual([
842
+ {
843
+ code: "OUT_OF_RANGE",
844
+ fix: "Make fields[0].default at least 1.",
845
+ message: "fields[0].default is 0, below the field's min of 1.",
846
+ path: "fields[0].default",
847
+ },
848
+ {
849
+ code: "DEFAULT_NOT_IN_OPTIONS",
850
+ fix: "Use an id from fields[1].options, or remove it from the default.",
851
+ message: 'Default "apac" is not the id of any option of the field.',
852
+ path: "fields[1].default",
853
+ },
854
+ ]);
855
+ });
856
+
857
+ it("runs the default rules only once the field's shape is valid", () => {
858
+ expect(
859
+ validateAskInput({
860
+ input: form([{default: 0, id: "seats", label: "Seats", min: "1", type: "number"}]),
861
+ kind: "form",
862
+ }).map(({code, path}) => ({code, path}))
863
+ ).toEqual([{code: "INVALID_TYPE", path: "fields[0].min"}]);
864
+ });
865
+
866
+ it("is not offered on the compact surface, because fields cannot be filled in there", () => {
867
+ expect(() =>
868
+ validateAskInput({
869
+ input: form([{id: "name", label: "Name", type: "text"}]),
870
+ kind: "form",
871
+ surface: "compact",
872
+ })
873
+ ).toThrow('The compact surface does not offer ask kind "form".');
874
+ });
875
+ });