@redrockswebdevelopment/formstack-form-testing 0.1.27 → 0.1.29

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
@@ -978,6 +978,19 @@ export function buildLiveFormProductValue(value, current = {}) {
978
978
  export function buildLiveFormRatingValue(value) {
979
979
  return { value: String(value) };
980
980
  }
981
+ export function readLiveFormRatingValue(value) {
982
+ if (typeof value === "string" || typeof value === "number")
983
+ return String(value);
984
+ if (!value || typeof value !== "object")
985
+ return null;
986
+ const selected = value.value;
987
+ return typeof selected === "string" || typeof selected === "number" ? String(selected) : null;
988
+ }
989
+ export function normalizeLiveFormRatingValue(value) {
990
+ if (typeof value === "string" || typeof value === "number")
991
+ return buildLiveFormRatingValue(value);
992
+ return buildLiveFormRatingValue(value.value);
993
+ }
981
994
  export function buildLiveFormMatrixValue(selections) {
982
995
  return {
983
996
  value: selections.map((selection) => {
@@ -2077,6 +2090,88 @@ export async function waitForLiveFormMatrixState(page, options) {
2077
2090
  }
2078
2091
  return latest;
2079
2092
  }
2093
+ export async function readLiveFormRatingState(page, options) {
2094
+ if (!hasEvaluate(page)) {
2095
+ throw new Error("readLiveFormRatingState requires a Playwright page with evaluate().");
2096
+ }
2097
+ return page.evaluate((stateOptions) => {
2098
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
2099
+ const root = stateOptions.selector != null
2100
+ ? document.querySelector(stateOptions.selector)
2101
+ : stateOptions.internalLabel != null
2102
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
2103
+ : stateOptions.fieldId == null
2104
+ ? null
2105
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
2106
+ const candidates = root
2107
+ ? Array.from(root.querySelectorAll("input, button, [role=\"radio\"], [role=\"button\"], [aria-checked], [aria-pressed], [aria-selected], [data-value], [data-rating], [data-score]"))
2108
+ : [];
2109
+ const controlValue = (element) => {
2110
+ const htmlInput = element instanceof HTMLInputElement ? element.value : "";
2111
+ const text = normalizeText(element.textContent);
2112
+ return element.getAttribute("value") ??
2113
+ (htmlInput.length > 0 ? htmlInput : null) ??
2114
+ element.getAttribute("data-value") ??
2115
+ element.getAttribute("data-rating") ??
2116
+ element.getAttribute("data-score") ??
2117
+ element.getAttribute("aria-label")?.match(/\d+(?:\.\d+)?/)?.[0] ??
2118
+ (text.length > 0 ? text : null);
2119
+ };
2120
+ const isSelected = (element) => {
2121
+ const checked = element instanceof HTMLInputElement ? element.checked : null;
2122
+ const ariaChecked = element.getAttribute("aria-checked");
2123
+ const ariaPressed = element.getAttribute("aria-pressed");
2124
+ const ariaSelected = element.getAttribute("aria-selected");
2125
+ const classes = Array.from(element.classList).join(" ");
2126
+ return checked === true ||
2127
+ ariaChecked === "true" ||
2128
+ ariaPressed === "true" ||
2129
+ ariaSelected === "true" ||
2130
+ /\b(active|checked|selected|filled|on)\b/i.test(classes);
2131
+ };
2132
+ const controls = candidates
2133
+ .map((element) => ({
2134
+ tagName: element.tagName.toLowerCase(),
2135
+ id: element.getAttribute("id"),
2136
+ value: controlValue(element),
2137
+ text: normalizeText(element.textContent),
2138
+ ariaChecked: element.getAttribute("aria-checked"),
2139
+ ariaPressed: element.getAttribute("aria-pressed"),
2140
+ ariaSelected: element.getAttribute("aria-selected"),
2141
+ checked: element instanceof HTMLInputElement ? element.checked : null,
2142
+ selected: isSelected(element),
2143
+ }))
2144
+ .filter((control) => control.value !== null && control.value.length > 0);
2145
+ const selectedControls = controls.filter((control) => control.selected);
2146
+ const selectedValue = selectedControls.length > 0
2147
+ ? selectedControls[selectedControls.length - 1]?.value ?? null
2148
+ : null;
2149
+ const allValues = controls
2150
+ .map((control) => control.value)
2151
+ .filter((value) => typeof value === "string" && value.length > 0);
2152
+ return {
2153
+ fieldMatched: Boolean(root),
2154
+ selectedValue,
2155
+ allValues,
2156
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
2157
+ matched: Boolean(root) &&
2158
+ selectedValue !== null &&
2159
+ (stateOptions.expectedValue === undefined || selectedValue === stateOptions.expectedValue),
2160
+ controls,
2161
+ };
2162
+ }, options);
2163
+ }
2164
+ export async function waitForLiveFormRatingState(page, options) {
2165
+ const timeoutMs = options.timeoutMs ?? 10_000;
2166
+ const intervalMs = options.intervalMs ?? 100;
2167
+ const startedAt = Date.now();
2168
+ let latest = await readLiveFormRatingState(page, options);
2169
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
2170
+ await delay(intervalMs);
2171
+ latest = await readLiveFormRatingState(page, options);
2172
+ }
2173
+ return latest;
2174
+ }
2080
2175
  export async function dispatchLiveFormFieldDomEvents(page, options) {
2081
2176
  if (!hasEvaluate(page)) {
2082
2177
  throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
@@ -2088,14 +2183,30 @@ export async function dispatchLiveFormFieldDomEvents(page, options) {
2088
2183
  : dispatchOptions.target.fieldId == null
2089
2184
  ? null
2090
2185
  : document.querySelector(`#fsCell${CSS.escape(String(dispatchOptions.target.fieldId))}, [data-testid="field-${CSS.escape(String(dispatchOptions.target.fieldId))}"]`);
2091
- const allControls = root ? Array.from(root.querySelectorAll("input, textarea, select")) : [];
2186
+ const isSelectedControl = (control) => {
2187
+ if (control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type))
2188
+ return control.checked;
2189
+ if (control instanceof HTMLInputElement && control.type === "hidden")
2190
+ return false;
2191
+ const ariaChecked = control.getAttribute("aria-checked");
2192
+ const ariaPressed = control.getAttribute("aria-pressed");
2193
+ const ariaSelected = control.getAttribute("aria-selected");
2194
+ const classes = Array.from(control.classList).join(" ");
2195
+ return ariaChecked === "true" ||
2196
+ ariaPressed === "true" ||
2197
+ ariaSelected === "true" ||
2198
+ /\b(active|checked|selected|filled|on)\b/i.test(classes);
2199
+ };
2200
+ const allControls = root
2201
+ ? Array.from(root.querySelectorAll("input, textarea, select, button, [role=\"radio\"], [role=\"button\"], [aria-checked], [aria-pressed], [aria-selected]"))
2202
+ : [];
2092
2203
  const controls = dispatchOptions.onlyCheckedChoices ?? true
2093
2204
  ? allControls.filter((control) => {
2094
- if (control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type))
2095
- return control.checked;
2096
- if (control instanceof HTMLInputElement && control.type === "hidden")
2097
- return false;
2098
- return true;
2205
+ if (control instanceof HTMLInputElement || control instanceof HTMLButtonElement)
2206
+ return isSelectedControl(control);
2207
+ if (control.matches("[role=\"radio\"], [role=\"button\"], [aria-checked], [aria-pressed], [aria-selected]"))
2208
+ return isSelectedControl(control);
2209
+ return !(control instanceof HTMLInputElement && control.type === "hidden");
2099
2210
  })
2100
2211
  : allControls;
2101
2212
  for (const control of controls) {
@@ -2112,7 +2223,7 @@ export async function dispatchLiveFormFieldDomEvents(page, options) {
2112
2223
  id: control.getAttribute("id"),
2113
2224
  name: control.getAttribute("name"),
2114
2225
  value: control.getAttribute("value"),
2115
- currentValue: "value" in control ? control.value : null,
2226
+ currentValue: "value" in control ? String(control.value) : null,
2116
2227
  checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
2117
2228
  })),
2118
2229
  };
@@ -3797,6 +3908,49 @@ export async function setLiveFormMatrixValues(page, options) {
3797
3908
  (visibility?.matched ?? true),
3798
3909
  };
3799
3910
  }
3911
+ export async function setLiveFormRatingValue(page, options) {
3912
+ const payload = normalizeLiveFormRatingValue(options.value);
3913
+ const value = payload.value;
3914
+ const transaction = await performLiveFormFieldTransaction(page, {
3915
+ write: {
3916
+ ...(options.write ?? {}),
3917
+ target: options.target,
3918
+ value: payload,
3919
+ notify: options.write?.notify ?? true,
3920
+ },
3921
+ fieldKind: "rating",
3922
+ waitForEvent: options.waitForEvent ?? true,
3923
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3924
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
3925
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
3926
+ });
3927
+ const dom = await waitForLiveFormRatingState(page, {
3928
+ ...options.target,
3929
+ expectedValue: value,
3930
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
3931
+ });
3932
+ const visibilityTargets = [
3933
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
3934
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
3935
+ ];
3936
+ const visibility = visibilityTargets.length > 0
3937
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
3938
+ : undefined;
3939
+ return {
3940
+ payload,
3941
+ value,
3942
+ transaction,
3943
+ dom,
3944
+ ...(visibility ? { visibility } : {}),
3945
+ ok: transaction.write.valueMatched &&
3946
+ (transaction.event?.matched ?? true) &&
3947
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
3948
+ (transaction.quiet?.quiet ?? true) &&
3949
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
3950
+ dom.matched &&
3951
+ (visibility?.matched ?? true),
3952
+ };
3953
+ }
3800
3954
  export async function answerProgressiveLiveFormField(page, options) {
3801
3955
  const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
3802
3956
  const checkboxTransaction = options.fieldKind === "checkbox"
@@ -3843,6 +3997,17 @@ export async function answerProgressiveLiveFormField(page, options) {
3843
3997
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
3844
3998
  })
3845
3999
  : undefined;
4000
+ const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "rating"
4001
+ ? await setLiveFormRatingValue(page, {
4002
+ target: options.target,
4003
+ value: options.value,
4004
+ ...(options.write === undefined ? {} : { write: options.write }),
4005
+ waitForEvent: options.waitForEvent ?? true,
4006
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4007
+ afterQuiet: options.afterQuiet ?? true,
4008
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4009
+ })
4010
+ : undefined;
3846
4011
  const transaction = checkboxTransaction
3847
4012
  ? checkboxTransaction.transaction
3848
4013
  : choiceTransaction
@@ -3851,19 +4016,21 @@ export async function answerProgressiveLiveFormField(page, options) {
3851
4016
  ? selectTransaction.transaction
3852
4017
  : matrixTransaction
3853
4018
  ? 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
- });
4019
+ : ratingTransaction
4020
+ ? ratingTransaction.transaction
4021
+ : await performLiveFormFieldTransaction(page, {
4022
+ write: {
4023
+ ...(options.write ?? {}),
4024
+ target: options.target,
4025
+ value: options.value,
4026
+ notify: options.write?.notify ?? true,
4027
+ },
4028
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4029
+ waitForEvent: options.waitForEvent ?? true,
4030
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4031
+ afterQuiet: options.afterQuiet ?? true,
4032
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4033
+ });
3867
4034
  const visibilityTargets = [
3868
4035
  ...(options.expectVisible ?? []),
3869
4036
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -5338,7 +5505,19 @@ export const liveFormFieldRecipes = {
5338
5505
  });
5339
5506
  },
5340
5507
  }),
5341
- setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),
5508
+ setRatingApi: (name, internalLabel, value, options = {}) => ({
5509
+ name,
5510
+ run: async (harness) => {
5511
+ await setLiveFormRatingValue(harness.page, {
5512
+ target: { internalLabel },
5513
+ value,
5514
+ write: { notify: options.notify ?? true },
5515
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
5516
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
5517
+ afterQuiet: options.waitForQuiet ?? true,
5518
+ });
5519
+ },
5520
+ }),
5342
5521
  setProductApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(value), options),
5343
5522
  setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),
5344
5523
  setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "name", dispatchDomEvents: "auto" }),