@redrockswebdevelopment/formstack-form-testing 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,6 +7,15 @@ 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
+ }
10
19
  function matchesPattern(value, pattern) {
11
20
  return typeof pattern === "string" ? value.includes(pattern) : pattern.test(value);
12
21
  }
@@ -103,6 +112,2000 @@ export function createFormBrowserMonitor(page, options = {}) {
103
112
  },
104
113
  };
105
114
  }
115
+ function hasEvaluate(page) {
116
+ return typeof page.evaluate === "function";
117
+ }
118
+ export function readValuePath(value, path) {
119
+ if (!path)
120
+ return value;
121
+ return path.split(".").reduce((current, segment) => {
122
+ if (current === null || current === undefined || typeof current !== "object")
123
+ return undefined;
124
+ return Reflect.get(current, segment);
125
+ }, value);
126
+ }
127
+ export function isLiveFormFieldValueEmpty(value) {
128
+ if (value === null || value === undefined)
129
+ return true;
130
+ if (typeof value === "string")
131
+ return value.trim().length === 0;
132
+ if (typeof value === "number" || typeof value === "boolean")
133
+ return false;
134
+ if (Array.isArray(value))
135
+ return value.length === 0 || value.every((item) => isLiveFormFieldValueEmpty(item));
136
+ if (typeof value === "object") {
137
+ const meaningfulValues = Object.entries(value).filter(([key]) => key !== "otherValue");
138
+ return meaningfulValues.length === 0 || meaningfulValues.every(([, item]) => isLiveFormFieldValueEmpty(item));
139
+ }
140
+ return false;
141
+ }
142
+ export function buildLiveFormNameValue(value, current = {}) {
143
+ return {
144
+ ...(current && typeof current === "object" ? current : {}),
145
+ ...value,
146
+ };
147
+ }
148
+ export function buildLiveFormAddressValue(value, current = {}) {
149
+ return {
150
+ ...(current && typeof current === "object" ? current : {}),
151
+ ...value,
152
+ };
153
+ }
154
+ export function buildLiveFormChoiceValue(value, otherValue = "") {
155
+ return { value, otherValue };
156
+ }
157
+ export function buildLiveFormSelectValue(value) {
158
+ return { value };
159
+ }
160
+ export function buildLiveFormCheckboxValue(values, otherValue = null) {
161
+ return Object.fromEntries([...values.map((value, index) => [String(index), value]), ["otherValue", otherValue]]);
162
+ }
163
+ export function buildLiveFormDateTimeValue(value, current = {}) {
164
+ return {
165
+ ...(current && typeof current === "object" ? current : {}),
166
+ ...(value.month !== undefined ? { M: value.month } : {}),
167
+ ...(value.day !== undefined ? { D: value.day } : {}),
168
+ ...(value.year !== undefined ? { Y: value.year } : {}),
169
+ ...(value.hour !== undefined ? { H: value.hour } : {}),
170
+ ...(value.minutes !== undefined ? { I: value.minutes } : {}),
171
+ ...(value.seconds !== undefined ? { S: value.seconds } : {}),
172
+ ...(value.ampm !== undefined ? { A: value.ampm } : {}),
173
+ };
174
+ }
175
+ export function buildLiveFormProductValue(value, current = {}) {
176
+ return {
177
+ ...(current && typeof current === "object" ? current : {}),
178
+ ...value,
179
+ };
180
+ }
181
+ export function buildLiveFormRatingValue(value) {
182
+ return { value: String(value) };
183
+ }
184
+ export function buildLiveFormMatrixValue(selections) {
185
+ return {
186
+ value: selections.map((selection) => {
187
+ if (typeof selection === "string")
188
+ return selection;
189
+ return `${selection.row} = ${selection.column}`;
190
+ }),
191
+ };
192
+ }
193
+ export async function readLiveFormFieldValue(page, options) {
194
+ if (!hasEvaluate(page)) {
195
+ throw new Error("readLiveFormFieldValue requires a Playwright page with evaluate().");
196
+ }
197
+ return page.evaluate(async (readOptions) => {
198
+ const waitForApiTimeoutMs = readOptions.waitForApiTimeoutMs ?? 10_000;
199
+ const readPath = (value, path) => {
200
+ if (!path)
201
+ return value;
202
+ return path.split(".").reduce((current, segment) => {
203
+ if (current === null || current === undefined || typeof current !== "object")
204
+ return undefined;
205
+ return Reflect.get(current, segment);
206
+ }, value);
207
+ };
208
+ const isEmpty = (value) => {
209
+ if (value === null || value === undefined)
210
+ return true;
211
+ if (typeof value === "string")
212
+ return value.trim().length === 0;
213
+ if (typeof value === "number" || typeof value === "boolean")
214
+ return false;
215
+ if (Array.isArray(value))
216
+ return value.length === 0 || value.every((item) => isEmpty(item));
217
+ if (typeof value === "object") {
218
+ const meaningfulValues = Object.entries(value).filter(([key]) => key !== "otherValue");
219
+ return meaningfulValues.length === 0 || meaningfulValues.every(([, item]) => isEmpty(item));
220
+ }
221
+ return false;
222
+ };
223
+ const valuesEqual = (left, right) => {
224
+ if (Object.is(left, right))
225
+ return true;
226
+ try {
227
+ return JSON.stringify(left) === JSON.stringify(right);
228
+ }
229
+ catch {
230
+ return false;
231
+ }
232
+ };
233
+ const readApi = () => {
234
+ const factory = Reflect.get(window, "fsApi");
235
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
236
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
237
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
238
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
239
+ };
240
+ const deadline = Date.now() + waitForApiTimeoutMs;
241
+ let form = null;
242
+ while (Date.now() < deadline) {
243
+ form = readApi().form;
244
+ if (form && typeof form === "object")
245
+ break;
246
+ await new Promise((resolve) => setTimeout(resolve, 50));
247
+ }
248
+ const apiAvailable = Boolean(form && typeof form === "object");
249
+ if (!apiAvailable || !form || typeof form !== "object") {
250
+ return {
251
+ apiAvailable: false,
252
+ fieldAvailable: false,
253
+ id: null,
254
+ value: null,
255
+ selectedValue: null,
256
+ ...(readOptions.valuePath === undefined ? {} : { valuePath: readOptions.valuePath }),
257
+ ...(readOptions.expectedValue === undefined ? {} : { expectedValue: readOptions.expectedValue, valueMatched: false }),
258
+ isEmpty: true,
259
+ };
260
+ }
261
+ const getField = Reflect.get(form, "getField");
262
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
263
+ const field = readOptions.target.fieldId != null && typeof getField === "function"
264
+ ? Reflect.apply(getField, form, [String(readOptions.target.fieldId)])
265
+ : readOptions.target.internalLabel && typeof getFieldByInternalLabel === "function"
266
+ ? Reflect.apply(getFieldByInternalLabel, form, [readOptions.target.internalLabel])
267
+ : null;
268
+ const fieldAvailable = Boolean(field && typeof field === "object");
269
+ if (!fieldAvailable || !field || typeof field !== "object") {
270
+ return {
271
+ apiAvailable: true,
272
+ fieldAvailable: false,
273
+ id: null,
274
+ value: null,
275
+ selectedValue: null,
276
+ ...(readOptions.valuePath === undefined ? {} : { valuePath: readOptions.valuePath }),
277
+ ...(readOptions.expectedValue === undefined ? {} : { expectedValue: readOptions.expectedValue, valueMatched: false }),
278
+ isEmpty: true,
279
+ };
280
+ }
281
+ const getId = Reflect.get(field, "getId");
282
+ const getValue = Reflect.get(field, "getValue");
283
+ const id = typeof getId === "function" ? Reflect.apply(getId, field, []) : Reflect.get(field, "id") ?? null;
284
+ const value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
285
+ const selectedValue = readPath(value, readOptions.valuePath);
286
+ return {
287
+ apiAvailable: true,
288
+ fieldAvailable: true,
289
+ id,
290
+ value,
291
+ selectedValue,
292
+ ...(readOptions.valuePath === undefined ? {} : { valuePath: readOptions.valuePath }),
293
+ ...(readOptions.expectedValue === undefined ? {} : { expectedValue: readOptions.expectedValue, valueMatched: valuesEqual(selectedValue, readOptions.expectedValue) }),
294
+ isEmpty: isEmpty(selectedValue),
295
+ };
296
+ }, options);
297
+ }
298
+ export async function waitForLiveFormFieldValue(page, options) {
299
+ const timeoutMs = options.timeoutMs ?? 10_000;
300
+ const intervalMs = options.intervalMs ?? 100;
301
+ const startedAt = Date.now();
302
+ let latest = await readLiveFormFieldValue(page, options);
303
+ const isMatched = (report) => options.expectedValue === undefined ? report.fieldAvailable && !report.isEmpty : report.valueMatched === true;
304
+ while (!isMatched(latest) && Date.now() - startedAt < timeoutMs) {
305
+ await delay(intervalMs);
306
+ latest = await readLiveFormFieldValue(page, options);
307
+ }
308
+ return latest;
309
+ }
310
+ export async function readLiveFormReadiness(page, options = {}) {
311
+ if (!hasEvaluate(page)) {
312
+ throw new Error("readLiveFormReadiness requires a Playwright page with evaluate().");
313
+ }
314
+ return page.evaluate(async (readinessOptions) => {
315
+ const fieldTargets = readinessOptions.fieldTargets ?? [];
316
+ const waitForApiTimeoutMs = readinessOptions.waitForApiTimeoutMs ?? 10_000;
317
+ const readPath = (value, path) => {
318
+ if (!path)
319
+ return value;
320
+ return path.split(".").reduce((current, segment) => {
321
+ if (current === null || current === undefined || typeof current !== "object")
322
+ return undefined;
323
+ return Reflect.get(current, segment);
324
+ }, value);
325
+ };
326
+ const isEmpty = (value) => {
327
+ if (value === null || value === undefined)
328
+ return true;
329
+ if (typeof value === "string")
330
+ return value.trim().length === 0;
331
+ if (typeof value === "number" || typeof value === "boolean")
332
+ return false;
333
+ if (Array.isArray(value))
334
+ return value.length === 0 || value.every((item) => isEmpty(item));
335
+ if (typeof value === "object") {
336
+ const meaningfulValues = Object.entries(value).filter(([key]) => key !== "otherValue");
337
+ return meaningfulValues.length === 0 || meaningfulValues.every(([, item]) => isEmpty(item));
338
+ }
339
+ return false;
340
+ };
341
+ const readApi = () => {
342
+ const factory = Reflect.get(window, "fsApi");
343
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
344
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
345
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
346
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
347
+ };
348
+ const deadline = Date.now() + waitForApiTimeoutMs;
349
+ let form = null;
350
+ while (Date.now() < deadline) {
351
+ form = readApi().form;
352
+ if (form && typeof form === "object")
353
+ break;
354
+ await new Promise((resolve) => setTimeout(resolve, 50));
355
+ }
356
+ const apiAvailable = Boolean(form && typeof form === "object");
357
+ if (!apiAvailable || !form || typeof form !== "object") {
358
+ return {
359
+ apiAvailable: false,
360
+ fields: [],
361
+ blockingFieldCount: 0,
362
+ isReady: false,
363
+ };
364
+ }
365
+ const getFieldTarget = (target) => {
366
+ const getField = Reflect.get(form, "getField");
367
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
368
+ if (target.fieldId != null && typeof getField === "function")
369
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
370
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
371
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
372
+ }
373
+ return null;
374
+ };
375
+ const snapshotField = (field, target) => {
376
+ if (!field || typeof field !== "object") {
377
+ const required = target.required ?? true;
378
+ return {
379
+ target,
380
+ available: false,
381
+ id: null,
382
+ value: null,
383
+ required,
384
+ apiHidden: false,
385
+ hidden: false,
386
+ domVisible: null,
387
+ requiredValuePaths: target.requiredValuePaths ?? [],
388
+ missingValuePaths: target.requiredValuePaths ?? [],
389
+ isComplete: false,
390
+ isBlocking: required,
391
+ };
392
+ }
393
+ const getId = Reflect.get(field, "getId");
394
+ const getValue = Reflect.get(field, "getValue");
395
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
396
+ let value = null;
397
+ let required = target.required ?? false;
398
+ let apiHidden = false;
399
+ try {
400
+ value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
401
+ }
402
+ catch {
403
+ value = null;
404
+ }
405
+ try {
406
+ if (typeof getGeneralAttribute === "function") {
407
+ required = target.required ?? Reflect.apply(getGeneralAttribute, field, ["required"]) === true;
408
+ apiHidden = Reflect.apply(getGeneralAttribute, field, ["hidden"]) === true;
409
+ }
410
+ }
411
+ catch {
412
+ required = target.required ?? false;
413
+ }
414
+ const id = typeof getId === "function" ? Reflect.apply(getId, field, []) : Reflect.get(field, "id") ?? null;
415
+ const idRoot = id == null
416
+ ? null
417
+ : document.querySelector(`#fsCell${CSS.escape(String(id))}, #fsFieldCell${CSS.escape(String(id))}, [data-testid="field-${CSS.escape(String(id))}"]`);
418
+ const domRoot = idRoot ?? (target.internalLabel != null
419
+ ? document.querySelector(`[data-internal-label="${CSS.escape(target.internalLabel)}"]`)
420
+ : null);
421
+ const isActuallyVisible = (element) => {
422
+ if (!(element instanceof HTMLElement))
423
+ return null;
424
+ for (let current = element; current; current = current.parentElement) {
425
+ const style = getComputedStyle(current);
426
+ const rect = current.getBoundingClientRect();
427
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
428
+ return false;
429
+ if (rect.width <= 0 || rect.height <= 0)
430
+ return false;
431
+ }
432
+ return element.getClientRects().length > 0;
433
+ };
434
+ const domVisible = isActuallyVisible(domRoot);
435
+ const hidden = apiHidden || domVisible === false;
436
+ const requiredValuePaths = target.requiredValuePaths ?? [];
437
+ const missingValuePaths = requiredValuePaths.length > 0
438
+ ? requiredValuePaths.filter((path) => isEmpty(readPath(value, path)))
439
+ : isEmpty(value)
440
+ ? [""]
441
+ : [];
442
+ const isComplete = missingValuePaths.length === 0;
443
+ const isBlocking = required && !hidden && !isComplete;
444
+ return {
445
+ target,
446
+ available: true,
447
+ id,
448
+ value,
449
+ required,
450
+ apiHidden,
451
+ hidden,
452
+ domVisible,
453
+ requiredValuePaths,
454
+ missingValuePaths,
455
+ isComplete,
456
+ isBlocking,
457
+ };
458
+ };
459
+ const fields = [];
460
+ if (readinessOptions.includeAllFields) {
461
+ const getFields = Reflect.get(form, "getFields");
462
+ const allFields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
463
+ if (Array.isArray(allFields)) {
464
+ for (const field of allFields)
465
+ fields.push(snapshotField(field, { label: "all-fields" }));
466
+ }
467
+ }
468
+ for (const target of fieldTargets) {
469
+ fields.push(snapshotField(getFieldTarget(target), target));
470
+ }
471
+ const blockingFieldCount = fields.filter((field) => field.isBlocking).length;
472
+ return {
473
+ apiAvailable: true,
474
+ fields,
475
+ blockingFieldCount,
476
+ isReady: blockingFieldCount === 0,
477
+ };
478
+ }, options);
479
+ }
480
+ function delay(ms) {
481
+ return new Promise((resolve) => setTimeout(resolve, ms));
482
+ }
483
+ export async function waitForLiveFormReadiness(page, options = {}) {
484
+ const timeoutMs = options.timeoutMs ?? 10_000;
485
+ const intervalMs = options.intervalMs ?? 100;
486
+ const startedAt = Date.now();
487
+ let latest = await readLiveFormReadiness(page, options);
488
+ while (!latest.isReady && Date.now() - startedAt < timeoutMs) {
489
+ await delay(intervalMs);
490
+ latest = await readLiveFormReadiness(page, options);
491
+ }
492
+ return latest;
493
+ }
494
+ export async function waitForLiveFormQuiet(page, options = {}) {
495
+ if (!hasEvaluate(page)) {
496
+ throw new Error("waitForLiveFormQuiet requires a Playwright page with evaluate().");
497
+ }
498
+ return page.evaluate(async (quietOptions) => {
499
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
500
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
501
+ const quietMs = quietOptions.quietMs ?? 250;
502
+ const timeoutMs = quietOptions.timeoutMs ?? 5_000;
503
+ const waitForApiTimeoutMs = quietOptions.waitForApiTimeoutMs ?? 2_000;
504
+ const eventTypes = quietOptions.eventTypes ?? ["input", "change", "blur", "click", "submit"];
505
+ const formEventTypes = quietOptions.formEventTypes ?? ["change", "calculations-complete", "validation-complete", "submit", "error"];
506
+ const includeEvents = quietOptions.includeEvents ?? true;
507
+ const events = [];
508
+ let lastEventAt = now();
509
+ const elapsedMs = () => Math.round(now() - startedAt);
510
+ const record = (kind, type, detail) => {
511
+ lastEventAt = now();
512
+ if (includeEvents) {
513
+ events.push({
514
+ kind,
515
+ type,
516
+ elapsedMs: elapsedMs(),
517
+ detail,
518
+ });
519
+ }
520
+ };
521
+ const readApi = () => {
522
+ const factory = Reflect.get(window, "fsApi");
523
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
524
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
525
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
526
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
527
+ };
528
+ const apiDeadline = Date.now() + waitForApiTimeoutMs;
529
+ let form = null;
530
+ while (Date.now() < apiDeadline) {
531
+ form = readApi().form;
532
+ if (form && typeof form === "object")
533
+ break;
534
+ await new Promise((resolve) => setTimeout(resolve, 50));
535
+ }
536
+ const apiAvailable = Boolean(form && typeof form === "object");
537
+ const domListener = (event) => {
538
+ const target = event.target instanceof Element ? event.target : null;
539
+ record("dom-event", event.type, {
540
+ targetId: target?.getAttribute("id") ?? null,
541
+ targetName: target?.getAttribute("name") ?? null,
542
+ internalLabel: target?.closest("[data-internal-label]")?.getAttribute("data-internal-label") ?? null,
543
+ });
544
+ };
545
+ for (const eventType of eventTypes)
546
+ document.addEventListener(eventType, domListener, true);
547
+ const formEventListeners = apiAvailable && form && typeof form === "object"
548
+ ? formEventTypes.map((type) => ({
549
+ type,
550
+ onFormEvent: (event) => {
551
+ const eventObject = event && typeof event === "object" ? event : {};
552
+ record("form-event", type, {
553
+ formId: Reflect.get(eventObject, "formId") ?? null,
554
+ data: Reflect.get(eventObject, "data") ?? null,
555
+ });
556
+ },
557
+ }))
558
+ : [];
559
+ const registerFormEventListener = apiAvailable && form && typeof form === "object" ? Reflect.get(form, "registerFormEventListener") : null;
560
+ const unregisterFormEventListener = apiAvailable && form && typeof form === "object" ? Reflect.get(form, "unregisterFormEventListener") : null;
561
+ if (typeof registerFormEventListener === "function") {
562
+ for (const listener of formEventListeners)
563
+ Reflect.apply(registerFormEventListener, form, [listener]);
564
+ }
565
+ const deadline = Date.now() + timeoutMs;
566
+ try {
567
+ while (Date.now() < deadline) {
568
+ const quietForMs = Math.round(now() - lastEventAt);
569
+ if (quietForMs >= quietMs) {
570
+ return {
571
+ apiAvailable,
572
+ quiet: true,
573
+ timedOut: false,
574
+ elapsedMs: elapsedMs(),
575
+ quietForMs,
576
+ eventCount: events.length,
577
+ ...(includeEvents ? { events } : {}),
578
+ };
579
+ }
580
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, Math.max(quietMs - quietForMs, 10))));
581
+ }
582
+ return {
583
+ apiAvailable,
584
+ quiet: false,
585
+ timedOut: true,
586
+ elapsedMs: elapsedMs(),
587
+ quietForMs: Math.round(now() - lastEventAt),
588
+ eventCount: events.length,
589
+ ...(includeEvents ? { events } : {}),
590
+ };
591
+ }
592
+ finally {
593
+ for (const eventType of eventTypes)
594
+ document.removeEventListener(eventType, domListener, true);
595
+ if (typeof unregisterFormEventListener === "function") {
596
+ for (const listener of formEventListeners)
597
+ Reflect.apply(unregisterFormEventListener, form, [listener]);
598
+ }
599
+ }
600
+ }, options);
601
+ }
602
+ export async function installLiveFormInitialValidationGuard(page, options) {
603
+ if (!hasEvaluate(page)) {
604
+ throw new Error("installLiveFormInitialValidationGuard requires a Playwright page with evaluate().");
605
+ }
606
+ return page.evaluate(async (guardOptions) => {
607
+ const waitForApiTimeoutMs = guardOptions.waitForApiTimeoutMs ?? 10_000;
608
+ const restoreDelayMs = guardOptions.restoreDelayMs ?? 700;
609
+ const restoreOnFirstFocusout = guardOptions.restoreOnFirstFocusout ?? true;
610
+ const guardedFields = [];
611
+ const missingTargets = [];
612
+ const readApi = () => {
613
+ const factory = Reflect.get(window, "fsApi");
614
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
615
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
616
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
617
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
618
+ };
619
+ const deadline = Date.now() + waitForApiTimeoutMs;
620
+ let form = null;
621
+ while (Date.now() < deadline) {
622
+ form = readApi().form;
623
+ if (form && typeof form === "object")
624
+ break;
625
+ await new Promise((resolve) => setTimeout(resolve, 50));
626
+ }
627
+ const apiAvailable = Boolean(form && typeof form === "object");
628
+ if (!apiAvailable || !form || typeof form !== "object") {
629
+ return {
630
+ apiAvailable: false,
631
+ installed: false,
632
+ guardedFieldCount: 0,
633
+ missingTargets: guardOptions.targets,
634
+ };
635
+ }
636
+ const resolveField = (target) => {
637
+ const getField = Reflect.get(form, "getField");
638
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
639
+ if (target.fieldId != null && typeof getField === "function")
640
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
641
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
642
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
643
+ }
644
+ return null;
645
+ };
646
+ const readSkipValidation = (field) => {
647
+ if (!field || typeof field !== "object")
648
+ return undefined;
649
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
650
+ return typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["shouldForceSkipValidation"]) : undefined;
651
+ };
652
+ const setSkipValidation = (field, value) => {
653
+ if (!field || typeof field !== "object")
654
+ return;
655
+ const setGeneralAttribute = Reflect.get(field, "setGeneralAttribute");
656
+ if (typeof setGeneralAttribute === "function")
657
+ Reflect.apply(setGeneralAttribute, field, ["shouldForceSkipValidation", value]);
658
+ };
659
+ for (const target of guardOptions.targets) {
660
+ const field = resolveField(target);
661
+ if (!field || typeof field !== "object") {
662
+ missingTargets.push(target);
663
+ continue;
664
+ }
665
+ guardedFields.push({ field, target, previousValue: readSkipValidation(field) });
666
+ setSkipValidation(field, true);
667
+ }
668
+ const restore = () => {
669
+ for (const entry of guardedFields)
670
+ setSkipValidation(entry.field, entry.previousValue === true);
671
+ document.removeEventListener("focusout", onFocusout, true);
672
+ Reflect.deleteProperty(window, "__fsptInitialValidationGuardRestore");
673
+ };
674
+ const matchesTarget = (element, target) => {
675
+ if (target.controlIds?.some((id) => element.id === id || element.closest(`#${CSS.escape(id)}`)))
676
+ return true;
677
+ if (target.controlSelectors?.some((selector) => element.matches(selector) || element.closest(selector)))
678
+ return true;
679
+ if (target.internalLabel && element.closest(`[data-internal-label="${CSS.escape(target.internalLabel)}"]`))
680
+ return true;
681
+ if (target.fieldId != null && element.closest(`#fsCell${CSS.escape(String(target.fieldId))}`))
682
+ return true;
683
+ return false;
684
+ };
685
+ function onFocusout(event) {
686
+ if (!restoreOnFirstFocusout || !(event.target instanceof Element))
687
+ return;
688
+ if (!guardOptions.targets.some((target) => matchesTarget(event.target, target)))
689
+ return;
690
+ window.setTimeout(restore, restoreDelayMs);
691
+ }
692
+ Object.defineProperty(window, "__fsptInitialValidationGuardRestore", {
693
+ configurable: true,
694
+ value: restore,
695
+ });
696
+ if (restoreOnFirstFocusout)
697
+ document.addEventListener("focusout", onFocusout, true);
698
+ return {
699
+ apiAvailable: true,
700
+ installed: guardedFields.length > 0,
701
+ guardedFieldCount: guardedFields.length,
702
+ missingTargets,
703
+ };
704
+ }, options);
705
+ }
706
+ export async function setLiveFormFieldValue(page, options) {
707
+ if (!hasEvaluate(page)) {
708
+ throw new Error("setLiveFormFieldValue requires a Playwright page with evaluate().");
709
+ }
710
+ return page.evaluate(async (writeOptions) => {
711
+ const timeoutMs = writeOptions.timeoutMs ?? 5_000;
712
+ const intervalMs = writeOptions.intervalMs ?? 50;
713
+ const settleMs = writeOptions.settleMs ?? 100;
714
+ const readApi = () => {
715
+ const factory = Reflect.get(window, "fsApi");
716
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
717
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
718
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
719
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
720
+ };
721
+ const valuesEqual = (left, right) => {
722
+ if (Object.is(left, right))
723
+ return true;
724
+ try {
725
+ return JSON.stringify(left) === JSON.stringify(right);
726
+ }
727
+ catch {
728
+ return false;
729
+ }
730
+ };
731
+ const readFieldValue = (field) => {
732
+ if (!field || typeof field !== "object")
733
+ return null;
734
+ const getValue = Reflect.get(field, "getValue");
735
+ return typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
736
+ };
737
+ const resolveField = (form) => {
738
+ if (!form || typeof form !== "object")
739
+ return null;
740
+ const getField = Reflect.get(form, "getField");
741
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
742
+ if (writeOptions.target.fieldId != null && typeof getField === "function") {
743
+ return Reflect.apply(getField, form, [String(writeOptions.target.fieldId)]);
744
+ }
745
+ if (writeOptions.target.internalLabel && typeof getFieldByInternalLabel === "function") {
746
+ return Reflect.apply(getFieldByInternalLabel, form, [writeOptions.target.internalLabel]);
747
+ }
748
+ return null;
749
+ };
750
+ const { form } = readApi();
751
+ const apiAvailable = Boolean(form && typeof form === "object");
752
+ if (!apiAvailable || !form || typeof form !== "object") {
753
+ return {
754
+ apiAvailable: false,
755
+ fieldAvailable: false,
756
+ id: null,
757
+ previousValue: null,
758
+ expectedValue: writeOptions.value,
759
+ finalValue: null,
760
+ valueMatched: false,
761
+ notified: false,
762
+ };
763
+ }
764
+ const field = resolveField(form);
765
+ const fieldAvailable = Boolean(field && typeof field === "object");
766
+ if (!fieldAvailable || !field || typeof field !== "object") {
767
+ return {
768
+ apiAvailable: true,
769
+ fieldAvailable: false,
770
+ id: null,
771
+ previousValue: null,
772
+ expectedValue: writeOptions.value,
773
+ finalValue: null,
774
+ valueMatched: false,
775
+ notified: false,
776
+ };
777
+ }
778
+ const setValue = Reflect.get(field, "setValue");
779
+ const previousValue = readFieldValue(field);
780
+ if (typeof setValue !== "function") {
781
+ return {
782
+ apiAvailable: true,
783
+ fieldAvailable: true,
784
+ id: Reflect.get(field, "id") ?? null,
785
+ previousValue,
786
+ expectedValue: writeOptions.value,
787
+ finalValue: previousValue,
788
+ valueMatched: false,
789
+ notified: false,
790
+ };
791
+ }
792
+ await Promise.resolve(Reflect.apply(setValue, field, [writeOptions.value]));
793
+ let finalValue = readFieldValue(field);
794
+ if (writeOptions.waitForValue ?? true) {
795
+ const deadline = Date.now() + timeoutMs;
796
+ while (!valuesEqual(finalValue, writeOptions.value) && Date.now() < deadline) {
797
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
798
+ finalValue = readFieldValue(field);
799
+ }
800
+ }
801
+ if (settleMs > 0)
802
+ await new Promise((resolve) => setTimeout(resolve, settleMs));
803
+ let notifyResult = null;
804
+ if (writeOptions.notify) {
805
+ const notifyFormEventListeners = Reflect.get(form, "notifyFormEventListeners");
806
+ notifyResult =
807
+ typeof notifyFormEventListeners === "function"
808
+ ? await Promise.resolve(Reflect.apply(notifyFormEventListeners, form, [
809
+ "change",
810
+ {
811
+ fieldId: Reflect.get(field, "id") ?? null,
812
+ type: "api-setValue",
813
+ value: writeOptions.value,
814
+ },
815
+ ]))
816
+ : null;
817
+ }
818
+ finalValue = readFieldValue(field);
819
+ return {
820
+ apiAvailable: true,
821
+ fieldAvailable: true,
822
+ id: Reflect.get(field, "id") ?? null,
823
+ previousValue,
824
+ expectedValue: writeOptions.value,
825
+ finalValue,
826
+ valueMatched: valuesEqual(finalValue, writeOptions.value),
827
+ notified: writeOptions.notify === true,
828
+ ...(writeOptions.notify ? { notifyResult } : {}),
829
+ };
830
+ }, options);
831
+ }
832
+ export async function snapshotLiveFormDomFields(page, options = {}) {
833
+ if (!hasEvaluate(page)) {
834
+ throw new Error("snapshotLiveFormDomFields requires a Playwright page with evaluate().");
835
+ }
836
+ return page.evaluate((snapshotOptions) => {
837
+ const selector = snapshotOptions.selector ?? "[data-internal-label]";
838
+ const textMaxLength = snapshotOptions.textMaxLength ?? 160;
839
+ return Array.from(document.querySelectorAll(selector)).map((element) => ({
840
+ internalLabel: element.getAttribute("data-internal-label"),
841
+ id: element.id,
842
+ text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, textMaxLength) ?? "",
843
+ controls: Array.from(element.querySelectorAll("input, textarea, select")).map((control) => {
844
+ const input = control;
845
+ return {
846
+ tagName: control.tagName.toLowerCase(),
847
+ type: control.getAttribute("type"),
848
+ id: control.getAttribute("id"),
849
+ name: control.getAttribute("name"),
850
+ value: control.getAttribute("value"),
851
+ currentValue: "value" in input ? input.value : null,
852
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
853
+ };
854
+ }),
855
+ }));
856
+ }, options);
857
+ }
858
+ export async function readLiveFormDomFieldState(page, options) {
859
+ if (!hasEvaluate(page)) {
860
+ throw new Error("readLiveFormDomFieldState requires a Playwright page with evaluate().");
861
+ }
862
+ return page.evaluate((stateOptions) => {
863
+ const controlSnapshot = (control) => {
864
+ const input = control;
865
+ return {
866
+ tagName: control.tagName.toLowerCase(),
867
+ type: control.getAttribute("type"),
868
+ id: control.getAttribute("id"),
869
+ name: control.getAttribute("name"),
870
+ value: control.getAttribute("value"),
871
+ currentValue: "value" in input ? input.value : null,
872
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
873
+ };
874
+ };
875
+ const root = stateOptions.selector != null
876
+ ? document.querySelector(stateOptions.selector)
877
+ : stateOptions.internalLabel != null
878
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
879
+ : stateOptions.fieldId == null
880
+ ? null
881
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
882
+ const fieldMatched = Boolean(root);
883
+ const field = root
884
+ ? {
885
+ internalLabel: root.getAttribute("data-internal-label"),
886
+ id: root.id,
887
+ text: root.textContent?.replace(/\s+/g, " ").trim().slice(0, 160) ?? "",
888
+ controls: Array.from(root.querySelectorAll("input, textarea, select")).map(controlSnapshot),
889
+ }
890
+ : null;
891
+ const controls = root == null
892
+ ? []
893
+ : Array.from(root.querySelectorAll(stateOptions.controlSelector ?? "input, textarea, select")).map(controlSnapshot);
894
+ const controlMatched = controls.some((control) => {
895
+ const valueMatched = stateOptions.expectedValue === undefined || control.currentValue === stateOptions.expectedValue || control.value === stateOptions.expectedValue;
896
+ const checkedMatched = stateOptions.expectedChecked === undefined || control.checked === stateOptions.expectedChecked;
897
+ return valueMatched && checkedMatched;
898
+ });
899
+ return {
900
+ matched: fieldMatched && controlMatched,
901
+ fieldMatched,
902
+ controlMatched,
903
+ field,
904
+ controls,
905
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
906
+ ...(stateOptions.expectedChecked === undefined ? {} : { expectedChecked: stateOptions.expectedChecked }),
907
+ };
908
+ }, options);
909
+ }
910
+ export async function waitForLiveFormDomFieldState(page, options) {
911
+ const timeoutMs = options.timeoutMs ?? 10_000;
912
+ const intervalMs = options.intervalMs ?? 100;
913
+ const startedAt = Date.now();
914
+ let latest = await readLiveFormDomFieldState(page, options);
915
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
916
+ await delay(intervalMs);
917
+ latest = await readLiveFormDomFieldState(page, options);
918
+ }
919
+ return latest;
920
+ }
921
+ export async function dispatchLiveFormFieldDomEvents(page, options) {
922
+ if (!hasEvaluate(page)) {
923
+ throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
924
+ }
925
+ return page.evaluate((dispatchOptions) => {
926
+ const eventTypes = dispatchOptions.eventTypes ?? ["input", "change"];
927
+ const root = dispatchOptions.target.internalLabel != null
928
+ ? document.querySelector(`[data-internal-label="${CSS.escape(dispatchOptions.target.internalLabel)}"]`)
929
+ : dispatchOptions.target.fieldId == null
930
+ ? null
931
+ : document.querySelector(`#fsCell${CSS.escape(String(dispatchOptions.target.fieldId))}, [data-testid="field-${CSS.escape(String(dispatchOptions.target.fieldId))}"]`);
932
+ const allControls = root ? Array.from(root.querySelectorAll("input, textarea, select")) : [];
933
+ const controls = dispatchOptions.onlyCheckedChoices ?? true
934
+ ? allControls.filter((control) => {
935
+ if (control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type))
936
+ return control.checked;
937
+ if (control instanceof HTMLInputElement && control.type === "hidden")
938
+ return false;
939
+ return true;
940
+ })
941
+ : allControls;
942
+ for (const control of controls) {
943
+ for (const type of eventTypes) {
944
+ control.dispatchEvent(new Event(type, { bubbles: true }));
945
+ }
946
+ }
947
+ return {
948
+ matchedControlCount: controls.length,
949
+ dispatchedEventCount: controls.length * eventTypes.length,
950
+ controls: controls.map((control) => ({
951
+ tagName: control.tagName.toLowerCase(),
952
+ type: control.getAttribute("type"),
953
+ id: control.getAttribute("id"),
954
+ name: control.getAttribute("name"),
955
+ value: control.getAttribute("value"),
956
+ currentValue: "value" in control ? control.value : null,
957
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
958
+ })),
959
+ };
960
+ }, options);
961
+ }
962
+ export async function installWindowMessageRecorder(page, options = {}) {
963
+ if (!hasEvaluate(page)) {
964
+ throw new Error("installWindowMessageRecorder requires a Playwright page with evaluate().");
965
+ }
966
+ return page.evaluate((recorderOptions) => {
967
+ const propertyName = recorderOptions.propertyName ?? "__fsptWindowMessages";
968
+ const allowedOrigins = recorderOptions.allowedOrigins ?? [];
969
+ const records = [];
970
+ const listener = (event) => {
971
+ if (allowedOrigins.length > 0 && !allowedOrigins.includes(event.origin))
972
+ return;
973
+ records.push({
974
+ origin: event.origin,
975
+ data: event.data,
976
+ });
977
+ };
978
+ window.addEventListener("message", listener);
979
+ Object.defineProperty(window, propertyName, {
980
+ configurable: true,
981
+ value: {
982
+ records,
983
+ dispose: () => window.removeEventListener("message", listener),
984
+ clear: () => {
985
+ records.length = 0;
986
+ },
987
+ },
988
+ });
989
+ return { installed: true, propertyName };
990
+ }, options);
991
+ }
992
+ export async function readWindowMessageRecords(page, propertyName = "__fsptWindowMessages") {
993
+ if (!hasEvaluate(page)) {
994
+ throw new Error("readWindowMessageRecords requires a Playwright page with evaluate().");
995
+ }
996
+ return page.evaluate((name) => {
997
+ const recorder = Reflect.get(window, name);
998
+ return Array.isArray(recorder?.records) ? [...recorder.records] : [];
999
+ }, propertyName);
1000
+ }
1001
+ export async function clearWindowMessageRecords(page, propertyName = "__fsptWindowMessages") {
1002
+ if (!hasEvaluate(page)) {
1003
+ throw new Error("clearWindowMessageRecords requires a Playwright page with evaluate().");
1004
+ }
1005
+ await page.evaluate((name) => {
1006
+ const recorder = Reflect.get(window, name);
1007
+ if (typeof recorder?.clear === "function")
1008
+ recorder.clear();
1009
+ }, propertyName);
1010
+ }
1011
+ export async function postMessageToIframe(page, options) {
1012
+ if (!hasEvaluate(page)) {
1013
+ throw new Error("postMessageToIframe requires a Playwright page with evaluate().");
1014
+ }
1015
+ return page.evaluate((postOptions) => {
1016
+ const iframe = document.querySelector(postOptions.iframeSelector);
1017
+ if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow)
1018
+ return false;
1019
+ iframe.contentWindow.postMessage(postOptions.message, postOptions.targetOrigin);
1020
+ return true;
1021
+ }, options);
1022
+ }
1023
+ export function buildFormstackIframeBridgeMessage(message) {
1024
+ return message;
1025
+ }
1026
+ export async function waitForWindowMessage(page, options = {}) {
1027
+ if (!hasEvaluate(page)) {
1028
+ throw new Error("waitForWindowMessage requires a Playwright page with evaluate().");
1029
+ }
1030
+ return page.evaluate(async (waitOptions) => {
1031
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
1032
+ const propertyName = waitOptions.propertyName ?? "__fsptWindowMessages";
1033
+ const timeoutMs = waitOptions.timeoutMs ?? 5_000;
1034
+ const deadline = Date.now() + timeoutMs;
1035
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1036
+ const matches = (record) => {
1037
+ const data = record.data && typeof record.data === "object" ? record.data : {};
1038
+ if (waitOptions.channel && Reflect.get(data, "channel") !== waitOptions.channel)
1039
+ return false;
1040
+ if (waitOptions.requestId && Reflect.get(data, "requestId") !== waitOptions.requestId)
1041
+ return false;
1042
+ if (waitOptions.type && Reflect.get(data, "type") !== waitOptions.type)
1043
+ return false;
1044
+ return true;
1045
+ };
1046
+ while (Date.now() < deadline) {
1047
+ const recorder = Reflect.get(window, propertyName);
1048
+ const records = Array.isArray(recorder?.records) ? recorder.records : [];
1049
+ const record = records.find(matches);
1050
+ if (record) {
1051
+ return {
1052
+ matched: true,
1053
+ timedOut: false,
1054
+ elapsedMs: Math.round(now() - startedAt),
1055
+ record,
1056
+ };
1057
+ }
1058
+ await new Promise((resolve) => setTimeout(resolve, 25));
1059
+ }
1060
+ return {
1061
+ matched: false,
1062
+ timedOut: true,
1063
+ elapsedMs: Math.round(now() - startedAt),
1064
+ };
1065
+ }, options);
1066
+ }
1067
+ export async function sendFormstackIframeBridgeMessage(page, options) {
1068
+ await installWindowMessageRecorder(page, {
1069
+ ...(options.propertyName === undefined ? {} : { propertyName: options.propertyName }),
1070
+ ...(options.allowedOrigins === undefined ? {} : { allowedOrigins: options.allowedOrigins }),
1071
+ });
1072
+ const posted = await postMessageToIframe(page, {
1073
+ iframeSelector: options.iframeSelector,
1074
+ targetOrigin: options.targetOrigin,
1075
+ message: buildFormstackIframeBridgeMessage({
1076
+ channel: options.channel,
1077
+ requestId: options.requestId,
1078
+ type: options.type,
1079
+ ...(options.payload === undefined ? {} : { payload: options.payload }),
1080
+ }),
1081
+ });
1082
+ const response = await waitForWindowMessage(page, {
1083
+ channel: options.channel,
1084
+ requestId: options.requestId,
1085
+ type: options.responseType ?? `${options.type}:response`,
1086
+ ...(options.propertyName === undefined ? {} : { propertyName: options.propertyName }),
1087
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
1088
+ });
1089
+ return { posted, response };
1090
+ }
1091
+ export async function waitForConsoleMessage(page, options = {}) {
1092
+ const timeoutMs = options.timeoutMs ?? 5_000;
1093
+ const startedAt = Date.now();
1094
+ return new Promise((resolve) => {
1095
+ let settled = false;
1096
+ const cleanup = () => {
1097
+ page.off?.("console", handler);
1098
+ };
1099
+ const finish = (report) => {
1100
+ if (settled)
1101
+ return;
1102
+ settled = true;
1103
+ cleanup();
1104
+ resolve(report);
1105
+ };
1106
+ const handler = (message) => {
1107
+ const type = message.type();
1108
+ const text = message.text();
1109
+ if (options.type && type !== options.type)
1110
+ return;
1111
+ if (options.text && !matchesPattern(text, options.text))
1112
+ return;
1113
+ finish({
1114
+ matched: true,
1115
+ timedOut: false,
1116
+ elapsedMs: Date.now() - startedAt,
1117
+ message: { type, text },
1118
+ });
1119
+ };
1120
+ page.on("console", handler);
1121
+ setTimeout(() => {
1122
+ finish({
1123
+ matched: false,
1124
+ timedOut: true,
1125
+ elapsedMs: Date.now() - startedAt,
1126
+ });
1127
+ }, timeoutMs);
1128
+ });
1129
+ }
1130
+ export async function waitForWindowFlag(page, options) {
1131
+ if (!hasEvaluate(page)) {
1132
+ throw new Error("waitForWindowFlag requires a Playwright page with evaluate().");
1133
+ }
1134
+ return page.evaluate(async (flagOptions) => {
1135
+ const startedAt = Date.now();
1136
+ const timeoutMs = flagOptions.timeoutMs ?? 5_000;
1137
+ const intervalMs = flagOptions.intervalMs ?? 50;
1138
+ const deadline = Date.now() + timeoutMs;
1139
+ const read = () => flagOptions.path.split(".").reduce((current, segment) => {
1140
+ if (current === null || current === undefined || typeof current !== "object")
1141
+ return undefined;
1142
+ return Reflect.get(current, segment);
1143
+ }, window);
1144
+ const matches = (value) => {
1145
+ if (flagOptions.expectedValue === undefined)
1146
+ return Boolean(value);
1147
+ return Object.is(value, flagOptions.expectedValue);
1148
+ };
1149
+ let value = read();
1150
+ while (!matches(value) && Date.now() < deadline) {
1151
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1152
+ value = read();
1153
+ }
1154
+ const matched = matches(value);
1155
+ return {
1156
+ matched,
1157
+ timedOut: !matched,
1158
+ elapsedMs: Date.now() - startedAt,
1159
+ path: flagOptions.path,
1160
+ value,
1161
+ };
1162
+ }, options);
1163
+ }
1164
+ export async function readLiveFormValidationState(page, options = {}) {
1165
+ if (!hasEvaluate(page)) {
1166
+ throw new Error("readLiveFormValidationState requires a Playwright page with evaluate().");
1167
+ }
1168
+ return page.evaluate(async (validationOptions) => {
1169
+ const factory = Reflect.get(window, "fsApi");
1170
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1171
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1172
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1173
+ const form = Array.isArray(forms) ? forms[0] ?? null : null;
1174
+ const apiAvailable = Boolean(form && typeof form === "object");
1175
+ let result = null;
1176
+ if (validationOptions.validate && apiAvailable && form && typeof form === "object") {
1177
+ const validateForm = Reflect.get(form, "validateForm");
1178
+ result = typeof validateForm === "function" ? await Promise.resolve(Reflect.apply(validateForm, form, [])) : null;
1179
+ }
1180
+ const isVisible = (element) => {
1181
+ if (!(element instanceof HTMLElement))
1182
+ return false;
1183
+ for (let current = element; current; current = current.parentElement) {
1184
+ const style = getComputedStyle(current);
1185
+ const rect = current.getBoundingClientRect();
1186
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
1187
+ return false;
1188
+ if (rect.width <= 0 || rect.height <= 0)
1189
+ return false;
1190
+ }
1191
+ return element.getClientRects().length > 0;
1192
+ };
1193
+ const inlineErrors = Array.from(document.querySelectorAll('[id^="inline-error-field"], .fsError, [role="alert"]'))
1194
+ .map((element) => ({
1195
+ id: element.getAttribute("id"),
1196
+ text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 240) ?? "",
1197
+ visible: isVisible(element),
1198
+ }))
1199
+ .filter((error) => validationOptions.includeEmptyErrors === true || error.text.length > 0)
1200
+ .filter((error) => validationOptions.includeHiddenErrors === true || error.visible)
1201
+ .map(({ id, text }) => ({ id, text }));
1202
+ return {
1203
+ apiAvailable,
1204
+ validated: validationOptions.validate === true,
1205
+ result,
1206
+ inlineErrors,
1207
+ };
1208
+ }, options);
1209
+ }
1210
+ export async function waitForLiveFormValidationState(page, options = {}) {
1211
+ const timeoutMs = options.timeoutMs ?? 10_000;
1212
+ const intervalMs = options.intervalMs ?? 100;
1213
+ const startedAt = Date.now();
1214
+ const matches = (state) => {
1215
+ if (options.expectNoErrors === true)
1216
+ return state.inlineErrors.length === 0;
1217
+ if (options.expectedErrorCount !== undefined && state.inlineErrors.length !== options.expectedErrorCount)
1218
+ return false;
1219
+ if (options.errorText !== undefined) {
1220
+ const errorText = options.errorText;
1221
+ return state.inlineErrors.some((error) => typeof errorText === "string" ? error.text.includes(errorText) : errorText.test(error.text));
1222
+ }
1223
+ return state.inlineErrors.length > 0;
1224
+ };
1225
+ let latest = await readLiveFormValidationState(page, options);
1226
+ while (!matches(latest) && Date.now() - startedAt < timeoutMs) {
1227
+ await delay(intervalMs);
1228
+ latest = await readLiveFormValidationState(page, options);
1229
+ }
1230
+ return latest;
1231
+ }
1232
+ export async function waitForLiveFormVisibility(page, options) {
1233
+ if (!hasEvaluate(page)) {
1234
+ throw new Error("waitForLiveFormVisibility requires a Playwright page with evaluate().");
1235
+ }
1236
+ return page.evaluate(async (visibilityOptions) => {
1237
+ const timeoutMs = visibilityOptions.timeoutMs ?? 5_000;
1238
+ const intervalMs = visibilityOptions.intervalMs ?? 50;
1239
+ const deadline = Date.now() + timeoutMs;
1240
+ const readApi = () => {
1241
+ const factory = Reflect.get(window, "fsApi");
1242
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1243
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1244
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1245
+ return Array.isArray(forms) ? forms[0] ?? null : null;
1246
+ };
1247
+ const snapshot = () => {
1248
+ const form = readApi();
1249
+ return visibilityOptions.targets.map((target) => {
1250
+ let id = target.fieldId ?? null;
1251
+ if (!id && form && typeof form === "object" && target.internalLabel) {
1252
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1253
+ const field = typeof getFieldByInternalLabel === "function"
1254
+ ? Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel])
1255
+ : null;
1256
+ id = field && typeof field === "object" ? Reflect.get(field, "id") ?? null : null;
1257
+ }
1258
+ const idRoot = id == null
1259
+ ? null
1260
+ : document.querySelector(`#fsCell${CSS.escape(String(id))}, #fsFieldCell${CSS.escape(String(id))}, [data-testid="field-${CSS.escape(String(id))}"]`);
1261
+ const root = target.selector != null
1262
+ ? document.querySelector(target.selector)
1263
+ : idRoot ?? (target.internalLabel != null
1264
+ ? document.querySelector(`[data-internal-label="${CSS.escape(target.internalLabel)}"]`)
1265
+ : null);
1266
+ const visible = root instanceof HTMLElement
1267
+ ? (() => {
1268
+ for (let current = root; current; current = current.parentElement) {
1269
+ const style = getComputedStyle(current);
1270
+ const rect = current.getBoundingClientRect();
1271
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
1272
+ return false;
1273
+ if (rect.width <= 0 || rect.height <= 0)
1274
+ return false;
1275
+ }
1276
+ return root.getClientRects().length > 0;
1277
+ })()
1278
+ : null;
1279
+ return {
1280
+ target,
1281
+ id,
1282
+ visible,
1283
+ matched: visible === target.visible,
1284
+ };
1285
+ });
1286
+ };
1287
+ let fields = snapshot();
1288
+ while (!fields.every((field) => field.matched) && Date.now() < deadline) {
1289
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1290
+ fields = snapshot();
1291
+ }
1292
+ const matched = fields.every((field) => field.matched);
1293
+ return {
1294
+ matched,
1295
+ timedOut: !matched,
1296
+ fields,
1297
+ };
1298
+ }, options);
1299
+ }
1300
+ export async function readLiveFormStateDrift(page, options = {}) {
1301
+ const readiness = await readLiveFormReadiness(page, options);
1302
+ const domFields = await snapshotLiveFormDomFields(page, options.snapshot ?? {});
1303
+ const domByLabel = new Map(domFields.map((field) => [field.internalLabel, field]));
1304
+ const issues = [];
1305
+ for (const field of readiness.fields) {
1306
+ const label = field.target.internalLabel ?? null;
1307
+ const domField = label ? domByLabel.get(label) : undefined;
1308
+ if (field.available && label && !domField) {
1309
+ issues.push({
1310
+ internalLabel: label,
1311
+ fieldId: field.id,
1312
+ kind: "missing-dom",
1313
+ message: "Field is available through Live Form API but no matching data-internal-label root was found in the DOM.",
1314
+ });
1315
+ }
1316
+ if (!field.available && domField) {
1317
+ issues.push({
1318
+ internalLabel: label,
1319
+ fieldId: field.id,
1320
+ kind: "missing-api",
1321
+ message: "Field root exists in the DOM but the Live Form API target was not available.",
1322
+ });
1323
+ }
1324
+ if (field.domVisible !== null && field.hidden === field.domVisible) {
1325
+ issues.push({
1326
+ internalLabel: label,
1327
+ fieldId: field.id,
1328
+ kind: "visibility",
1329
+ message: "Effective hidden state does not align with DOM visibility.",
1330
+ });
1331
+ }
1332
+ }
1333
+ return {
1334
+ readiness,
1335
+ domFields,
1336
+ issues,
1337
+ hasDrift: issues.length > 0,
1338
+ };
1339
+ }
1340
+ export async function discoverLiveFormFieldShapes(page, options = {}) {
1341
+ if (!hasEvaluate(page)) {
1342
+ throw new Error("discoverLiveFormFieldShapes requires a Playwright page with evaluate().");
1343
+ }
1344
+ return page.evaluate((discoverOptions) => {
1345
+ const selector = discoverOptions.selector ?? "[data-internal-label]";
1346
+ const includeValues = discoverOptions.includeValues ?? true;
1347
+ const factory = Reflect.get(window, "fsApi");
1348
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1349
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1350
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1351
+ const form = Array.isArray(forms) ? forms[0] ?? null : null;
1352
+ const apiAvailable = Boolean(form && typeof form === "object");
1353
+ const getApiField = (internalLabel) => {
1354
+ if (!internalLabel || !form || typeof form !== "object")
1355
+ return null;
1356
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1357
+ return typeof getFieldByInternalLabel === "function"
1358
+ ? Reflect.apply(getFieldByInternalLabel, form, [internalLabel])
1359
+ : null;
1360
+ };
1361
+ const controlSnapshot = (control) => {
1362
+ const input = control;
1363
+ return {
1364
+ tagName: control.tagName.toLowerCase(),
1365
+ type: control.getAttribute("type"),
1366
+ id: control.getAttribute("id"),
1367
+ name: control.getAttribute("name"),
1368
+ value: control.getAttribute("value"),
1369
+ currentValue: "value" in input ? input.value : null,
1370
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1371
+ };
1372
+ };
1373
+ const inferKind = (element, controls) => {
1374
+ const text = element.textContent?.toLowerCase() ?? "";
1375
+ const classes = element.className.toString().toLowerCase();
1376
+ const types = new Set(controls.map((control) => control.type ?? control.tagName));
1377
+ const internalLabel = element.getAttribute("data-internal-label")?.toLowerCase() ?? "";
1378
+ if (internalLabel.includes("code_embed") || text.startsWith(":root{") || text.includes("<script"))
1379
+ return "embed";
1380
+ if (controls.length === 0)
1381
+ return "section";
1382
+ if (classes.includes("matrix") || internalLabel.includes("matrix") || element.querySelector("[id^='matrix-row-'], table"))
1383
+ return "matrix";
1384
+ if (classes.includes("rating") || internalLabel.includes("rating"))
1385
+ return "rating";
1386
+ if (classes.includes("signature") || internalLabel.includes("signature"))
1387
+ return "signature";
1388
+ if (classes.includes("product") || internalLabel.includes("product") || types.has("product"))
1389
+ return "product";
1390
+ if (classes.includes("address") || controls.some((control) => control.id?.includes("-address")))
1391
+ return "address";
1392
+ if (classes.includes("name") || controls.some((control) => control.id?.includes("-first") || control.id?.includes("-last")))
1393
+ return "name";
1394
+ if (types.has("checkbox"))
1395
+ return "checkbox";
1396
+ if (types.has("radio"))
1397
+ return "radio";
1398
+ if (types.has("file"))
1399
+ return "file";
1400
+ if (controls.some((control) => control.tagName === "select"))
1401
+ return "select";
1402
+ if (controls.some((control) => control.tagName === "textarea"))
1403
+ return "textarea";
1404
+ if (types.has("email"))
1405
+ return "email";
1406
+ if (types.has("tel"))
1407
+ return "phone";
1408
+ if (types.has("number") || internalLabel.includes("number"))
1409
+ return "number";
1410
+ if (types.has("date") || internalLabel.includes("date"))
1411
+ return "date";
1412
+ if (types.has("text"))
1413
+ return "text";
1414
+ return "unknown";
1415
+ };
1416
+ const isActuallyVisible = (element) => {
1417
+ if (!(element instanceof HTMLElement))
1418
+ return false;
1419
+ for (let current = element; current; current = current.parentElement) {
1420
+ const style = getComputedStyle(current);
1421
+ const rect = current.getBoundingClientRect();
1422
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
1423
+ return false;
1424
+ if (rect.width <= 0 || rect.height <= 0)
1425
+ return false;
1426
+ }
1427
+ return element.getClientRects().length > 0;
1428
+ };
1429
+ const fields = Array.from(document.querySelectorAll(selector)).map((element) => {
1430
+ const internalLabel = element.getAttribute("data-internal-label");
1431
+ const apiField = getApiField(internalLabel);
1432
+ const getValue = apiField && typeof apiField === "object" ? Reflect.get(apiField, "getValue") : null;
1433
+ const getGeneralAttribute = apiField && typeof apiField === "object" ? Reflect.get(apiField, "getGeneralAttribute") : null;
1434
+ const apiValue = includeValues && typeof getValue === "function" ? Reflect.apply(getValue, apiField, []) : undefined;
1435
+ const apiHidden = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, apiField, ["hidden"]) === true : undefined;
1436
+ const controls = Array.from(element.querySelectorAll("input, textarea, select")).map(controlSnapshot);
1437
+ const domVisible = isActuallyVisible(element);
1438
+ return {
1439
+ internalLabel,
1440
+ fieldId: apiField && typeof apiField === "object" ? Reflect.get(apiField, "id") ?? null : null,
1441
+ domId: element.id,
1442
+ labelText: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 160) ?? "",
1443
+ apiAvailable: Boolean(apiField && typeof apiField === "object"),
1444
+ ...(includeValues ? { apiValue } : {}),
1445
+ ...(apiHidden === undefined ? {} : { apiHidden }),
1446
+ domVisible,
1447
+ controlCount: controls.length,
1448
+ controlTypes: [...new Set(controls.map((control) => control.type ?? control.tagName))],
1449
+ inferredKind: inferKind(element, controls),
1450
+ controls,
1451
+ };
1452
+ });
1453
+ return { apiAvailable, fields };
1454
+ }, options);
1455
+ }
1456
+ export async function submitWithLiveFormLifecycleProbe(page, options = {}) {
1457
+ const submitSelector = options.submitSelector ?? formstackSelectors.submitButton;
1458
+ return runWithLiveFormLifecycleProbe(page, options, async () => {
1459
+ try {
1460
+ await page.locator(submitSelector).first().click(options.clickTimeoutMs === undefined ? undefined : { timeout: options.clickTimeoutMs });
1461
+ return { clicked: true };
1462
+ }
1463
+ catch (error) {
1464
+ const message = error instanceof Error ? error.message : String(error);
1465
+ if (hasEvaluate(page)) {
1466
+ await page.evaluate((detail) => {
1467
+ const probe = Reflect.get(window, "__fsptLiveFormLifecycleProbe");
1468
+ if (typeof probe?.record === "function")
1469
+ probe.record("submit-error", detail);
1470
+ }, { message, phase: "click" });
1471
+ }
1472
+ if (options.throwOnClickFailure ?? false)
1473
+ throw error;
1474
+ return { clicked: false, error: message };
1475
+ }
1476
+ });
1477
+ }
1478
+ export async function probeLiveFormLifecycle(page, options = {}) {
1479
+ if (!hasEvaluate(page)) {
1480
+ throw new Error("probeLiveFormLifecycle requires a Playwright page with evaluate().");
1481
+ }
1482
+ return page.evaluate(async (probeOptions) => {
1483
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
1484
+ const eventTypes = probeOptions.eventTypes ?? ["input", "change", "blur", "focus", "click", "submit"];
1485
+ const events = [];
1486
+ const fieldTargets = probeOptions.fieldTargets ?? [];
1487
+ const waitForApiTimeoutMs = probeOptions.waitForApiTimeoutMs ?? 10_000;
1488
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1489
+ const record = (kind, detail) => {
1490
+ const at = now();
1491
+ const event = {
1492
+ kind,
1493
+ at,
1494
+ elapsedMs: Math.round(at - startedAt),
1495
+ detail,
1496
+ };
1497
+ events.push(event);
1498
+ return event;
1499
+ };
1500
+ const safeText = (value) => {
1501
+ const text = value?.replace(/\s+/g, " ").trim() ?? "";
1502
+ return text ? text.slice(0, 180) : null;
1503
+ };
1504
+ const selectorForElement = (target) => {
1505
+ if (!(target instanceof Element))
1506
+ return null;
1507
+ const id = target.getAttribute("id");
1508
+ if (id)
1509
+ return `#${CSS.escape(id)}`;
1510
+ const name = target.getAttribute("name");
1511
+ if (name)
1512
+ return `[name="${CSS.escape(name)}"]`;
1513
+ const internalLabel = target.closest("[data-internal-label]")?.getAttribute("data-internal-label");
1514
+ if (internalLabel)
1515
+ return `[data-internal-label="${CSS.escape(internalLabel)}"]`;
1516
+ return target.tagName.toLowerCase();
1517
+ };
1518
+ const serializeControl = (control) => {
1519
+ const input = control;
1520
+ return {
1521
+ tagName: control.tagName.toLowerCase(),
1522
+ type: control instanceof HTMLInputElement ? input.type : null,
1523
+ id: control.getAttribute("id"),
1524
+ name: control.getAttribute("name"),
1525
+ value: "value" in input ? input.value : null,
1526
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1527
+ disabled: "disabled" in input ? input.disabled : null,
1528
+ required: "required" in input ? input.required : null,
1529
+ ariaInvalid: control.getAttribute("aria-invalid"),
1530
+ };
1531
+ };
1532
+ const listener = (event) => {
1533
+ const target = event.target;
1534
+ const element = target instanceof Element ? target : null;
1535
+ const fieldRoot = element?.closest("[data-internal-label], [id^='fsCell'], [data-testid^='field-']");
1536
+ const fsCell = element?.closest("[id^='fsCell']");
1537
+ const idDerivedField = element?.getAttribute("id")?.match(/^field(\d+)/)?.[1] ?? null;
1538
+ record("dom-event", {
1539
+ type: event.type,
1540
+ selector: selectorForElement(target),
1541
+ fieldId: fieldRoot?.getAttribute("data-fs-field-id") ??
1542
+ fsCell?.id?.replace(/^fsCell/, "") ??
1543
+ idDerivedField ??
1544
+ fieldRoot?.id?.replace(/^fsCell/, "") ??
1545
+ null,
1546
+ internalLabel: fieldRoot?.getAttribute("data-internal-label") ?? null,
1547
+ control: element ? serializeControl(element) : null,
1548
+ });
1549
+ };
1550
+ for (const type of eventTypes) {
1551
+ document.addEventListener(type, listener, true);
1552
+ }
1553
+ record("probe-installed", {
1554
+ eventTypes,
1555
+ fieldTargetCount: fieldTargets.length,
1556
+ });
1557
+ const readApi = () => {
1558
+ const factory = Reflect.get(window, "fsApi");
1559
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1560
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1561
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1562
+ return { api, form: Array.isArray(forms) ? forms[0] ?? null : null };
1563
+ };
1564
+ const deadline = Date.now() + waitForApiTimeoutMs;
1565
+ let apiAvailable = false;
1566
+ let form = null;
1567
+ while (Date.now() < deadline) {
1568
+ const resolved = readApi();
1569
+ form = resolved.form;
1570
+ if (form && typeof form === "object") {
1571
+ apiAvailable = true;
1572
+ break;
1573
+ }
1574
+ await new Promise((resolve) => setTimeout(resolve, 50));
1575
+ }
1576
+ if (apiAvailable) {
1577
+ const getFields = Reflect.get(form, "getFields");
1578
+ const rawFields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
1579
+ record("fs-api-ready", {
1580
+ fieldCount: Array.isArray(rawFields) ? rawFields.length : null,
1581
+ methods: Object.keys(form).filter((key) => typeof Reflect.get(form, key) === "function").sort(),
1582
+ });
1583
+ }
1584
+ else {
1585
+ record("fs-api-timeout", { timeoutMs: waitForApiTimeoutMs });
1586
+ }
1587
+ const getFieldTarget = (target) => {
1588
+ if (!apiAvailable || !form || typeof form !== "object")
1589
+ return null;
1590
+ const getField = Reflect.get(form, "getField");
1591
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1592
+ if (target.fieldId != null && typeof getField === "function")
1593
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
1594
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
1595
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
1596
+ }
1597
+ return null;
1598
+ };
1599
+ const snapshotField = (field, target) => {
1600
+ if (!field || typeof field !== "object") {
1601
+ record("field-snapshot", { target, available: false });
1602
+ return;
1603
+ }
1604
+ const getId = Reflect.get(field, "getId");
1605
+ const getValue = Reflect.get(field, "getValue");
1606
+ const getFieldInfo = Reflect.get(field, "getFieldInfo");
1607
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
1608
+ const setValue = Reflect.get(field, "setValue");
1609
+ let value = null;
1610
+ let info = null;
1611
+ let required = null;
1612
+ let hidden = null;
1613
+ let skipValidation = null;
1614
+ try {
1615
+ value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
1616
+ }
1617
+ catch (error) {
1618
+ value = { error: error instanceof Error ? error.message : String(error) };
1619
+ }
1620
+ try {
1621
+ info = typeof getFieldInfo === "function" ? Reflect.apply(getFieldInfo, field, []) : null;
1622
+ }
1623
+ catch {
1624
+ info = null;
1625
+ }
1626
+ try {
1627
+ required = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["required"]) : null;
1628
+ hidden = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["hidden"]) : null;
1629
+ skipValidation =
1630
+ typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["shouldForceSkipValidation"]) : null;
1631
+ }
1632
+ catch {
1633
+ required = null;
1634
+ }
1635
+ record("field-snapshot", {
1636
+ target,
1637
+ available: true,
1638
+ id: typeof getId === "function" ? Reflect.apply(getId, field, []) : null,
1639
+ hasSetValue: typeof setValue === "function",
1640
+ value,
1641
+ info,
1642
+ required,
1643
+ hidden,
1644
+ shouldForceSkipValidation: skipValidation,
1645
+ });
1646
+ };
1647
+ if (apiAvailable && form && typeof form === "object") {
1648
+ if (probeOptions.includeAllFields) {
1649
+ const getFields = Reflect.get(form, "getFields");
1650
+ const fields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
1651
+ if (Array.isArray(fields)) {
1652
+ for (const field of fields)
1653
+ snapshotField(field, { label: "all-fields" });
1654
+ }
1655
+ }
1656
+ for (const target of fieldTargets) {
1657
+ snapshotField(getFieldTarget(target), target);
1658
+ }
1659
+ }
1660
+ if (probeOptions.validate && apiAvailable && form && typeof form === "object") {
1661
+ const validateForm = Reflect.get(form, "validateForm");
1662
+ try {
1663
+ const result = typeof validateForm === "function" ? await Promise.resolve(Reflect.apply(validateForm, form, [])) : null;
1664
+ record("validation-result", { result });
1665
+ }
1666
+ catch (error) {
1667
+ record("validation-error", { message: error instanceof Error ? error.message : String(error) });
1668
+ }
1669
+ }
1670
+ if (probeOptions.submit && apiAvailable && form && typeof form === "object") {
1671
+ const submitForm = Reflect.get(form, "submitForm");
1672
+ try {
1673
+ const result = typeof submitForm === "function" ? await Promise.resolve(Reflect.apply(submitForm, form, [])) : null;
1674
+ record("submit-invoked", { result });
1675
+ }
1676
+ catch (error) {
1677
+ record("submit-error", { message: error instanceof Error ? error.message : String(error) });
1678
+ }
1679
+ }
1680
+ await new Promise((resolve) => setTimeout(resolve, 0));
1681
+ for (const type of eventTypes) {
1682
+ document.removeEventListener(type, listener, true);
1683
+ }
1684
+ const validation = events.find((event) => event.kind === "validation-result" || event.kind === "validation-error");
1685
+ const submit = events.find((event) => event.kind === "submit-invoked" || event.kind === "submit-error");
1686
+ return {
1687
+ apiAvailable,
1688
+ events,
1689
+ fieldSnapshots: events.filter((event) => event.kind === "field-snapshot"),
1690
+ ...(validation ? { validation } : {}),
1691
+ ...(submit ? { submit } : {}),
1692
+ };
1693
+ }, options);
1694
+ }
1695
+ export async function runWithLiveFormLifecycleProbe(page, options, run) {
1696
+ if (!hasEvaluate(page)) {
1697
+ throw new Error("runWithLiveFormLifecycleProbe requires a Playwright page with evaluate().");
1698
+ }
1699
+ await page.evaluate(async (probeOptions) => {
1700
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
1701
+ const eventTypes = probeOptions.eventTypes ?? ["input", "change", "blur", "focus", "click", "submit"];
1702
+ const formEventTypes = probeOptions.formEventTypes ?? [];
1703
+ const waitForApiTimeoutMs = probeOptions.waitForApiTimeoutMs ?? 10_000;
1704
+ const events = [];
1705
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1706
+ const record = (kind, detail) => {
1707
+ const at = now();
1708
+ const event = { kind, at, elapsedMs: Math.round(at - startedAt), detail };
1709
+ events.push(event);
1710
+ return event;
1711
+ };
1712
+ const controlValue = (element) => {
1713
+ if (!element)
1714
+ return null;
1715
+ const input = element;
1716
+ return {
1717
+ tagName: element.tagName.toLowerCase(),
1718
+ type: element instanceof HTMLInputElement ? input.type : null,
1719
+ id: element.getAttribute("id"),
1720
+ name: element.getAttribute("name"),
1721
+ value: "value" in input ? input.value : null,
1722
+ checked: element instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? element.checked : null,
1723
+ ariaInvalid: element.getAttribute("aria-invalid"),
1724
+ };
1725
+ };
1726
+ const listener = (event) => {
1727
+ const element = event.target instanceof Element ? event.target : null;
1728
+ const fieldRoot = element?.closest("[data-internal-label], [id^='fsCell'], [data-testid^='field-']");
1729
+ const fsCell = element?.closest("[id^='fsCell']");
1730
+ const idDerivedField = element?.getAttribute("id")?.match(/^field(\d+)/)?.[1] ?? null;
1731
+ record("dom-event", {
1732
+ type: event.type,
1733
+ fieldId: fieldRoot?.getAttribute("data-fs-field-id") ??
1734
+ fsCell?.id?.replace(/^fsCell/, "") ??
1735
+ idDerivedField ??
1736
+ fieldRoot?.id?.replace(/^fsCell/, "") ??
1737
+ null,
1738
+ internalLabel: fieldRoot?.getAttribute("data-internal-label") ?? null,
1739
+ control: controlValue(element),
1740
+ });
1741
+ };
1742
+ for (const type of eventTypes)
1743
+ document.addEventListener(type, listener, true);
1744
+ record("probe-installed", { eventTypes, formEventTypes });
1745
+ const readApi = () => {
1746
+ const factory = Reflect.get(window, "fsApi");
1747
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1748
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1749
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1750
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
1751
+ };
1752
+ const deadline = Date.now() + waitForApiTimeoutMs;
1753
+ let form = null;
1754
+ while (formEventTypes.length > 0 && Date.now() < deadline) {
1755
+ form = readApi().form;
1756
+ if (form && typeof form === "object")
1757
+ break;
1758
+ await new Promise((resolve) => setTimeout(resolve, 50));
1759
+ }
1760
+ const formEventListeners = formEventTypes.length > 0 && form && typeof form === "object"
1761
+ ? formEventTypes.map((type) => ({
1762
+ type,
1763
+ onFormEvent: (event) => {
1764
+ const eventObject = event && typeof event === "object" ? event : {};
1765
+ record("form-event", {
1766
+ type,
1767
+ formId: Reflect.get(eventObject, "formId") ?? null,
1768
+ data: Reflect.get(eventObject, "data") ?? null,
1769
+ hasPreventDefault: typeof Reflect.get(eventObject, "preventDefault") === "function",
1770
+ });
1771
+ },
1772
+ }))
1773
+ : [];
1774
+ if (form && typeof form === "object" && formEventListeners.length > 0) {
1775
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
1776
+ if (typeof registerFormEventListener === "function") {
1777
+ for (const formEventListener of formEventListeners) {
1778
+ Reflect.apply(registerFormEventListener, form, [formEventListener]);
1779
+ }
1780
+ }
1781
+ }
1782
+ Object.defineProperty(window, "__fsptLiveFormLifecycleProbe", {
1783
+ configurable: true,
1784
+ value: {
1785
+ events,
1786
+ eventTypes,
1787
+ formEventListeners,
1788
+ listener,
1789
+ record,
1790
+ },
1791
+ });
1792
+ }, options);
1793
+ const result = await run();
1794
+ const report = await page.evaluate(async (probeOptions) => {
1795
+ const postRunSettleMs = probeOptions.postRunSettleMs ?? 100;
1796
+ if (postRunSettleMs > 0)
1797
+ await new Promise((resolve) => setTimeout(resolve, postRunSettleMs));
1798
+ const probe = Reflect.get(window, "__fsptLiveFormLifecycleProbe");
1799
+ if (!probe) {
1800
+ return {
1801
+ apiAvailable: false,
1802
+ events: [],
1803
+ fieldSnapshots: [],
1804
+ };
1805
+ }
1806
+ if (probeOptions.postRunQuietMs !== undefined) {
1807
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1808
+ const quietMs = probeOptions.postRunQuietMs;
1809
+ const quietTimeoutMs = probeOptions.postRunQuietTimeoutMs ?? 2_500;
1810
+ const deadline = Date.now() + quietTimeoutMs;
1811
+ let timedOut = false;
1812
+ while (Date.now() < deadline) {
1813
+ const lastEvent = probe.events.at(-1);
1814
+ const elapsedSinceLastEvent = lastEvent ? now() - lastEvent.at : quietMs;
1815
+ if (elapsedSinceLastEvent >= quietMs)
1816
+ break;
1817
+ await new Promise((resolve) => setTimeout(resolve, Math.max(10, Math.min(50, quietMs - elapsedSinceLastEvent))));
1818
+ }
1819
+ const lastEvent = probe.events.at(-1);
1820
+ if (lastEvent && now() - lastEvent.at < quietMs)
1821
+ timedOut = true;
1822
+ probe.record("quiet-wait", {
1823
+ quietMs,
1824
+ timeoutMs: quietTimeoutMs,
1825
+ timedOut,
1826
+ lastEventKind: lastEvent?.kind ?? null,
1827
+ lastEventElapsedMs: lastEvent?.elapsedMs ?? null,
1828
+ });
1829
+ }
1830
+ const factory = Reflect.get(window, "fsApi");
1831
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1832
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1833
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1834
+ const form = Array.isArray(forms) ? forms[0] ?? null : null;
1835
+ const apiAvailable = Boolean(form && typeof form === "object");
1836
+ if (apiAvailable && form && typeof form === "object") {
1837
+ const getFields = Reflect.get(form, "getFields");
1838
+ const fields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
1839
+ probe.record("fs-api-ready", {
1840
+ fieldCount: Array.isArray(fields) ? fields.length : null,
1841
+ });
1842
+ const targets = probeOptions.includeAllFields && Array.isArray(fields) ? fields : [];
1843
+ for (const field of targets) {
1844
+ const getId = Reflect.get(field, "getId");
1845
+ const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
1846
+ const getValue = Reflect.get(field, "getValue");
1847
+ const getFieldInfo = Reflect.get(field, "getFieldInfo");
1848
+ const keys = Object.keys(field).sort();
1849
+ const methodNames = keys.filter((key) => typeof Reflect.get(field, key) === "function");
1850
+ const idCandidates = [];
1851
+ for (const candidate of ["id", "fieldId", "_id"])
1852
+ idCandidates.push(Reflect.get(field, candidate));
1853
+ try {
1854
+ if (typeof getId === "function")
1855
+ idCandidates.push(Reflect.apply(getId, field, []));
1856
+ }
1857
+ catch {
1858
+ idCandidates.push("getId:error");
1859
+ }
1860
+ try {
1861
+ if (typeof getGeneralAttribute === "function")
1862
+ idCandidates.push(Reflect.apply(getGeneralAttribute, field, ["id"]));
1863
+ }
1864
+ catch {
1865
+ idCandidates.push("getGeneralAttribute:id:error");
1866
+ }
1867
+ let value = null;
1868
+ let info = null;
1869
+ try {
1870
+ value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
1871
+ }
1872
+ catch (error) {
1873
+ value = { error: error instanceof Error ? error.message : String(error) };
1874
+ }
1875
+ try {
1876
+ info = typeof getFieldInfo === "function" ? Reflect.apply(getFieldInfo, field, []) : null;
1877
+ }
1878
+ catch {
1879
+ info = null;
1880
+ }
1881
+ probe.record("field-snapshot", {
1882
+ id: idCandidates.find((candidate) => candidate !== undefined && candidate !== null && candidate !== "") ?? null,
1883
+ idCandidates,
1884
+ keys,
1885
+ methodNames,
1886
+ value,
1887
+ info,
1888
+ });
1889
+ }
1890
+ if (probeOptions.validate) {
1891
+ const validateForm = Reflect.get(form, "validateForm");
1892
+ try {
1893
+ const validation = typeof validateForm === "function" ? await Promise.resolve(Reflect.apply(validateForm, form, [])) : null;
1894
+ const inlineErrors = Array.from(document.querySelectorAll('[id^="inline-error-field"], .fsError, [role="alert"]')).map((element) => ({
1895
+ id: element.getAttribute("id"),
1896
+ text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 240) ?? "",
1897
+ }));
1898
+ probe.record("validation-result", { result: validation, inlineErrors });
1899
+ }
1900
+ catch (error) {
1901
+ probe.record("validation-error", { message: error instanceof Error ? error.message : String(error) });
1902
+ }
1903
+ }
1904
+ }
1905
+ if (apiAvailable && form && typeof form === "object" && Array.isArray(probe.formEventListeners)) {
1906
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
1907
+ if (typeof unregisterFormEventListener === "function") {
1908
+ for (const formEventListener of probe.formEventListeners) {
1909
+ Reflect.apply(unregisterFormEventListener, form, [formEventListener]);
1910
+ }
1911
+ }
1912
+ }
1913
+ for (const type of probe.eventTypes)
1914
+ document.removeEventListener(type, probe.listener, true);
1915
+ Reflect.deleteProperty(window, "__fsptLiveFormLifecycleProbe");
1916
+ const validation = probe.events.find((event) => event.kind === "validation-result" || event.kind === "validation-error");
1917
+ const submit = probe.events.find((event) => event.kind === "submit-invoked" || event.kind === "submit-error");
1918
+ return {
1919
+ apiAvailable,
1920
+ events: probe.events,
1921
+ fieldSnapshots: probe.events.filter((event) => event.kind === "field-snapshot"),
1922
+ ...(validation ? { validation } : {}),
1923
+ ...(submit ? { submit } : {}),
1924
+ };
1925
+ }, options);
1926
+ return { result, report };
1927
+ }
1928
+ export async function waitForLiveFormEvent(page, options) {
1929
+ if (!hasEvaluate(page)) {
1930
+ throw new Error("waitForLiveFormEvent requires a Playwright page with evaluate().");
1931
+ }
1932
+ return page.evaluate(async (waitOptions) => {
1933
+ const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
1934
+ const timeoutMs = waitOptions.timeoutMs ?? 5_000;
1935
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
1936
+ const readApi = () => {
1937
+ const factory = Reflect.get(window, "fsApi");
1938
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1939
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1940
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1941
+ return { form: Array.isArray(forms) ? forms[0] ?? null : null };
1942
+ };
1943
+ const deadline = Date.now() + timeoutMs;
1944
+ let form = null;
1945
+ while (Date.now() < deadline) {
1946
+ form = readApi().form;
1947
+ if (form && typeof form === "object")
1948
+ break;
1949
+ await new Promise((resolve) => setTimeout(resolve, 50));
1950
+ }
1951
+ const elapsedMs = () => Math.round(now() - startedAt);
1952
+ if (!form || typeof form !== "object") {
1953
+ return {
1954
+ matched: false,
1955
+ timedOut: true,
1956
+ elapsedMs: elapsedMs(),
1957
+ };
1958
+ }
1959
+ const findField = (target) => {
1960
+ if (!target)
1961
+ return null;
1962
+ const byInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1963
+ if (target.internalLabel && typeof byInternalLabel === "function") {
1964
+ const field = Reflect.apply(byInternalLabel, form, [target.internalLabel]);
1965
+ if (field)
1966
+ return field;
1967
+ }
1968
+ const getFields = Reflect.get(form, "getFields");
1969
+ const fields = typeof getFields === "function" ? Reflect.apply(getFields, form, []) : [];
1970
+ if (!Array.isArray(fields))
1971
+ return null;
1972
+ const targetFieldId = target.fieldId == null ? null : String(target.fieldId);
1973
+ return (fields.find((field) => {
1974
+ if (!field || typeof field !== "object")
1975
+ return false;
1976
+ const fieldId = Reflect.get(field, "id");
1977
+ return targetFieldId !== null && String(fieldId) === targetFieldId;
1978
+ }) ?? null);
1979
+ };
1980
+ const targetField = findField(waitOptions.target);
1981
+ const targetFieldId = targetField && typeof targetField === "object"
1982
+ ? Reflect.get(targetField, "id") ?? null
1983
+ : waitOptions.target?.fieldId ?? null;
1984
+ const targetFieldIdString = targetFieldId == null ? null : String(targetFieldId);
1985
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
1986
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
1987
+ if (typeof registerFormEventListener !== "function") {
1988
+ return {
1989
+ matched: false,
1990
+ timedOut: true,
1991
+ elapsedMs: elapsedMs(),
1992
+ targetFieldId,
1993
+ };
1994
+ }
1995
+ let matchedEvent;
1996
+ let resolveWait;
1997
+ const wait = new Promise((resolve) => {
1998
+ resolveWait = resolve;
1999
+ });
2000
+ const timeout = setTimeout(() => resolveWait?.(), Math.max(0, deadline - Date.now()));
2001
+ const listeners = waitOptions.eventTypes.map((type) => ({
2002
+ type,
2003
+ onFormEvent: (event) => {
2004
+ const eventObject = event && typeof event === "object" ? event : {};
2005
+ const data = Reflect.get(eventObject, "data");
2006
+ const dataFieldId = data && typeof data === "object" ? Reflect.get(data, "fieldId") : null;
2007
+ const dataType = data && typeof data === "object" ? Reflect.get(data, "type") : null;
2008
+ const fieldMatches = targetFieldIdString === null || String(dataFieldId ?? "") === targetFieldIdString;
2009
+ const dataTypeMatches = !waitOptions.dataType || dataType === waitOptions.dataType;
2010
+ if (!fieldMatches || !dataTypeMatches || matchedEvent)
2011
+ return;
2012
+ matchedEvent = {
2013
+ type,
2014
+ formId: Reflect.get(eventObject, "formId") ?? null,
2015
+ data,
2016
+ hasPreventDefault: typeof Reflect.get(eventObject, "preventDefault") === "function",
2017
+ };
2018
+ resolveWait?.();
2019
+ },
2020
+ }));
2021
+ for (const listener of listeners) {
2022
+ Reflect.apply(registerFormEventListener, form, [listener]);
2023
+ }
2024
+ await wait;
2025
+ clearTimeout(timeout);
2026
+ if (typeof unregisterFormEventListener === "function") {
2027
+ for (const listener of listeners) {
2028
+ Reflect.apply(unregisterFormEventListener, form, [listener]);
2029
+ }
2030
+ }
2031
+ return {
2032
+ matched: Boolean(matchedEvent),
2033
+ timedOut: !matchedEvent,
2034
+ elapsedMs: elapsedMs(),
2035
+ targetFieldId,
2036
+ ...(matchedEvent ? { event: matchedEvent } : {}),
2037
+ };
2038
+ }, options);
2039
+ }
2040
+ export async function setLiveFormFieldValueTransaction(page, options) {
2041
+ if (!hasEvaluate(page)) {
2042
+ throw new Error("setLiveFormFieldValueTransaction requires a Playwright page with evaluate().");
2043
+ }
2044
+ const shouldWaitForEvent = options.waitForEvent === true || (options.waitForEvent === undefined && options.write.notify === true);
2045
+ const defaultEventOptions = {
2046
+ eventTypes: ["change"],
2047
+ target: options.write.target,
2048
+ dataType: "api-setValue",
2049
+ ...(options.write.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
2050
+ };
2051
+ const eventOptions = typeof options.waitForEvent === "object"
2052
+ ? options.waitForEvent
2053
+ : shouldWaitForEvent
2054
+ ? defaultEventOptions
2055
+ : null;
2056
+ const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2057
+ const write = await setLiveFormFieldValue(page, options.write);
2058
+ const event = eventPromise ? await eventPromise : undefined;
2059
+ const domDispatch = options.dispatchDomEvents === undefined || options.dispatchDomEvents === false
2060
+ ? undefined
2061
+ : await dispatchLiveFormFieldDomEvents(page, {
2062
+ ...(typeof options.dispatchDomEvents === "object" ? options.dispatchDomEvents : {}),
2063
+ target: options.write.target,
2064
+ });
2065
+ const readiness = options.readiness ? await readLiveFormReadiness(page, options.readiness) : undefined;
2066
+ const domFields = options.snapshot
2067
+ ? await snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {})
2068
+ : undefined;
2069
+ return {
2070
+ write,
2071
+ ...(event ? { event } : {}),
2072
+ ...(domDispatch ? { domDispatch } : {}),
2073
+ ...(readiness ? { readiness } : {}),
2074
+ ...(domFields ? { domFields } : {}),
2075
+ };
2076
+ }
2077
+ export async function setLiveFormFieldValuesTransaction(page, options) {
2078
+ if (!hasEvaluate(page)) {
2079
+ throw new Error("setLiveFormFieldValuesTransaction requires a Playwright page with evaluate().");
2080
+ }
2081
+ const writes = [];
2082
+ const events = [];
2083
+ const domDispatches = [];
2084
+ const waitForEvents = options.waitForEvents ?? true;
2085
+ for (const writeOptions of options.writes) {
2086
+ const transaction = await setLiveFormFieldValueTransaction(page, {
2087
+ write: writeOptions,
2088
+ waitForEvent: waitForEvents && writeOptions.notify === true,
2089
+ ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
2090
+ });
2091
+ writes.push(transaction.write);
2092
+ if (transaction.event)
2093
+ events.push(transaction.event);
2094
+ if (transaction.domDispatch)
2095
+ domDispatches.push(transaction.domDispatch);
2096
+ }
2097
+ const readiness = options.readiness ? await readLiveFormReadiness(page, options.readiness) : undefined;
2098
+ const domFields = options.snapshot
2099
+ ? await snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {})
2100
+ : undefined;
2101
+ return {
2102
+ writes,
2103
+ events,
2104
+ domDispatches,
2105
+ ...(readiness ? { readiness } : {}),
2106
+ ...(domFields ? { domFields } : {}),
2107
+ };
2108
+ }
106
2109
  async function setControlValue(locator, value) {
107
2110
  try {
108
2111
  await locator.fill(value);
@@ -167,12 +2170,12 @@ export function createFormTestHarness(page, options = {}) {
167
2170
  selectOption: (internalLabel, value) => page.locator(formstackSelectors.fieldInputByInternalLabel(internalLabel)).first().selectOption(value),
168
2171
  chooseRadio: (internalLabel, value) => page.locator(formstackSelectors.optionLabelByValue(internalLabel, "radio", value)).first().click(),
169
2172
  setCheckbox: (internalLabel, value, checked) => {
170
- const locator = page.locator(formstackSelectors.optionLabelByValue(internalLabel, "checkbox", value)).first();
171
- return checked ? locator.click() : locator.click();
2173
+ const locator = page.locator(formstackSelectors.optionInput(internalLabel, "checkbox", value)).first();
2174
+ return checked ? locator.check() : locator.uncheck();
172
2175
  },
173
2176
  setCheckboxes: async (internalLabel, values) => {
174
2177
  for (const value of values) {
175
- await page.locator(formstackSelectors.optionLabelByValue(internalLabel, "checkbox", value)).first().click();
2178
+ await page.locator(formstackSelectors.optionInput(internalLabel, "checkbox", value)).first().check();
176
2179
  }
177
2180
  },
178
2181
  selectMatrixCell: (fieldId, row, column) => page.locator(formstackSelectors.matrixInputByPosition(fieldId, row, column)).first().check(),
@@ -194,10 +2197,38 @@ function stepName(step, index) {
194
2197
  function stepRunner(step) {
195
2198
  return typeof step === "function" ? step : step.run;
196
2199
  }
2200
+ function errorReport(error) {
2201
+ if (error instanceof Error) {
2202
+ return {
2203
+ name: error.name,
2204
+ message: error.message,
2205
+ ...(error.stack ? { stack: error.stack } : {}),
2206
+ };
2207
+ }
2208
+ return {
2209
+ name: "Error",
2210
+ message: String(error),
2211
+ };
2212
+ }
197
2213
  export async function runFormScenario(page, scenario, options = {}) {
198
2214
  const monitor = createFormBrowserMonitor(page, scenario.diagnostics ?? options.diagnostics);
199
2215
  const harness = createFormTestHarness(page, options);
200
2216
  const steps = [];
2217
+ const started = Date.now();
2218
+ const startedAt = new Date(started).toISOString();
2219
+ const buildResult = (status) => {
2220
+ const ended = Date.now();
2221
+ return {
2222
+ name: scenario.name,
2223
+ status,
2224
+ startedAt,
2225
+ endedAt: new Date(ended).toISOString(),
2226
+ durationMs: ended - started,
2227
+ steps,
2228
+ diagnostics: monitor.report(),
2229
+ ...(options.artifacts === undefined ? {} : { artifacts: options.artifacts }),
2230
+ };
2231
+ };
201
2232
  try {
202
2233
  if (scenario.url) {
203
2234
  await page.goto(scenario.url);
@@ -208,18 +2239,42 @@ export async function runFormScenario(page, scenario, options = {}) {
208
2239
  if (!step) {
209
2240
  continue;
210
2241
  }
211
- await stepRunner(step)(harness);
212
- steps.push({ name: stepName(step, index) });
2242
+ const name = stepName(step, index);
2243
+ const stepStarted = Date.now();
2244
+ try {
2245
+ await stepRunner(step)(harness);
2246
+ const stepEnded = Date.now();
2247
+ steps.push({
2248
+ name,
2249
+ status: "passed",
2250
+ startedAt: new Date(stepStarted).toISOString(),
2251
+ endedAt: new Date(stepEnded).toISOString(),
2252
+ durationMs: stepEnded - stepStarted,
2253
+ });
2254
+ }
2255
+ catch (error) {
2256
+ const stepEnded = Date.now();
2257
+ steps.push({
2258
+ name,
2259
+ status: "failed",
2260
+ startedAt: new Date(stepStarted).toISOString(),
2261
+ endedAt: new Date(stepEnded).toISOString(),
2262
+ durationMs: stepEnded - stepStarted,
2263
+ error: errorReport(error),
2264
+ });
2265
+ throw new FormScenarioRunError(buildResult("failed"));
2266
+ }
213
2267
  }
214
2268
  const shouldThrow = options.throwOnDiagnostics ?? true;
215
2269
  if (shouldThrow) {
216
- monitor.assertNoErrors();
2270
+ try {
2271
+ monitor.assertNoErrors();
2272
+ }
2273
+ catch {
2274
+ throw new FormScenarioRunError(buildResult("failed"));
2275
+ }
217
2276
  }
218
- return {
219
- name: scenario.name,
220
- steps,
221
- diagnostics: monitor.report(),
222
- };
2277
+ return buildResult("passed");
223
2278
  }
224
2279
  finally {
225
2280
  monitor.detach();
@@ -228,4 +2283,183 @@ export async function runFormScenario(page, scenario, options = {}) {
228
2283
  export function defineFormScenario(scenario) {
229
2284
  return scenario;
230
2285
  }
2286
+ export const liveFormScenarioSteps = {
2287
+ writeField: (name, options) => ({
2288
+ name,
2289
+ run: (harness) => setLiveFormFieldValueTransaction(harness.page, options).then(() => undefined),
2290
+ }),
2291
+ writeFields: (name, options) => ({
2292
+ name,
2293
+ run: (harness) => setLiveFormFieldValuesTransaction(harness.page, options).then(() => undefined),
2294
+ }),
2295
+ expectReady: (name, options) => ({
2296
+ name,
2297
+ run: async (harness) => {
2298
+ const readiness = await waitForLiveFormReadiness(harness.page, options);
2299
+ if (!readiness.isReady) {
2300
+ throw new Error(`Expected live form to be ready, but ${readiness.blockingFieldCount} field(s) are blocking.`);
2301
+ }
2302
+ },
2303
+ }),
2304
+ waitForQuiet: (name, options = {}) => ({
2305
+ name,
2306
+ run: async (harness) => {
2307
+ const quiet = await waitForLiveFormQuiet(harness.page, options);
2308
+ if (!quiet.quiet) {
2309
+ throw new Error("Expected live form to become quiet before timeout.");
2310
+ }
2311
+ },
2312
+ }),
2313
+ expectFieldValue: (name, options) => ({
2314
+ name,
2315
+ run: async (harness) => {
2316
+ const value = await waitForLiveFormFieldValue(harness.page, options);
2317
+ const matched = options.expectedValue === undefined ? value.fieldAvailable && !value.isEmpty : value.valueMatched === true;
2318
+ if (!matched) {
2319
+ throw new Error("Expected live form field value condition was not met.");
2320
+ }
2321
+ },
2322
+ }),
2323
+ expectDomFieldState: (name, options) => ({
2324
+ name,
2325
+ run: async (harness) => {
2326
+ const state = await waitForLiveFormDomFieldState(harness.page, options);
2327
+ if (!state.matched) {
2328
+ throw new Error("Expected live form DOM field state condition was not met.");
2329
+ }
2330
+ },
2331
+ }),
2332
+ expectValidationErrors: (name, options = {}) => ({
2333
+ name,
2334
+ run: async (harness) => {
2335
+ const validation = await waitForLiveFormValidationState(harness.page, options);
2336
+ const expectedCountMatched = options.expectedErrorCount === undefined || validation.inlineErrors.length === options.expectedErrorCount;
2337
+ const errorText = options.errorText;
2338
+ const textMatched = errorText === undefined ||
2339
+ validation.inlineErrors.some((error) => typeof errorText === "string" ? error.text.includes(errorText) : errorText.test(error.text));
2340
+ if (validation.inlineErrors.length === 0 || !expectedCountMatched || !textMatched) {
2341
+ throw new Error("Expected live form validation errors were not present.");
2342
+ }
2343
+ },
2344
+ }),
2345
+ expectNoValidationErrors: (name, options = {}) => ({
2346
+ name,
2347
+ run: async (harness) => {
2348
+ const validation = await waitForLiveFormValidationState(harness.page, { ...options, expectNoErrors: true });
2349
+ if (validation.inlineErrors.length > 0) {
2350
+ throw new Error(`Expected no live form validation errors, but found ${validation.inlineErrors.length}.`);
2351
+ }
2352
+ },
2353
+ }),
2354
+ expectConsoleMessage: (name, options = {}) => ({
2355
+ name,
2356
+ run: async (harness) => {
2357
+ const consoleMessage = await waitForConsoleMessage(harness.page, options);
2358
+ if (!consoleMessage.matched) {
2359
+ throw new Error("Expected console message was not observed before timeout.");
2360
+ }
2361
+ },
2362
+ }),
2363
+ expectWindowFlag: (name, options) => ({
2364
+ name,
2365
+ run: async (harness) => {
2366
+ const flag = await waitForWindowFlag(harness.page, options);
2367
+ if (!flag.matched) {
2368
+ throw new Error(`Expected window flag "${options.path}" condition was not met before timeout.`);
2369
+ }
2370
+ },
2371
+ }),
2372
+ expectVisibility: (name, options) => ({
2373
+ name,
2374
+ run: async (harness) => {
2375
+ const visibility = await waitForLiveFormVisibility(harness.page, options);
2376
+ if (!visibility.matched) {
2377
+ throw new Error("Expected live form visibility conditions were not met.");
2378
+ }
2379
+ },
2380
+ }),
2381
+ expectNoStateDrift: (name, options) => ({
2382
+ name,
2383
+ run: async (harness) => {
2384
+ const drift = await readLiveFormStateDrift(harness.page, options);
2385
+ if (drift.hasDrift) {
2386
+ throw new Error(`Expected no live form state drift, but found ${drift.issues.length} issue(s).`);
2387
+ }
2388
+ },
2389
+ }),
2390
+ submitAndProbe: (name, options = {}) => ({
2391
+ name,
2392
+ run: (harness) => submitWithLiveFormLifecycleProbe(harness.page, options).then(() => undefined),
2393
+ }),
2394
+ };
2395
+ async function maybeWaitForQuiet(page, options) {
2396
+ if (!options.waitForQuiet)
2397
+ return;
2398
+ const quiet = await waitForLiveFormQuiet(page, typeof options.waitForQuiet === "object" ? options.waitForQuiet : {});
2399
+ if (!quiet.quiet) {
2400
+ throw new Error("Expected live form to become quiet before timeout.");
2401
+ }
2402
+ }
2403
+ function apiRecipeStep(name, target, value, options = {}) {
2404
+ return {
2405
+ name,
2406
+ run: async (harness) => {
2407
+ await setLiveFormFieldValueTransaction(harness.page, {
2408
+ write: {
2409
+ target,
2410
+ value,
2411
+ notify: options.notify ?? true,
2412
+ },
2413
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
2414
+ ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
2415
+ });
2416
+ await maybeWaitForQuiet(harness.page, options);
2417
+ },
2418
+ };
2419
+ }
2420
+ export const liveFormFieldRecipes = {
2421
+ fillTextDom: (name, internalLabel, value) => ({
2422
+ name,
2423
+ run: (harness) => harness.fillText(internalLabel, value),
2424
+ }),
2425
+ fillNameDom: (name, internalLabel, value) => ({
2426
+ name,
2427
+ run: (harness) => harness.fillName(internalLabel, value),
2428
+ }),
2429
+ fillAddressDom: (name, internalLabel, value) => ({
2430
+ name,
2431
+ run: (harness) => harness.fillAddress(internalLabel, value),
2432
+ }),
2433
+ chooseRadioDom: (name, internalLabel, value) => ({
2434
+ name,
2435
+ run: (harness) => harness.chooseRadio(internalLabel, value),
2436
+ }),
2437
+ setCheckboxDom: (name, internalLabel, value, checked) => ({
2438
+ name,
2439
+ run: (harness) => harness.setCheckbox(internalLabel, value, checked),
2440
+ }),
2441
+ selectMatrixCellDom: (name, fieldId, row, column) => ({
2442
+ name,
2443
+ run: (harness) => harness.selectMatrixCell(fieldId, row, column),
2444
+ }),
2445
+ setRatingDom: (name, fieldId, zeroBasedIndex) => ({
2446
+ name,
2447
+ run: (harness) => harness.setRating(fieldId, zeroBasedIndex),
2448
+ }),
2449
+ uploadFileDom: (name, internalLabel, files) => ({
2450
+ name,
2451
+ run: (harness) => harness.uploadFile(internalLabel, files),
2452
+ }),
2453
+ setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
2454
+ setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
2455
+ setChoiceApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormChoiceValue(value), options),
2456
+ setSelectApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormSelectValue(value), options),
2457
+ setCheckboxesApi: (name, internalLabel, values, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormCheckboxValue(values), options),
2458
+ setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
2459
+ setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
2460
+ setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
2461
+ setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
2462
+ setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: false } }),
2463
+ setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: true } }),
2464
+ };
231
2465
  //# sourceMappingURL=index.js.map