@redrockswebdevelopment/formstack-form-testing 0.1.0 → 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
@@ -7,6 +7,283 @@ export class FormBrowserDiagnosticsError extends Error {
7
7
  this.diagnostics = diagnostics;
8
8
  }
9
9
  }
10
+ export class FormScenarioRunError extends Error {
11
+ result;
12
+ constructor(result) {
13
+ const failedStep = result.steps.find((step) => step.status === "failed");
14
+ super(failedStep ? `Form scenario "${result.name}" failed at step "${failedStep.name}": ${failedStep.error?.message ?? "unknown error"}` : `Form scenario "${result.name}" failed.`);
15
+ this.name = "FormScenarioRunError";
16
+ this.result = result;
17
+ }
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
+ }
10
287
  function matchesPattern(value, pattern) {
11
288
  return typeof pattern === "string" ? value.includes(pattern) : pattern.test(value);
12
289
  }
@@ -103,6 +380,2126 @@ export function createFormBrowserMonitor(page, options = {}) {
103
380
  },
104
381
  };
105
382
  }
383
+ function hasEvaluate(page) {
384
+ return typeof page.evaluate === "function";
385
+ }
386
+ export function readValuePath(value, path) {
387
+ if (!path)
388
+ return value;
389
+ return path.split(".").reduce((current, segment) => {
390
+ if (current === null || current === undefined || typeof current !== "object")
391
+ return undefined;
392
+ return Reflect.get(current, segment);
393
+ }, value);
394
+ }
395
+ export function isLiveFormFieldValueEmpty(value) {
396
+ if (value === null || value === undefined)
397
+ return true;
398
+ if (typeof value === "string")
399
+ return value.trim().length === 0;
400
+ if (typeof value === "number" || typeof value === "boolean")
401
+ return false;
402
+ if (Array.isArray(value))
403
+ return value.length === 0 || value.every((item) => isLiveFormFieldValueEmpty(item));
404
+ if (typeof value === "object") {
405
+ const meaningfulValues = Object.entries(value).filter(([key]) => key !== "otherValue");
406
+ return meaningfulValues.length === 0 || meaningfulValues.every(([, item]) => isLiveFormFieldValueEmpty(item));
407
+ }
408
+ return false;
409
+ }
410
+ export function buildLiveFormNameValue(value, current = {}) {
411
+ return {
412
+ ...(current && typeof current === "object" ? current : {}),
413
+ ...value,
414
+ };
415
+ }
416
+ export function buildLiveFormAddressValue(value, current = {}) {
417
+ return {
418
+ ...(current && typeof current === "object" ? current : {}),
419
+ ...value,
420
+ };
421
+ }
422
+ export function buildLiveFormChoiceValue(value, otherValue = "") {
423
+ return { value, otherValue };
424
+ }
425
+ export function buildLiveFormSelectValue(value) {
426
+ return { value };
427
+ }
428
+ export function buildLiveFormCheckboxValue(values, otherValue = null) {
429
+ return Object.fromEntries([...values.map((value, index) => [String(index), value]), ["otherValue", otherValue]]);
430
+ }
431
+ export function buildLiveFormDateTimeValue(value, current = {}) {
432
+ return {
433
+ ...(current && typeof current === "object" ? current : {}),
434
+ ...(value.month !== undefined ? { M: value.month } : {}),
435
+ ...(value.day !== undefined ? { D: value.day } : {}),
436
+ ...(value.year !== undefined ? { Y: value.year } : {}),
437
+ ...(value.hour !== undefined ? { H: value.hour } : {}),
438
+ ...(value.minutes !== undefined ? { I: value.minutes } : {}),
439
+ ...(value.seconds !== undefined ? { S: value.seconds } : {}),
440
+ ...(value.ampm !== undefined ? { A: value.ampm } : {}),
441
+ };
442
+ }
443
+ export function buildLiveFormProductValue(value, current = {}) {
444
+ return {
445
+ ...(current && typeof current === "object" ? current : {}),
446
+ ...value,
447
+ };
448
+ }
449
+ export function buildLiveFormRatingValue(value) {
450
+ return { value: String(value) };
451
+ }
452
+ export function buildLiveFormMatrixValue(selections) {
453
+ return {
454
+ value: selections.map((selection) => {
455
+ if (typeof selection === "string")
456
+ return selection;
457
+ return `${selection.row} = ${selection.column}`;
458
+ }),
459
+ };
460
+ }
461
+ export async function readLiveFormFieldValue(page, options) {
462
+ if (!hasEvaluate(page)) {
463
+ throw new Error("readLiveFormFieldValue requires a Playwright page with evaluate().");
464
+ }
465
+ return page.evaluate(async (readOptions) => {
466
+ const waitForApiTimeoutMs = readOptions.waitForApiTimeoutMs ?? 10_000;
467
+ const readPath = (value, path) => {
468
+ if (!path)
469
+ return value;
470
+ return path.split(".").reduce((current, segment) => {
471
+ if (current === null || current === undefined || typeof current !== "object")
472
+ return undefined;
473
+ return Reflect.get(current, segment);
474
+ }, value);
475
+ };
476
+ const isEmpty = (value) => {
477
+ if (value === null || value === undefined)
478
+ return true;
479
+ if (typeof value === "string")
480
+ return value.trim().length === 0;
481
+ if (typeof value === "number" || typeof value === "boolean")
482
+ return false;
483
+ if (Array.isArray(value))
484
+ return value.length === 0 || value.every((item) => isEmpty(item));
485
+ if (typeof value === "object") {
486
+ const meaningfulValues = Object.entries(value).filter(([key]) => key !== "otherValue");
487
+ return meaningfulValues.length === 0 || meaningfulValues.every(([, item]) => isEmpty(item));
488
+ }
489
+ return false;
490
+ };
491
+ const valuesEqual = (left, right) => {
492
+ if (Object.is(left, right))
493
+ return true;
494
+ try {
495
+ return JSON.stringify(left) === JSON.stringify(right);
496
+ }
497
+ catch {
498
+ return false;
499
+ }
500
+ };
501
+ const readApi = () => {
502
+ const factory = Reflect.get(window, "fsApi");
503
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
504
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
505
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
506
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
507
+ };
508
+ const deadline = Date.now() + waitForApiTimeoutMs;
509
+ let form = null;
510
+ while (Date.now() < deadline) {
511
+ form = readApi().form;
512
+ if (form && typeof form === "object")
513
+ break;
514
+ await new Promise((resolve) => setTimeout(resolve, 50));
515
+ }
516
+ const apiAvailable = Boolean(form && typeof form === "object");
517
+ if (!apiAvailable || !form || typeof form !== "object") {
518
+ return {
519
+ apiAvailable: false,
520
+ fieldAvailable: false,
521
+ id: null,
522
+ value: null,
523
+ selectedValue: null,
524
+ ...(readOptions.valuePath === undefined ? {} : { valuePath: readOptions.valuePath }),
525
+ ...(readOptions.expectedValue === undefined ? {} : { expectedValue: readOptions.expectedValue, valueMatched: false }),
526
+ isEmpty: true,
527
+ };
528
+ }
529
+ const getField = Reflect.get(form, "getField");
530
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
531
+ const field = readOptions.target.fieldId != null && typeof getField === "function"
532
+ ? Reflect.apply(getField, form, [String(readOptions.target.fieldId)])
533
+ : readOptions.target.internalLabel && typeof getFieldByInternalLabel === "function"
534
+ ? Reflect.apply(getFieldByInternalLabel, form, [readOptions.target.internalLabel])
535
+ : null;
536
+ const fieldAvailable = Boolean(field && typeof field === "object");
537
+ if (!fieldAvailable || !field || typeof field !== "object") {
538
+ return {
539
+ apiAvailable: true,
540
+ fieldAvailable: false,
541
+ id: null,
542
+ value: null,
543
+ selectedValue: null,
544
+ ...(readOptions.valuePath === undefined ? {} : { valuePath: readOptions.valuePath }),
545
+ ...(readOptions.expectedValue === undefined ? {} : { expectedValue: readOptions.expectedValue, valueMatched: false }),
546
+ isEmpty: true,
547
+ };
548
+ }
549
+ const getId = Reflect.get(field, "getId");
550
+ const getValue = Reflect.get(field, "getValue");
551
+ const id = typeof getId === "function" ? Reflect.apply(getId, field, []) : Reflect.get(field, "id") ?? null;
552
+ const value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
553
+ const selectedValue = readPath(value, readOptions.valuePath);
554
+ return {
555
+ apiAvailable: true,
556
+ fieldAvailable: true,
557
+ id,
558
+ value,
559
+ selectedValue,
560
+ ...(readOptions.valuePath === undefined ? {} : { valuePath: readOptions.valuePath }),
561
+ ...(readOptions.expectedValue === undefined ? {} : { expectedValue: readOptions.expectedValue, valueMatched: valuesEqual(selectedValue, readOptions.expectedValue) }),
562
+ isEmpty: isEmpty(selectedValue),
563
+ };
564
+ }, options);
565
+ }
566
+ export async function waitForLiveFormFieldValue(page, options) {
567
+ const timeoutMs = options.timeoutMs ?? 10_000;
568
+ const intervalMs = options.intervalMs ?? 100;
569
+ const startedAt = Date.now();
570
+ let latest = await readLiveFormFieldValue(page, options);
571
+ const isMatched = (report) => options.expectedValue === undefined ? report.fieldAvailable && !report.isEmpty : report.valueMatched === true;
572
+ while (!isMatched(latest) && Date.now() - startedAt < timeoutMs) {
573
+ await delay(intervalMs);
574
+ latest = await readLiveFormFieldValue(page, options);
575
+ }
576
+ return latest;
577
+ }
578
+ export async function readLiveFormReadiness(page, options = {}) {
579
+ if (!hasEvaluate(page)) {
580
+ throw new Error("readLiveFormReadiness requires a Playwright page with evaluate().");
581
+ }
582
+ return page.evaluate(async (readinessOptions) => {
583
+ const fieldTargets = readinessOptions.fieldTargets ?? [];
584
+ const waitForApiTimeoutMs = readinessOptions.waitForApiTimeoutMs ?? 10_000;
585
+ const readPath = (value, path) => {
586
+ if (!path)
587
+ return value;
588
+ return path.split(".").reduce((current, segment) => {
589
+ if (current === null || current === undefined || typeof current !== "object")
590
+ return undefined;
591
+ return Reflect.get(current, segment);
592
+ }, value);
593
+ };
594
+ const isEmpty = (value) => {
595
+ if (value === null || value === undefined)
596
+ return true;
597
+ if (typeof value === "string")
598
+ return value.trim().length === 0;
599
+ if (typeof value === "number" || typeof value === "boolean")
600
+ return false;
601
+ if (Array.isArray(value))
602
+ return value.length === 0 || value.every((item) => isEmpty(item));
603
+ if (typeof value === "object") {
604
+ const meaningfulValues = Object.entries(value).filter(([key]) => key !== "otherValue");
605
+ return meaningfulValues.length === 0 || meaningfulValues.every(([, item]) => isEmpty(item));
606
+ }
607
+ return false;
608
+ };
609
+ const readApi = () => {
610
+ const factory = Reflect.get(window, "fsApi");
611
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
612
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
613
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
614
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
615
+ };
616
+ const deadline = Date.now() + waitForApiTimeoutMs;
617
+ let form = null;
618
+ while (Date.now() < deadline) {
619
+ form = readApi().form;
620
+ if (form && typeof form === "object")
621
+ break;
622
+ await new Promise((resolve) => setTimeout(resolve, 50));
623
+ }
624
+ const apiAvailable = Boolean(form && typeof form === "object");
625
+ if (!apiAvailable || !form || typeof form !== "object") {
626
+ return {
627
+ apiAvailable: false,
628
+ fields: [],
629
+ blockingFieldCount: 0,
630
+ isReady: false,
631
+ };
632
+ }
633
+ const getFieldTarget = (target) => {
634
+ const getField = Reflect.get(form, "getField");
635
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
636
+ if (target.fieldId != null && typeof getField === "function")
637
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
638
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
639
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
640
+ }
641
+ return null;
642
+ };
643
+ const snapshotField = (field, target) => {
644
+ if (!field || typeof field !== "object") {
645
+ const required = target.required ?? true;
646
+ return {
647
+ target,
648
+ available: false,
649
+ id: null,
650
+ value: null,
651
+ required,
652
+ apiHidden: false,
653
+ hidden: false,
654
+ domVisible: null,
655
+ requiredValuePaths: target.requiredValuePaths ?? [],
656
+ missingValuePaths: target.requiredValuePaths ?? [],
657
+ isComplete: false,
658
+ isBlocking: required,
659
+ };
660
+ }
661
+ const getId = Reflect.get(field, "getId");
662
+ const getValue = Reflect.get(field, "getValue");
663
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
664
+ let value = null;
665
+ let required = target.required ?? false;
666
+ let apiHidden = false;
667
+ try {
668
+ value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
669
+ }
670
+ catch {
671
+ value = null;
672
+ }
673
+ try {
674
+ if (typeof getGeneralAttribute === "function") {
675
+ required = target.required ?? Reflect.apply(getGeneralAttribute, field, ["required"]) === true;
676
+ apiHidden = Reflect.apply(getGeneralAttribute, field, ["hidden"]) === true;
677
+ }
678
+ }
679
+ catch {
680
+ required = target.required ?? false;
681
+ }
682
+ const id = typeof getId === "function" ? Reflect.apply(getId, field, []) : Reflect.get(field, "id") ?? null;
683
+ const idRoot = id == null
684
+ ? null
685
+ : document.querySelector(`#fsCell${CSS.escape(String(id))}, #fsFieldCell${CSS.escape(String(id))}, [data-testid="field-${CSS.escape(String(id))}"]`);
686
+ const domRoot = idRoot ?? (target.internalLabel != null
687
+ ? document.querySelector(`[data-internal-label="${CSS.escape(target.internalLabel)}"]`)
688
+ : null);
689
+ const isActuallyVisible = (element) => {
690
+ if (!(element instanceof HTMLElement))
691
+ return null;
692
+ for (let current = element; current; current = current.parentElement) {
693
+ const style = getComputedStyle(current);
694
+ const rect = current.getBoundingClientRect();
695
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
696
+ return false;
697
+ if (rect.width <= 0 || rect.height <= 0)
698
+ return false;
699
+ }
700
+ return element.getClientRects().length > 0;
701
+ };
702
+ const domVisible = isActuallyVisible(domRoot);
703
+ const hidden = apiHidden || domVisible === false;
704
+ const requiredValuePaths = target.requiredValuePaths ?? [];
705
+ const missingValuePaths = requiredValuePaths.length > 0
706
+ ? requiredValuePaths.filter((path) => isEmpty(readPath(value, path)))
707
+ : isEmpty(value)
708
+ ? [""]
709
+ : [];
710
+ const isComplete = missingValuePaths.length === 0;
711
+ const isBlocking = required && !hidden && !isComplete;
712
+ return {
713
+ target,
714
+ available: true,
715
+ id,
716
+ value,
717
+ required,
718
+ apiHidden,
719
+ hidden,
720
+ domVisible,
721
+ requiredValuePaths,
722
+ missingValuePaths,
723
+ isComplete,
724
+ isBlocking,
725
+ };
726
+ };
727
+ const fields = [];
728
+ if (readinessOptions.includeAllFields) {
729
+ const getFields = Reflect.get(form, "getFields");
730
+ const allFields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
731
+ if (Array.isArray(allFields)) {
732
+ for (const field of allFields)
733
+ fields.push(snapshotField(field, { label: "all-fields" }));
734
+ }
735
+ }
736
+ for (const target of fieldTargets) {
737
+ fields.push(snapshotField(getFieldTarget(target), target));
738
+ }
739
+ const blockingFieldCount = fields.filter((field) => field.isBlocking).length;
740
+ return {
741
+ apiAvailable: true,
742
+ fields,
743
+ blockingFieldCount,
744
+ isReady: blockingFieldCount === 0,
745
+ };
746
+ }, options);
747
+ }
748
+ function delay(ms) {
749
+ return new Promise((resolve) => setTimeout(resolve, ms));
750
+ }
751
+ export async function waitForLiveFormReadiness(page, options = {}) {
752
+ const timeoutMs = options.timeoutMs ?? 10_000;
753
+ const intervalMs = options.intervalMs ?? 100;
754
+ const startedAt = Date.now();
755
+ let latest = await readLiveFormReadiness(page, options);
756
+ while (!latest.isReady && Date.now() - startedAt < timeoutMs) {
757
+ await delay(intervalMs);
758
+ latest = await readLiveFormReadiness(page, options);
759
+ }
760
+ return latest;
761
+ }
762
+ export async function waitForLiveFormQuiet(page, options = {}) {
763
+ if (!hasEvaluate(page)) {
764
+ throw new Error("waitForLiveFormQuiet requires a Playwright page with evaluate().");
765
+ }
766
+ return page.evaluate(async (quietOptions) => {
767
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
768
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
769
+ const quietMs = quietOptions.quietMs ?? 250;
770
+ const timeoutMs = quietOptions.timeoutMs ?? 5_000;
771
+ const waitForApiTimeoutMs = quietOptions.waitForApiTimeoutMs ?? 2_000;
772
+ const eventTypes = quietOptions.eventTypes ?? ["input", "change", "blur", "click", "submit"];
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";
776
+ const includeEvents = quietOptions.includeEvents ?? true;
777
+ const events = [];
778
+ let lastEventAt = now();
779
+ const elapsedMs = () => Math.round(now() - startedAt);
780
+ const record = (kind, type, detail) => {
781
+ lastEventAt = now();
782
+ if (includeEvents) {
783
+ events.push({
784
+ kind,
785
+ type,
786
+ elapsedMs: elapsedMs(),
787
+ detail,
788
+ });
789
+ }
790
+ };
791
+ const readApi = () => {
792
+ const factory = Reflect.get(window, "fsApi");
793
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
794
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
795
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
796
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
797
+ };
798
+ const apiDeadline = Date.now() + waitForApiTimeoutMs;
799
+ let form = null;
800
+ while (Date.now() < apiDeadline) {
801
+ form = readApi().form;
802
+ if (form && typeof form === "object")
803
+ break;
804
+ await new Promise((resolve) => setTimeout(resolve, 50));
805
+ }
806
+ const apiAvailable = Boolean(form && typeof form === "object");
807
+ const domListener = (event) => {
808
+ const target = event.target instanceof Element ? event.target : null;
809
+ record("dom-event", event.type, {
810
+ targetId: target?.getAttribute("id") ?? null,
811
+ targetName: target?.getAttribute("name") ?? null,
812
+ internalLabel: target?.closest("[data-internal-label]")?.getAttribute("data-internal-label") ?? null,
813
+ });
814
+ };
815
+ for (const eventType of eventTypes)
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
+ });
842
+ const formEventListeners = apiAvailable && form && typeof form === "object"
843
+ ? formEventTypes.map((type) => ({
844
+ type,
845
+ onFormEvent: (event) => {
846
+ const eventObject = event && typeof event === "object" ? event : {};
847
+ record("form-event", type, {
848
+ formId: Reflect.get(eventObject, "formId") ?? null,
849
+ data: Reflect.get(eventObject, "data") ?? null,
850
+ });
851
+ },
852
+ }))
853
+ : [];
854
+ const registerFormEventListener = apiAvailable && form && typeof form === "object" ? Reflect.get(form, "registerFormEventListener") : null;
855
+ const unregisterFormEventListener = apiAvailable && form && typeof form === "object" ? Reflect.get(form, "unregisterFormEventListener") : null;
856
+ if (typeof registerFormEventListener === "function") {
857
+ for (const listener of formEventListeners)
858
+ Reflect.apply(registerFormEventListener, form, [listener]);
859
+ }
860
+ const deadline = Date.now() + timeoutMs;
861
+ try {
862
+ while (Date.now() < deadline) {
863
+ const quietForMs = Math.round(now() - lastEventAt);
864
+ if (quietForMs >= quietMs) {
865
+ return {
866
+ apiAvailable,
867
+ quiet: true,
868
+ timedOut: false,
869
+ elapsedMs: elapsedMs(),
870
+ quietForMs,
871
+ eventCount: events.length,
872
+ ...(includeEvents ? { events } : {}),
873
+ };
874
+ }
875
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, Math.max(quietMs - quietForMs, 10))));
876
+ }
877
+ return {
878
+ apiAvailable,
879
+ quiet: false,
880
+ timedOut: true,
881
+ elapsedMs: elapsedMs(),
882
+ quietForMs: Math.round(now() - lastEventAt),
883
+ eventCount: events.length,
884
+ ...(includeEvents ? { events } : {}),
885
+ };
886
+ }
887
+ finally {
888
+ for (const eventType of eventTypes)
889
+ document.removeEventListener(eventType, domListener, true);
890
+ mutationObserver?.disconnect();
891
+ if (typeof unregisterFormEventListener === "function") {
892
+ for (const listener of formEventListeners)
893
+ Reflect.apply(unregisterFormEventListener, form, [listener]);
894
+ }
895
+ }
896
+ }, options);
897
+ }
898
+ export async function installLiveFormInitialValidationGuard(page, options) {
899
+ if (!hasEvaluate(page)) {
900
+ throw new Error("installLiveFormInitialValidationGuard requires a Playwright page with evaluate().");
901
+ }
902
+ return page.evaluate(async (guardOptions) => {
903
+ const waitForApiTimeoutMs = guardOptions.waitForApiTimeoutMs ?? 10_000;
904
+ const restoreDelayMs = guardOptions.restoreDelayMs ?? 700;
905
+ const restoreOnFirstFocusout = guardOptions.restoreOnFirstFocusout ?? true;
906
+ const guardedFields = [];
907
+ const missingTargets = [];
908
+ const readApi = () => {
909
+ const factory = Reflect.get(window, "fsApi");
910
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
911
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
912
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
913
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
914
+ };
915
+ const deadline = Date.now() + waitForApiTimeoutMs;
916
+ let form = null;
917
+ while (Date.now() < deadline) {
918
+ form = readApi().form;
919
+ if (form && typeof form === "object")
920
+ break;
921
+ await new Promise((resolve) => setTimeout(resolve, 50));
922
+ }
923
+ const apiAvailable = Boolean(form && typeof form === "object");
924
+ if (!apiAvailable || !form || typeof form !== "object") {
925
+ return {
926
+ apiAvailable: false,
927
+ installed: false,
928
+ guardedFieldCount: 0,
929
+ missingTargets: guardOptions.targets,
930
+ };
931
+ }
932
+ const resolveField = (target) => {
933
+ const getField = Reflect.get(form, "getField");
934
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
935
+ if (target.fieldId != null && typeof getField === "function")
936
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
937
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
938
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
939
+ }
940
+ return null;
941
+ };
942
+ const readSkipValidation = (field) => {
943
+ if (!field || typeof field !== "object")
944
+ return undefined;
945
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
946
+ return typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["shouldForceSkipValidation"]) : undefined;
947
+ };
948
+ const setSkipValidation = (field, value) => {
949
+ if (!field || typeof field !== "object")
950
+ return;
951
+ const setGeneralAttribute = Reflect.get(field, "setGeneralAttribute");
952
+ if (typeof setGeneralAttribute === "function")
953
+ Reflect.apply(setGeneralAttribute, field, ["shouldForceSkipValidation", value]);
954
+ };
955
+ for (const target of guardOptions.targets) {
956
+ const field = resolveField(target);
957
+ if (!field || typeof field !== "object") {
958
+ missingTargets.push(target);
959
+ continue;
960
+ }
961
+ guardedFields.push({ field, target, previousValue: readSkipValidation(field) });
962
+ setSkipValidation(field, true);
963
+ }
964
+ const restore = () => {
965
+ for (const entry of guardedFields)
966
+ setSkipValidation(entry.field, entry.previousValue === true);
967
+ document.removeEventListener("focusout", onFocusout, true);
968
+ Reflect.deleteProperty(window, "__fsptInitialValidationGuardRestore");
969
+ };
970
+ const matchesTarget = (element, target) => {
971
+ if (target.controlIds?.some((id) => element.id === id || element.closest(`#${CSS.escape(id)}`)))
972
+ return true;
973
+ if (target.controlSelectors?.some((selector) => element.matches(selector) || element.closest(selector)))
974
+ return true;
975
+ if (target.internalLabel && element.closest(`[data-internal-label="${CSS.escape(target.internalLabel)}"]`))
976
+ return true;
977
+ if (target.fieldId != null && element.closest(`#fsCell${CSS.escape(String(target.fieldId))}`))
978
+ return true;
979
+ return false;
980
+ };
981
+ function onFocusout(event) {
982
+ if (!restoreOnFirstFocusout || !(event.target instanceof Element))
983
+ return;
984
+ if (!guardOptions.targets.some((target) => matchesTarget(event.target, target)))
985
+ return;
986
+ window.setTimeout(restore, restoreDelayMs);
987
+ }
988
+ Object.defineProperty(window, "__fsptInitialValidationGuardRestore", {
989
+ configurable: true,
990
+ value: restore,
991
+ });
992
+ if (restoreOnFirstFocusout)
993
+ document.addEventListener("focusout", onFocusout, true);
994
+ return {
995
+ apiAvailable: true,
996
+ installed: guardedFields.length > 0,
997
+ guardedFieldCount: guardedFields.length,
998
+ missingTargets,
999
+ };
1000
+ }, options);
1001
+ }
1002
+ export async function setLiveFormFieldValue(page, options) {
1003
+ if (!hasEvaluate(page)) {
1004
+ throw new Error("setLiveFormFieldValue requires a Playwright page with evaluate().");
1005
+ }
1006
+ return page.evaluate(async (writeOptions) => {
1007
+ const timeoutMs = writeOptions.timeoutMs ?? 5_000;
1008
+ const intervalMs = writeOptions.intervalMs ?? 50;
1009
+ const settleMs = writeOptions.settleMs ?? 100;
1010
+ const readApi = () => {
1011
+ const factory = Reflect.get(window, "fsApi");
1012
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1013
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1014
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1015
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
1016
+ };
1017
+ const valuesEqual = (left, right) => {
1018
+ if (Object.is(left, right))
1019
+ return true;
1020
+ try {
1021
+ return JSON.stringify(left) === JSON.stringify(right);
1022
+ }
1023
+ catch {
1024
+ return false;
1025
+ }
1026
+ };
1027
+ const readFieldValue = (field) => {
1028
+ if (!field || typeof field !== "object")
1029
+ return null;
1030
+ const getValue = Reflect.get(field, "getValue");
1031
+ return typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
1032
+ };
1033
+ const resolveField = (form) => {
1034
+ if (!form || typeof form !== "object")
1035
+ return null;
1036
+ const getField = Reflect.get(form, "getField");
1037
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1038
+ if (writeOptions.target.fieldId != null && typeof getField === "function") {
1039
+ return Reflect.apply(getField, form, [String(writeOptions.target.fieldId)]);
1040
+ }
1041
+ if (writeOptions.target.internalLabel && typeof getFieldByInternalLabel === "function") {
1042
+ return Reflect.apply(getFieldByInternalLabel, form, [writeOptions.target.internalLabel]);
1043
+ }
1044
+ return null;
1045
+ };
1046
+ const { form } = readApi();
1047
+ const apiAvailable = Boolean(form && typeof form === "object");
1048
+ if (!apiAvailable || !form || typeof form !== "object") {
1049
+ return {
1050
+ apiAvailable: false,
1051
+ fieldAvailable: false,
1052
+ id: null,
1053
+ previousValue: null,
1054
+ expectedValue: writeOptions.value,
1055
+ finalValue: null,
1056
+ valueMatched: false,
1057
+ notified: false,
1058
+ };
1059
+ }
1060
+ const field = resolveField(form);
1061
+ const fieldAvailable = Boolean(field && typeof field === "object");
1062
+ if (!fieldAvailable || !field || typeof field !== "object") {
1063
+ return {
1064
+ apiAvailable: true,
1065
+ fieldAvailable: false,
1066
+ id: null,
1067
+ previousValue: null,
1068
+ expectedValue: writeOptions.value,
1069
+ finalValue: null,
1070
+ valueMatched: false,
1071
+ notified: false,
1072
+ };
1073
+ }
1074
+ const setValue = Reflect.get(field, "setValue");
1075
+ const previousValue = readFieldValue(field);
1076
+ if (typeof setValue !== "function") {
1077
+ return {
1078
+ apiAvailable: true,
1079
+ fieldAvailable: true,
1080
+ id: Reflect.get(field, "id") ?? null,
1081
+ previousValue,
1082
+ expectedValue: writeOptions.value,
1083
+ finalValue: previousValue,
1084
+ valueMatched: false,
1085
+ notified: false,
1086
+ };
1087
+ }
1088
+ await Promise.resolve(Reflect.apply(setValue, field, [writeOptions.value]));
1089
+ let finalValue = readFieldValue(field);
1090
+ if (writeOptions.waitForValue ?? true) {
1091
+ const deadline = Date.now() + timeoutMs;
1092
+ while (!valuesEqual(finalValue, writeOptions.value) && Date.now() < deadline) {
1093
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1094
+ finalValue = readFieldValue(field);
1095
+ }
1096
+ }
1097
+ if (settleMs > 0)
1098
+ await new Promise((resolve) => setTimeout(resolve, settleMs));
1099
+ let notifyResult = null;
1100
+ if (writeOptions.notify) {
1101
+ const notifyFormEventListeners = Reflect.get(form, "notifyFormEventListeners");
1102
+ notifyResult =
1103
+ typeof notifyFormEventListeners === "function"
1104
+ ? await Promise.resolve(Reflect.apply(notifyFormEventListeners, form, [
1105
+ "change",
1106
+ {
1107
+ fieldId: Reflect.get(field, "id") ?? null,
1108
+ type: "api-setValue",
1109
+ value: writeOptions.value,
1110
+ },
1111
+ ]))
1112
+ : null;
1113
+ }
1114
+ finalValue = readFieldValue(field);
1115
+ return {
1116
+ apiAvailable: true,
1117
+ fieldAvailable: true,
1118
+ id: Reflect.get(field, "id") ?? null,
1119
+ previousValue,
1120
+ expectedValue: writeOptions.value,
1121
+ finalValue,
1122
+ valueMatched: valuesEqual(finalValue, writeOptions.value),
1123
+ notified: writeOptions.notify === true,
1124
+ ...(writeOptions.notify ? { notifyResult } : {}),
1125
+ };
1126
+ }, options);
1127
+ }
1128
+ export async function snapshotLiveFormDomFields(page, options = {}) {
1129
+ if (!hasEvaluate(page)) {
1130
+ throw new Error("snapshotLiveFormDomFields requires a Playwright page with evaluate().");
1131
+ }
1132
+ return page.evaluate((snapshotOptions) => {
1133
+ const selector = snapshotOptions.selector ?? "[data-internal-label]";
1134
+ const textMaxLength = snapshotOptions.textMaxLength ?? 160;
1135
+ return Array.from(document.querySelectorAll(selector)).map((element) => ({
1136
+ internalLabel: element.getAttribute("data-internal-label"),
1137
+ id: element.id,
1138
+ text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, textMaxLength) ?? "",
1139
+ controls: Array.from(element.querySelectorAll("input, textarea, select")).map((control) => {
1140
+ const input = control;
1141
+ return {
1142
+ tagName: control.tagName.toLowerCase(),
1143
+ type: control.getAttribute("type"),
1144
+ id: control.getAttribute("id"),
1145
+ name: control.getAttribute("name"),
1146
+ value: control.getAttribute("value"),
1147
+ currentValue: "value" in input ? input.value : null,
1148
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1149
+ };
1150
+ }),
1151
+ }));
1152
+ }, options);
1153
+ }
1154
+ export async function readLiveFormDomFieldState(page, options) {
1155
+ if (!hasEvaluate(page)) {
1156
+ throw new Error("readLiveFormDomFieldState requires a Playwright page with evaluate().");
1157
+ }
1158
+ return page.evaluate((stateOptions) => {
1159
+ const controlSnapshot = (control) => {
1160
+ const input = control;
1161
+ return {
1162
+ tagName: control.tagName.toLowerCase(),
1163
+ type: control.getAttribute("type"),
1164
+ id: control.getAttribute("id"),
1165
+ name: control.getAttribute("name"),
1166
+ value: control.getAttribute("value"),
1167
+ currentValue: "value" in input ? input.value : null,
1168
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1169
+ };
1170
+ };
1171
+ const root = stateOptions.selector != null
1172
+ ? document.querySelector(stateOptions.selector)
1173
+ : stateOptions.internalLabel != null
1174
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
1175
+ : stateOptions.fieldId == null
1176
+ ? null
1177
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
1178
+ const fieldMatched = Boolean(root);
1179
+ const field = root
1180
+ ? {
1181
+ internalLabel: root.getAttribute("data-internal-label"),
1182
+ id: root.id,
1183
+ text: root.textContent?.replace(/\s+/g, " ").trim().slice(0, 160) ?? "",
1184
+ controls: Array.from(root.querySelectorAll("input, textarea, select")).map(controlSnapshot),
1185
+ }
1186
+ : null;
1187
+ const controls = root == null
1188
+ ? []
1189
+ : Array.from(root.querySelectorAll(stateOptions.controlSelector ?? "input, textarea, select")).map(controlSnapshot);
1190
+ const controlMatched = controls.some((control) => {
1191
+ const valueMatched = stateOptions.expectedValue === undefined || control.currentValue === stateOptions.expectedValue || control.value === stateOptions.expectedValue;
1192
+ const checkedMatched = stateOptions.expectedChecked === undefined || control.checked === stateOptions.expectedChecked;
1193
+ return valueMatched && checkedMatched;
1194
+ });
1195
+ return {
1196
+ matched: fieldMatched && controlMatched,
1197
+ fieldMatched,
1198
+ controlMatched,
1199
+ field,
1200
+ controls,
1201
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
1202
+ ...(stateOptions.expectedChecked === undefined ? {} : { expectedChecked: stateOptions.expectedChecked }),
1203
+ };
1204
+ }, options);
1205
+ }
1206
+ export async function waitForLiveFormDomFieldState(page, options) {
1207
+ const timeoutMs = options.timeoutMs ?? 10_000;
1208
+ const intervalMs = options.intervalMs ?? 100;
1209
+ const startedAt = Date.now();
1210
+ let latest = await readLiveFormDomFieldState(page, options);
1211
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
1212
+ await delay(intervalMs);
1213
+ latest = await readLiveFormDomFieldState(page, options);
1214
+ }
1215
+ return latest;
1216
+ }
1217
+ export async function dispatchLiveFormFieldDomEvents(page, options) {
1218
+ if (!hasEvaluate(page)) {
1219
+ throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
1220
+ }
1221
+ return page.evaluate((dispatchOptions) => {
1222
+ const eventTypes = dispatchOptions.eventTypes ?? ["input", "change"];
1223
+ const root = dispatchOptions.target.internalLabel != null
1224
+ ? document.querySelector(`[data-internal-label="${CSS.escape(dispatchOptions.target.internalLabel)}"]`)
1225
+ : dispatchOptions.target.fieldId == null
1226
+ ? null
1227
+ : document.querySelector(`#fsCell${CSS.escape(String(dispatchOptions.target.fieldId))}, [data-testid="field-${CSS.escape(String(dispatchOptions.target.fieldId))}"]`);
1228
+ const allControls = root ? Array.from(root.querySelectorAll("input, textarea, select")) : [];
1229
+ const controls = dispatchOptions.onlyCheckedChoices ?? true
1230
+ ? allControls.filter((control) => {
1231
+ if (control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type))
1232
+ return control.checked;
1233
+ if (control instanceof HTMLInputElement && control.type === "hidden")
1234
+ return false;
1235
+ return true;
1236
+ })
1237
+ : allControls;
1238
+ for (const control of controls) {
1239
+ for (const type of eventTypes) {
1240
+ control.dispatchEvent(new Event(type, { bubbles: true }));
1241
+ }
1242
+ }
1243
+ return {
1244
+ matchedControlCount: controls.length,
1245
+ dispatchedEventCount: controls.length * eventTypes.length,
1246
+ controls: controls.map((control) => ({
1247
+ tagName: control.tagName.toLowerCase(),
1248
+ type: control.getAttribute("type"),
1249
+ id: control.getAttribute("id"),
1250
+ name: control.getAttribute("name"),
1251
+ value: control.getAttribute("value"),
1252
+ currentValue: "value" in control ? control.value : null,
1253
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
1254
+ })),
1255
+ };
1256
+ }, options);
1257
+ }
1258
+ export async function installWindowMessageRecorder(page, options = {}) {
1259
+ if (!hasEvaluate(page)) {
1260
+ throw new Error("installWindowMessageRecorder requires a Playwright page with evaluate().");
1261
+ }
1262
+ return page.evaluate((recorderOptions) => {
1263
+ const propertyName = recorderOptions.propertyName ?? "__fsptWindowMessages";
1264
+ const allowedOrigins = recorderOptions.allowedOrigins ?? [];
1265
+ const records = [];
1266
+ const listener = (event) => {
1267
+ if (allowedOrigins.length > 0 && !allowedOrigins.includes(event.origin))
1268
+ return;
1269
+ records.push({
1270
+ origin: event.origin,
1271
+ data: event.data,
1272
+ });
1273
+ };
1274
+ window.addEventListener("message", listener);
1275
+ Object.defineProperty(window, propertyName, {
1276
+ configurable: true,
1277
+ value: {
1278
+ records,
1279
+ dispose: () => window.removeEventListener("message", listener),
1280
+ clear: () => {
1281
+ records.length = 0;
1282
+ },
1283
+ },
1284
+ });
1285
+ return { installed: true, propertyName };
1286
+ }, options);
1287
+ }
1288
+ export async function readWindowMessageRecords(page, propertyName = "__fsptWindowMessages") {
1289
+ if (!hasEvaluate(page)) {
1290
+ throw new Error("readWindowMessageRecords requires a Playwright page with evaluate().");
1291
+ }
1292
+ return page.evaluate((name) => {
1293
+ const recorder = Reflect.get(window, name);
1294
+ return Array.isArray(recorder?.records) ? [...recorder.records] : [];
1295
+ }, propertyName);
1296
+ }
1297
+ export async function clearWindowMessageRecords(page, propertyName = "__fsptWindowMessages") {
1298
+ if (!hasEvaluate(page)) {
1299
+ throw new Error("clearWindowMessageRecords requires a Playwright page with evaluate().");
1300
+ }
1301
+ await page.evaluate((name) => {
1302
+ const recorder = Reflect.get(window, name);
1303
+ if (typeof recorder?.clear === "function")
1304
+ recorder.clear();
1305
+ }, propertyName);
1306
+ }
1307
+ export async function postMessageToIframe(page, options) {
1308
+ if (!hasEvaluate(page)) {
1309
+ throw new Error("postMessageToIframe requires a Playwright page with evaluate().");
1310
+ }
1311
+ return page.evaluate((postOptions) => {
1312
+ const iframe = document.querySelector(postOptions.iframeSelector);
1313
+ if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow)
1314
+ return false;
1315
+ iframe.contentWindow.postMessage(postOptions.message, postOptions.targetOrigin);
1316
+ return true;
1317
+ }, options);
1318
+ }
1319
+ export function buildFormstackIframeBridgeMessage(message) {
1320
+ return message;
1321
+ }
1322
+ export async function waitForWindowMessage(page, options = {}) {
1323
+ if (!hasEvaluate(page)) {
1324
+ throw new Error("waitForWindowMessage requires a Playwright page with evaluate().");
1325
+ }
1326
+ return page.evaluate(async (waitOptions) => {
1327
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
1328
+ const propertyName = waitOptions.propertyName ?? "__fsptWindowMessages";
1329
+ const timeoutMs = waitOptions.timeoutMs ?? 5_000;
1330
+ const deadline = Date.now() + timeoutMs;
1331
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1332
+ const matches = (record) => {
1333
+ const data = record.data && typeof record.data === "object" ? record.data : {};
1334
+ if (waitOptions.channel && Reflect.get(data, "channel") !== waitOptions.channel)
1335
+ return false;
1336
+ if (waitOptions.requestId && Reflect.get(data, "requestId") !== waitOptions.requestId)
1337
+ return false;
1338
+ if (waitOptions.type && Reflect.get(data, "type") !== waitOptions.type)
1339
+ return false;
1340
+ return true;
1341
+ };
1342
+ while (Date.now() < deadline) {
1343
+ const recorder = Reflect.get(window, propertyName);
1344
+ const records = Array.isArray(recorder?.records) ? recorder.records : [];
1345
+ const record = records.find(matches);
1346
+ if (record) {
1347
+ return {
1348
+ matched: true,
1349
+ timedOut: false,
1350
+ elapsedMs: Math.round(now() - startedAt),
1351
+ record,
1352
+ };
1353
+ }
1354
+ await new Promise((resolve) => setTimeout(resolve, 25));
1355
+ }
1356
+ return {
1357
+ matched: false,
1358
+ timedOut: true,
1359
+ elapsedMs: Math.round(now() - startedAt),
1360
+ };
1361
+ }, options);
1362
+ }
1363
+ export async function sendFormstackIframeBridgeMessage(page, options) {
1364
+ await installWindowMessageRecorder(page, {
1365
+ ...(options.propertyName === undefined ? {} : { propertyName: options.propertyName }),
1366
+ ...(options.allowedOrigins === undefined ? {} : { allowedOrigins: options.allowedOrigins }),
1367
+ });
1368
+ const posted = await postMessageToIframe(page, {
1369
+ iframeSelector: options.iframeSelector,
1370
+ targetOrigin: options.targetOrigin,
1371
+ message: buildFormstackIframeBridgeMessage({
1372
+ channel: options.channel,
1373
+ requestId: options.requestId,
1374
+ type: options.type,
1375
+ ...(options.payload === undefined ? {} : { payload: options.payload }),
1376
+ }),
1377
+ });
1378
+ const response = await waitForWindowMessage(page, {
1379
+ channel: options.channel,
1380
+ requestId: options.requestId,
1381
+ type: options.responseType ?? `${options.type}:response`,
1382
+ ...(options.propertyName === undefined ? {} : { propertyName: options.propertyName }),
1383
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
1384
+ });
1385
+ return { posted, response };
1386
+ }
1387
+ export async function waitForConsoleMessage(page, options = {}) {
1388
+ const timeoutMs = options.timeoutMs ?? 5_000;
1389
+ const startedAt = Date.now();
1390
+ return new Promise((resolve) => {
1391
+ let settled = false;
1392
+ const cleanup = () => {
1393
+ page.off?.("console", handler);
1394
+ };
1395
+ const finish = (report) => {
1396
+ if (settled)
1397
+ return;
1398
+ settled = true;
1399
+ cleanup();
1400
+ resolve(report);
1401
+ };
1402
+ const handler = (message) => {
1403
+ const type = message.type();
1404
+ const text = message.text();
1405
+ if (options.type && type !== options.type)
1406
+ return;
1407
+ if (options.text && !matchesPattern(text, options.text))
1408
+ return;
1409
+ finish({
1410
+ matched: true,
1411
+ timedOut: false,
1412
+ elapsedMs: Date.now() - startedAt,
1413
+ message: { type, text },
1414
+ });
1415
+ };
1416
+ page.on("console", handler);
1417
+ setTimeout(() => {
1418
+ finish({
1419
+ matched: false,
1420
+ timedOut: true,
1421
+ elapsedMs: Date.now() - startedAt,
1422
+ });
1423
+ }, timeoutMs);
1424
+ });
1425
+ }
1426
+ export async function waitForWindowFlag(page, options) {
1427
+ if (!hasEvaluate(page)) {
1428
+ throw new Error("waitForWindowFlag requires a Playwright page with evaluate().");
1429
+ }
1430
+ return page.evaluate(async (flagOptions) => {
1431
+ const startedAt = Date.now();
1432
+ const timeoutMs = flagOptions.timeoutMs ?? 5_000;
1433
+ const intervalMs = flagOptions.intervalMs ?? 50;
1434
+ const deadline = Date.now() + timeoutMs;
1435
+ const read = () => flagOptions.path.split(".").reduce((current, segment) => {
1436
+ if (current === null || current === undefined || typeof current !== "object")
1437
+ return undefined;
1438
+ return Reflect.get(current, segment);
1439
+ }, window);
1440
+ const matches = (value) => {
1441
+ if (flagOptions.expectedValue === undefined)
1442
+ return Boolean(value);
1443
+ return Object.is(value, flagOptions.expectedValue);
1444
+ };
1445
+ let value = read();
1446
+ while (!matches(value) && Date.now() < deadline) {
1447
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1448
+ value = read();
1449
+ }
1450
+ const matched = matches(value);
1451
+ return {
1452
+ matched,
1453
+ timedOut: !matched,
1454
+ elapsedMs: Date.now() - startedAt,
1455
+ path: flagOptions.path,
1456
+ value,
1457
+ };
1458
+ }, options);
1459
+ }
1460
+ export async function readLiveFormValidationState(page, options = {}) {
1461
+ if (!hasEvaluate(page)) {
1462
+ throw new Error("readLiveFormValidationState requires a Playwright page with evaluate().");
1463
+ }
1464
+ return page.evaluate(async (validationOptions) => {
1465
+ const factory = Reflect.get(window, "fsApi");
1466
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1467
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1468
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1469
+ const form = Array.isArray(forms) ? forms[0] ?? null : null;
1470
+ const apiAvailable = Boolean(form && typeof form === "object");
1471
+ let result = null;
1472
+ if (validationOptions.validate && apiAvailable && form && typeof form === "object") {
1473
+ const validateForm = Reflect.get(form, "validateForm");
1474
+ result = typeof validateForm === "function" ? await Promise.resolve(Reflect.apply(validateForm, form, [])) : null;
1475
+ }
1476
+ const isVisible = (element) => {
1477
+ if (!(element instanceof HTMLElement))
1478
+ return false;
1479
+ for (let current = element; current; current = current.parentElement) {
1480
+ const style = getComputedStyle(current);
1481
+ const rect = current.getBoundingClientRect();
1482
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
1483
+ return false;
1484
+ if (rect.width <= 0 || rect.height <= 0)
1485
+ return false;
1486
+ }
1487
+ return element.getClientRects().length > 0;
1488
+ };
1489
+ const inlineErrors = Array.from(document.querySelectorAll('[id^="inline-error-field"], .fsError, [role="alert"]'))
1490
+ .map((element) => ({
1491
+ id: element.getAttribute("id"),
1492
+ text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 240) ?? "",
1493
+ visible: isVisible(element),
1494
+ }))
1495
+ .filter((error) => validationOptions.includeEmptyErrors === true || error.text.length > 0)
1496
+ .filter((error) => validationOptions.includeHiddenErrors === true || error.visible)
1497
+ .map(({ id, text }) => ({ id, text }));
1498
+ return {
1499
+ apiAvailable,
1500
+ validated: validationOptions.validate === true,
1501
+ result,
1502
+ inlineErrors,
1503
+ };
1504
+ }, options);
1505
+ }
1506
+ export async function waitForLiveFormValidationState(page, options = {}) {
1507
+ const timeoutMs = options.timeoutMs ?? 10_000;
1508
+ const intervalMs = options.intervalMs ?? 100;
1509
+ const startedAt = Date.now();
1510
+ const matches = (state) => {
1511
+ if (options.expectNoErrors === true)
1512
+ return state.inlineErrors.length === 0;
1513
+ if (options.expectedErrorCount !== undefined && state.inlineErrors.length !== options.expectedErrorCount)
1514
+ return false;
1515
+ if (options.errorText !== undefined) {
1516
+ const errorText = options.errorText;
1517
+ return state.inlineErrors.some((error) => typeof errorText === "string" ? error.text.includes(errorText) : errorText.test(error.text));
1518
+ }
1519
+ return state.inlineErrors.length > 0;
1520
+ };
1521
+ let latest = await readLiveFormValidationState(page, options);
1522
+ while (!matches(latest) && Date.now() - startedAt < timeoutMs) {
1523
+ await delay(intervalMs);
1524
+ latest = await readLiveFormValidationState(page, options);
1525
+ }
1526
+ return latest;
1527
+ }
1528
+ export async function waitForLiveFormVisibility(page, options) {
1529
+ if (!hasEvaluate(page)) {
1530
+ throw new Error("waitForLiveFormVisibility requires a Playwright page with evaluate().");
1531
+ }
1532
+ return page.evaluate(async (visibilityOptions) => {
1533
+ const timeoutMs = visibilityOptions.timeoutMs ?? 5_000;
1534
+ const intervalMs = visibilityOptions.intervalMs ?? 50;
1535
+ const deadline = Date.now() + timeoutMs;
1536
+ const readApi = () => {
1537
+ const factory = Reflect.get(window, "fsApi");
1538
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1539
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1540
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1541
+ return Array.isArray(forms) ? forms[0] ?? null : null;
1542
+ };
1543
+ const snapshot = () => {
1544
+ const form = readApi();
1545
+ return visibilityOptions.targets.map((target) => {
1546
+ let id = target.fieldId ?? null;
1547
+ if (!id && form && typeof form === "object" && target.internalLabel) {
1548
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1549
+ const field = typeof getFieldByInternalLabel === "function"
1550
+ ? Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel])
1551
+ : null;
1552
+ id = field && typeof field === "object" ? Reflect.get(field, "id") ?? null : null;
1553
+ }
1554
+ const idRoot = id == null
1555
+ ? null
1556
+ : document.querySelector(`#fsCell${CSS.escape(String(id))}, #fsFieldCell${CSS.escape(String(id))}, [data-testid="field-${CSS.escape(String(id))}"]`);
1557
+ const root = target.selector != null
1558
+ ? document.querySelector(target.selector)
1559
+ : idRoot ?? (target.internalLabel != null
1560
+ ? document.querySelector(`[data-internal-label="${CSS.escape(target.internalLabel)}"]`)
1561
+ : null);
1562
+ const visible = root instanceof HTMLElement
1563
+ ? (() => {
1564
+ for (let current = root; current; current = current.parentElement) {
1565
+ const style = getComputedStyle(current);
1566
+ const rect = current.getBoundingClientRect();
1567
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
1568
+ return false;
1569
+ if (rect.width <= 0 || rect.height <= 0)
1570
+ return false;
1571
+ }
1572
+ return root.getClientRects().length > 0;
1573
+ })()
1574
+ : null;
1575
+ return {
1576
+ target,
1577
+ id,
1578
+ visible,
1579
+ matched: visible === target.visible,
1580
+ };
1581
+ });
1582
+ };
1583
+ let fields = snapshot();
1584
+ while (!fields.every((field) => field.matched) && Date.now() < deadline) {
1585
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1586
+ fields = snapshot();
1587
+ }
1588
+ const matched = fields.every((field) => field.matched);
1589
+ return {
1590
+ matched,
1591
+ timedOut: !matched,
1592
+ fields,
1593
+ };
1594
+ }, options);
1595
+ }
1596
+ export async function readLiveFormStateDrift(page, options = {}) {
1597
+ const readiness = await readLiveFormReadiness(page, options);
1598
+ const domFields = await snapshotLiveFormDomFields(page, options.snapshot ?? {});
1599
+ const domByLabel = new Map(domFields.map((field) => [field.internalLabel, field]));
1600
+ const issues = [];
1601
+ for (const field of readiness.fields) {
1602
+ const label = field.target.internalLabel ?? null;
1603
+ const domField = label ? domByLabel.get(label) : undefined;
1604
+ if (field.available && label && !domField) {
1605
+ issues.push({
1606
+ internalLabel: label,
1607
+ fieldId: field.id,
1608
+ kind: "missing-dom",
1609
+ message: "Field is available through Live Form API but no matching data-internal-label root was found in the DOM.",
1610
+ });
1611
+ }
1612
+ if (!field.available && domField) {
1613
+ issues.push({
1614
+ internalLabel: label,
1615
+ fieldId: field.id,
1616
+ kind: "missing-api",
1617
+ message: "Field root exists in the DOM but the Live Form API target was not available.",
1618
+ });
1619
+ }
1620
+ if (field.domVisible !== null && field.hidden === field.domVisible) {
1621
+ issues.push({
1622
+ internalLabel: label,
1623
+ fieldId: field.id,
1624
+ kind: "visibility",
1625
+ message: "Effective hidden state does not align with DOM visibility.",
1626
+ });
1627
+ }
1628
+ }
1629
+ return {
1630
+ readiness,
1631
+ domFields,
1632
+ issues,
1633
+ hasDrift: issues.length > 0,
1634
+ };
1635
+ }
1636
+ export async function discoverLiveFormFieldShapes(page, options = {}) {
1637
+ if (!hasEvaluate(page)) {
1638
+ throw new Error("discoverLiveFormFieldShapes requires a Playwright page with evaluate().");
1639
+ }
1640
+ return page.evaluate((discoverOptions) => {
1641
+ const selector = discoverOptions.selector ?? "[data-internal-label]";
1642
+ const includeValues = discoverOptions.includeValues ?? true;
1643
+ const factory = Reflect.get(window, "fsApi");
1644
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1645
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1646
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1647
+ const form = Array.isArray(forms) ? forms[0] ?? null : null;
1648
+ const apiAvailable = Boolean(form && typeof form === "object");
1649
+ const getApiField = (internalLabel) => {
1650
+ if (!internalLabel || !form || typeof form !== "object")
1651
+ return null;
1652
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1653
+ return typeof getFieldByInternalLabel === "function"
1654
+ ? Reflect.apply(getFieldByInternalLabel, form, [internalLabel])
1655
+ : null;
1656
+ };
1657
+ const controlSnapshot = (control) => {
1658
+ const input = control;
1659
+ return {
1660
+ tagName: control.tagName.toLowerCase(),
1661
+ type: control.getAttribute("type"),
1662
+ id: control.getAttribute("id"),
1663
+ name: control.getAttribute("name"),
1664
+ value: control.getAttribute("value"),
1665
+ currentValue: "value" in input ? input.value : null,
1666
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1667
+ };
1668
+ };
1669
+ const inferKind = (element, controls) => {
1670
+ const text = element.textContent?.toLowerCase() ?? "";
1671
+ const classes = element.className.toString().toLowerCase();
1672
+ const types = new Set(controls.map((control) => control.type ?? control.tagName));
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
+ }
1684
+ if (internalLabel.includes("code_embed") || text.startsWith(":root{") || text.includes("<script"))
1685
+ return "embed";
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
+ }
1693
+ return "section";
1694
+ }
1695
+ if (classes.includes("matrix") || internalLabel.includes("matrix") || element.querySelector("[id^='matrix-row-'], table"))
1696
+ return "matrix";
1697
+ if (classes.includes("rating") || internalLabel.includes("rating"))
1698
+ return "rating";
1699
+ if (classes.includes("signature") || internalLabel.includes("signature"))
1700
+ return "signature";
1701
+ if (classes.includes("product") || internalLabel.includes("product") || types.has("product"))
1702
+ return "product";
1703
+ if (classes.includes("address") || controls.some((control) => control.id?.includes("-address")))
1704
+ return "address";
1705
+ if (classes.includes("name") || controls.some((control) => control.id?.includes("-first") || control.id?.includes("-last")))
1706
+ return "name";
1707
+ if (types.has("checkbox"))
1708
+ return "checkbox";
1709
+ if (types.has("radio"))
1710
+ return "radio";
1711
+ if (types.has("file"))
1712
+ return "file";
1713
+ if (controls.some((control) => control.tagName === "select"))
1714
+ return "select";
1715
+ if (controls.some((control) => control.tagName === "textarea"))
1716
+ return "textarea";
1717
+ if (types.has("email"))
1718
+ return "email";
1719
+ if (types.has("tel"))
1720
+ return "phone";
1721
+ if (types.has("number") || internalLabel.includes("number"))
1722
+ return "number";
1723
+ if (internalLabel.includes("date_time") || internalLabel.includes("datetime") || text.includes("am/pm") || text.includes("minutes"))
1724
+ return "datetime";
1725
+ if (types.has("date") || internalLabel.includes("date"))
1726
+ return "date";
1727
+ if (types.has("text"))
1728
+ return "text";
1729
+ return "unknown";
1730
+ };
1731
+ const isActuallyVisible = (element) => {
1732
+ if (!(element instanceof HTMLElement))
1733
+ return false;
1734
+ for (let current = element; current; current = current.parentElement) {
1735
+ const style = getComputedStyle(current);
1736
+ const rect = current.getBoundingClientRect();
1737
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
1738
+ return false;
1739
+ if (rect.width <= 0 || rect.height <= 0)
1740
+ return false;
1741
+ }
1742
+ return element.getClientRects().length > 0;
1743
+ };
1744
+ const fields = Array.from(document.querySelectorAll(selector)).map((element) => {
1745
+ const internalLabel = element.getAttribute("data-internal-label");
1746
+ const apiField = getApiField(internalLabel);
1747
+ const getValue = apiField && typeof apiField === "object" ? Reflect.get(apiField, "getValue") : null;
1748
+ const getGeneralAttribute = apiField && typeof apiField === "object" ? Reflect.get(apiField, "getGeneralAttribute") : null;
1749
+ const apiValue = includeValues && typeof getValue === "function" ? Reflect.apply(getValue, apiField, []) : undefined;
1750
+ const apiHidden = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, apiField, ["hidden"]) === true : undefined;
1751
+ const controls = Array.from(element.querySelectorAll("input, textarea, select")).map(controlSnapshot);
1752
+ const domVisible = isActuallyVisible(element);
1753
+ return {
1754
+ internalLabel,
1755
+ fieldId: apiField && typeof apiField === "object" ? Reflect.get(apiField, "id") ?? null : null,
1756
+ domId: element.id,
1757
+ labelText: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 160) ?? "",
1758
+ apiAvailable: Boolean(apiField && typeof apiField === "object"),
1759
+ ...(includeValues ? { apiValue } : {}),
1760
+ ...(apiHidden === undefined ? {} : { apiHidden }),
1761
+ domVisible,
1762
+ controlCount: controls.length,
1763
+ controlTypes: [...new Set(controls.map((control) => control.type ?? control.tagName))],
1764
+ inferredKind: inferKind(element, controls),
1765
+ controls,
1766
+ };
1767
+ });
1768
+ return { apiAvailable, fields };
1769
+ }, options);
1770
+ }
1771
+ export async function submitWithLiveFormLifecycleProbe(page, options = {}) {
1772
+ const submitSelector = options.submitSelector ?? formstackSelectors.submitButton;
1773
+ return runWithLiveFormLifecycleProbe(page, options, async () => {
1774
+ try {
1775
+ await page.locator(submitSelector).first().click(options.clickTimeoutMs === undefined ? undefined : { timeout: options.clickTimeoutMs });
1776
+ return { clicked: true };
1777
+ }
1778
+ catch (error) {
1779
+ const message = error instanceof Error ? error.message : String(error);
1780
+ if (hasEvaluate(page)) {
1781
+ await page.evaluate((detail) => {
1782
+ const probe = Reflect.get(window, "__fsptLiveFormLifecycleProbe");
1783
+ if (typeof probe?.record === "function")
1784
+ probe.record("submit-error", detail);
1785
+ }, { message, phase: "click" });
1786
+ }
1787
+ if (options.throwOnClickFailure ?? false)
1788
+ throw error;
1789
+ return { clicked: false, error: message };
1790
+ }
1791
+ });
1792
+ }
1793
+ export async function probeLiveFormLifecycle(page, options = {}) {
1794
+ if (!hasEvaluate(page)) {
1795
+ throw new Error("probeLiveFormLifecycle requires a Playwright page with evaluate().");
1796
+ }
1797
+ return page.evaluate(async (probeOptions) => {
1798
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
1799
+ const eventTypes = probeOptions.eventTypes ?? ["input", "change", "blur", "focus", "click", "submit"];
1800
+ const events = [];
1801
+ const fieldTargets = probeOptions.fieldTargets ?? [];
1802
+ const waitForApiTimeoutMs = probeOptions.waitForApiTimeoutMs ?? 10_000;
1803
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1804
+ const record = (kind, detail) => {
1805
+ const at = now();
1806
+ const event = {
1807
+ kind,
1808
+ at,
1809
+ elapsedMs: Math.round(at - startedAt),
1810
+ detail,
1811
+ };
1812
+ events.push(event);
1813
+ return event;
1814
+ };
1815
+ const safeText = (value) => {
1816
+ const text = value?.replace(/\s+/g, " ").trim() ?? "";
1817
+ return text ? text.slice(0, 180) : null;
1818
+ };
1819
+ const selectorForElement = (target) => {
1820
+ if (!(target instanceof Element))
1821
+ return null;
1822
+ const id = target.getAttribute("id");
1823
+ if (id)
1824
+ return `#${CSS.escape(id)}`;
1825
+ const name = target.getAttribute("name");
1826
+ if (name)
1827
+ return `[name="${CSS.escape(name)}"]`;
1828
+ const internalLabel = target.closest("[data-internal-label]")?.getAttribute("data-internal-label");
1829
+ if (internalLabel)
1830
+ return `[data-internal-label="${CSS.escape(internalLabel)}"]`;
1831
+ return target.tagName.toLowerCase();
1832
+ };
1833
+ const serializeControl = (control) => {
1834
+ const input = control;
1835
+ return {
1836
+ tagName: control.tagName.toLowerCase(),
1837
+ type: control instanceof HTMLInputElement ? input.type : null,
1838
+ id: control.getAttribute("id"),
1839
+ name: control.getAttribute("name"),
1840
+ value: "value" in input ? input.value : null,
1841
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1842
+ disabled: "disabled" in input ? input.disabled : null,
1843
+ required: "required" in input ? input.required : null,
1844
+ ariaInvalid: control.getAttribute("aria-invalid"),
1845
+ };
1846
+ };
1847
+ const listener = (event) => {
1848
+ const target = event.target;
1849
+ const element = target instanceof Element ? target : null;
1850
+ const fieldRoot = element?.closest("[data-internal-label], [id^='fsCell'], [data-testid^='field-']");
1851
+ const fsCell = element?.closest("[id^='fsCell']");
1852
+ const idDerivedField = element?.getAttribute("id")?.match(/^field(\d+)/)?.[1] ?? null;
1853
+ record("dom-event", {
1854
+ type: event.type,
1855
+ selector: selectorForElement(target),
1856
+ fieldId: fieldRoot?.getAttribute("data-fs-field-id") ??
1857
+ fsCell?.id?.replace(/^fsCell/, "") ??
1858
+ idDerivedField ??
1859
+ fieldRoot?.id?.replace(/^fsCell/, "") ??
1860
+ null,
1861
+ internalLabel: fieldRoot?.getAttribute("data-internal-label") ?? null,
1862
+ control: element ? serializeControl(element) : null,
1863
+ });
1864
+ };
1865
+ for (const type of eventTypes) {
1866
+ document.addEventListener(type, listener, true);
1867
+ }
1868
+ record("probe-installed", {
1869
+ eventTypes,
1870
+ fieldTargetCount: fieldTargets.length,
1871
+ });
1872
+ const readApi = () => {
1873
+ const factory = Reflect.get(window, "fsApi");
1874
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1875
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1876
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1877
+ return { api, form: Array.isArray(forms) ? forms[0] ?? null : null };
1878
+ };
1879
+ const deadline = Date.now() + waitForApiTimeoutMs;
1880
+ let apiAvailable = false;
1881
+ let form = null;
1882
+ while (Date.now() < deadline) {
1883
+ const resolved = readApi();
1884
+ form = resolved.form;
1885
+ if (form && typeof form === "object") {
1886
+ apiAvailable = true;
1887
+ break;
1888
+ }
1889
+ await new Promise((resolve) => setTimeout(resolve, 50));
1890
+ }
1891
+ if (apiAvailable) {
1892
+ const getFields = Reflect.get(form, "getFields");
1893
+ const rawFields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
1894
+ record("fs-api-ready", {
1895
+ fieldCount: Array.isArray(rawFields) ? rawFields.length : null,
1896
+ methods: Object.keys(form).filter((key) => typeof Reflect.get(form, key) === "function").sort(),
1897
+ });
1898
+ }
1899
+ else {
1900
+ record("fs-api-timeout", { timeoutMs: waitForApiTimeoutMs });
1901
+ }
1902
+ const getFieldTarget = (target) => {
1903
+ if (!apiAvailable || !form || typeof form !== "object")
1904
+ return null;
1905
+ const getField = Reflect.get(form, "getField");
1906
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1907
+ if (target.fieldId != null && typeof getField === "function")
1908
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
1909
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
1910
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
1911
+ }
1912
+ return null;
1913
+ };
1914
+ const snapshotField = (field, target) => {
1915
+ if (!field || typeof field !== "object") {
1916
+ record("field-snapshot", { target, available: false });
1917
+ return;
1918
+ }
1919
+ const getId = Reflect.get(field, "getId");
1920
+ const getValue = Reflect.get(field, "getValue");
1921
+ const getFieldInfo = Reflect.get(field, "getFieldInfo");
1922
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
1923
+ const setValue = Reflect.get(field, "setValue");
1924
+ let value = null;
1925
+ let info = null;
1926
+ let required = null;
1927
+ let hidden = null;
1928
+ let skipValidation = null;
1929
+ try {
1930
+ value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
1931
+ }
1932
+ catch (error) {
1933
+ value = { error: error instanceof Error ? error.message : String(error) };
1934
+ }
1935
+ try {
1936
+ info = typeof getFieldInfo === "function" ? Reflect.apply(getFieldInfo, field, []) : null;
1937
+ }
1938
+ catch {
1939
+ info = null;
1940
+ }
1941
+ try {
1942
+ required = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["required"]) : null;
1943
+ hidden = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["hidden"]) : null;
1944
+ skipValidation =
1945
+ typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["shouldForceSkipValidation"]) : null;
1946
+ }
1947
+ catch {
1948
+ required = null;
1949
+ }
1950
+ record("field-snapshot", {
1951
+ target,
1952
+ available: true,
1953
+ id: typeof getId === "function" ? Reflect.apply(getId, field, []) : null,
1954
+ hasSetValue: typeof setValue === "function",
1955
+ value,
1956
+ info,
1957
+ required,
1958
+ hidden,
1959
+ shouldForceSkipValidation: skipValidation,
1960
+ });
1961
+ };
1962
+ if (apiAvailable && form && typeof form === "object") {
1963
+ if (probeOptions.includeAllFields) {
1964
+ const getFields = Reflect.get(form, "getFields");
1965
+ const fields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
1966
+ if (Array.isArray(fields)) {
1967
+ for (const field of fields)
1968
+ snapshotField(field, { label: "all-fields" });
1969
+ }
1970
+ }
1971
+ for (const target of fieldTargets) {
1972
+ snapshotField(getFieldTarget(target), target);
1973
+ }
1974
+ }
1975
+ if (probeOptions.validate && apiAvailable && form && typeof form === "object") {
1976
+ const validateForm = Reflect.get(form, "validateForm");
1977
+ try {
1978
+ const result = typeof validateForm === "function" ? await Promise.resolve(Reflect.apply(validateForm, form, [])) : null;
1979
+ record("validation-result", { result });
1980
+ }
1981
+ catch (error) {
1982
+ record("validation-error", { message: error instanceof Error ? error.message : String(error) });
1983
+ }
1984
+ }
1985
+ if (probeOptions.submit && apiAvailable && form && typeof form === "object") {
1986
+ const submitForm = Reflect.get(form, "submitForm");
1987
+ try {
1988
+ const result = typeof submitForm === "function" ? await Promise.resolve(Reflect.apply(submitForm, form, [])) : null;
1989
+ record("submit-invoked", { result });
1990
+ }
1991
+ catch (error) {
1992
+ record("submit-error", { message: error instanceof Error ? error.message : String(error) });
1993
+ }
1994
+ }
1995
+ await new Promise((resolve) => setTimeout(resolve, 0));
1996
+ for (const type of eventTypes) {
1997
+ document.removeEventListener(type, listener, true);
1998
+ }
1999
+ const validation = events.find((event) => event.kind === "validation-result" || event.kind === "validation-error");
2000
+ const submit = events.find((event) => event.kind === "submit-invoked" || event.kind === "submit-error");
2001
+ return {
2002
+ apiAvailable,
2003
+ events,
2004
+ fieldSnapshots: events.filter((event) => event.kind === "field-snapshot"),
2005
+ ...(validation ? { validation } : {}),
2006
+ ...(submit ? { submit } : {}),
2007
+ };
2008
+ }, options);
2009
+ }
2010
+ export async function runWithLiveFormLifecycleProbe(page, options, run) {
2011
+ if (!hasEvaluate(page)) {
2012
+ throw new Error("runWithLiveFormLifecycleProbe requires a Playwright page with evaluate().");
2013
+ }
2014
+ await page.evaluate(async (probeOptions) => {
2015
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
2016
+ const eventTypes = probeOptions.eventTypes ?? ["input", "change", "blur", "focus", "click", "submit"];
2017
+ const formEventTypes = probeOptions.formEventTypes ?? [];
2018
+ const waitForApiTimeoutMs = probeOptions.waitForApiTimeoutMs ?? 10_000;
2019
+ const events = [];
2020
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
2021
+ const record = (kind, detail) => {
2022
+ const at = now();
2023
+ const event = { kind, at, elapsedMs: Math.round(at - startedAt), detail };
2024
+ events.push(event);
2025
+ return event;
2026
+ };
2027
+ const controlValue = (element) => {
2028
+ if (!element)
2029
+ return null;
2030
+ const input = element;
2031
+ return {
2032
+ tagName: element.tagName.toLowerCase(),
2033
+ type: element instanceof HTMLInputElement ? input.type : null,
2034
+ id: element.getAttribute("id"),
2035
+ name: element.getAttribute("name"),
2036
+ value: "value" in input ? input.value : null,
2037
+ checked: element instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? element.checked : null,
2038
+ ariaInvalid: element.getAttribute("aria-invalid"),
2039
+ };
2040
+ };
2041
+ const listener = (event) => {
2042
+ const element = event.target instanceof Element ? event.target : null;
2043
+ const fieldRoot = element?.closest("[data-internal-label], [id^='fsCell'], [data-testid^='field-']");
2044
+ const fsCell = element?.closest("[id^='fsCell']");
2045
+ const idDerivedField = element?.getAttribute("id")?.match(/^field(\d+)/)?.[1] ?? null;
2046
+ record("dom-event", {
2047
+ type: event.type,
2048
+ fieldId: fieldRoot?.getAttribute("data-fs-field-id") ??
2049
+ fsCell?.id?.replace(/^fsCell/, "") ??
2050
+ idDerivedField ??
2051
+ fieldRoot?.id?.replace(/^fsCell/, "") ??
2052
+ null,
2053
+ internalLabel: fieldRoot?.getAttribute("data-internal-label") ?? null,
2054
+ control: controlValue(element),
2055
+ });
2056
+ };
2057
+ for (const type of eventTypes)
2058
+ document.addEventListener(type, listener, true);
2059
+ record("probe-installed", { eventTypes, formEventTypes });
2060
+ const readApi = () => {
2061
+ const factory = Reflect.get(window, "fsApi");
2062
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
2063
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
2064
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
2065
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
2066
+ };
2067
+ const deadline = Date.now() + waitForApiTimeoutMs;
2068
+ let form = null;
2069
+ while (formEventTypes.length > 0 && Date.now() < deadline) {
2070
+ form = readApi().form;
2071
+ if (form && typeof form === "object")
2072
+ break;
2073
+ await new Promise((resolve) => setTimeout(resolve, 50));
2074
+ }
2075
+ const formEventListeners = formEventTypes.length > 0 && form && typeof form === "object"
2076
+ ? formEventTypes.map((type) => ({
2077
+ type,
2078
+ onFormEvent: (event) => {
2079
+ const eventObject = event && typeof event === "object" ? event : {};
2080
+ record("form-event", {
2081
+ type,
2082
+ formId: Reflect.get(eventObject, "formId") ?? null,
2083
+ data: Reflect.get(eventObject, "data") ?? null,
2084
+ hasPreventDefault: typeof Reflect.get(eventObject, "preventDefault") === "function",
2085
+ });
2086
+ },
2087
+ }))
2088
+ : [];
2089
+ if (form && typeof form === "object" && formEventListeners.length > 0) {
2090
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
2091
+ if (typeof registerFormEventListener === "function") {
2092
+ for (const formEventListener of formEventListeners) {
2093
+ Reflect.apply(registerFormEventListener, form, [formEventListener]);
2094
+ }
2095
+ }
2096
+ }
2097
+ Object.defineProperty(window, "__fsptLiveFormLifecycleProbe", {
2098
+ configurable: true,
2099
+ value: {
2100
+ events,
2101
+ eventTypes,
2102
+ formEventListeners,
2103
+ listener,
2104
+ record,
2105
+ },
2106
+ });
2107
+ }, options);
2108
+ const result = await run();
2109
+ const report = await page.evaluate(async (probeOptions) => {
2110
+ const postRunSettleMs = probeOptions.postRunSettleMs ?? 100;
2111
+ if (postRunSettleMs > 0)
2112
+ await new Promise((resolve) => setTimeout(resolve, postRunSettleMs));
2113
+ const probe = Reflect.get(window, "__fsptLiveFormLifecycleProbe");
2114
+ if (!probe) {
2115
+ return {
2116
+ apiAvailable: false,
2117
+ events: [],
2118
+ fieldSnapshots: [],
2119
+ };
2120
+ }
2121
+ if (probeOptions.postRunQuietMs !== undefined) {
2122
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
2123
+ const quietMs = probeOptions.postRunQuietMs;
2124
+ const quietTimeoutMs = probeOptions.postRunQuietTimeoutMs ?? 2_500;
2125
+ const deadline = Date.now() + quietTimeoutMs;
2126
+ let timedOut = false;
2127
+ while (Date.now() < deadline) {
2128
+ const lastEvent = probe.events.at(-1);
2129
+ const elapsedSinceLastEvent = lastEvent ? now() - lastEvent.at : quietMs;
2130
+ if (elapsedSinceLastEvent >= quietMs)
2131
+ break;
2132
+ await new Promise((resolve) => setTimeout(resolve, Math.max(10, Math.min(50, quietMs - elapsedSinceLastEvent))));
2133
+ }
2134
+ const lastEvent = probe.events.at(-1);
2135
+ if (lastEvent && now() - lastEvent.at < quietMs)
2136
+ timedOut = true;
2137
+ probe.record("quiet-wait", {
2138
+ quietMs,
2139
+ timeoutMs: quietTimeoutMs,
2140
+ timedOut,
2141
+ lastEventKind: lastEvent?.kind ?? null,
2142
+ lastEventElapsedMs: lastEvent?.elapsedMs ?? null,
2143
+ });
2144
+ }
2145
+ const factory = Reflect.get(window, "fsApi");
2146
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
2147
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
2148
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
2149
+ const form = Array.isArray(forms) ? forms[0] ?? null : null;
2150
+ const apiAvailable = Boolean(form && typeof form === "object");
2151
+ if (apiAvailable && form && typeof form === "object") {
2152
+ const getFields = Reflect.get(form, "getFields");
2153
+ const fields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
2154
+ probe.record("fs-api-ready", {
2155
+ fieldCount: Array.isArray(fields) ? fields.length : null,
2156
+ });
2157
+ const targets = probeOptions.includeAllFields && Array.isArray(fields) ? fields : [];
2158
+ for (const field of targets) {
2159
+ const getId = Reflect.get(field, "getId");
2160
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
2161
+ const getValue = Reflect.get(field, "getValue");
2162
+ const getFieldInfo = Reflect.get(field, "getFieldInfo");
2163
+ const keys = Object.keys(field).sort();
2164
+ const methodNames = keys.filter((key) => typeof Reflect.get(field, key) === "function");
2165
+ const idCandidates = [];
2166
+ for (const candidate of ["id", "fieldId", "_id"])
2167
+ idCandidates.push(Reflect.get(field, candidate));
2168
+ try {
2169
+ if (typeof getId === "function")
2170
+ idCandidates.push(Reflect.apply(getId, field, []));
2171
+ }
2172
+ catch {
2173
+ idCandidates.push("getId:error");
2174
+ }
2175
+ try {
2176
+ if (typeof getGeneralAttribute === "function")
2177
+ idCandidates.push(Reflect.apply(getGeneralAttribute, field, ["id"]));
2178
+ }
2179
+ catch {
2180
+ idCandidates.push("getGeneralAttribute:id:error");
2181
+ }
2182
+ let value = null;
2183
+ let info = null;
2184
+ try {
2185
+ value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
2186
+ }
2187
+ catch (error) {
2188
+ value = { error: error instanceof Error ? error.message : String(error) };
2189
+ }
2190
+ try {
2191
+ info = typeof getFieldInfo === "function" ? Reflect.apply(getFieldInfo, field, []) : null;
2192
+ }
2193
+ catch {
2194
+ info = null;
2195
+ }
2196
+ probe.record("field-snapshot", {
2197
+ id: idCandidates.find((candidate) => candidate !== undefined && candidate !== null && candidate !== "") ?? null,
2198
+ idCandidates,
2199
+ keys,
2200
+ methodNames,
2201
+ value,
2202
+ info,
2203
+ });
2204
+ }
2205
+ if (probeOptions.validate) {
2206
+ const validateForm = Reflect.get(form, "validateForm");
2207
+ try {
2208
+ const validation = typeof validateForm === "function" ? await Promise.resolve(Reflect.apply(validateForm, form, [])) : null;
2209
+ const inlineErrors = Array.from(document.querySelectorAll('[id^="inline-error-field"], .fsError, [role="alert"]')).map((element) => ({
2210
+ id: element.getAttribute("id"),
2211
+ text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 240) ?? "",
2212
+ }));
2213
+ probe.record("validation-result", { result: validation, inlineErrors });
2214
+ }
2215
+ catch (error) {
2216
+ probe.record("validation-error", { message: error instanceof Error ? error.message : String(error) });
2217
+ }
2218
+ }
2219
+ }
2220
+ if (apiAvailable && form && typeof form === "object" && Array.isArray(probe.formEventListeners)) {
2221
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
2222
+ if (typeof unregisterFormEventListener === "function") {
2223
+ for (const formEventListener of probe.formEventListeners) {
2224
+ Reflect.apply(unregisterFormEventListener, form, [formEventListener]);
2225
+ }
2226
+ }
2227
+ }
2228
+ for (const type of probe.eventTypes)
2229
+ document.removeEventListener(type, probe.listener, true);
2230
+ Reflect.deleteProperty(window, "__fsptLiveFormLifecycleProbe");
2231
+ const validation = probe.events.find((event) => event.kind === "validation-result" || event.kind === "validation-error");
2232
+ const submit = probe.events.find((event) => event.kind === "submit-invoked" || event.kind === "submit-error");
2233
+ return {
2234
+ apiAvailable,
2235
+ events: probe.events,
2236
+ fieldSnapshots: probe.events.filter((event) => event.kind === "field-snapshot"),
2237
+ ...(validation ? { validation } : {}),
2238
+ ...(submit ? { submit } : {}),
2239
+ };
2240
+ }, options);
2241
+ return { result, report };
2242
+ }
2243
+ export async function waitForLiveFormEvent(page, options) {
2244
+ if (!hasEvaluate(page)) {
2245
+ throw new Error("waitForLiveFormEvent requires a Playwright page with evaluate().");
2246
+ }
2247
+ return page.evaluate(async (waitOptions) => {
2248
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
2249
+ const timeoutMs = waitOptions.timeoutMs ?? 5_000;
2250
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
2251
+ const readApi = () => {
2252
+ const factory = Reflect.get(window, "fsApi");
2253
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
2254
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
2255
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
2256
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
2257
+ };
2258
+ const deadline = Date.now() + timeoutMs;
2259
+ let form = null;
2260
+ while (Date.now() < deadline) {
2261
+ form = readApi().form;
2262
+ if (form && typeof form === "object")
2263
+ break;
2264
+ await new Promise((resolve) => setTimeout(resolve, 50));
2265
+ }
2266
+ const elapsedMs = () => Math.round(now() - startedAt);
2267
+ if (!form || typeof form !== "object") {
2268
+ return {
2269
+ matched: false,
2270
+ timedOut: true,
2271
+ elapsedMs: elapsedMs(),
2272
+ };
2273
+ }
2274
+ const findField = (target) => {
2275
+ if (!target)
2276
+ return null;
2277
+ const byInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
2278
+ if (target.internalLabel && typeof byInternalLabel === "function") {
2279
+ const field = Reflect.apply(byInternalLabel, form, [target.internalLabel]);
2280
+ if (field)
2281
+ return field;
2282
+ }
2283
+ const getFields = Reflect.get(form, "getFields");
2284
+ const fields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
2285
+ if (!Array.isArray(fields))
2286
+ return null;
2287
+ const targetFieldId = target.fieldId == null ? null : String(target.fieldId);
2288
+ return (fields.find((field) => {
2289
+ if (!field || typeof field !== "object")
2290
+ return false;
2291
+ const fieldId = Reflect.get(field, "id");
2292
+ return targetFieldId !== null && String(fieldId) === targetFieldId;
2293
+ }) ?? null);
2294
+ };
2295
+ const targetField = findField(waitOptions.target);
2296
+ const targetFieldId = targetField && typeof targetField === "object"
2297
+ ? Reflect.get(targetField, "id") ?? null
2298
+ : waitOptions.target?.fieldId ?? null;
2299
+ const targetFieldIdString = targetFieldId == null ? null : String(targetFieldId);
2300
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
2301
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
2302
+ if (typeof registerFormEventListener !== "function") {
2303
+ return {
2304
+ matched: false,
2305
+ timedOut: true,
2306
+ elapsedMs: elapsedMs(),
2307
+ targetFieldId,
2308
+ };
2309
+ }
2310
+ let matchedEvent;
2311
+ let resolveWait;
2312
+ const wait = new Promise((resolve) => {
2313
+ resolveWait = resolve;
2314
+ });
2315
+ const timeout = setTimeout(() => resolveWait?.(), Math.max(0, deadline - Date.now()));
2316
+ const listeners = waitOptions.eventTypes.map((type) => ({
2317
+ type,
2318
+ onFormEvent: (event) => {
2319
+ const eventObject = event && typeof event === "object" ? event : {};
2320
+ const data = Reflect.get(eventObject, "data");
2321
+ const dataFieldId = data && typeof data === "object" ? Reflect.get(data, "fieldId") : null;
2322
+ const dataType = data && typeof data === "object" ? Reflect.get(data, "type") : null;
2323
+ const fieldMatches = targetFieldIdString === null || String(dataFieldId ?? "") === targetFieldIdString;
2324
+ const dataTypeMatches = !waitOptions.dataType || dataType === waitOptions.dataType;
2325
+ if (!fieldMatches || !dataTypeMatches || matchedEvent)
2326
+ return;
2327
+ matchedEvent = {
2328
+ type,
2329
+ formId: Reflect.get(eventObject, "formId") ?? null,
2330
+ data,
2331
+ hasPreventDefault: typeof Reflect.get(eventObject, "preventDefault") === "function",
2332
+ };
2333
+ resolveWait?.();
2334
+ },
2335
+ }));
2336
+ for (const listener of listeners) {
2337
+ Reflect.apply(registerFormEventListener, form, [listener]);
2338
+ }
2339
+ await wait;
2340
+ clearTimeout(timeout);
2341
+ if (typeof unregisterFormEventListener === "function") {
2342
+ for (const listener of listeners) {
2343
+ Reflect.apply(unregisterFormEventListener, form, [listener]);
2344
+ }
2345
+ }
2346
+ return {
2347
+ matched: Boolean(matchedEvent),
2348
+ timedOut: !matchedEvent,
2349
+ elapsedMs: elapsedMs(),
2350
+ targetFieldId,
2351
+ ...(matchedEvent ? { event: matchedEvent } : {}),
2352
+ };
2353
+ }, options);
2354
+ }
2355
+ export async function setLiveFormFieldValueTransaction(page, options) {
2356
+ if (!hasEvaluate(page)) {
2357
+ throw new Error("setLiveFormFieldValueTransaction requires a Playwright page with evaluate().");
2358
+ }
2359
+ const shouldWaitForEvent = options.waitForEvent === true || (options.waitForEvent === undefined && options.write.notify === true);
2360
+ const defaultEventOptions = {
2361
+ eventTypes: ["change"],
2362
+ target: options.write.target,
2363
+ dataType: "api-setValue",
2364
+ ...(options.write.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
2365
+ };
2366
+ const eventOptions = typeof options.waitForEvent === "object"
2367
+ ? options.waitForEvent
2368
+ : shouldWaitForEvent
2369
+ ? defaultEventOptions
2370
+ : null;
2371
+ const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2372
+ const write = await setLiveFormFieldValue(page, options.write);
2373
+ const event = eventPromise ? await eventPromise : undefined;
2374
+ const domDispatch = options.dispatchDomEvents === undefined || options.dispatchDomEvents === false
2375
+ ? undefined
2376
+ : await dispatchLiveFormFieldDomEvents(page, {
2377
+ ...(typeof options.dispatchDomEvents === "object" ? options.dispatchDomEvents : {}),
2378
+ target: options.write.target,
2379
+ });
2380
+ const readiness = options.readiness ? await readLiveFormReadiness(page, options.readiness) : undefined;
2381
+ const domFields = options.snapshot
2382
+ ? await snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {})
2383
+ : undefined;
2384
+ return {
2385
+ write,
2386
+ ...(event ? { event } : {}),
2387
+ ...(domDispatch ? { domDispatch } : {}),
2388
+ ...(readiness ? { readiness } : {}),
2389
+ ...(domFields ? { domFields } : {}),
2390
+ };
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
+ }
2471
+ export async function setLiveFormFieldValuesTransaction(page, options) {
2472
+ if (!hasEvaluate(page)) {
2473
+ throw new Error("setLiveFormFieldValuesTransaction requires a Playwright page with evaluate().");
2474
+ }
2475
+ const writes = [];
2476
+ const events = [];
2477
+ const domDispatches = [];
2478
+ const waitForEvents = options.waitForEvents ?? true;
2479
+ for (const writeOptions of options.writes) {
2480
+ const transaction = await setLiveFormFieldValueTransaction(page, {
2481
+ write: writeOptions,
2482
+ waitForEvent: waitForEvents && writeOptions.notify === true,
2483
+ ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
2484
+ });
2485
+ writes.push(transaction.write);
2486
+ if (transaction.event)
2487
+ events.push(transaction.event);
2488
+ if (transaction.domDispatch)
2489
+ domDispatches.push(transaction.domDispatch);
2490
+ }
2491
+ const readiness = options.readiness ? await readLiveFormReadiness(page, options.readiness) : undefined;
2492
+ const domFields = options.snapshot
2493
+ ? await snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {})
2494
+ : undefined;
2495
+ return {
2496
+ writes,
2497
+ events,
2498
+ domDispatches,
2499
+ ...(readiness ? { readiness } : {}),
2500
+ ...(domFields ? { domFields } : {}),
2501
+ };
2502
+ }
106
2503
  async function setControlValue(locator, value) {
107
2504
  try {
108
2505
  await locator.fill(value);
@@ -167,12 +2564,12 @@ export function createFormTestHarness(page, options = {}) {
167
2564
  selectOption: (internalLabel, value) => page.locator(formstackSelectors.fieldInputByInternalLabel(internalLabel)).first().selectOption(value),
168
2565
  chooseRadio: (internalLabel, value) => page.locator(formstackSelectors.optionLabelByValue(internalLabel, "radio", value)).first().click(),
169
2566
  setCheckbox: (internalLabel, value, checked) => {
170
- const locator = page.locator(formstackSelectors.optionLabelByValue(internalLabel, "checkbox", value)).first();
171
- return checked ? locator.click() : locator.click();
2567
+ const locator = page.locator(formstackSelectors.optionInput(internalLabel, "checkbox", value)).first();
2568
+ return checked ? locator.check() : locator.uncheck();
172
2569
  },
173
2570
  setCheckboxes: async (internalLabel, values) => {
174
2571
  for (const value of values) {
175
- await page.locator(formstackSelectors.optionLabelByValue(internalLabel, "checkbox", value)).first().click();
2572
+ await page.locator(formstackSelectors.optionInput(internalLabel, "checkbox", value)).first().check();
176
2573
  }
177
2574
  },
178
2575
  selectMatrixCell: (fieldId, row, column) => page.locator(formstackSelectors.matrixInputByPosition(fieldId, row, column)).first().check(),
@@ -194,10 +2591,38 @@ function stepName(step, index) {
194
2591
  function stepRunner(step) {
195
2592
  return typeof step === "function" ? step : step.run;
196
2593
  }
2594
+ function errorReport(error) {
2595
+ if (error instanceof Error) {
2596
+ return {
2597
+ name: error.name,
2598
+ message: error.message,
2599
+ ...(error.stack ? { stack: error.stack } : {}),
2600
+ };
2601
+ }
2602
+ return {
2603
+ name: "Error",
2604
+ message: String(error),
2605
+ };
2606
+ }
197
2607
  export async function runFormScenario(page, scenario, options = {}) {
198
2608
  const monitor = createFormBrowserMonitor(page, scenario.diagnostics ?? options.diagnostics);
199
2609
  const harness = createFormTestHarness(page, options);
200
2610
  const steps = [];
2611
+ const started = Date.now();
2612
+ const startedAt = new Date(started).toISOString();
2613
+ const buildResult = (status) => {
2614
+ const ended = Date.now();
2615
+ return {
2616
+ name: scenario.name,
2617
+ status,
2618
+ startedAt,
2619
+ endedAt: new Date(ended).toISOString(),
2620
+ durationMs: ended - started,
2621
+ steps,
2622
+ diagnostics: monitor.report(),
2623
+ ...(options.artifacts === undefined ? {} : { artifacts: options.artifacts }),
2624
+ };
2625
+ };
201
2626
  try {
202
2627
  if (scenario.url) {
203
2628
  await page.goto(scenario.url);
@@ -208,18 +2633,42 @@ export async function runFormScenario(page, scenario, options = {}) {
208
2633
  if (!step) {
209
2634
  continue;
210
2635
  }
211
- await stepRunner(step)(harness);
212
- steps.push({ name: stepName(step, index) });
2636
+ const name = stepName(step, index);
2637
+ const stepStarted = Date.now();
2638
+ try {
2639
+ await stepRunner(step)(harness);
2640
+ const stepEnded = Date.now();
2641
+ steps.push({
2642
+ name,
2643
+ status: "passed",
2644
+ startedAt: new Date(stepStarted).toISOString(),
2645
+ endedAt: new Date(stepEnded).toISOString(),
2646
+ durationMs: stepEnded - stepStarted,
2647
+ });
2648
+ }
2649
+ catch (error) {
2650
+ const stepEnded = Date.now();
2651
+ steps.push({
2652
+ name,
2653
+ status: "failed",
2654
+ startedAt: new Date(stepStarted).toISOString(),
2655
+ endedAt: new Date(stepEnded).toISOString(),
2656
+ durationMs: stepEnded - stepStarted,
2657
+ error: errorReport(error),
2658
+ });
2659
+ throw new FormScenarioRunError(buildResult("failed"));
2660
+ }
213
2661
  }
214
2662
  const shouldThrow = options.throwOnDiagnostics ?? true;
215
2663
  if (shouldThrow) {
216
- monitor.assertNoErrors();
2664
+ try {
2665
+ monitor.assertNoErrors();
2666
+ }
2667
+ catch {
2668
+ throw new FormScenarioRunError(buildResult("failed"));
2669
+ }
217
2670
  }
218
- return {
219
- name: scenario.name,
220
- steps,
221
- diagnostics: monitor.report(),
222
- };
2671
+ return buildResult("passed");
223
2672
  }
224
2673
  finally {
225
2674
  monitor.detach();
@@ -228,4 +2677,194 @@ export async function runFormScenario(page, scenario, options = {}) {
228
2677
  export function defineFormScenario(scenario) {
229
2678
  return scenario;
230
2679
  }
2680
+ export const liveFormScenarioSteps = {
2681
+ writeField: (name, options) => ({
2682
+ name,
2683
+ run: (harness) => setLiveFormFieldValueTransaction(harness.page, options).then(() => undefined),
2684
+ }),
2685
+ writeFields: (name, options) => ({
2686
+ name,
2687
+ run: (harness) => setLiveFormFieldValuesTransaction(harness.page, options).then(() => undefined),
2688
+ }),
2689
+ expectReady: (name, options) => ({
2690
+ name,
2691
+ run: async (harness) => {
2692
+ const readiness = await waitForLiveFormReadiness(harness.page, options);
2693
+ if (!readiness.isReady) {
2694
+ throw new Error(`Expected live form to be ready, but ${readiness.blockingFieldCount} field(s) are blocking.`);
2695
+ }
2696
+ },
2697
+ }),
2698
+ waitForQuiet: (name, options = {}) => ({
2699
+ name,
2700
+ run: async (harness) => {
2701
+ const quiet = await waitForLiveFormQuiet(harness.page, options);
2702
+ if (!quiet.quiet) {
2703
+ throw new Error("Expected live form to become quiet before timeout.");
2704
+ }
2705
+ },
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
+ }),
2714
+ expectFieldValue: (name, options) => ({
2715
+ name,
2716
+ run: async (harness) => {
2717
+ const value = await waitForLiveFormFieldValue(harness.page, options);
2718
+ const matched = options.expectedValue === undefined ? value.fieldAvailable && !value.isEmpty : value.valueMatched === true;
2719
+ if (!matched) {
2720
+ throw new Error("Expected live form field value condition was not met.");
2721
+ }
2722
+ },
2723
+ }),
2724
+ expectDomFieldState: (name, options) => ({
2725
+ name,
2726
+ run: async (harness) => {
2727
+ const state = await waitForLiveFormDomFieldState(harness.page, options);
2728
+ if (!state.matched) {
2729
+ throw new Error("Expected live form DOM field state condition was not met.");
2730
+ }
2731
+ },
2732
+ }),
2733
+ expectValidationErrors: (name, options = {}) => ({
2734
+ name,
2735
+ run: async (harness) => {
2736
+ const validation = await waitForLiveFormValidationState(harness.page, options);
2737
+ const expectedCountMatched = options.expectedErrorCount === undefined || validation.inlineErrors.length === options.expectedErrorCount;
2738
+ const errorText = options.errorText;
2739
+ const textMatched = errorText === undefined ||
2740
+ validation.inlineErrors.some((error) => typeof errorText === "string" ? error.text.includes(errorText) : errorText.test(error.text));
2741
+ if (validation.inlineErrors.length === 0 || !expectedCountMatched || !textMatched) {
2742
+ throw new Error("Expected live form validation errors were not present.");
2743
+ }
2744
+ },
2745
+ }),
2746
+ expectNoValidationErrors: (name, options = {}) => ({
2747
+ name,
2748
+ run: async (harness) => {
2749
+ const validation = await waitForLiveFormValidationState(harness.page, { ...options, expectNoErrors: true });
2750
+ if (validation.inlineErrors.length > 0) {
2751
+ throw new Error(`Expected no live form validation errors, but found ${validation.inlineErrors.length}.`);
2752
+ }
2753
+ },
2754
+ }),
2755
+ expectConsoleMessage: (name, options = {}) => ({
2756
+ name,
2757
+ run: async (harness) => {
2758
+ const consoleMessage = await waitForConsoleMessage(harness.page, options);
2759
+ if (!consoleMessage.matched) {
2760
+ throw new Error("Expected console message was not observed before timeout.");
2761
+ }
2762
+ },
2763
+ }),
2764
+ expectWindowFlag: (name, options) => ({
2765
+ name,
2766
+ run: async (harness) => {
2767
+ const flag = await waitForWindowFlag(harness.page, options);
2768
+ if (!flag.matched) {
2769
+ throw new Error(`Expected window flag "${options.path}" condition was not met before timeout.`);
2770
+ }
2771
+ },
2772
+ }),
2773
+ expectVisibility: (name, options) => ({
2774
+ name,
2775
+ run: async (harness) => {
2776
+ const visibility = await waitForLiveFormVisibility(harness.page, options);
2777
+ if (!visibility.matched) {
2778
+ throw new Error("Expected live form visibility conditions were not met.");
2779
+ }
2780
+ },
2781
+ }),
2782
+ expectNoStateDrift: (name, options) => ({
2783
+ name,
2784
+ run: async (harness) => {
2785
+ const drift = await readLiveFormStateDrift(harness.page, options);
2786
+ if (drift.hasDrift) {
2787
+ throw new Error(`Expected no live form state drift, but found ${drift.issues.length} issue(s).`);
2788
+ }
2789
+ },
2790
+ }),
2791
+ submitAndProbe: (name, options = {}) => ({
2792
+ name,
2793
+ run: (harness) => submitWithLiveFormLifecycleProbe(harness.page, options).then(() => undefined),
2794
+ }),
2795
+ };
2796
+ async function maybeWaitForQuiet(page, options) {
2797
+ if (!options.waitForQuiet)
2798
+ return;
2799
+ const quiet = await waitForLiveFormQuiet(page, typeof options.waitForQuiet === "object" ? options.waitForQuiet : {});
2800
+ if (!quiet.quiet) {
2801
+ throw new Error("Expected live form to become quiet before timeout.");
2802
+ }
2803
+ }
2804
+ function apiRecipeStep(name, target, value, options = {}) {
2805
+ return {
2806
+ name,
2807
+ run: async (harness) => {
2808
+ await setLiveFormFieldValueTransaction(harness.page, {
2809
+ write: {
2810
+ target,
2811
+ value,
2812
+ notify: options.notify ?? true,
2813
+ },
2814
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
2815
+ ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
2816
+ });
2817
+ await maybeWaitForQuiet(harness.page, options);
2818
+ },
2819
+ };
2820
+ }
2821
+ export const liveFormFieldRecipes = {
2822
+ fillTextDom: (name, internalLabel, value) => ({
2823
+ name,
2824
+ run: (harness) => harness.fillText(internalLabel, value),
2825
+ }),
2826
+ fillNameDom: (name, internalLabel, value) => ({
2827
+ name,
2828
+ run: (harness) => harness.fillName(internalLabel, value),
2829
+ }),
2830
+ fillAddressDom: (name, internalLabel, value) => ({
2831
+ name,
2832
+ run: (harness) => harness.fillAddress(internalLabel, value),
2833
+ }),
2834
+ chooseRadioDom: (name, internalLabel, value) => ({
2835
+ name,
2836
+ run: (harness) => harness.chooseRadio(internalLabel, value),
2837
+ }),
2838
+ setCheckboxDom: (name, internalLabel, value, checked) => ({
2839
+ name,
2840
+ run: (harness) => harness.setCheckbox(internalLabel, value, checked),
2841
+ }),
2842
+ selectMatrixCellDom: (name, fieldId, row, column) => ({
2843
+ name,
2844
+ run: (harness) => harness.selectMatrixCell(fieldId, row, column),
2845
+ }),
2846
+ setRatingDom: (name, fieldId, zeroBasedIndex) => ({
2847
+ name,
2848
+ run: (harness) => harness.setRating(fieldId, zeroBasedIndex),
2849
+ }),
2850
+ uploadFileDom: (name, internalLabel, files) => ({
2851
+ name,
2852
+ run: (harness) => harness.uploadFile(internalLabel, files),
2853
+ }),
2854
+ clearSignatureDom: (name, internalLabel) => ({
2855
+ name,
2856
+ run: (harness) => harness.clearSignature(internalLabel),
2857
+ }),
2858
+ setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
2859
+ setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
2860
+ setChoiceApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormChoiceValue(value), options),
2861
+ setSelectApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormSelectValue(value), options),
2862
+ setCheckboxesApi: (name, internalLabel, values, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormCheckboxValue(values), options),
2863
+ setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
2864
+ setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
2865
+ setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
2866
+ setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
2867
+ setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: false } }),
2868
+ setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: true } }),
2869
+ };
231
2870
  //# sourceMappingURL=index.js.map