@redrockswebdevelopment/formstack-form-testing 0.1.25 → 0.1.27

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
@@ -987,6 +987,22 @@ export function buildLiveFormMatrixValue(selections) {
987
987
  }),
988
988
  };
989
989
  }
990
+ export function readLiveFormMatrixSelections(value) {
991
+ if (Array.isArray(value)) {
992
+ return value.filter((entry) => typeof entry === "string");
993
+ }
994
+ if (!value || typeof value !== "object")
995
+ return [];
996
+ const selections = value.value;
997
+ return Array.isArray(selections)
998
+ ? selections.filter((entry) => typeof entry === "string")
999
+ : [];
1000
+ }
1001
+ export function normalizeLiveFormMatrixValue(selections) {
1002
+ if (Array.isArray(selections))
1003
+ return buildLiveFormMatrixValue(selections);
1004
+ return buildLiveFormMatrixValue(readLiveFormMatrixSelections(selections));
1005
+ }
990
1006
  export async function readLiveFormFieldValue(page, options) {
991
1007
  if (!hasEvaluate(page)) {
992
1008
  throw new Error("readLiveFormFieldValue requires a Playwright page with evaluate().");
@@ -1959,6 +1975,108 @@ export async function waitForLiveFormSelectState(page, options) {
1959
1975
  }
1960
1976
  return latest;
1961
1977
  }
1978
+ export async function readLiveFormMatrixState(page, options) {
1979
+ if (!hasEvaluate(page)) {
1980
+ throw new Error("readLiveFormMatrixState requires a Playwright page with evaluate().");
1981
+ }
1982
+ return page.evaluate((stateOptions) => {
1983
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
1984
+ const controlSnapshot = (control) => ({
1985
+ tagName: control.tagName.toLowerCase(),
1986
+ type: control.getAttribute("type"),
1987
+ id: control.getAttribute("id"),
1988
+ name: control.getAttribute("name"),
1989
+ value: control.getAttribute("value"),
1990
+ currentValue: control.value,
1991
+ checked: control.checked,
1992
+ });
1993
+ const root = stateOptions.selector != null
1994
+ ? document.querySelector(stateOptions.selector)
1995
+ : stateOptions.internalLabel != null
1996
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
1997
+ : stateOptions.fieldId == null
1998
+ ? null
1999
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
2000
+ const controls = root
2001
+ ? Array.from(root.querySelectorAll("input[type=\"radio\"], input[type=\"checkbox\"]"))
2002
+ : [];
2003
+ const inferCell = (control) => {
2004
+ const tableCell = control.closest("td, th");
2005
+ const tableRow = control.closest("tr");
2006
+ const table = control.closest("table");
2007
+ const rowHeader = tableRow
2008
+ ? tableRow.querySelector("th") ??
2009
+ Array.from(tableRow.children).find((child) => !child.contains(control))
2010
+ : null;
2011
+ const columnIndex = tableCell?.cellIndex ?? -1;
2012
+ const headerRows = table ? Array.from(table.querySelectorAll("thead tr, tr")).filter((row) => !row.contains(control)) : [];
2013
+ const columnHeader = columnIndex >= 0
2014
+ ? headerRows
2015
+ .map((row) => row.children.item(columnIndex))
2016
+ .find((cell) => cell && normalizeText(cell.textContent).length > 0) ?? null
2017
+ : null;
2018
+ const ariaLabel = control.getAttribute("aria-label");
2019
+ const label = control.id ? root?.querySelector(`label[for="${CSS.escape(control.id)}"]`) : null;
2020
+ const row = normalizeText(control.getAttribute("data-row-label")) ||
2021
+ normalizeText(rowHeader?.textContent) ||
2022
+ normalizeText(control.getAttribute("data-row")) ||
2023
+ null;
2024
+ const column = normalizeText(control.getAttribute("data-column-label")) ||
2025
+ normalizeText(columnHeader?.textContent) ||
2026
+ normalizeText(control.getAttribute("data-column")) ||
2027
+ normalizeText(ariaLabel) ||
2028
+ normalizeText(label?.textContent) ||
2029
+ normalizeText(control.value) ||
2030
+ null;
2031
+ const selection = row && column
2032
+ ? `${row} = ${column}`
2033
+ : control.value.length > 0
2034
+ ? control.value
2035
+ : null;
2036
+ return {
2037
+ row,
2038
+ column,
2039
+ value: control.value,
2040
+ selection,
2041
+ checked: control.checked,
2042
+ control: controlSnapshot(control),
2043
+ };
2044
+ };
2045
+ const cells = controls.map(inferCell);
2046
+ const selectedSelections = cells
2047
+ .filter((cell) => cell.checked && cell.selection)
2048
+ .map((cell) => cell.selection);
2049
+ const allSelections = cells
2050
+ .map((cell) => cell.selection)
2051
+ .filter((selection) => typeof selection === "string" && selection.length > 0);
2052
+ const expectedSelections = [...(stateOptions.expectedSelections ?? [])];
2053
+ const missingSelections = expectedSelections.filter((selection) => !selectedSelections.includes(selection));
2054
+ const unexpectedSelections = selectedSelections.filter((selection) => !expectedSelections.includes(selection));
2055
+ const exact = stateOptions.exact !== false;
2056
+ return {
2057
+ fieldMatched: Boolean(root),
2058
+ selectedSelections,
2059
+ allSelections,
2060
+ expectedSelections,
2061
+ missingSelections,
2062
+ unexpectedSelections,
2063
+ exact,
2064
+ matched: Boolean(root) && missingSelections.length === 0 && (!exact || unexpectedSelections.length === 0),
2065
+ cells,
2066
+ };
2067
+ }, options);
2068
+ }
2069
+ export async function waitForLiveFormMatrixState(page, options) {
2070
+ const timeoutMs = options.timeoutMs ?? 10_000;
2071
+ const intervalMs = options.intervalMs ?? 100;
2072
+ const startedAt = Date.now();
2073
+ let latest = await readLiveFormMatrixState(page, options);
2074
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
2075
+ await delay(intervalMs);
2076
+ latest = await readLiveFormMatrixState(page, options);
2077
+ }
2078
+ return latest;
2079
+ }
1962
2080
  export async function dispatchLiveFormFieldDomEvents(page, options) {
1963
2081
  if (!hasEvaluate(page)) {
1964
2082
  throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
@@ -3635,6 +3753,50 @@ export async function setLiveFormSelectValue(page, options) {
3635
3753
  (visibility?.matched ?? true),
3636
3754
  };
3637
3755
  }
3756
+ export async function setLiveFormMatrixValues(page, options) {
3757
+ const payload = normalizeLiveFormMatrixValue(options.selections);
3758
+ const selections = payload.value;
3759
+ const transaction = await performLiveFormFieldTransaction(page, {
3760
+ write: {
3761
+ ...(options.write ?? {}),
3762
+ target: options.target,
3763
+ value: payload,
3764
+ notify: options.write?.notify ?? true,
3765
+ },
3766
+ fieldKind: "matrix",
3767
+ waitForEvent: options.waitForEvent ?? true,
3768
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3769
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
3770
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
3771
+ });
3772
+ const dom = await waitForLiveFormMatrixState(page, {
3773
+ ...options.target,
3774
+ expectedSelections: selections,
3775
+ exact: options.exactDomSelection ?? true,
3776
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
3777
+ });
3778
+ const visibilityTargets = [
3779
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
3780
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
3781
+ ];
3782
+ const visibility = visibilityTargets.length > 0
3783
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
3784
+ : undefined;
3785
+ return {
3786
+ payload,
3787
+ selections,
3788
+ transaction,
3789
+ dom,
3790
+ ...(visibility ? { visibility } : {}),
3791
+ ok: transaction.write.valueMatched &&
3792
+ (transaction.event?.matched ?? true) &&
3793
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= selections.length : true) &&
3794
+ (transaction.quiet?.quiet ?? true) &&
3795
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
3796
+ dom.matched &&
3797
+ (visibility?.matched ?? true),
3798
+ };
3799
+ }
3638
3800
  export async function answerProgressiveLiveFormField(page, options) {
3639
3801
  const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
3640
3802
  const checkboxTransaction = options.fieldKind === "checkbox"
@@ -3670,25 +3832,38 @@ export async function answerProgressiveLiveFormField(page, options) {
3670
3832
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
3671
3833
  })
