@redrockswebdevelopment/formstack-form-testing 0.1.33 → 0.1.34
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 +51 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +207 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -150,8 +150,8 @@ export const liveFormFieldTypeCoverage = {
|
|
|
150
150
|
apiWrite: true,
|
|
151
151
|
domInteraction: true,
|
|
152
152
|
domAssertion: true,
|
|
153
|
-
reason: "Product API state can be set, but visible quantity
|
|
154
|
-
recommendedHelpers: ["buildLiveFormProductValue", "
|
|
153
|
+
reason: "Product API state can be set, but visible quantity, fixed amount, and selected product UI can diverge by product configuration.",
|
|
154
|
+
recommendedHelpers: ["buildLiveFormProductValue", "setLiveFormProductValue", "readLiveFormProductState", "fillProduct"],
|
|
155
155
|
},
|
|
156
156
|
radio: {
|
|
157
157
|
kind: "radio",
|
|
@@ -1030,6 +1030,13 @@ export function buildLiveFormProductValue(value, current = {}) {
|
|
|
1030
1030
|
...value,
|
|
1031
1031
|
};
|
|
1032
1032
|
}
|
|
1033
|
+
export function normalizeLiveFormProductValue(value, current = {}) {
|
|
1034
|
+
return buildLiveFormProductValue({
|
|
1035
|
+
...(value.fixedAmount === undefined ? {} : { fixedAmount: String(value.fixedAmount) }),
|
|
1036
|
+
...(value.quantity === undefined ? {} : { quantity: String(value.quantity) }),
|
|
1037
|
+
...("value" in value && value.value !== undefined ? { value: String(value.value) } : {}),
|
|
1038
|
+
}, current);
|
|
1039
|
+
}
|
|
1033
1040
|
export function buildLiveFormRatingValue(value) {
|
|
1034
1041
|
return { value: String(value) };
|
|
1035
1042
|
}
|
|
@@ -1827,6 +1834,119 @@ export async function waitForLiveFormDomFieldState(page, options) {
|
|
|
1827
1834
|
}
|
|
1828
1835
|
return latest;
|
|
1829
1836
|
}
|
|
1837
|
+
export async function readLiveFormProductState(page, options) {
|
|
1838
|
+
if (!hasEvaluate(page)) {
|
|
1839
|
+
throw new Error("readLiveFormProductState requires a Playwright page with evaluate().");
|
|
1840
|
+
}
|
|
1841
|
+
return page.evaluate((stateOptions) => {
|
|
1842
|
+
const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
1843
|
+
const inferPart = (control) => {
|
|
1844
|
+
const label = control.id ? document.querySelector(`label[for="${CSS.escape(control.id)}"]`) : null;
|
|
1845
|
+
const haystack = [
|
|
1846
|
+
control.name,
|
|
1847
|
+
control.id,
|
|
1848
|
+
control.className,
|
|
1849
|
+
control.getAttribute("placeholder"),
|
|
1850
|
+
control.getAttribute("aria-label"),
|
|
1851
|
+
label?.textContent,
|
|
1852
|
+
].map(normalizeText).join(" ").toLowerCase();
|
|
1853
|
+
if (/\b(quantity|qty|\[quantity\]|-quantity$|_quantity$)/i.test(haystack))
|
|
1854
|
+
return "quantity";
|
|
1855
|
+
if (/\b(fixed amount|fixedamount|amount|price|\[fixedamount\]|-fixedamount$|_fixedamount$)/i.test(haystack))
|
|
1856
|
+
return "fixedAmount";
|
|
1857
|
+
if (/\b(product|option|value|\[value\]|-value$|_value$)/i.test(haystack))
|
|
1858
|
+
return "value";
|
|
1859
|
+
return null;
|
|
1860
|
+
};
|
|
1861
|
+
const root = stateOptions.selector != null
|
|
1862
|
+
? document.querySelector(stateOptions.selector)
|
|
1863
|
+
: stateOptions.internalLabel != null
|
|
1864
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
|
|
1865
|
+
: stateOptions.fieldId == null
|
|
1866
|
+
? null
|
|
1867
|
+
: document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
|
|
1868
|
+
const nativeControls = root
|
|
1869
|
+
? Array.from(root.querySelectorAll("input, textarea, select"))
|
|
1870
|
+
: [];
|
|
1871
|
+
const controls = nativeControls.map((control) => {
|
|
1872
|
+
const selected = control instanceof HTMLSelectElement
|
|
1873
|
+
? control.selectedIndex >= 0
|
|
1874
|
+
: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type)
|
|
1875
|
+
? control.checked
|
|
1876
|
+
: null;
|
|
1877
|
+
return {
|
|
1878
|
+
tagName: control.tagName.toLowerCase(),
|
|
1879
|
+
type: control.getAttribute("type"),
|
|
1880
|
+
id: control.getAttribute("id"),
|
|
1881
|
+
name: control.getAttribute("name"),
|
|
1882
|
+
value: control.getAttribute("value"),
|
|
1883
|
+
currentValue: "value" in control ? control.value : "",
|
|
1884
|
+
checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
|
|
1885
|
+
part: inferPart(control),
|
|
1886
|
+
text: control instanceof HTMLSelectElement
|
|
1887
|
+
? normalizeText(control.selectedOptions.item(0)?.textContent)
|
|
1888
|
+
: normalizeText(control.textContent),
|
|
1889
|
+
selected,
|
|
1890
|
+
disabled: "disabled" in control ? Boolean(control.disabled) : null,
|
|
1891
|
+
};
|
|
1892
|
+
});
|
|
1893
|
+
const activeControls = controls.filter((control) => control.disabled !== true &&
|
|
1894
|
+
((control.tagName === "select" && (control.currentValue ?? "").length > 0) ||
|
|
1895
|
+
(["checkbox", "radio"].includes(control.type ?? "") ? control.checked === true : (control.currentValue ?? "").length > 0)));
|
|
1896
|
+
const findPart = (part) => activeControls.find((control) => control.part === part)?.currentValue ?? null;
|
|
1897
|
+
const selectedValue = findPart("value") ?? activeControls.find((control) => ["checkbox", "radio"].includes(control.type ?? "") && control.checked === true)?.currentValue ?? null;
|
|
1898
|
+
const quantity = findPart("quantity");
|
|
1899
|
+
const fixedAmount = findPart("fixedAmount");
|
|
1900
|
+
const amountText = root
|
|
1901
|
+
? Array.from(root.querySelectorAll("[class*=\"amount\"], [class*=\"price\"], [data-testid*=\"amount\"], [data-testid*=\"price\"]"))
|
|
1902
|
+
.map((element) => normalizeText(element.textContent))
|
|
1903
|
+
.find((text) => text.length > 0 && /[$€£]|\d/.test(text)) ?? null
|
|
1904
|
+
: null;
|
|
1905
|
+
const expected = stateOptions.expectedValue ?? {};
|
|
1906
|
+
const expectedEntries = Object.entries(expected).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
|
|
1907
|
+
const selectedParts = {
|
|
1908
|
+
value: selectedValue,
|
|
1909
|
+
quantity,
|
|
1910
|
+
fixedAmount,
|
|
1911
|
+
};
|
|
1912
|
+
const renderedExpectedEntries = expectedEntries.filter(([part]) => selectedParts[part] !== null);
|
|
1913
|
+
const unavailableParts = expectedEntries
|
|
1914
|
+
.filter(([part]) => selectedParts[part] === null)
|
|
1915
|
+
.map(([part]) => part);
|
|
1916
|
+
const missingParts = renderedExpectedEntries
|
|
1917
|
+
.filter(([part]) => selectedParts[part] === "")
|
|
1918
|
+
.map(([part]) => part);
|
|
1919
|
+
const mismatchedParts = renderedExpectedEntries
|
|
1920
|
+
.filter(([part, value]) => selectedParts[part] !== "" && selectedParts[part] !== value)
|
|
1921
|
+
.map(([part]) => part);
|
|
1922
|
+
const renderedValueAvailable = renderedExpectedEntries.length > 0 || activeControls.length > 0 || amountText !== null;
|
|
1923
|
+
return {
|
|
1924
|
+
fieldMatched: Boolean(root),
|
|
1925
|
+
selectedValue,
|
|
1926
|
+
quantity,
|
|
1927
|
+
fixedAmount,
|
|
1928
|
+
amountText,
|
|
1929
|
+
...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
|
|
1930
|
+
renderedValueAvailable,
|
|
1931
|
+
unavailableParts,
|
|
1932
|
+
missingParts,
|
|
1933
|
+
mismatchedParts,
|
|
1934
|
+
matched: Boolean(root) && missingParts.length === 0 && mismatchedParts.length === 0,
|
|
1935
|
+
controls,
|
|
1936
|
+
};
|
|
1937
|
+
}, options);
|
|
1938
|
+
}
|
|
1939
|
+
export async function waitForLiveFormProductState(page, options) {
|
|
1940
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
1941
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
1942
|
+
const startedAt = Date.now();
|
|
1943
|
+
let latest = await readLiveFormProductState(page, options);
|
|
1944
|
+
while (!latest.matched && Date.now() - startedAt < timeoutMs) {
|
|
1945
|
+
await delay(intervalMs);
|
|
1946
|
+
latest = await readLiveFormProductState(page, options);
|
|
1947
|
+
}
|
|
1948
|
+
return latest;
|
|
1949
|
+
}
|
|
1830
1950
|
export async function readLiveFormCheckboxState(page, options) {
|
|
1831
1951
|
if (!hasEvaluate(page)) {
|
|
1832
1952
|
throw new Error("readLiveFormCheckboxState requires a Playwright page with evaluate().");
|
|
@@ -4081,6 +4201,47 @@ export async function setLiveFormMatrixValues(page, options) {
|
|
|
4081
4201
|
(visibility?.matched ?? true),
|
|
4082
4202
|
};
|
|
4083
4203
|
}
|
|
4204
|
+
export async function setLiveFormProductValue(page, options) {
|
|
4205
|
+
const payload = normalizeLiveFormProductValue(options.value, options.current);
|
|
4206
|
+
const transaction = await performLiveFormFieldTransaction(page, {
|
|
4207
|
+
write: {
|
|
4208
|
+
...(options.write ?? {}),
|
|
4209
|
+
target: options.target,
|
|
4210
|
+
value: payload,
|
|
4211
|
+
notify: options.write?.notify ?? true,
|
|
4212
|
+
},
|
|
4213
|
+
fieldKind: "product",
|
|
4214
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
4215
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4216
|
+
afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
|
|
4217
|
+
...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
|
|
4218
|
+
});
|
|
4219
|
+
const dom = await waitForLiveFormProductState(page, {
|
|
4220
|
+
...options.target,
|
|
4221
|
+
expectedValue: payload,
|
|
4222
|
+
...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
|
|
4223
|
+
});
|
|
4224
|
+
const visibilityTargets = [
|
|
4225
|
+
...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
|
|
4226
|
+
...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
|
|
4227
|
+
];
|
|
4228
|
+
const visibility = visibilityTargets.length > 0
|
|
4229
|
+
? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
|
|
4230
|
+
: undefined;
|
|
4231
|
+
return {
|
|
4232
|
+
payload,
|
|
4233
|
+
transaction,
|
|
4234
|
+
dom,
|
|
4235
|
+
...(visibility ? { visibility } : {}),
|
|
4236
|
+
ok: transaction.write.valueMatched &&
|
|
4237
|
+
(transaction.event?.matched ?? true) &&
|
|
4238
|
+
(transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
|
|
4239
|
+
(transaction.quiet?.quiet ?? true) &&
|
|
4240
|
+
((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
|
|
4241
|
+
dom.matched &&
|
|
4242
|
+
(visibility?.matched ?? true),
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4084
4245
|
export async function setLiveFormRatingValue(page, options) {
|
|
4085
4246
|
const payload = normalizeLiveFormRatingValue(options.value);
|
|
4086
4247
|
const value = payload.value;
|
|
@@ -4211,7 +4372,18 @@ export async function answerProgressiveLiveFormField(page, options) {
|
|
|
4211
4372
|
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
4212
4373
|
})
|
|
4213
4374
|
: undefined;
|
|
4214
|
-
const
|
|
4375
|
+
const productTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "product"
|
|
4376
|
+
? await setLiveFormProductValue(page, {
|
|
4377
|
+
target: options.target,
|
|
4378
|
+
value: options.value,
|
|
4379
|
+
...(options.write === undefined ? {} : { write: options.write }),
|
|
4380
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
4381
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4382
|
+
afterQuiet: options.afterQuiet ?? true,
|
|
4383
|
+
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
4384
|
+
})
|
|
4385
|
+
: undefined;
|
|
4386
|
+
const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !productTransaction && options.fieldKind === "rating"
|
|
4215
4387
|
? await setLiveFormRatingValue(page, {
|
|
4216
4388
|
target: options.target,
|
|
4217
4389
|
value: options.value,
|
|
@@ -4241,23 +4413,25 @@ export async function answerProgressiveLiveFormField(page, options) {
|
|
|
4241
4413
|
? selectTransaction.transaction
|
|
4242
4414
|
: matrixTransaction
|
|
4243
4415
|
? matrixTransaction.transaction
|
|
4244
|
-
:
|
|
4245
|
-
?
|
|
4246
|
-
:
|
|
4247
|
-
?
|
|
4248
|
-
:
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4416
|
+
: productTransaction
|
|
4417
|
+
? productTransaction.transaction
|
|
4418
|
+
: ratingTransaction
|
|
4419
|
+
? ratingTransaction.transaction
|
|
4420
|
+
: dateTimeTransaction
|
|
4421
|
+
? dateTimeTransaction.transaction
|
|
4422
|
+
: await performLiveFormFieldTransaction(page, {
|
|
4423
|
+
write: {
|
|
4424
|
+
...(options.write ?? {}),
|
|
4425
|
+
target: options.target,
|
|
4426
|
+
value: options.value,
|
|
4427
|
+
notify: options.write?.notify ?? true,
|
|
4428
|
+
},
|
|
4429
|
+
...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
|
|
4430
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
4431
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4432
|
+
afterQuiet: options.afterQuiet ?? true,
|
|
4433
|
+
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
4434
|
+
});
|
|
4261
4435
|
const visibilityTargets = [
|
|
4262
4436
|
...(options.expectVisible ?? []),
|
|
4263
4437
|
...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
|
|
@@ -6041,7 +6215,19 @@ export const liveFormFieldRecipes = {
|
|
|
6041
6215
|
});
|
|
6042
6216
|
},
|
|
6043
6217
|
}),
|
|
6044
|
-
setProductApi: (name, internalLabel, value, options) =>
|
|
6218
|
+
setProductApi: (name, internalLabel, value, options = {}) => ({
|
|
6219
|
+
name,
|
|
6220
|
+
run: async (harness) => {
|
|
6221
|
+
await setLiveFormProductValue(harness.page, {
|
|
6222
|
+
target: { internalLabel },
|
|
6223
|
+
value,
|
|
6224
|
+
write: { notify: options.notify ?? true },
|
|
6225
|
+
waitForEvent: options.waitForEvent ?? (options.notify ?? true),
|
|
6226
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
6227
|
+
afterQuiet: options.waitForQuiet ?? true,
|
|
6228
|
+
});
|
|
6229
|
+
},
|
|
6230
|
+
}),
|
|
6045
6231
|
setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),
|
|
6046
6232
|
setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "name", dispatchDomEvents: "auto" }),
|
|
6047
6233
|
setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "radio", dispatchDomEvents: "auto" }),
|