@redrockswebdevelopment/formstack-form-testing 0.1.24 → 0.1.26

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
@@ -920,6 +920,19 @@ export function normalizeLiveFormChoiceValue(value, otherValue = "") {
920
920
  export function buildLiveFormSelectValue(value) {
921
921
  return { value };
922
922
  }
923
+ export function readLiveFormSelectValue(value) {
924
+ if (typeof value === "string")
925
+ return value;
926
+ if (!value || typeof value !== "object")
927
+ return null;
928
+ const selected = value.value;
929
+ return typeof selected === "string" && selected.length > 0 ? selected : null;
930
+ }
931
+ export function normalizeLiveFormSelectValue(value) {
932
+ if (typeof value === "string")
933
+ return buildLiveFormSelectValue(value);
934
+ return buildLiveFormSelectValue(value.value);
935
+ }
923
936
  export function buildLiveFormCheckboxValue(values, otherValue = null) {
924
937
  return Object.fromEntries([...values.map((value, index) => [String(index), value]), ["otherValue", otherValue]]);
925
938
  }
@@ -974,6 +987,22 @@ export function buildLiveFormMatrixValue(selections) {
974
987
  }),
975
988
  };
976
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
+ }
977
1006
  export async function readLiveFormFieldValue(page, options) {
978
1007
  if (!hasEvaluate(page)) {
979
1008
  throw new Error("readLiveFormFieldValue requires a Playwright page with evaluate().");
@@ -1882,6 +1911,168 @@ export async function waitForLiveFormChoiceState(page, options) {
1882
1911
  }
1883
1912
  return latest;
1884
1913
  }
1914
+ export async function readLiveFormSelectState(page, options) {
1915
+ if (!hasEvaluate(page)) {
1916
+ throw new Error("readLiveFormSelectState requires a Playwright page with evaluate().");
1917
+ }
1918
+ return page.evaluate((stateOptions) => {
1919
+ const controlSnapshot = (control) => ({
1920
+ tagName: control.tagName.toLowerCase(),
1921
+ type: control.getAttribute("type"),
1922
+ id: control.getAttribute("id"),
1923
+ name: control.getAttribute("name"),
1924
+ value: control.getAttribute("value"),
1925
+ currentValue: control.value,
1926
+ checked: null,
1927
+ });
1928
+ const root = stateOptions.selector != null
1929
+ ? document.querySelector(stateOptions.selector)
1930
+ : stateOptions.internalLabel != null
1931
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
1932
+ : stateOptions.fieldId == null
1933
+ ? null
1934
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
1935
+ const selects = root
1936
+ ? Array.from(root.querySelectorAll("select"))
1937
+ : [];
1938
+ const controls = selects.map(controlSnapshot);
1939
+ const primary = selects[0] ?? null;
1940
+ const options = primary
1941
+ ? Array.from(primary.options).map((option) => ({
1942
+ value: option.value,
1943
+ text: option.textContent?.replace(/\s+/g, " ").trim() ?? "",
1944
+ selected: option.selected,
1945
+ }))
1946
+ : [];
1947
+ const selected = options.find((option) => option.selected) ?? null;
1948
+ const selectedValue = primary ? primary.value : null;
1949
+ const selectedText = selected?.text ?? null;
1950
+ const allValues = options
1951
+ .map((option) => option.value)
1952
+ .filter((value) => value.length > 0);
1953
+ return {
1954
+ fieldMatched: Boolean(root),
1955
+ selectedValue,
1956
+ selectedText,
1957
+ allValues,
1958
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
1959
+ matched: Boolean(root) &&
1960
+ primary !== null &&
1961
+ (stateOptions.expectedValue === undefined || selectedValue === stateOptions.expectedValue),
1962
+ controls,
1963
+ options,
1964
+ };
1965
+ }, options);
1966
+ }
1967
+ export async function waitForLiveFormSelectState(page, options) {
1968
+ const timeoutMs = options.timeoutMs ?? 10_000;
1969
+ const intervalMs = options.intervalMs ?? 100;
1970
+ const startedAt = Date.now();
1971
+ let latest = await readLiveFormSelectState(page, options);
1972
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
1973
+ await delay(intervalMs);
1974
+ latest = await readLiveFormSelectState(page, options);
1975
+ }
1976
+ return latest;
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 ? `${row} = ${column}` : null;
2032
+ return {
2033
+ row,
2034
+ column,
2035
+ value: control.value,
2036
+ selection,
2037
+ checked: control.checked,
2038
+ control: controlSnapshot(control),
2039
+ };
2040
+ };
2041
+ const cells = controls.map(inferCell);
2042
+ const selectedSelections = cells
2043
+ .filter((cell) => cell.checked && cell.selection)
2044
+ .map((cell) => cell.selection);
2045
+ const allSelections = cells
2046
+ .map((cell) => cell.selection)
2047
+ .filter((selection) => typeof selection === "string" && selection.length > 0);
2048
+ const expectedSelections = [...(stateOptions.expectedSelections ?? [])];
2049
+ const missingSelections = expectedSelections.filter((selection) => !selectedSelections.includes(selection));
2050
+ const unexpectedSelections = selectedSelections.filter((selection) => !expectedSelections.includes(selection));
2051
+ const exact = stateOptions.exact !== false;
2052
+ return {
2053
+ fieldMatched: Boolean(root),
2054
+ selectedSelections,
2055
+ allSelections,
2056
+ expectedSelections,
2057
+ missingSelections,
2058
+ unexpectedSelections,
2059
+ exact,
2060
+ matched: Boolean(root) && missingSelections.length === 0 && (!exact || unexpectedSelections.length === 0),
2061
+ cells,
2062
+ };
2063
+ }, options);
2064
+ }
2065
+ export async function waitForLiveFormMatrixState(page, options) {
2066
+ const timeoutMs = options.timeoutMs ?? 10_000;
2067
+ const intervalMs = options.intervalMs ?? 100;
2068
+ const startedAt = Date.now();
2069
+ let latest = await readLiveFormMatrixState(page, options);
2070
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
2071
+ await delay(intervalMs);
2072
+ latest = await readLiveFormMatrixState(page, options);
2073
+ }
2074
+ return latest;
2075
+ }
1885
2076
  export async function dispatchLiveFormFieldDomEvents(page, options) {
1886
2077
  if (!hasEvaluate(page)) {
1887
2078
  throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
@@ -3515,6 +3706,93 @@ export async function setLiveFormChoiceValue(page, options) {
3515
3706
  (visibility?.matched ?? true),
3516
3707
  };
3517
3708
  }
3709
+ export async function setLiveFormSelectValue(page, options) {
3710
+ const payload = normalizeLiveFormSelectValue(options.value);
3711
+ const value = payload.value;
3712
+ const transaction = await performLiveFormFieldTransaction(page, {
3713
+ write: {
3714
+ ...(options.write ?? {}),
3715
+ target: options.target,
3716
+ value: payload,
3717
+ notify: options.write?.notify ?? true,
3718
+ },
3719
+ fieldKind: "select",
3720
+ waitForEvent: options.waitForEvent ?? true,
3721
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3722
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
3723
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
3724
+ });
3725
+ const dom = await waitForLiveFormSelectState(page, {
3726
+ ...options.target,
3727
+ expectedValue: value,
3728
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
3729
+ });
3730
+ const visibilityTargets = [
3731
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
3732
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
3733
+ ];
3734
+ const visibility = visibilityTargets.length > 0
3735
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
3736
+ : undefined;
3737
+ return {
3738
+ payload,
3739
+ value,
3740
+ transaction,
3741
+ dom,
3742
+ ...(visibility ? { visibility } : {}),
3743
+ ok: transaction.write.valueMatched &&
3744
+ (transaction.event?.matched ?? true) &&
3745
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
3746
+ (transaction.quiet?.quiet ?? true) &&
3747
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
3748
+ dom.matched &&
3749
+ (visibility?.matched ?? true),
3750
+ };
3751
+ }
3752
+ export async function setLiveFormMatrixValues(page, options) {
3753
+ const payload = normalizeLiveFormMatrixValue(options.selections);
3754
+ const selections = payload.value;
3755
+ const transaction = await performLiveFormFieldTransaction(page, {
3756
+ write: {
3757
+ ...(options.write ?? {}),
3758
+ target: options.target,
3759
+ value: payload,
3760
+ notify: options.write?.notify ?? true,
3761
+ },
3762
+ fieldKind: "matrix",
3763
+ waitForEvent: options.waitForEvent ?? true,
3764
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3765
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
3766
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
3767
+ });
3768
+ const dom = await waitForLiveFormMatrixState(page, {
3769
+ ...options.target,
3770
+ expectedSelections: selections,
3771
+ exact: options.exactDomSelection ?? true,
3772
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
3773
+ });
3774
+ const visibilityTargets = [
3775
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
3776
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
3777
+ ];
3778
+ const visibility = visibilityTargets.length > 0
3779
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
3780
+ : undefined;
3781
+ return {
3782
+ payload,
3783
+ selections,
3784
+ transaction,
3785
+ dom,
3786
+ ...(visibility ? { visibility } : {}),
3787
+ ok: transaction.write.valueMatched &&
3788
+ (transaction.event?.matched ?? true) &&
3789
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= selections.length : true) &&
3790
+ (transaction.quiet?.quiet ?? true) &&
3791
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
3792
+ dom.matched &&
3793
+ (visibility?.matched ?? true),
3794
+ };
3795
+ }
3518
3796
  export async function answerProgressiveLiveFormField(page, options) {
3519
3797
  const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
3520
3798
  const checkboxTransaction = options.fieldKind === "checkbox"
@@ -3539,23 +3817,49 @@ export async function answerProgressiveLiveFormField(page, options) {
3539
3817
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
3540
3818
  })
