@redrockswebdevelopment/formstack-form-testing 0.1.6 → 0.1.8

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
@@ -290,6 +290,301 @@ function matchesPattern(value, pattern) {
290
290
  function isIgnored(value, patterns) {
291
291
  return patterns?.some((pattern) => matchesPattern(value, pattern)) ?? false;
292
292
  }
293
+ function liveFormTargetMatches(source, event) {
294
+ if (source.internalLabel && event.internalLabel)
295
+ return source.internalLabel === event.internalLabel;
296
+ if (source.internalLabel && event.target.internalLabel)
297
+ return source.internalLabel === event.target.internalLabel;
298
+ if (source.fieldId != null && event.fieldId != null)
299
+ return String(source.fieldId) === String(event.fieldId);
300
+ if (source.fieldId != null && event.target.fieldId != null)
301
+ return String(source.fieldId) === String(event.target.fieldId);
302
+ return false;
303
+ }
304
+ function createLiveFormEffectTransactionId(prefix = "fspt-effect") {
305
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
306
+ }
307
+ export function createLiveFormEffectController(adapter, options = {}) {
308
+ const startedAt = adapter.now?.() ?? Date.now();
309
+ const now = () => adapter.now?.() ?? Date.now();
310
+ const effects = new Map();
311
+ const traces = [];
312
+ const runCounts = new Map();
313
+ const latestSequenceByEffect = new Map();
314
+ const maxRunsPerTransaction = options.maxRunsPerTransaction ?? 25;
315
+ const emitTrace = (kind, detail, effectId, transactionId) => {
316
+ const event = {
317
+ kind,
318
+ at: new Date().toISOString(),
319
+ elapsedMs: Math.round(now() - startedAt),
320
+ ...(effectId ? { effectId } : {}),
321
+ ...(transactionId ? { transactionId } : {}),
322
+ detail,
323
+ };
324
+ if (options.trace !== false)
325
+ traces.push(event);
326
+ options.onTrace?.(event);
327
+ };
328
+ const runEffect = async (effect, event) => {
329
+ const transactionId = event.transactionId ?? createLiveFormEffectTransactionId();
330
+ if (effect.ignoreTransactions?.includes(transactionId)) {
331
+ emitTrace("effect-skip", { reason: "ignored-transaction" }, effect.id, transactionId);
332
+ return;
333
+ }
334
+ const runKey = `${transactionId}:${effect.id}`;
335
+ const runIndex = (runCounts.get(runKey) ?? 0) + 1;
336
+ runCounts.set(runKey, runIndex);
337
+ if (runIndex > maxRunsPerTransaction) {
338
+ emitTrace("effect-skip", { reason: "max-runs-exceeded", runIndex, maxRunsPerTransaction }, effect.id, transactionId);
339
+ return;
340
+ }
341
+ const sequence = (latestSequenceByEffect.get(effect.id) ?? 0) + 1;
342
+ latestSequenceByEffect.set(effect.id, sequence);
343
+ const waitForPhase = async () => {
344
+ if (effect.phase === "afterQuiet" || effect.phase === "afterValidation")
345
+ await adapter.waitForQuiet?.();
346
+ if (effect.phase === "afterValidation")
347
+ await adapter.validate?.({ validate: true });
348
+ };
349
+ emitTrace("effect-start", { phase: effect.phase ?? "afterValue", runIndex }, effect.id, transactionId);
350
+ try {
351
+ await waitForPhase();
352
+ if (effect.latestOnly !== false && latestSequenceByEffect.get(effect.id) !== sequence) {
353
+ emitTrace("effect-skip", { reason: "stale-run", sequence }, effect.id, transactionId);
354
+ return;
355
+ }
356
+ const context = {
357
+ event,
358
+ effectId: effect.id,
359
+ transactionId,
360
+ runIndex,
361
+ readField: (target) => adapter.readField(target),
362
+ writeField: async (target, value, writeOptions = {}) => {
363
+ const writeTransactionId = writeOptions.transactionId ?? transactionId;
364
+ emitTrace("write", {
365
+ target,
366
+ value,
367
+ source: writeOptions.source ?? effect.id,
368
+ suppressEffects: writeOptions.suppressEffects ?? false,
369
+ }, effect.id, writeTransactionId);
370
+ await adapter.writeField(target, value, {
371
+ ...writeOptions,
372
+ source: writeOptions.source ?? effect.id,
373
+ transactionId: writeTransactionId,
374
+ });
375
+ },
376
+ waitForQuiet: async (quietOptions) => {
377
+ await adapter.waitForQuiet?.(quietOptions);
378
+ },
379
+ validate: async (validationOptions) => adapter.validate?.(validationOptions),
380
+ trace: (kind, detail = {}) => emitTrace(kind, detail, effect.id, transactionId),
381
+ };
382
+ await effect.handler(context);
383
+ emitTrace("effect-complete", { runIndex }, effect.id, transactionId);
384
+ }
385
+ catch (error) {
386
+ emitTrace("effect-error", { message: error instanceof Error ? error.message : String(error) }, effect.id, transactionId);
387
+ throw error;
388
+ }
389
+ };
390
+ const emit = async (event) => {
391
+ emitTrace("change", {
392
+ target: event.target,
393
+ fieldId: event.fieldId ?? null,
394
+ internalLabel: event.internalLabel ?? null,
395
+ source: event.source ?? null,
396
+ }, undefined, event.transactionId);
397
+ const matching = [...effects.values()].filter((effect) => effect.sources.some((source) => liveFormTargetMatches(source, event)));
398
+ for (const effect of matching)
399
+ await runEffect(effect, event);
400
+ };
401
+ const unsubscribe = adapter.onFieldChange((event) => {
402
+ void emit(event);
403
+ });
404
+ return {
405
+ register: (effect) => {
406
+ if (effects.has(effect.id))
407
+ throw new Error(`Live form effect "${effect.id}" is already registered.`);
408
+ effects.set(effect.id, effect);
409
+ emitTrace("registered", { sources: effect.sources, phase: effect.phase ?? "afterValue" }, effect.id);
410
+ return () => {
411
+ effects.delete(effect.id);
412
+ };
413
+ },
414
+ emit,
415
+ writeField: async (target, value, writeOptions = {}) => {
416
+ const transactionId = writeOptions.transactionId ?? createLiveFormEffectTransactionId();
417
+ emitTrace("write", { target, value, source: writeOptions.source ?? "controller", suppressEffects: writeOptions.suppressEffects ?? false }, undefined, transactionId);
418
+ await adapter.writeField(target, value, { ...writeOptions, transactionId });
419
+ },
420
+ readField: (target) => adapter.readField(target),
421
+ waitForQuiet: async (quietOptions) => {
422
+ await adapter.waitForQuiet?.(quietOptions);
423
+ },
424
+ validate: async (validationOptions) => adapter.validate?.(validationOptions),
425
+ traces: () => [...traces],
426
+ dispose: () => {
427
+ effects.clear();
428
+ unsubscribe();
429
+ },
430
+ };
431
+ }
432
+ export function createFormstackLiveFormEffectAdapter(options = {}) {
433
+ const readApi = () => {
434
+ const globalObject = globalThis;
435
+ const factory = globalObject.window && typeof globalObject.window === "object"
436
+ ? Reflect.get(globalObject.window, "fsApi")
437
+ : Reflect.get(globalObject, "fsApi");
438
+ const api = typeof factory === "function" ? Reflect.apply(factory, globalObject.window ?? globalObject, []) : null;
439
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
440
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
441
+ return Array.isArray(forms) ? forms[0] ?? null : null;
442
+ };
443
+ const waitForForm = async () => {
444
+ const timeoutMs = options.waitForApiTimeoutMs ?? 10_000;
445
+ const deadline = Date.now() + timeoutMs;
446
+ let form = readApi();
447
+ while ((!form || typeof form !== "object") && Date.now() < deadline) {
448
+ await delay(50);
449
+ form = readApi();
450
+ }
451
+ if (!form || typeof form !== "object")
452
+ throw new Error("Formstack Live Form API did not become available.");
453
+ return form;
454
+ };
455
+ const resolveField = async (target) => {
456
+ const form = await waitForForm();
457
+ const getField = Reflect.get(form, "getField");
458
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
459
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
460
+ const field = Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
461
+ if (field)
462
+ return field;
463
+ }
464
+ if (target.fieldId != null && typeof getField === "function")
465
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
466
+ return null;
467
+ };
468
+ const readFieldValue = (field) => {
469
+ if (!field || typeof field !== "object")
470
+ return null;
471
+ const getValue = Reflect.get(field, "getValue");
472
+ return typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
473
+ };
474
+ const readFieldId = (field) => {
475
+ if (!field || typeof field !== "object")
476
+ return null;
477
+ const getId = Reflect.get(field, "getId");
478
+ if (typeof getId === "function")
479
+ return Reflect.apply(getId, field, []);
480
+ return Reflect.get(field, "id") ?? null;
481
+ };
482
+ const listeners = new Set();
483
+ let formListenerDispose = null;
484
+ const ensureFormListener = async () => {
485
+ if (formListenerDispose)
486
+ return;
487
+ const form = await waitForForm();
488
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
489
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
490
+ if (typeof registerFormEventListener !== "function")
491
+ return;
492
+ const formEventListener = {
493
+ type: "change",
494
+ onFormEvent: async (event) => {
495
+ const eventObject = event && typeof event === "object" ? event : {};
496
+ const data = Reflect.get(eventObject, "data");
497
+ const dataObject = data && typeof data === "object" ? data : {};
498
+ const fieldId = Reflect.get(dataObject, "fieldId") ?? null;
499
+ const field = fieldId == null ? null : typeof Reflect.get(form, "getField") === "function"
500
+ ? Reflect.apply(Reflect.get(form, "getField"), form, [String(fieldId)])
501
+ : null;
502
+ const currentValue = readFieldValue(field);
503
+ const internalLabel = field && typeof field === "object"
504
+ ? Reflect.get(field, "internalLabel") ?? null
505
+ : null;
506
+ const changeEvent = {
507
+ target: fieldId == null ? {} : { fieldId },
508
+ fieldId,
509
+ internalLabel,
510
+ currentValue,
511
+ source: Reflect.get(dataObject, "type") ?? "formstack-change",
512
+ };
513
+ for (const listener of listeners)
514
+ listener(changeEvent);
515
+ },
516
+ };
517
+ Reflect.apply(registerFormEventListener, form, [formEventListener]);
518
+ formListenerDispose = () => {
519
+ if (typeof unregisterFormEventListener === "function")
520
+ Reflect.apply(unregisterFormEventListener, form, [formEventListener]);
521
+ formListenerDispose = null;
522
+ };
523
+ };
524
+ return {
525
+ readField: async (target) => readFieldValue(await resolveField(target)),
526
+ writeField: async (target, value, writeOptions = {}) => {
527
+ const form = await waitForForm();
528
+ const field = await resolveField(target);
529
+ if (!field || typeof field !== "object")
530
+ throw new Error(`Unable to resolve Formstack field ${target.internalLabel ?? target.fieldId ?? "unknown"}.`);
531
+ const setValue = Reflect.get(field, "setValue");
532
+ if (typeof setValue !== "function")
533
+ throw new Error("Resolved Formstack field does not support setValue().");
534
+ const previousValue = readFieldValue(field);
535
+ await Promise.resolve(Reflect.apply(setValue, field, [value]));
536
+ if (options.notifyWrites ?? true) {
537
+ const notifyFormEventListeners = Reflect.get(form, "notifyFormEventListeners");
538
+ if (typeof notifyFormEventListeners === "function") {
539
+ await Promise.resolve(Reflect.apply(notifyFormEventListeners, form, [
540
+ "change",
541
+ {
542
+ fieldId: readFieldId(field),
543
+ type: writeOptions.source ?? "effect-write",
544
+ transactionId: writeOptions.transactionId,
545
+ suppressEffects: writeOptions.suppressEffects ?? false,
546
+ value,
547
+ },
548
+ ]));
549
+ }
550
+ }
551
+ if (!writeOptions.suppressEffects) {
552
+ const currentValue = readFieldValue(field);
553
+ const event = {
554
+ target,
555
+ fieldId: readFieldId(field),
556
+ internalLabel: target.internalLabel ?? null,
557
+ previousValue,
558
+ currentValue,
559
+ source: writeOptions.source ?? "effect-write",
560
+ ...(writeOptions.transactionId ? { transactionId: writeOptions.transactionId } : {}),
561
+ };
562
+ for (const listener of listeners)
563
+ listener(event);
564
+ }
565
+ },
566
+ onFieldChange: (listener) => {
567
+ listeners.add(listener);
568
+ void ensureFormListener();
569
+ return () => {
570
+ listeners.delete(listener);
571
+ if (listeners.size === 0)
572
+ formListenerDispose?.();
573
+ };
574
+ },
575
+ waitForQuiet: async (quietOptions = {}) => {
576
+ await delay(quietOptions.quietMs ?? options.quietMs ?? 250);
577
+ },
578
+ validate: async (validationOptions = {}) => {
579
+ const form = await waitForForm();
580
+ if (!validationOptions.validate)
581
+ return null;
582
+ const validateForm = Reflect.get(form, "validateForm");
583
+ return typeof validateForm === "function" ? Reflect.apply(validateForm, form, []) : null;
584
+ },
585
+ now: () => Date.now(),
586
+ };
587
+ }
293
588
  function defaultDiagnosticsOptions(options = {}) {
294
589
  return {
295
590
  failOnConsoleTypes: options.failOnConsoleTypes ?? ["error"],
@@ -1407,6 +1702,20 @@ export async function postMessageToIframe(page, options) {
1407
1702
  export function buildFormstackIframeBridgeMessage(message) {
1408
1703
  return message;
1409
1704
  }
1705
+ export function createFormstackIframeBridgeRequest(options) {
1706
+ return buildFormstackIframeBridgeMessage({
1707
+ channel: options.channel,
1708
+ requestId: options.requestId ?? `fspt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
1709
+ type: options.type,
1710
+ ...(options.payload === undefined ? {} : { payload: options.payload }),
1711
+ });
1712
+ }
1713
+ export function createFormstackIframePrefillRequest(options) {
1714
+ return createFormstackIframeBridgeRequest({
1715
+ ...options,
1716
+ type: options.type ?? "prefill",
1717
+ });
1718
+ }
1410
1719
  export async function waitForWindowMessage(page, options = {}) {
1411
1720
  if (!hasEvaluate(page)) {
1412
1721
  throw new Error("waitForWindowMessage requires a Playwright page with evaluate().");
@@ -1472,6 +1781,13 @@ export async function sendFormstackIframeBridgeMessage(page, options) {
1472
1781
  });
1473
1782
  return { posted, response };
1474
1783
  }
1784
+ export async function sendFormstackIframePrefillMessage(page, options) {
1785
+ return sendFormstackIframeBridgeMessage(page, {
1786
+ ...options,
1787
+ type: options.type ?? "prefill",
1788
+ responseType: options.responseType ?? `${options.type ?? "prefill"}:response`,
1789
+ });
1790
+ }
1475
1791
  export async function waitForConsoleMessage(page, options = {}) {
1476
1792
  const timeoutMs = options.timeoutMs ?? 5_000;
1477
1793
  const startedAt = Date.now();
@@ -2440,6 +2756,57 @@ export async function waitForLiveFormEvent(page, options) {
2440
2756
  };
2441
2757
  }, options);
2442
2758
  }
2759
+ export function defaultLiveFormFieldEventPlan(kind = "unknown") {
2760
+ if (["radio", "checkbox", "matrix", "rating"].includes(kind)) {
2761
+ return {
2762
+ eventTypes: ["input", "change", "blur"],
2763
+ onlyCheckedChoices: true,
2764
+ reason: "Choice-like fields should notify checked controls and then blur so custom scripts and validation see a user-like selection.",
2765
+ };
2766
+ }
2767
+ if (["name", "address", "date", "datetime", "product"].includes(kind)) {
2768
+ return {
2769
+ eventTypes: ["input", "change", "blur"],
2770
+ onlyCheckedChoices: false,
2771
+ reason: "Compound fields can render several controls, so dispatch across all visible subcontrols after an API value write.",
2772
+ };
2773
+ }
2774
+ if (["select"].includes(kind)) {
2775
+ return {
2776
+ eventTypes: ["change", "blur"],
2777
+ onlyCheckedChoices: false,
2778
+ reason: "Select fields primarily publish change events, with blur included for validation parity.",
2779
+ };
2780
+ }
2781
+ if (["text", "textarea", "email", "phone", "number"].includes(kind)) {
2782
+ return {
2783
+ eventTypes: ["input", "change", "blur"],
2784
+ onlyCheckedChoices: false,
2785
+ reason: "Text-like fields should emit input, change, and blur to match Formstack custom-script and validation timing.",
2786
+ };
2787
+ }
2788
+ return {
2789
+ eventTypes: ["input", "change", "blur"],
2790
+ onlyCheckedChoices: true,
2791
+ reason: "Unknown fields use the conservative user-like event sequence while avoiding unchecked choice controls.",
2792
+ };
2793
+ }
2794
+ function liveFormFieldTargetKey(target) {
2795
+ if (target.internalLabel)
2796
+ return target.internalLabel;
2797
+ if (target.fieldId != null)
2798
+ return String(target.fieldId);
2799
+ return null;
2800
+ }
2801
+ function resolveLiveFormDispatchOptions(dispatch, fieldKind) {
2802
+ if (dispatch === undefined || dispatch === false)
2803
+ return null;
2804
+ if (dispatch === true || dispatch === "auto") {
2805
+ const { eventTypes, onlyCheckedChoices } = defaultLiveFormFieldEventPlan(fieldKind);
2806
+ return { eventTypes, onlyCheckedChoices };
2807
+ }
2808
+ return dispatch;
2809
+ }
2443
2810
  export async function setLiveFormFieldValueTransaction(page, options) {
2444
2811
  if (!hasEvaluate(page)) {
2445
2812
  throw new Error("setLiveFormFieldValueTransaction requires a Playwright page with evaluate().");
@@ -2459,12 +2826,13 @@ export async function setLiveFormFieldValueTransaction(page, options) {
2459
2826
  const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2460
2827
  const write = await setLiveFormFieldValue(page, options.write);
2461
2828
  const event = eventPromise ? await eventPromise : undefined;
2462
- const domDispatch = options.dispatchDomEvents === undefined || options.dispatchDomEvents === false
2463
- ? undefined
2464
- : await dispatchLiveFormFieldDomEvents(page, {
2465
- ...(typeof options.dispatchDomEvents === "object" ? options.dispatchDomEvents : {}),
2829
+ const dispatchOptions = resolveLiveFormDispatchOptions(options.dispatchDomEvents, options.fieldKind);
2830
+ const domDispatch = dispatchOptions
2831
+ ? await dispatchLiveFormFieldDomEvents(page, {
2832
+ ...dispatchOptions,
2466
2833
  target: options.write.target,
2467
- });
2834
+ })
2835
+ : undefined;
2468
2836
  const readiness = options.readiness ? await readLiveFormReadiness(page, options.readiness) : undefined;
2469
2837
  const domFields = options.snapshot
2470
2838
  ? await snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {})
@@ -2527,9 +2895,10 @@ export async function setLiveFormFieldValueTimedTransaction(page, options) {
2527
2895
  const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2528
2896
  const write = await runPhase("write", () => setLiveFormFieldValue(page, options.write));
2529
2897
  const event = eventPromise ? await runPhase("wait-for-event", () => eventPromise) : undefined;
2530
- const domDispatch = options.dispatchDomEvents
2898
+ const dispatchOptions = resolveLiveFormDispatchOptions(options.dispatchDomEvents, options.fieldKind);
2899
+ const domDispatch = dispatchOptions
2531
2900
  ? await runPhase("dispatch-dom-events", () => dispatchLiveFormFieldDomEvents(page, {
2532
- ...(typeof options.dispatchDomEvents === "object" ? options.dispatchDomEvents : {}),
2901
+ ...dispatchOptions,
2533
2902
  target: options.write.target,
2534
2903
  }))
2535
2904
  : undefined;
@@ -2556,6 +2925,104 @@ export async function setLiveFormFieldValueTimedTransaction(page, options) {
2556
2925
  ...(validation ? { validation } : {}),
2557
2926
  };
2558
2927
  }
2928
+ export async function waitForLiveFormReady(page, options = {}) {
2929
+ const report = await waitForLiveFormReadiness(page, options);
2930
+ if (options.throwOnBlockingFields !== false && !report.isReady) {
2931
+ throw new Error(`Live form did not become ready; ${report.blockingFieldCount} field(s) are blocking.`);
2932
+ }
2933
+ return report;
2934
+ }
2935
+ export async function waitForLiveFormFieldStable(page, options) {
2936
+ const quiet = options.quiet
2937
+ ? await waitForLiveFormQuiet(page, typeof options.quiet === "object" ? options.quiet : {})
2938
+ : undefined;
2939
+ const value = options.value ? await waitForLiveFormFieldValue(page, { ...options.value, target: options.target }) : undefined;
2940
+ const dom = options.dom ? await waitForLiveFormDomFieldState(page, { ...options.target, ...options.dom }) : undefined;
2941
+ const stable = (quiet?.quiet ?? true) && (value ? (value.expectedValue === undefined ? value.fieldAvailable && !value.isEmpty : value.valueMatched === true) : true) && (dom?.matched ?? true);
2942
+ return {
2943
+ ...(quiet ? { quiet } : {}),
2944
+ ...(value ? { value } : {}),
2945
+ ...(dom ? { dom } : {}),
2946
+ stable,
2947
+ };
2948
+ }
2949
+ export async function waitForLiveFormValidationSettled(page, options = {}) {
2950
+ const validation = await waitForLiveFormValidationState(page, {
2951
+ ...options,
2952
+ ...(options.requireNoErrors === false ? {} : { expectNoErrors: options.expectNoErrors ?? true }),
2953
+ });
2954
+ if (options.requireNoErrors !== false && validation.inlineErrors.length > 0) {
2955
+ throw new Error(`Live form validation did not settle cleanly; found ${validation.inlineErrors.length} visible error(s).`);
2956
+ }
2957
+ return validation;
2958
+ }
2959
+ export async function waitForLiveFormLogicSettled(page, options = {}) {
2960
+ const quiet = options.quiet
2961
+ ? await waitForLiveFormQuiet(page, typeof options.quiet === "object" ? options.quiet : {})
2962
+ : undefined;
2963
+ const visibility = options.visibility ? await waitForLiveFormVisibility(page, options.visibility) : undefined;
2964
+ const readiness = options.readiness ? await waitForLiveFormReadiness(page, options.readiness) : undefined;
2965
+ const validation = options.validation
2966
+ ? await waitForLiveFormValidationSettled(page, typeof options.validation === "object" ? options.validation : {})
2967
+ : undefined;
2968
+ return {
2969
+ ...(quiet ? { quiet } : {}),
2970
+ ...(visibility ? { visibility } : {}),
2971
+ ...(readiness ? { readiness } : {}),
2972
+ ...(validation ? { validation } : {}),
2973
+ settled: (quiet?.quiet ?? true) && (visibility?.matched ?? true) && (readiness?.isReady ?? true) && ((validation?.inlineErrors.length ?? 0) === 0),
2974
+ };
2975
+ }
2976
+ export async function performLiveFormFieldTransaction(page, options) {
2977
+ return setLiveFormFieldValueTimedTransaction(page, {
2978
+ ...options,
2979
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
2980
+ dispatchDomEvents: options.dispatchDomEvents ?? (options.autoDispatchDomEvents === false ? false : "auto"),
2981
+ afterQuiet: options.afterQuiet ?? true,
2982
+ });
2983
+ }
2984
+ export async function answerProgressiveLiveFormField(page, options) {
2985
+ const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
2986
+ const transaction = await performLiveFormFieldTransaction(page, {
2987
+ write: {
2988
+ ...(options.write ?? {}),
2989
+ target: options.target,
2990
+ value: options.value,
2991
+ notify: options.write?.notify ?? true,
2992
+ },
2993
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
2994
+ waitForEvent: options.waitForEvent ?? true,
2995
+ afterQuiet: options.afterQuiet ?? true,
2996
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
2997
+ });
2998
+ const visibilityTargets = [
2999
+ ...(options.expectVisible ?? []),
3000
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
3001
+ ];
3002
+ const visibility = visibilityTargets.length > 0
3003
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
3004
+ : undefined;
3005
+ if (visibility && !visibility.matched) {
3006
+ throw new Error("Progressive live form answer did not produce the expected visibility state.");
3007
+ }
3008
+ const resetValues = [];
3009
+ for (const reset of options.expectReset ?? []) {
3010
+ const value = await readLiveFormFieldValue(page, reset);
3011
+ if (!value.isEmpty) {
3012
+ throw new Error(`Expected progressive answer reset target ${reset.target.internalLabel ?? reset.target.fieldId ?? "unknown"} to be empty.`);
3013
+ }
3014
+ resetValues.push(value);
3015
+ }
3016
+ const validation = validationOptions
3017
+ ? await waitForLiveFormValidationSettled(page, validationOptions)
3018
+ : undefined;
3019
+ return {
3020
+ transaction,
3021
+ ...(visibility ? { visibility } : {}),
3022
+ resetValues,
3023
+ ...(validation ? { validation } : {}),
3024
+ };
3025
+ }
2559
3026
  function mergeTraceFieldValueTarget(target, options) {
2560
3027
  if (!target || options === false || options === undefined)
2561
3028
  return null;
@@ -2653,10 +3120,12 @@ export async function setLiveFormFieldValuesTransaction(page, options) {
2653
3120
  const domDispatches = [];
2654
3121
  const waitForEvents = options.waitForEvents ?? true;
2655
3122
  for (const writeOptions of options.writes) {
3123
+ const fieldKey = liveFormFieldTargetKey(writeOptions.target);
2656
3124
  const transaction = await setLiveFormFieldValueTransaction(page, {
2657
3125
  write: writeOptions,
2658
3126
  waitForEvent: waitForEvents && writeOptions.notify === true,
2659
3127
  ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
3128
+ ...(fieldKey && options.fieldKinds?.[fieldKey] ? { fieldKind: options.fieldKinds[fieldKey] } : {}),
2660
3129
  });
2661
3130
  writes.push(transaction.write);
2662
3131
  if (transaction.event)
@@ -2901,6 +3370,14 @@ export const liveFormScenarioSteps = {
2901
3370
  name,
2902
3371
  run: (harness) => setLiveFormFieldValueTransaction(harness.page, options).then(() => undefined),
2903
3372
  }),
3373
+ performFieldTransaction: (name, options) => ({
3374
+ name,
3375
+ run: (harness) => performLiveFormFieldTransaction(harness.page, options).then(() => undefined),
3376
+ }),
3377
+ answerProgressiveField: (name, options) => ({
3378
+ name,
3379
+ run: (harness) => answerProgressiveLiveFormField(harness.page, options).then(() => undefined),
3380
+ }),
2904
3381
  writeFields: (name, options) => ({
2905
3382
  name,
2906
3383
  run: (harness) => setLiveFormFieldValuesTransaction(harness.page, options).then(() => undefined),
@@ -2908,12 +3385,30 @@ export const liveFormScenarioSteps = {
2908
3385
  expectReady: (name, options) => ({
2909
3386
  name,
2910
3387
  run: async (harness) => {
2911
- const readiness = await waitForLiveFormReadiness(harness.page, options);
3388
+ const readiness = await waitForLiveFormReady(harness.page, options);
2912
3389
  if (!readiness.isReady) {
2913
3390
  throw new Error(`Expected live form to be ready, but ${readiness.blockingFieldCount} field(s) are blocking.`);
2914
3391
  }
2915
3392
  },
2916
3393
  }),
3394
+ expectFieldStable: (name, options) => ({
3395
+ name,
3396
+ run: async (harness) => {
3397
+ const stable = await waitForLiveFormFieldStable(harness.page, options);
3398
+ if (!stable.stable) {
3399
+ throw new Error("Expected live form field to become stable before timeout.");
3400
+ }
3401
+ },
3402
+ }),
3403
+ expectLogicSettled: (name, options = {}) => ({
3404
+ name,
3405
+ run: async (harness) => {
3406
+ const settled = await waitForLiveFormLogicSettled(harness.page, options);
3407
+ if (!settled.settled) {
3408
+ throw new Error("Expected live form logic to settle before timeout.");
3409
+ }
3410
+ },
3411
+ }),
2917
3412
  waitForQuiet: (name, options = {}) => ({
2918
3413
  name,
2919
3414
  run: async (harness) => {
@@ -3039,6 +3534,7 @@ function apiRecipeStep(name, target, value, options = {}) {
3039
3534
  },
3040
3535
  waitForEvent: options.waitForEvent ?? (options.notify ?? true),
3041
3536
  ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
3537
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3042
3538
  });
3043
3539
  await maybeWaitForQuiet(harness.page, options);
3044
3540
  },
@@ -3094,7 +3590,8 @@ export const liveFormFieldRecipes = {
3094
3590
  setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
3095
3591
  setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
3096
3592
  setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
3097
- setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: false } }),
3098
- setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: true } }),
3593
+ setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),
3594
+ setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "name", dispatchDomEvents: "auto" }),
3595
+ setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "radio", dispatchDomEvents: "auto" }),
3099
3596
  };
3100
3597
  //# sourceMappingURL=index.js.map