@redrockswebdevelopment/formstack-form-testing 0.1.1 → 0.1.2

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/dist/index.js CHANGED
@@ -16,6 +16,274 @@ export class FormScenarioRunError extends Error {
16
16
  this.result = result;
17
17
  }
18
18
  }
19
+ export const liveFormFieldTypeCoverage = {
20
+ address: {
21
+ kind: "address",
22
+ strategy: "api-safe",
23
+ apiWrite: true,
24
+ domInteraction: true,
25
+ domAssertion: true,
26
+ reason: "Compound address values can be written through Live Form API, while layout and subfield UX should be asserted through DOM controls.",
27
+ recommendedHelpers: ["buildLiveFormAddressValue", "setLiveFormFieldValueTransaction", "fillAddress", "readLiveFormDomFieldState"],
28
+ },
29
+ checkbox: {
30
+ kind: "checkbox",
31
+ strategy: "api-safe",
32
+ apiWrite: true,
33
+ domInteraction: true,
34
+ domAssertion: true,
35
+ reason: "Checkbox values are stable when using option values. Multi-select UX should still dispatch DOM events or use DOM interaction.",
36
+ recommendedHelpers: ["buildLiveFormCheckboxValue", "setLiveFormFieldValueTransaction", "setCheckboxes", "waitForLiveFormQuiet"],
37
+ },
38
+ date: {
39
+ kind: "date",
40
+ strategy: "api-with-dom-caveats",
41
+ apiWrite: true,
42
+ domInteraction: true,
43
+ domAssertion: true,
44
+ reason: "Date API state can be set reliably, but split/calendar renderer variants should be verified through DOM assertions.",
45
+ recommendedHelpers: ["buildLiveFormDateTimeValue", "setLiveFormFieldValueTransaction", "fillDateTime", "readLiveFormDomFieldState"],
46
+ },
47
+ datetime: {
48
+ kind: "datetime",
49
+ strategy: "api-with-dom-caveats",
50
+ apiWrite: true,
51
+ domInteraction: true,
52
+ domAssertion: true,
53
+ reason: "Date/time API state can be set, but visible split controls may lag or vary by renderer.",
54
+ recommendedHelpers: ["buildLiveFormDateTimeValue", "setLiveFormFieldValueTransaction", "fillDateTime", "readLiveFormDomFieldState"],
55
+ },
56
+ description: {
57
+ kind: "description",
58
+ strategy: "assertion-only",
59
+ apiWrite: false,
60
+ domInteraction: false,
61
+ domAssertion: true,
62
+ reason: "Description fields do not collect user values; test rendered content, visibility, and styling.",
63
+ recommendedHelpers: ["snapshotLiveFormDomFields", "waitForLiveFormVisibility"],
64
+ },
65
+ embed: {
66
+ kind: "embed",
67
+ strategy: "assertion-only",
68
+ apiWrite: false,
69
+ domInteraction: false,
70
+ domAssertion: true,
71
+ reason: "Embed fields generally provide scripts/styles/markup; test side effects and diagnostics, not value writes.",
72
+ recommendedHelpers: ["createFormBrowserMonitor", "waitForConsoleMessage", "waitForWindowFlag", "snapshotLiveFormDomFields"],
73
+ },
74
+ email: {
75
+ kind: "email",
76
+ strategy: "api-safe",
77
+ apiWrite: true,
78
+ domInteraction: true,
79
+ domAssertion: true,
80
+ reason: "Email fields behave like text fields with validation-specific assertions.",
81
+ recommendedHelpers: ["setLiveFormFieldValueTransaction", "fillEmail", "waitForLiveFormValidationState"],
82
+ },
83
+ file: {
84
+ kind: "file",
85
+ strategy: "dom-only",
86
+ apiWrite: false,
87
+ domInteraction: true,
88
+ domAssertion: true,
89
+ reason: "File fields must be tested through browser file upload controls, not generic Live Form API setValue payloads.",
90
+ recommendedHelpers: ["uploadFile", "liveFormFieldRecipes.uploadFileDom", "readLiveFormDomFieldState"],
91
+ },
92
+ matrix: {
93
+ kind: "matrix",
94
+ strategy: "api-safe",
95
+ apiWrite: true,
96
+ domInteraction: true,
97
+ domAssertion: true,
98
+ reason: "Matrix selections can be written by row/column labels or selected through DOM cells.",
99
+ recommendedHelpers: ["buildLiveFormMatrixValue", "setLiveFormFieldValueTransaction", "selectMatrixCell", "readLiveFormDomFieldState"],
100
+ },
101
+ name: {
102
+ kind: "name",
103
+ strategy: "api-safe",
104
+ apiWrite: true,
105
+ domInteraction: true,
106
+ domAssertion: true,
107
+ reason: "Name compound fields support direct API values and stable subfield DOM automation.",
108
+ recommendedHelpers: ["buildLiveFormNameValue", "setLiveFormFieldValueTransaction", "fillName", "readLiveFormReadiness"],
109
+ },
110
+ number: {
111
+ kind: "number",
112
+ strategy: "api-safe",
113
+ apiWrite: true,
114
+ domInteraction: true,
115
+ domAssertion: true,
116
+ reason: "Number fields use simple value payloads; use validation assertions for numeric constraints.",
117
+ recommendedHelpers: ["setLiveFormFieldValueTransaction", "fillNumber", "waitForLiveFormValidationState"],
118
+ },
119
+ pagebreak: {
120
+ kind: "pagebreak",
121
+ strategy: "layout-only",
122
+ apiWrite: false,
123
+ domInteraction: true,
124
+ domAssertion: true,
125
+ reason: "Page breaks do not collect values; test navigation, visible page state, and progress UI.",
126
+ recommendedHelpers: ["clickNext", "clickPrevious", "waitForLiveFormVisibility"],
127
+ },
128
+ payment: {
129
+ kind: "payment",
130
+ strategy: "excluded",
131
+ apiWrite: false,
132
+ domInteraction: false,
133
+ domAssertion: false,
134
+ paymentRisk: true,
135
+ reason: "Payment fields are intentionally excluded from generic helpers because custom embedded code can trigger Formstack attestation and compliance review.",
136
+ recommendedHelpers: ["createFormBrowserMonitor"],
137
+ },
138
+ phone: {
139
+ kind: "phone",
140
+ strategy: "api-safe",
141
+ apiWrite: true,
142
+ domInteraction: true,
143
+ domAssertion: true,
144
+ reason: "Phone fields use simple value payloads; use DOM and validation assertions for formatting behavior.",
145
+ recommendedHelpers: ["setLiveFormFieldValueTransaction", "fillPhone", "waitForLiveFormValidationState"],
146
+ },
147
+ product: {
148
+ kind: "product",
149
+ strategy: "api-with-dom-caveats",
150
+ apiWrite: true,
151
+ domInteraction: true,
152
+ domAssertion: true,
153
+ reason: "Product API state can be set, but visible quantity and amount UI can diverge by product configuration.",
154
+ recommendedHelpers: ["buildLiveFormProductValue", "setLiveFormFieldValueTransaction", "fillProduct", "readLiveFormDomFieldState"],
155
+ },
156
+ radio: {
157
+ kind: "radio",
158
+ strategy: "api-safe",
159
+ apiWrite: true,
160
+ domInteraction: true,
161
+ domAssertion: true,
162
+ reason: "Radio fields are stable when using option values. Dispatch DOM change for custom scripts.",
163
+ recommendedHelpers: ["buildLiveFormChoiceValue", "setLiveFormFieldValueTransaction", "chooseRadio", "waitForLiveFormQuiet"],
164
+ },
165
+ rating: {
166
+ kind: "rating",
167
+ strategy: "api-with-dom-caveats",
168
+ apiWrite: true,
169
+ domInteraction: true,
170
+ domAssertion: true,
171
+ reason: "Rating values can be written through API, but visual stars/buttons should be asserted separately.",
172
+ recommendedHelpers: ["buildLiveFormRatingValue", "setLiveFormFieldValueTransaction", "setRating", "readLiveFormDomFieldState"],
173
+ },
174
+ section: {
175
+ kind: "section",
176
+ strategy: "layout-only",
177
+ apiWrite: false,
178
+ domInteraction: false,
179
+ domAssertion: true,
180
+ reason: "Sections group content and logic visibility; test rendered content and conditional visibility.",
181
+ recommendedHelpers: ["snapshotLiveFormDomFields", "waitForLiveFormVisibility"],
182
+ },
183
+ select: {
184
+ kind: "select",
185
+ strategy: "api-safe",
186
+ apiWrite: true,
187
+ domInteraction: true,
188
+ domAssertion: true,
189
+ reason: "Select fields support stable option values through API or DOM select operations.",
190
+ recommendedHelpers: ["buildLiveFormSelectValue", "setLiveFormFieldValueTransaction", "selectOption", "readLiveFormDomFieldState"],
191
+ },
192
+ signature: {
193
+ kind: "signature",
194
+ strategy: "dom-only",
195
+ apiWrite: false,
196
+ domInteraction: true,
197
+ domAssertion: true,
198
+ reason: "Signature fields require specialized canvas/user-event automation; the portable helper safely supports clearing and DOM assertions.",
199
+ recommendedHelpers: ["clearSignature", "liveFormFieldRecipes.clearSignatureDom", "readLiveFormDomFieldState"],
200
+ },
201
+ textarea: {
202
+ kind: "textarea",
203
+ strategy: "api-safe",
204
+ apiWrite: true,
205
+ domInteraction: true,
206
+ domAssertion: true,
207
+ reason: "Textarea fields use simple value payloads and stable DOM fill operations.",
208
+ recommendedHelpers: ["setLiveFormFieldValueTransaction", "fillTextarea", "readLiveFormDomFieldState"],
209
+ },
210
+ text: {
211
+ kind: "text",
212
+ strategy: "api-safe",
213
+ apiWrite: true,
214
+ domInteraction: true,
215
+ domAssertion: true,
216
+ reason: "Text fields use simple value payloads and stable DOM fill operations.",
217
+ recommendedHelpers: ["setLiveFormFieldValueTransaction", "fillText", "readLiveFormDomFieldState"],
218
+ },
219
+ unknown: {
220
+ kind: "unknown",
221
+ strategy: "excluded",
222
+ apiWrite: false,
223
+ domInteraction: false,
224
+ domAssertion: false,
225
+ reason: "Unknown fields need discovery before using generic automation.",
226
+ recommendedHelpers: ["discoverLiveFormFieldShapes", "snapshotLiveFormDomFields"],
227
+ },
228
+ };
229
+ export function getLiveFormFieldTypeCoverage(kind) {
230
+ return liveFormFieldTypeCoverage[kind];
231
+ }
232
+ export function summarizeLiveFormFieldCoverage(reportOrFields) {
233
+ const fields = "fields" in reportOrFields ? reportOrFields.fields : reportOrFields;
234
+ const countsByKind = {};
235
+ const fieldsWithCoverage = fields.map((field) => {
236
+ countsByKind[field.inferredKind] = (countsByKind[field.inferredKind] ?? 0) + 1;
237
+ return { ...field, coverage: getLiveFormFieldTypeCoverage(field.inferredKind) };
238
+ });
239
+ const uncovered = fields.filter((field) => field.inferredKind === "unknown");
240
+ const paymentFields = fields.filter((field) => field.inferredKind === "payment");
241
+ return {
242
+ fields: fieldsWithCoverage,
243
+ countsByKind,
244
+ uncovered,
245
+ paymentFields,
246
+ hasUnknownFields: uncovered.length > 0,
247
+ hasPaymentFields: paymentFields.length > 0,
248
+ };
249
+ }
250
+ export async function readLiveFormFieldCoverage(page, options = {}) {
251
+ return summarizeLiveFormFieldCoverage(await discoverLiveFormFieldShapes(page, options));
252
+ }
253
+ function formatCoverageField(field) {
254
+ return `${field.inferredKind}:${field.internalLabel ?? field.domId ?? "unlabeled"}`;
255
+ }
256
+ export function assertLiveFormFieldCoverage(report, options = {}) {
257
+ const failures = [];
258
+ if (!options.allowUnknown && report.hasUnknownFields) {
259
+ failures.push(`unknown fields: ${report.uncovered.map(formatCoverageField).join(", ")}`);
260
+ }
261
+ if (!options.allowPayment && report.hasPaymentFields) {
262
+ failures.push(`payment fields: ${report.paymentFields.map(formatCoverageField).join(", ")}`);
263
+ }
264
+ const allowedStrategies = new Set(options.allowedStrategies ?? []);
265
+ if (allowedStrategies.size > 0) {
266
+ const invalid = report.fields.filter((field) => !allowedStrategies.has(field.coverage.strategy));
267
+ if (invalid.length > 0) {
268
+ failures.push(`fields outside allowed strategies: ${invalid.map((field) => `${formatCoverageField(field)}=${field.coverage.strategy}`).join(", ")}`);
269
+ }
270
+ }
271
+ const disallowedStrategies = new Set(options.disallowedStrategies ?? []);
272
+ if (disallowedStrategies.size > 0) {
273
+ const invalid = report.fields.filter((field) => disallowedStrategies.has(field.coverage.strategy));
274
+ if (invalid.length > 0) {
275
+ failures.push(`fields using disallowed strategies: ${invalid.map((field) => `${formatCoverageField(field)}=${field.coverage.strategy}`).join(", ")}`);
276
+ }
277
+ }
278
+ for (const requiredKind of options.requiredKinds ?? []) {
279
+ if ((report.countsByKind[requiredKind] ?? 0) === 0) {
280
+ failures.push(`missing required field kind: ${requiredKind}`);
281
+ }
282
+ }
283
+ if (failures.length > 0) {
284
+ throw new Error(`Live form field coverage assertion failed: ${failures.join("; ")}`);
285
+ }
286
+ }
19
287
  function matchesPattern(value, pattern) {
20
288
  return typeof pattern === "string" ? value.includes(pattern) : pattern.test(value);
21
289
  }