3541
3819
  : undefined;
3820
+ const selectTransaction = !checkboxTransaction && !choiceTransaction && options.fieldKind === "select"
3821
+ ? await setLiveFormSelectValue(page, {
3822
+ target: options.target,
3823
+ value: options.value,
3824
+ ...(options.write === undefined ? {} : { write: options.write }),
3825
+ waitForEvent: options.waitForEvent ?? true,
3826
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3827
+ afterQuiet: options.afterQuiet ?? true,
3828
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3829
+ })
3830
+ : undefined;
3831
+ const matrixTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && options.fieldKind === "matrix"
3832
+ ? await setLiveFormMatrixValues(page, {
3833
+ target: options.target,
3834
+ selections: options.value,
3835
+ ...(options.write === undefined ? {} : { write: options.write }),
3836
+ waitForEvent: options.waitForEvent ?? true,
3837
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3838
+ afterQuiet: options.afterQuiet ?? true,
3839
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3840
+ })
3841
+ : undefined;
3542
3842
  const transaction = checkboxTransaction
3543
3843
  ? checkboxTransaction.transaction
3544
3844
  : choiceTransaction
3545
3845
  ? choiceTransaction.transaction
3546
- : await performLiveFormFieldTransaction(page, {
3547
- write: {
3548
- ...(options.write ?? {}),
3549
- target: options.target,
3550
- value: options.value,
3551
- notify: options.write?.notify ?? true,
3552
- },
3553
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3554
- waitForEvent: options.waitForEvent ?? true,
3555
- dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3556
- afterQuiet: options.afterQuiet ?? true,
3557
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
3558
- });
3846
+ : selectTransaction
3847
+ ? selectTransaction.transaction
3848
+ : matrixTransaction
3849
+ ? matrixTransaction.transaction
3850
+ : await performLiveFormFieldTransaction(page, {
3851
+ write: {
3852
+ ...(options.write ?? {}),
3853
+ target: options.target,
3854
+ value: options.value,
3855
+ notify: options.write?.notify ?? true,
3856
+ },
3857
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3858
+ waitForEvent: options.waitForEvent ?? true,
3859
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3860
+ afterQuiet: options.afterQuiet ?? true,
3861
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3862
+ });
3559
3863
  const visibilityTargets = [
3560
3864
  ...(options.expectVisible ?? []),
3561
3865
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -4990,7 +5294,19 @@ export const liveFormFieldRecipes = {
4990
5294
  });
