@redrockswebdevelopment/formstack-form-testing 0.1.39 → 0.1.41

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
@@ -60,7 +60,7 @@ export const liveFormFieldTypeCoverage = {
60
60
  domInteraction: false,
61
61
  domAssertion: true,
62
62
  reason: "Description fields do not collect user values; test rendered content, visibility, and styling.",
63
- recommendedHelpers: ["snapshotLiveFormDomFields", "waitForLiveFormVisibility"],
63
+ recommendedHelpers: ["readLiveFormStructuralState", "waitForLiveFormStructuralState", "liveFormScenarioSteps.expectStructuralField"],
64
64
  },
65
65
  embed: {
66
66
  kind: "embed",
@@ -69,7 +69,7 @@ export const liveFormFieldTypeCoverage = {
69
69
  domInteraction: false,
70
70
  domAssertion: true,
71
71
  reason: "Embed fields generally provide scripts/styles/markup; test side effects and diagnostics, not value writes.",
72
- recommendedHelpers: ["createFormBrowserMonitor", "waitForConsoleMessage", "waitForWindowFlag", "snapshotLiveFormDomFields"],
72
+ recommendedHelpers: ["readLiveFormStructuralState", "createFormBrowserMonitor", "waitForConsoleMessage", "waitForWindowFlag"],
73
73
  },
74
74
  email: {
75
75
  kind: "email",
@@ -123,7 +123,7 @@ export const liveFormFieldTypeCoverage = {
123
123
  domInteraction: true,
124
124
  domAssertion: true,
125
125
  reason: "Page breaks do not collect values; test navigation, visible page state, and progress UI.",
126
- recommendedHelpers: ["clickNext", "clickPrevious", "waitForLiveFormVisibility"],
126
+ recommendedHelpers: ["readLiveFormStructuralState", "clickNext", "clickPrevious", "waitForLiveFormVisibility"],
127
127
  },
128
128
  payment: {
129
129
  kind: "payment",
@@ -178,7 +178,7 @@ export const liveFormFieldTypeCoverage = {
178
178
  domInteraction: false,
179
179
  domAssertion: true,
180
180
  reason: "Sections group content and logic visibility; test rendered content and conditional visibility.",
181
- recommendedHelpers: ["snapshotLiveFormDomFields", "waitForLiveFormVisibility"],
181
+ recommendedHelpers: ["readLiveFormStructuralState", "waitForLiveFormStructuralState", "liveFormScenarioSteps.expectStructuralField"],
182
182
  },
183
183
  select: {
184
184
  kind: "select",
@@ -1853,6 +1853,112 @@ export async function waitForLiveFormDomFieldState(page, options) {
1853
1853
  }
1854
1854
  return latest;
1855
1855
  }
1856
+ export async function readLiveFormStructuralState(page, options) {
1857
+ if (!hasEvaluate(page)) {
1858
+ throw new Error("readLiveFormStructuralState requires a Playwright page with evaluate().");
1859
+ }
1860
+ return page.evaluate((stateOptions) => {
1861
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
1862
+ const controlSnapshot = (control) => {
1863
+ const input = control;
1864
+ return {
1865
+ tagName: control.tagName.toLowerCase(),
1866
+ type: control.getAttribute("type"),
1867
+ id: control.getAttribute("id"),
1868
+ name: control.getAttribute("name"),
1869
+ value: control.getAttribute("value"),
1870
+ currentValue: "value" in input ? input.value : null,
1871
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
1872
+ };
1873
+ };
1874
+ const inferKind = (element) => {
1875
+ if (!element)
1876
+ return "unknown";
1877
+ const internalLabel = element.getAttribute("data-internal-label")?.toLowerCase() ?? "";
1878
+ const id = element.getAttribute("id")?.toLowerCase() ?? "";
1879
+ const classes = Array.from(element.classList).join(" ").toLowerCase();
1880
+ const dataTestId = element.getAttribute("data-testid")?.toLowerCase() ?? "";
1881
+ const text = normalizeText(element.textContent).toLowerCase();
1882
+ if (internalLabel.includes("code_embed") || text.startsWith(":root{") || text.includes("<script"))
1883
+ return "embed";
1884
+ if (classes.includes("pagebreak") || classes.includes("page-break") || dataTestId.includes("pagebreak") || id.includes("pagebreak"))
1885
+ return "pagebreak";
1886
+ if (classes.includes("description") ||
1887
+ classes.includes("richtext") ||
1888
+ internalLabel.includes("description") ||
1889
+ internalLabel.includes("richtext") ||
1890
+ dataTestId.includes("description") ||
1891
+ dataTestId.includes("richtext"))
1892
+ return "description";
1893
+ if (classes.includes("section") || id.includes("section") || element.querySelector(".fsSectionHeader, [class*='SectionHeader']"))
1894
+ return "section";
1895
+ return "unknown";
1896
+ };
1897
+ const isVisible = (element) => {
1898
+ if (!element || !(element instanceof HTMLElement))
1899
+ return false;
1900
+ const style = window.getComputedStyle(element);
1901
+ return style.display !== "none" &&
1902
+ style.visibility !== "hidden" &&
1903
+ style.opacity !== "0" &&
1904
+ element.getClientRects().length > 0;
1905
+ };
1906
+ const root = stateOptions.selector != null
1907
+ ? document.querySelector(stateOptions.selector)
1908
+ : stateOptions.internalLabel != null
1909
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
1910
+ : stateOptions.fieldId == null
1911
+ ? null
1912
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
1913
+ const controls = root
1914
+ ? Array.from(root.querySelectorAll("input, textarea, select")).map(controlSnapshot)
1915
+ : [];
1916
+ const text = normalizeText(root?.textContent);
1917
+ const inferredKind = inferKind(root);
1918
+ const expectedText = stateOptions.expectedText;
1919
+ const textMatched = expectedText === undefined
1920
+ ? true
1921
+ : typeof expectedText === "string"
1922
+ ? text.includes(expectedText)
1923
+ : expectedText.test(text);
1924
+ const expectedVisible = stateOptions.expectedVisible;
1925
+ const visible = isVisible(root);
1926
+ const visibilityMatched = expectedVisible === undefined || visible === expectedVisible;
1927
+ const kindMatched = stateOptions.expectedKind === undefined || inferredKind === stateOptions.expectedKind;
1928
+ const controlsMatched = stateOptions.allowControls === true ||
1929
+ (stateOptions.allowControls !== false && inferredKind === "section") ||
1930
+ controls.length === 0;
1931
+ return {
1932
+ matched: Boolean(root) && kindMatched && textMatched && visibilityMatched && controlsMatched,
1933
+ elementMatched: Boolean(root),
1934
+ id: root?.getAttribute("id") ?? null,
1935
+ internalLabel: root?.getAttribute("data-internal-label") ?? null,
1936
+ inferredKind,
1937
+ visible,
1938
+ text,
1939
+ controlCount: controls.length,
1940
+ controls,
1941
+ ...(stateOptions.expectedKind === undefined ? {} : { expectedKind: stateOptions.expectedKind }),
1942
+ ...(expectedText === undefined ? {} : { expectedText: String(expectedText) }),
1943
+ ...(expectedVisible === undefined ? {} : { expectedVisible }),
1944
+ kindMatched,
1945
+ textMatched,
1946
+ visibilityMatched,
1947
+ controlsMatched,
1948
+ };
1949
+ }, options);
1950
+ }
1951
+ export async function waitForLiveFormStructuralState(page, options) {
1952
+ const timeoutMs = options.timeoutMs ?? 10_000;
1953
+ const intervalMs = options.intervalMs ?? 100;
1954
+ const startedAt = Date.now();
1955
+ let latest = await readLiveFormStructuralState(page, options);
1956
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
1957
+ await delay(intervalMs);
1958
+ latest = await readLiveFormStructuralState(page, options);
1959
+ }
1960
+ return latest;
1961
+ }
1856
1962
  export async function readLiveFormProductState(page, options) {
1857
1963
  if (!hasEvaluate(page)) {
1858
1964
  throw new Error("readLiveFormProductState requires a Playwright page with evaluate().");
@@ -3339,10 +3445,18 @@ export async function discoverLiveFormFieldShapes(page, options = {}) {
3339
3445
  if (classes.includes("pagebreak") || classes.includes("page-break") || dataTestId.includes("pagebreak") || id.includes("pagebreak")) {
3340
3446
  return "pagebreak";
3341
3447
  }
3342
- if (classes.includes("description") || internalLabel.includes("description") || dataTestId.includes("description")) {
3448
+ if (classes.includes("description") ||
3449
+ classes.includes("richtext") ||
3450
+ internalLabel.includes("description") ||
3451
+ internalLabel.includes("richtext") ||
3452
+ dataTestId.includes("description") ||
3453
+ dataTestId.includes("richtext")) {
3343
3454
  return "description";
3344
3455
  }
3345
- return "section";
3456
+ if (classes.includes("section") || id.includes("section") || element.querySelector(".fsSectionHeader, [class*='SectionHeader']")) {
3457
+ return "section";
3458
+ }
3459
+ return "description";
3346
3460
  }
3347
3461
  if (classes.includes("matrix") || internalLabel.includes("matrix") || element.querySelector("[id^='matrix-row-'], table"))
3348
3462
  return "matrix";
@@ -6365,6 +6479,15 @@ export const liveFormScenarioSteps = {
6365
6479
  }
6366
6480
  },
6367
6481
  }),
6482
+ expectStructuralField: (name, options) => ({
6483
+ name,
6484
+ run: async (harness) => {
6485
+ const state = await waitForLiveFormStructuralState(harness.page, options);
6486
+ if (!state.matched) {
6487
+ throw new Error(`Expected structural form element ${options.internalLabel ?? options.fieldId ?? options.selector ?? "unknown"} to match.`);
6488
+ }
6489
+ },
6490
+ }),
6368
6491
  expectValidationErrors: (name, options = {}) => ({
6369
6492
  name,
6370
6493
  run: async (harness) => {