@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.d.ts +64 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +406 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
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),
|