@@ -503,6 +771,8 @@ export async function waitForLiveFormQuiet(page, options = {}) {
503
771
  const waitForApiTimeoutMs = quietOptions.waitForApiTimeoutMs ?? 2_000;
504
772
  const eventTypes = quietOptions.eventTypes ?? ["input", "change", "blur", "click", "submit"];
505
773
  const formEventTypes = quietOptions.formEventTypes ?? ["change", "calculations-complete", "validation-complete", "submit", "error"];
774
+ const observeMutations = quietOptions.observeMutations ?? true;
775
+ const mutationSelector = quietOptions.mutationSelector ?? "form, [id^='fsForm-'], body";
506
776
  const includeEvents = quietOptions.includeEvents ?? true;
507
777
  const events = [];
508
778
  let lastEventAt = now();
@@ -544,6 +814,31 @@ export async function waitForLiveFormQuiet(page, options = {}) {
544
814
  };
545
815
  for (const eventType of eventTypes)
546
816
  document.addEventListener(eventType, domListener, true);
817
+ const mutationTarget = document.querySelector(mutationSelector) ?? document.body;
818
+ const mutationObserver = observeMutations && typeof MutationObserver !== "undefined" && mutationTarget
819
+ ? new MutationObserver((mutations) => {
820
+ const mutationTypes = [...new Set(mutations.map((mutation) => mutation.type))];
821
+ const changedFieldLabels = [
822
+ ...new Set(mutations
823
+ .flatMap((mutation) => [mutation.target, ...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)])
824
+ .filter((node) => node instanceof Element)
825
+ .map((element) => element.closest("[data-internal-label]")?.getAttribute("data-internal-label"))
826
+ .filter((label) => Boolean(label))),
827
+ ].slice(0, 20);
828
+ record("mutation", "mutation", {
829
+ count: mutations.length,
830
+ types: mutationTypes,
831
+ changedFieldLabels,
832
+ });
833
+ })
834
+ : null;
835
+ mutationObserver?.observe(mutationTarget, {
836
+ attributes: true,
837
+ childList: true,
838
+ subtree: true,
839
+ characterData: true,
840
+ attributeFilter: ["class", "style", "aria-invalid", "hidden", "data-internal-label"],
841
+ });
547
842
  const formEventListeners = apiAvailable && form && typeof form === "object"