3672
3834
  : undefined;
3835
+ const matrixTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && options.fieldKind === "matrix"
3836
+ ? await setLiveFormMatrixValues(page, {
3837
+ target: options.target,
3838
+ selections: options.value,
3839
+ ...(options.write === undefined ? {} : { write: options.write }),
3840
+ waitForEvent: options.waitForEvent ?? true,
3841
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3842
+ afterQuiet: options.afterQuiet ?? true,
3843
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3844
+ })
3845
+ : undefined;
3673
3846
  const transaction = checkboxTransaction
3674
3847
  ? checkboxTransaction.transaction
3675
3848
  : choiceTransaction
3676
3849
  ? choiceTransaction.transaction
3677
3850
  : selectTransaction
3678
3851
  ? selectTransaction.transaction
3679
- : await performLiveFormFieldTransaction(page, {
3680
- write: {
3681
- ...(options.write ?? {}),
3682
- target: options.target,
3683
- value: options.value,
3684
- notify: options.write?.notify ?? true,
3685
- },
3686
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3687
- waitForEvent: options.waitForEvent ?? true,
3688
- dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3689
- afterQuiet: options.afterQuiet ?? true,
3690
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
3691
- });
3852
+ : matrixTransaction
3853
+ ? matrixTransaction.transaction
3854
+ : await performLiveFormFieldTransaction(page, {
3855
+ write: {
3856
+ ...(options.write ?? {}),
3857
+ target: options.target,
3858
+ value: options.value,
3859
+ notify: options.write?.notify ?? true,
3860
+ },
3861
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3862
+ waitForEvent: options.waitForEvent ?? true,
3863
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3864
+ afterQuiet: options.afterQuiet ?? true,
3865
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3866
+ });
3692
3867
  const visibilityTargets = [
3693
3868
  ...(options.expectVisible ?? []),
3694
3869
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -5150,7 +5325,19 @@ export const liveFormFieldRecipes = {
5150
5325
  },
5151
5326
  }),
5152
5327
  setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
5153
- setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
5328
+ setMatrixApi: (name, internalLabel, selections, options = {}) => ({
5329
+ name,
5330
+ run: async (harness) => {
5331
+ await setLiveFormMatrixValues(harness.page, {
5332
+ target: { internalLabel },
5333
+ selections,
5334
+ write: { notify: options.notify ?? true },
5335
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
5336
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
5337
+ afterQuiet: options.waitForQuiet ?? true,
5338
+ });
5339
+ },
5340
+ }),
5154
5341
  setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
5155
5342
  setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
5156
5343
  setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),