@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.d.ts +98 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +217 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
...
|
|
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
|
|
2603
|
+
const dispatchOptions = resolveLiveFormDispatchOptions(options.dispatchDomEvents, options.fieldKind);
|
|
2604
|
+
const domDispatch = dispatchOptions
|
|
2531
2605
|
? await runPhase("dispatch-dom-events", () => dispatchLiveFormFieldDomEvents(page, {
|
|
2532
|
-
...
|
|
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
|
|
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
|
-
|
|
3093
|
-
|
|
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
|