@redrockswebdevelopment/formstack-form-testing 0.1.21 → 0.1.23
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 +64 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -910,6 +910,27 @@ export function buildLiveFormSelectValue(value) {
|
|
|
910
910
|
export function buildLiveFormCheckboxValue(values, otherValue = null) {
|
|
911
911
|
return Object.fromEntries([...values.map((value, index) => [String(index), value]), ["otherValue", otherValue]]);
|
|
912
912
|
}
|
|
913
|
+
export function readLiveFormCheckboxValues(value) {
|
|
914
|
+
if (Array.isArray(value)) {
|
|
915
|
+
return value.filter((entry) => typeof entry === "string");
|
|
916
|
+
}
|
|
917
|
+
if (!value || typeof value !== "object")
|
|
918
|
+
return [];
|
|
919
|
+
const record = value;
|
|
920
|
+
if (Array.isArray(record.values)) {
|
|
921
|
+
return record.values.filter((entry) => typeof entry === "string");
|
|
922
|
+
}
|
|
923
|
+
return Object.entries(record)
|
|
924
|
+
.filter(([key, entry]) => /^\d+$/.test(key) && typeof entry === "string" && entry.length > 0)
|
|
925
|
+
.sort(([left], [right]) => Number(left) - Number(right))
|
|
926
|
+
.map(([, entry]) => entry);
|
|
927
|
+
}
|
|
928
|
+
export function normalizeLiveFormCheckboxValue(value, otherValue = null) {
|
|
929
|
+
const explicitOtherValue = value && typeof value === "object" && !Array.isArray(value) && "otherValue" in value
|
|
930
|
+
? value.otherValue
|
|
931
|
+
: otherValue;
|
|
932
|
+
return buildLiveFormCheckboxValue(readLiveFormCheckboxValues(value), typeof explicitOtherValue === "string" ? explicitOtherValue : explicitOtherValue === null ? null : otherValue);
|
|
933
|
+
}
|
|
913
934
|
export function buildLiveFormDateTimeValue(value, current = {}) {
|
|
914
935
|
return {
|
|
915
936
|
...(current && typeof current === "object" ? current : {}),
|
|
@@ -1696,6 +1717,104 @@ export async function waitForLiveFormDomFieldState(page, options) {
|
|
|
1696
1717
|
}
|
|
1697
1718
|
return latest;
|
|
1698
1719
|
}
|
|
1720
|
+
export async function readLiveFormCheckboxState(page, options) {
|
|
1721
|
+
if (!hasEvaluate(page)) {
|
|
1722
|
+
throw new Error("readLiveFormCheckboxState requires a Playwright page with evaluate().");
|
|
1723
|
+
}
|
|
1724
|
+
return page.evaluate((stateOptions) => {
|
|
1725
|
+
const controlSnapshot = (control) => ({
|
|
1726
|
+
tagName: control.tagName.toLowerCase(),
|
|
1727
|
+
type: control.getAttribute("type"),
|
|
1728
|
+
id: control.getAttribute("id"),
|
|
1729
|
+
name: control.getAttribute("name"),
|
|
1730
|
+
value: control.getAttribute("value"),
|
|
1731
|
+
currentValue: control.value,
|
|
1732
|
+
checked: control.checked,
|
|
1733
|
+
});
|
|
1734
|
+
const root = stateOptions.selector != null
|
|
1735
|
+
? document.querySelector(stateOptions.selector)
|
|
1736
|
+
: stateOptions.internalLabel != null
|
|
1737
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
|
|
1738
|
+
: stateOptions.fieldId == null
|
|
1739
|
+
? null
|
|
1740
|
+
: document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
|
|
1741
|
+
const controls = root
|
|
1742
|
+
? Array.from(root.querySelectorAll("input[type=\"checkbox\"]")).map(controlSnapshot)
|
|
1743
|
+
: [];
|
|
1744
|
+
const selectedValues = controls
|
|
1745
|
+
.filter((control) => control.checked === true)
|
|
1746
|
+
.map((control) => control.currentValue ?? control.value ?? "")
|
|
1747
|
+
.filter((value) => value.length > 0);
|
|
1748
|
+
const allValues = controls
|
|
1749
|
+
.map((control) => control.currentValue ?? control.value ?? "")
|
|
1750
|
+
.filter((value) => value.length > 0);
|
|
1751
|
+
const expectedValues = [...(stateOptions.expectedValues ?? [])];
|
|
1752
|
+
const missingValues = expectedValues.filter((value) => !selectedValues.includes(value));
|
|
1753
|
+
const unexpectedValues = selectedValues.filter((value) => !expectedValues.includes(value));
|
|
1754
|
+
const exact = stateOptions.exact !== false;
|
|
1755
|
+
return {
|
|
1756
|
+
fieldMatched: Boolean(root),
|
|
1757
|
+
selectedValues,
|
|
1758
|
+
allValues,
|
|
1759
|
+
expectedValues,
|
|
1760
|
+
missingValues,
|
|
1761
|
+
unexpectedValues,
|
|
1762
|
+
exact,
|
|
1763
|
+
matched: Boolean(root) && missingValues.length === 0 && (!exact || unexpectedValues.length === 0),
|
|
1764
|
+
controls,
|
|
1765
|
+
};
|
|
1766
|
+
}, options);
|
|
1767
|
+
}
|
|
1768
|
+
export async function waitForLiveFormCheckboxState(page, options) {
|
|
1769
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
1770
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
1771
|
+
const startedAt = Date.now();
|
|
1772
|
+
let latest = await readLiveFormCheckboxState(page, options);
|
|
1773
|
+
while (!latest.matched && Date.now() - startedAt < timeoutMs) {
|
|
1774
|
+
await delay(intervalMs);
|
|
1775
|
+
latest = await readLiveFormCheckboxState(page, options);
|
|
1776
|
+
}
|
|
1777
|
+
return latest;
|
|
1778
|
+
}
|
|
1779
|
+
export async function diagnoseLiveFormCommitControl(page, options) {
|
|
1780
|
+
if (!hasEvaluate(page)) {
|
|
1781
|
+
throw new Error("diagnoseLiveFormCommitControl requires a Playwright page with evaluate().");
|
|
1782
|
+
}
|
|
1783
|
+
return page.evaluate((commitOptions) => {
|
|
1784
|
+
const element = document.querySelector(commitOptions.selector);
|
|
1785
|
+
if (!(element instanceof HTMLElement)) {
|
|
1786
|
+
return {
|
|
1787
|
+
selector: commitOptions.selector,
|
|
1788
|
+
exists: false,
|
|
1789
|
+
visible: null,
|
|
1790
|
+
disabled: null,
|
|
1791
|
+
ariaDisabled: null,
|
|
1792
|
+
text: null,
|
|
1793
|
+
reason: "missing",
|
|
1794
|
+
ready: false,
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const style = getComputedStyle(element);
|
|
1798
|
+
const rect = element.getBoundingClientRect();
|
|
1799
|
+
const disabled = "disabled" in element ? Boolean(element.disabled) : element.getAttribute("disabled") !== null;
|
|
1800
|
+
const ariaDisabled = element.getAttribute("aria-disabled") === "true";
|
|
1801
|
+
const visible = style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity) !== 0 && rect.width > 0 && rect.height > 0;
|
|
1802
|
+
const reason = !visible ? (style.display === "none" ? "display-none" : style.visibility === "hidden" ? "visibility-hidden" : rect.width <= 0 || rect.height <= 0 ? "zero-size" : "transparent")
|
|
1803
|
+
: disabled ? "disabled"
|
|
1804
|
+
: ariaDisabled ? "aria-disabled"
|
|
1805
|
+
: null;
|
|
1806
|
+
return {
|
|
1807
|
+
selector: commitOptions.selector,
|
|
1808
|
+
exists: true,
|
|
1809
|
+
visible,
|
|
1810
|
+
disabled,
|
|
1811
|
+
ariaDisabled,
|
|
1812
|
+
text: element.textContent?.replace(/\s+/g, " ").trim() ?? "",
|
|
1813
|
+
reason,
|
|
1814
|
+
ready: visible && !disabled && !ariaDisabled,
|
|
1815
|
+
};
|
|
1816
|
+
}, options);
|
|
1817
|
+
}
|
|
1699
1818
|
export async function dispatchLiveFormFieldDomEvents(page, options) {
|
|
1700
1819
|
if (!hasEvaluate(page)) {
|
|
1701
1820
|
throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
|
|
@@ -3231,21 +3350,89 @@ export async function performLiveFormFieldTransaction(page, options) {
|
|
|
3231
3350
|
afterQuiet: options.afterQuiet ?? true,
|
|
3232
3351
|
});
|
|
3233
3352
|
}
|
|
3234
|
-
export async function
|
|
3235
|
-
const
|
|
3353
|
+
export async function setLiveFormCheckboxValues(page, options) {
|
|
3354
|
+
const payload = normalizeLiveFormCheckboxValue(options.values);
|
|
3355
|
+
const values = readLiveFormCheckboxValues(payload);
|
|
3236
3356
|
const transaction = await performLiveFormFieldTransaction(page, {
|
|
3237
3357
|
write: {
|
|
3238
3358
|
...(options.write ?? {}),
|
|
3239
3359
|
target: options.target,
|
|
3240
|
-
value:
|
|
3360
|
+
value: payload,
|
|
3241
3361
|
notify: options.write?.notify ?? true,
|
|
3242
3362
|
},
|
|
3243
|
-
|
|
3363
|
+
fieldKind: "checkbox",
|
|
3244
3364
|
waitForEvent: options.waitForEvent ?? true,
|
|
3245
3365
|
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
3246
|
-
afterQuiet: options.afterQuiet ??
|
|
3247
|
-
...(
|
|
3366
|
+
afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
|
|
3367
|
+
...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
|
|
3368
|
+
});
|
|
3369
|
+
const dom = await waitForLiveFormCheckboxState(page, {
|
|
3370
|
+
...options.target,
|
|
3371
|
+
expectedValues: values,
|
|
3372
|
+
exact: options.exactDomSelection ?? true,
|
|
3373
|
+
...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
|
|
3248
3374
|
});
|
|
3375
|
+
let commitControl;
|
|
3376
|
+
let clickedCommit = false;
|
|
3377
|
+
if (options.commitSelector) {
|
|
3378
|
+
const deadline = Date.now() + (options.commitTimeoutMs ?? 5_000);
|
|
3379
|
+
commitControl = await diagnoseLiveFormCommitControl(page, { selector: options.commitSelector });
|
|
3380
|
+
while (!commitControl.ready && Date.now() < deadline) {
|
|
3381
|
+
await delay(100);
|
|
3382
|
+
commitControl = await diagnoseLiveFormCommitControl(page, { selector: options.commitSelector });
|
|
3383
|
+
}
|
|
3384
|
+
if (options.clickCommit === true) {
|
|
3385
|
+
if (!commitControl.ready) {
|
|
3386
|
+
throw new Error(`Checkbox commit control "${options.commitSelector}" is not ready after selecting ${values.length} value(s): ${commitControl.reason ?? "unknown"}.`);
|
|
3387
|
+
}
|
|
3388
|
+
await page.locator(options.commitSelector).first().click({ timeout: options.commitTimeoutMs ?? 5_000 });
|
|
3389
|
+
clickedCommit = true;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return {
|
|
3393
|
+
payload,
|
|
3394
|
+
values,
|
|
3395
|
+
transaction,
|
|
3396
|
+
dom,
|
|
3397
|
+
...(commitControl ? { commitControl } : {}),
|
|
3398
|
+
clickedCommit,
|
|
3399
|
+
ok: transaction.write.valueMatched &&
|
|
3400
|
+
(transaction.event?.matched ?? true) &&
|
|
3401
|
+
(transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= values.length : true) &&
|
|
3402
|
+
(transaction.quiet?.quiet ?? true) &&
|
|
3403
|
+
((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
|
|
3404
|
+
dom.matched &&
|
|
3405
|
+
(commitControl?.ready ?? true),
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
export async function answerProgressiveLiveFormField(page, options) {
|
|
3409
|
+
const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
|
|
3410
|
+
const checkboxTransaction = options.fieldKind === "checkbox"
|
|
3411
|
+
? await setLiveFormCheckboxValues(page, {
|
|
3412
|
+
target: options.target,
|
|
3413
|
+
values: options.value,
|
|
3414
|
+
...(options.write === undefined ? {} : { write: options.write }),
|
|
3415
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
3416
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
3417
|
+
afterQuiet: options.afterQuiet ?? true,
|
|
3418
|
+
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
3419
|
+
})
|
|
3420
|
+
: undefined;
|
|
3421
|
+
const transaction = checkboxTransaction
|
|
3422
|
+
? checkboxTransaction.transaction
|
|
3423
|
+
: await performLiveFormFieldTransaction(page, {
|
|
3424
|
+
write: {
|
|
3425
|
+
...(options.write ?? {}),
|
|
3426
|
+
target: options.target,
|
|
3427
|
+
value: options.value,
|
|
3428
|
+
notify: options.write?.notify ?? true,
|
|
3429
|
+
},
|
|
3430
|
+
...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
|
|
3431
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
3432
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
3433
|
+
afterQuiet: options.afterQuiet ?? true,
|
|
3434
|
+
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
3435
|
+
});
|
|
3249
3436
|
const visibilityTargets = [
|
|
3250
3437
|
...(options.expectVisible ?? []),
|
|
3251
3438
|
...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
|
|
@@ -3450,8 +3637,8 @@ export function normalizeLiveFormSampleValue(kind = "unknown", value) {
|
|
|
3450
3637
|
if (["radio", "rating", "select"].includes(kind) && typeof value === "string") {
|
|
3451
3638
|
return { value };
|
|
3452
3639
|
}
|
|
3453
|
-
if (kind === "checkbox" && Array.isArray(value)) {
|
|
3454
|
-
return
|
|
3640
|
+
if (kind === "checkbox" && (Array.isArray(value) || (value && typeof value === "object"))) {
|
|
3641
|
+
return normalizeLiveFormCheckboxValue(value);
|
|
3455
3642
|
}
|
|
3456
3643
|
if (kind === "checkbox" && typeof value === "boolean") {
|
|
3457
3644
|
return { checked: value };
|
|
@@ -3637,7 +3824,12 @@ export async function runLiveFormProgressiveSampleFlow(page, definition, sampleV
|
|
|
3637
3824
|
...(step.expectResetAfter === undefined ? {} : { expectReset: step.expectResetAfter }),
|
|
3638
3825
|
}));
|
|
3639
3826
|
if (step.continueSelector) {
|
|
3640
|
-
|
|
3827
|
+
try {
|
|
3828
|
+
await page.locator(step.continueSelector).first().click({ timeout: 5_000 });
|
|
3829
|
+
}
|
|
3830
|
+
catch (error) {
|
|
3831
|
+
throw new Error(`Progressive step "${step.key}" could not click continue selector "${step.continueSelector}". The control may still be disabled because the preceding field value did not trigger Formstack side effects. Cause: ${error.message}`);
|
|
3832
|
+
}
|
|
3641
3833
|
const continueVisibilityTargets = [
|
|
3642
3834
|
...(step.expectVisibleAfterContinue ?? []).map((target) => ({ ...target, visible: true })),
|
|
3643
3835
|
...(step.expectHiddenAfterContinue ?? []).map((target) => ({ ...target, visible: false })),
|
|
@@ -4664,7 +4856,19 @@ export const liveFormFieldRecipes = {
|
|
|
4664
4856
|
setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
|
|
4665
4857
|
setChoiceApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormChoiceValue(value), options),
|
|
4666
4858
|
setSelectApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormSelectValue(value), options),
|
|
4667
|
-
setCheckboxesApi: (name, internalLabel, values, options) =>
|
|
4859
|
+
setCheckboxesApi: (name, internalLabel, values, options = {}) => ({
|
|
4860
|
+
name,
|
|
4861
|
+
run: async (harness) => {
|
|
4862
|
+
await setLiveFormCheckboxValues(harness.page, {
|
|
4863
|
+
target: { internalLabel },
|
|
4864
|
+
values,
|
|
4865
|
+
write: { notify: options.notify ?? true },
|
|
4866
|
+
waitForEvent: options.waitForEvent ?? (options.notify ?? true),
|
|
4867
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4868
|
+
afterQuiet: options.waitForQuiet ?? true,
|
|
4869
|
+
});
|
|
4870
|
+
},
|
|
4871
|
+
}),
|
|
4668
4872
|
setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
|
|
4669
4873
|
setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
|
|
4670
4874
|
setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
|