@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.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 and amount UI can diverge by product configuration.",
154
- recommendedHelpers: ["buildLiveFormProductValue", "setLiveFormFieldValueTransaction", "fillProduct", "readLiveFormDomFieldState"],
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 ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "rating"
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
- : ratingTransaction
4245
- ? ratingTransaction.transaction
4246
- : dateTimeTransaction
4247
- ? dateTimeTransaction.transaction
4248
- : await performLiveFormFieldTransaction(page, {
4249
- write: {
4250
- ...(options.write ?? {}),
4251
- target: options.target,
4252
- value: options.value,
4253
- notify: options.write?.notify ?? true,
4254
- },
4255
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4256
- waitForEvent: options.waitForEvent ?? true,
4257
- dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4258
- afterQuiet: options.afterQuiet ?? true,
4259
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
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) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(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" }),