4991
5295
  },
4992
5296
  }),
4993
- setSelectApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormSelectValue(value), options),
5297
+ setSelectApi: (name, internalLabel, value, options = {}) => ({
5298
+ name,
5299
+ run: async (harness) => {
5300
+ await setLiveFormSelectValue(harness.page, {
5301
+ target: { internalLabel },
5302
+ value,
5303
+ write: { notify: options.notify ?? true },
5304
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
5305
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
5306
+ afterQuiet: options.waitForQuiet ?? true,
5307
+ });
5308
+ },
5309
+ }),
4994
5310
  setCheckboxesApi: (name, internalLabel, values, options = {}) => ({
4995
5311
  name,
4996
5312
  run: async (harness) => {
@@ -5005,7 +5321,19 @@ export const liveFormFieldRecipes = {
5005
5321
  },
5006
5322
  }),
5007
5323
  setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
5008
- setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
5324
+ setMatrixApi: (name, internalLabel, selections, options = {}) => ({
5325
+ name,
5326
+ run: async (harness) => {
5327
+ await setLiveFormMatrixValues(harness.page, {
5328
+ target: { internalLabel },
5329
+ selections,
5330
+ write: { notify: options.notify ?? true },
5331
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
5332
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
5333
+ afterQuiet: options.waitForQuiet ?? true,
5334
+ });
5335
+ },
5336
+ }),
5009
5337
  setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
5010
5338
  setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
5011
5339
  setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),