@redrockswebdevelopment/formstack-form-testing 0.1.5 → 0.1.7

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
@@ -1407,6 +1407,20 @@ export async function postMessageToIframe(page, options) {
1407
1407
  export function buildFormstackIframeBridgeMessage(message) {
1408
1408
  return message;
1409
1409
  }
1410
+ export function createFormstackIframeBridgeRequest(options) {
1411
+ return buildFormstackIframeBridgeMessage({
1412
+ channel: options.channel,
1413
+ requestId: options.requestId ?? `fspt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
1414
+ type: options.type,
1415
+ ...(options.payload === undefined ? {} : { payload: options.payload }),
1416
+ });
1417
+ }
1418
+ export function createFormstackIframePrefillRequest(options) {
1419
+ return createFormstackIframeBridgeRequest({
1420
+ ...options,
1421
+ type: options.type ?? "prefill",
1422
+ });
1423
+ }
1410
1424
  export async function waitForWindowMessage(page, options = {}) {
1411
1425
  if (!hasEvaluate(page)) {
1412
1426
  throw new Error("waitForWindowMessage requires a Playwright page with evaluate().");
@@ -1472,6 +1486,13 @@ export async function sendFormstackIframeBridgeMessage(page, options) {
1472
1486
  });
1473
1487
  return { posted, response };
1474
1488
  }
1489
+ export async function sendFormstackIframePrefillMessage(page, options) {
1490
+ return sendFormstackIframeBridgeMessage(page, {
1491
+ ...options,
1492
+ type: options.type ?? "prefill",
1493
+ responseType: options.responseType ?? `${options.type ?? "prefill"}:response`,
1494
+ });
1495
+ }
1475
1496
  export async function waitForConsoleMessage(page, options = {}) {
1476
1497
  const timeoutMs = options.timeoutMs ?? 5_000;
1477
1498
  const startedAt = Date.now();
@@ -2440,6 +2461,57 @@ export async function waitForLiveFormEvent(page, options) {
2440
2461
  };
2441
2462
  }, options);
2442
2463
  }
2464
+ export function defaultLiveFormFieldEventPlan(kind = "unknown") {
2465
+ if (["radio", "checkbox", "matrix", "rating"].includes(kind)) {
2466
+ return {
2467
+ eventTypes: ["input", "change", "blur"],
2468
+ onlyCheckedChoices: true,
2469
+ reason: "Choice-like fields should notify checked controls and then blur so custom scripts and validation see a user-like selection.",
2470
+ };
2471
+ }
2472
+ if (["name", "address", "date", "datetime", "product"].includes(kind)) {
2473
+ return {
2474
+ eventTypes: ["input", "change", "blur"],
2475
+ onlyCheckedChoices: false,
2476
+ reason: "Compound fields can render several controls, so dispatch across all visible subcontrols after an API value write.",
2477
+ };
2478
+ }
2479
+ if (["select"].includes(kind)) {
2480
+ return {
2481
+ eventTypes: ["change", "blur"],
2482
+ onlyCheckedChoices: false,
2483
+ reason: "Select fields primarily publish change events, with blur included for validation parity.",
2484
+ };
2485
+ }
2486
+ if (["text", "textarea", "email", "phone", "number"].includes(kind)) {
2487
+ return {
2488
+ eventTypes: ["input", "change", "blur"],
2489
+ onlyCheckedChoices: false,
2490
+ reason: "Text-like fields should emit input, change, and blur to match Formstack custom-script and validation timing.",
2491
+ };
2492
+ }
2493
+ return {
2494
+ eventTypes: ["input", "change", "blur"],
2495
+ onlyCheckedChoices: true,
2496
+ reason: "Unknown fields use the conservative user-like event sequence while avoiding unchecked choice controls.",
2497
+ };
2498
+ }
2499
+ function liveFormFieldTargetKey(target) {
2500
+ if (target.internalLabel)
2501
+ return target.internalLabel;
2502
+ if (target.fieldId != null)
2503
+ return String(target.fieldId);
2504
+ return null;
2505
+ }
2506
+ function resolveLiveFormDispatchOptions(dispatch, fieldKind) {
2507
+ if (dispatch === undefined || dispatch === false)
2508
+ return null;
2509
+ if (dispatch === true || dispatch === "auto") {
2510
+ const { eventTypes, onlyCheckedChoices } = defaultLiveFormFieldEventPlan(fieldKind);
2511
+ return { eventTypes, onlyCheckedChoices };
2512
+ }
2513
+ return dispatch;
2514
+ }
2443
2515
  export async function setLiveFormFieldValueTransaction(page, options) {
2444
2516
  if (!hasEvaluate(page)) {
2445
2517
  throw new Error("setLiveFormFieldValueTransaction requires a Playwright page with evaluate().");
@@ -2459,12 +2531,13 @@ export async function setLiveFormFieldValueTransaction(page, options) {
2459
2531
  const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2460
2532
  const write = await setLiveFormFieldValue(page, options.write);
2461
2533
  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 : {}),
2534
+ const dispatchOptions = resolveLiveFormDispatchOptions(options.dispatchDomEvents, options.fieldKind);
2535
+ const domDispatch = dispatchOptions
2536
+ ? await dispatchLiveFormFieldDomEvents(page, {
2537
+ ...dispatchOptions,
2466
2538
  target: options.write.target,
2467
- });
2539
+ })
2540
+ : undefined;
2468
2541
  const readiness = options.readiness ? await readLiveFormReadiness(page, options.readiness) : undefined;
2469
2542
  const domFields = options.snapshot
2470
2543
  ? await snapshotLiveFormDomFields(page, typeof options.snapshot === "object" ? options.snapshot : {})
@@ -2527,9 +2600,10 @@ export async function setLiveFormFieldValueTimedTransaction(page, options) {
2527
2600
  const eventPromise = eventOptions ? waitForLiveFormEvent(page, eventOptions) : null;
2528
2601
  const write = await runPhase("write", () => setLiveFormFieldValue(page, options.write));
2529
2602
  const event = eventPromise ? await runPhase("wait-for-event", () => eventPromise) : undefined;
2530
- const domDispatch = options.dispatchDomEvents
2603
+ const dispatchOptions = resolveLiveFormDispatchOptions(options.dispatchDomEvents, options.fieldKind);
2604
+ const domDispatch = dispatchOptions
2531
2605
  ? await runPhase("dispatch-dom-events", () => dispatchLiveFormFieldDomEvents(page, {
2532
- ...(typeof options.dispatchDomEvents === "object" ? options.dispatchDomEvents : {}),
2606
+ ...dispatchOptions,
2533
2607
  target: options.write.target,
2534
2608
  }))
2535
2609
  : undefined;
@@ -2556,6 +2630,104 @@ export async function setLiveFormFieldValueTimedTransaction(page, options) {
2556
2630
  ...(validation ? { validation } : {}),
2557
2631
  };
2558
2632
  }
2633
+ export async function waitForLiveFormReady(page, options = {}) {
2634
+ const report = await waitForLiveFormReadiness(page, options);
2635
+ if (options.throwOnBlockingFields !== false && !report.isReady) {
2636
+ throw new Error(`Live form did not become ready; ${report.blockingFieldCount} field(s) are blocking.`);
2637
+ }
2638
+ return report;
2639
+ }
2640
+ export async function waitForLiveFormFieldStable(page, options) {
2641
+ const quiet = options.quiet
2642
+ ? await waitForLiveFormQuiet(page, typeof options.quiet === "object" ? options.quiet : {})
2643
+ : undefined;
2644
+ const value = options.value ? await waitForLiveFormFieldValue(page, { ...options.value, target: options.target }) : undefined;
2645
+ const dom = options.dom ? await waitForLiveFormDomFieldState(page, { ...options.target, ...options.dom }) : undefined;
2646
+ const stable = (quiet?.quiet ?? true) && (value ? (value.expectedValue === undefined ? value.fieldAvailable && !value.isEmpty : value.valueMatched === true) : true) && (dom?.matched ?? true);
2647
+ return {
2648
+ ...(quiet ? { quiet } : {}),
2649
+ ...(value ? { value } : {}),
2650
+ ...(dom ? { dom } : {}),
2651
+ stable,
2652
+ };
2653
+ }
2654
+ export async function waitForLiveFormValidationSettled(page, options = {}) {
2655
+ const validation = await waitForLiveFormValidationState(page, {
2656
+ ...options,
2657
+ ...(options.requireNoErrors === false ? {} : { expectNoErrors: options.expectNoErrors ?? true }),
2658
+ });
2659
+ if (options.requireNoErrors !== false && validation.inlineErrors.length > 0) {
2660
+ throw new Error(`Live form validation did not settle cleanly; found ${validation.inlineErrors.length} visible error(s).`);
2661
+ }
2662
+ return validation;
2663
+ }
2664
+ export async function waitForLiveFormLogicSettled(page, options = {}) {
2665
+ const quiet = options.quiet
2666
+ ? await waitForLiveFormQuiet(page, typeof options.quiet === "object" ? options.quiet : {})
2667
+ : undefined;
2668
+ const visibility = options.visibility ? await waitForLiveFormVisibility(page, options.visibility) : undefined;
2669
+ const readiness = options.readiness ? await waitForLiveFormReadiness(page, options.readiness) : undefined;
2670
+ const validation = options.validation
2671
+ ? await waitForLiveFormValidationSettled(page, typeof options.validation === "object" ? options.validation : {})
2672
+ : undefined;
2673
+ return {
2674
+ ...(quiet ? { quiet } : {}),
2675
+ ...(visibility ? { visibility } : {}),
2676
+ ...(readiness ? { readiness } : {}),
2677
+ ...(validation ? { validation } : {}),
2678
+ settled: (quiet?.quiet ?? true) && (visibility?.matched ?? true) && (readiness?.isReady ?? true) && ((validation?.inlineErrors.length ?? 0) === 0),
2679
+ };
2680
+ }
2681
+ export async function performLiveFormFieldTransaction(page, options) {
2682
+ return setLiveFormFieldValueTimedTransaction(page, {
2683
+ ...options,
2684
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
2685
+ dispatchDomEvents: options.dispatchDomEvents ?? (options.autoDispatchDomEvents === false ? false : "auto"),
2686
+ afterQuiet: options.afterQuiet ?? true,
2687
+ });
2688
+ }
2689
+ export async function answerProgressiveLiveFormField(page, options) {
2690
+ const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
2691
+ const transaction = await performLiveFormFieldTransaction(page, {
2692
+ write: {
2693
+ ...(options.write ?? {}),
2694
+ target: options.target,
2695
+ value: options.value,
2696
+ notify: options.write?.notify ?? true,
2697
+ },
2698
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
2699
+ waitForEvent: options.waitForEvent ?? true,
2700
+ afterQuiet: options.afterQuiet ?? true,
2701
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
2702
+ });
2703
+ const visibilityTargets = [
2704
+ ...(options.expectVisible ?? []),
2705
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
2706
+ ];
2707
+ const visibility = visibilityTargets.length > 0
2708
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
2709
+ : undefined;
2710
+ if (visibility && !visibility.matched) {
2711
+ throw new Error("Progressive live form answer did not produce the expected visibility state.");
2712
+ }
2713
+ const resetValues = [];
2714
+ for (const reset of options.expectReset ?? []) {
2715
+ const value = await readLiveFormFieldValue(page, reset);
2716
+ if (!value.isEmpty) {
2717
+ throw new Error(`Expected progressive answer reset target ${reset.target.internalLabel ?? reset.target.fieldId ?? "unknown"} to be empty.`);
2718
+ }
2719
+ resetValues.push(value);
2720
+ }
2721
+ const validation = validationOptions
2722
+ ? await waitForLiveFormValidationSettled(page, validationOptions)
2723
+ : undefined;
2724
+ return {
2725
+ transaction,
2726
+ ...(visibility ? { visibility } : {}),
2727
+ resetValues,
2728
+ ...(validation ? { validation } : {}),
2729
+ };
2730
+ }
2559
2731
  function mergeTraceFieldValueTarget(target, options) {
2560
2732
  if (!target || options === false || options === undefined)
2561
2733
  return null;
@@ -2653,10 +2825,12 @@ export async function setLiveFormFieldValuesTransaction(page, options) {
2653
2825
  const domDispatches = [];
2654
2826
  const waitForEvents = options.waitForEvents ?? true;
2655
2827
  for (const writeOptions of options.writes) {
2828
+ const fieldKey = liveFormFieldTargetKey(writeOptions.target);
2656
2829
  const transaction = await setLiveFormFieldValueTransaction(page, {
2657
2830
  write: writeOptions,
2658
2831
  waitForEvent: waitForEvents && writeOptions.notify === true,
2659
2832
  ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
2833
+ ...(fieldKey && options.fieldKinds?.[fieldKey] ? { fieldKind: options.fieldKinds[fieldKey] } : {}),
2660
2834
  });
2661
2835
  writes.push(transaction.write);
2662
2836
  if (transaction.event)
@@ -2820,6 +2994,9 @@ function errorReport(error) {
2820
2994
  }
2821
2995
  export async function runFormScenario(page, scenario, options = {}) {
2822
2996
  const monitor = createFormBrowserMonitor(page, scenario.diagnostics ?? options.diagnostics);
2997
+ const activityRecorder = options.activity
2998
+ ? createFormBrowserActivityRecorder(page, typeof options.activity === "object" ? options.activity : {})
2999
+ : null;
2823
3000
  const harness = createFormTestHarness(page, options);
2824
3001
  const steps = [];
2825
3002
  const started = Date.now();
@@ -2834,6 +3011,7 @@ export async function runFormScenario(page, scenario, options = {}) {
2834
3011
  durationMs: ended - started,
2835
3012
  steps,
2836
3013
  diagnostics: monitor.report(),
3014
+ ...(activityRecorder ? { activity: activityRecorder.report() } : {}),
2837
3015
  ...(options.artifacts === undefined ? {} : { artifacts: options.artifacts }),
2838
3016
  };
2839
3017
  };
@@ -2886,6 +3064,7 @@ export async function runFormScenario(page, scenario, options = {}) {
2886
3064
  }
2887
3065
  finally {
2888
3066
  monitor.detach();
3067
+ activityRecorder?.detach();
2889
3068
  }
2890
3069
  }
2891
3070
  export function defineFormScenario(scenario) {
@@ -2896,6 +3075,14 @@ export const liveFormScenarioSteps = {
2896
3075
  name,
2897
3076
  run: (harness) => setLiveFormFieldValueTransaction(harness.page, options).then(() => undefined),
2898
3077
  }),
3078
+ performFieldTransaction: (name, options) => ({
3079
+ name,
3080
+ run: (harness) => performLiveFormFieldTransaction(harness.page, options).then(() => undefined),
3081
+ }),
3082
+ answerProgressiveField: (name, options) => ({
3083
+ name,
3084
+ run: (harness) => answerProgressiveLiveFormField(harness.page, options).then(() => undefined),
3085
+ }),
2899
3086
  writeFields: (name, options) => ({
2900
3087
  name,
2901
3088
  run: (harness) => setLiveFormFieldValuesTransaction(harness.page, options).then(() => undefined),
@@ -2903,12 +3090,30 @@ export const liveFormScenarioSteps = {
2903
3090
  expectReady: (name, options) => ({
2904
3091
  name,
2905
3092
  run: async (harness) => {
2906
- const readiness = await waitForLiveFormReadiness(harness.page, options);
3093
+ const readiness = await waitForLiveFormReady(harness.page, options);
2907
3094
  if (!readiness.isReady) {
2908
3095
  throw new Error(`Expected live form to be ready, but ${readiness.blockingFieldCount} field(s) are blocking.`);
2909
3096
  }
2910
3097
  },
2911
3098
  }),
3099
+ expectFieldStable: (name, options) => ({
3100
+ name,
3101
+ run: async (harness) => {
3102
+ const stable = await waitForLiveFormFieldStable(harness.page, options);
3103
+ if (!stable.stable) {
3104
+ throw new Error("Expected live form field to become stable before timeout.");
3105
+ }
3106
+ },
3107
+ }),
3108
+ expectLogicSettled: (name, options = {}) => ({
3109
+ name,
3110
+ run: async (harness) => {
3111
+ const settled = await waitForLiveFormLogicSettled(harness.page, options);
3112
+ if (!settled.settled) {
3113
+ throw new Error("Expected live form logic to settle before timeout.");
3114
+ }
3115
+ },
3116
+ }),
2912
3117
  waitForQuiet: (name, options = {}) => ({
2913
3118
  name,
2914
3119
  run: async (harness) => {
@@ -3034,6 +3239,7 @@ function apiRecipeStep(name, target, value, options = {}) {
3034
3239
  },
3035
3240
  waitForEvent: options.waitForEvent ?? (options.notify ?? true),
3036
3241
  ...(options.dispatchDomEvents === undefined ? {} : { dispatchDomEvents: options.dispatchDomEvents }),
3242
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3037
3243
  });
3038
3244
  await maybeWaitForQuiet(harness.page, options);
3039
3245
  },
@@ -3089,7 +3295,8 @@ export const liveFormFieldRecipes = {
3089
3295
  setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
3090
3296
  setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
3091
3297
  setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
3092
- setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: false } }),
3093
- setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, dispatchDomEvents: { eventTypes: ["input", "change", "blur"], onlyCheckedChoices: true } }),
3298
+ setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),
3299
+ setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "name", dispatchDomEvents: "auto" }),
3300
+ setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "radio", dispatchDomEvents: "auto" }),
3094
3301
  };
3095
3302
  //# sourceMappingURL=index.js.map