548
843
  ? formEventTypes.map((type) => ({
549
844
  type,
@@ -592,6 +887,7 @@ export async function waitForLiveFormQuiet(page, options = {}) {
592
887
  finally {
593
888
  for (const eventType of eventTypes)
594
889
  document.removeEventListener(eventType, domListener, true);
890
+ mutationObserver?.disconnect();
595
891
  if (typeof unregisterFormEventListener === "function") {
596
892
  for (const listener of formEventListeners)
597
893
  Reflect.apply(unregisterFormEventListener, form, [listener]);
@@ -1375,10 +1671,27 @@ export async function discoverLiveFormFieldShapes(page, options = {}) {
1375
1671
  const classes = element.className.toString().toLowerCase();
1376
1672
  const types = new Set(controls.map((control) => control.type ?? control.tagName));
1377
1673
  const internalLabel = element.getAttribute("data-internal-label")?.toLowerCase() ?? "";
1674
+ const id = element.getAttribute("id")?.toLowerCase() ?? "";
1675
+ const dataTestId = element.getAttribute("data-testid")?.toLowerCase() ?? "";
1676
+ if (internalLabel.includes("payment") ||
1677
+ internalLabel.includes("credit_card") ||
1678
+ internalLabel.includes("credit-card") ||
1679
+ classes.includes("payment") ||
1680
+ text.includes("credit card") ||
1681
+ controls.some((control) => control.name?.toLowerCase().includes("payment") || control.name?.toLowerCase().includes("credit"))) {
1682
+ return "payment";
1683
+ }
1378
1684
  if (internalLabel.includes("code_embed") || text.startsWith(":root{") || text.includes("<script"))
1379
1685
  return "embed";
1380
- if (controls.length === 0)
1686
+ if (controls.length === 0) {
1687
+ if (classes.includes("pagebreak") || classes.includes("page-break") || dataTestId.includes("pagebreak") || id.includes("pagebreak")) {
1688
+ return "pagebreak";
1689
+ }
1690
+ if (classes.includes("description") || internalLabel.includes("description") || dataTestId.includes("description")) {
1691
+ return "description";
1692
+ }
1381
1693
  return "section";
1694
+ }
1382
1695
  if (classes.includes("matrix") || internalLabel.includes("matrix") || element.querySelector("[id^='matrix-row-'], table"))
1383
1696
  return "matrix";
1384
1697
  if (classes.includes("rating") || internalLabel.includes("rating"))
@@ -1407,6 +1720,8 @@ export async function discoverLiveFormFieldShapes(page, options = {}) {
1407
1720
  return "phone";
1408
1721
  if (types.has("number") || internalLabel.includes("number"))
1409
1722
  return "number";
1723
+ if (internalLabel.includes("date_time") || internalLabel.includes("datetime") || text.includes("am/pm") || text.includes("minutes"))
1724
+ return "datetime";
1410
1725
  if (types.has("date") || internalLabel.includes("date"))
1411
1726
  return "date";
1412
1727
  if (types.has("text"))
@@ -2074,6 +2389,85 @@ export async function setLiveFormFieldValueTransaction(page, options) {
2074
2389
  ...(domFields ? { domFields } : {}),
2075
2390
  };
2076
2391
  }
2392
+ export async function setLiveFormFieldValueTimedTransaction(page, options) {
2393
+ const timeline = [];
2394
+ const startedAt = Date.now();
2395
+ const runPhase = async (name, run) => {
2396
+ const phaseStartedAt = Date.now();
2397
+ try {
2398
+ const result = await run();
2399
+ const endedAt = Date.now();
2400
+ timeline.push({
2401
+ name,
2402
+ startedAtMs: phaseStartedAt - startedAt,
2403
+ endedAtMs: endedAt - startedAt,
2404
+ durationMs: endedAt - phaseStartedAt,
2405
+ ok: true,
2406
+ });
2407
+ return result;
2408
+ }
2409
+ catch (error) {
2410
+ const endedAt = Date.now();
2411
+ timeline.push({
2412
+ name,
2413
+ startedAtMs: phaseStartedAt - startedAt,
2414
+ endedAtMs: endedAt - startedAt,
2415
+ durationMs: endedAt - phaseStartedAt,
2416
+ ok: false,
2417
+ detail: { message: error instanceof Error ? error.message : String(error) },
2418
+ });
2419
+ throw error;
2420
+ }
2421
+ };
2422
+ const beforeReadiness = options.beforeReadiness
2423
+ ? await runPhase("before-readiness", () => readLiveFormReadiness(page, options.beforeReadiness))
2424
+ : undefined;
2425
+ const beforeDomFields = options.beforeSnapshot
2426
+ ? await runPhase("before-snapshot", () => snapshotLiveFormDomFields(page, typeof options.beforeSnapshot === "object" ? options.beforeSnapshot : {}))
2427
+ : undefined;
2428
+ const shouldWaitForEvent = options.waitForEvent === true || (options.waitForEvent === undefined && options.write.notify === true);
2429
+ const defaultEventOptions = {
2430
+ eventTypes: ["change"],
2431
+ target: options.write.target,
2432
+ ...(options.write.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
2433
+ };
2434
+ const eventOptions = typeof options.waitForEvent === "object"
2435
+ ? options.waitForEvent
2436
+ : shouldWaitForEvent
2437
+ ? defaultEventOptions
2438
+ : undefined;
2439
+ const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2440
+ const write = await runPhase("write", () => setLiveFormFieldValue(page, options.write));
2441
+ const event = eventPromise ? await runPhase("wait-for-event", () => eventPromise) : undefined;
2442
+ const domDispatch = options.dispatchDomEvents
2443
+ ? await runPhase("dispatch-dom-events", () => dispatchLiveFormFieldDomEvents(page, {
2444
+ ...(typeof options.dispatchDomEvents === "object" ? options.dispatchDomEvents : {}),
2445
+ target: options.write.target,
2446
+ }))
2447
+ : undefined;
2448
+ const quiet = options.afterQuiet
2449
+ ? await runPhase("after-quiet", () => waitForLiveFormQuiet(page, typeof options.afterQuiet === "object" ? options.afterQuiet : {}))
2450
+ : undefined;
2451
+ const readiness = options.readiness ? await runPhase("readiness", () => readLiveFormReadiness(page, options.readiness)) : undefined;
2452
+ const validation = options.afterValidation
2453
+ ? await runPhase("after-validation", () => waitForLiveFormValidationState(page, typeof options.afterValidation === "object" ? options.afterValidation : {}))
2454
+ : undefined;
2455
+ const domFields = options.snapshot
2456
+ ? await runPhase("snapshot", () => snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {}))
2457
+ : undefined;
2458
+ return {
2459
+ timeline,
2460
+ write,
2461
+ ...(event ? { event } : {}),
2462
+ ...(domDispatch ? { domDispatch } : {}),
2463
+ ...(readiness ? { readiness } : {}),
2464
+ ...(domFields ? { domFields } : {}),
2465
+ ...(beforeReadiness ? { beforeReadiness } : {}),
2466
+ ...(beforeDomFields ? { beforeDomFields } : {}),
2467
+ ...(quiet ? { quiet } : {}),
2468
+ ...(validation ? { validation } : {}),
2469
+ };
2470
+ }
2077
2471
  export async function setLiveFormFieldValuesTransaction(page, options) {
2078
2472
  if (!hasEvaluate(page)) {
2079
2473
  throw new Error("setLiveFormFieldValuesTransaction requires a Playwright page with evaluate().");
@@ -2310,6 +2704,13 @@ export const liveFormScenarioSteps = {
2310
2704
  }
2311
2705
  },
2312
2706
  }),
2707
+ expectFieldCoverage: (name, options = {}) => ({
2708
+ name,
2709
+ run: async (harness) => {
2710
+ const coverage = await readLiveFormFieldCoverage(harness.page, options);
2711
+ assertLiveFormFieldCoverage(coverage, options);
2712
+ },
2713
+ }),
2313
2714
  expectFieldValue: (name, options) => ({
2314
2715
  name,
2315
2716
  run: async (harness) => {
@@ -2450,6 +2851,10 @@ export const liveFormFieldRecipes = {
2450
2851
  name,
2451
2852
  run: (harness) => harness.uploadFile(internalLabel, files),
2452
2853
  }),
2854
+ clearSignatureDom: (name, internalLabel) => ({
2855
+ name,
2856
+ run: (harness) => harness.clearSignature(internalLabel),
2857
+ }),
2453
2858
  setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
2454
2859
  setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
2455
2860
  setChoiceApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormChoiceValue(value), options),