@redrockswebdevelopment/formstack-form-testing 0.1.22 → 0.1.23

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
@@ -910,6 +910,27 @@ export function buildLiveFormSelectValue(value) {
910
910
  export function buildLiveFormCheckboxValue(values, otherValue = null) {
911
911
  return Object.fromEntries([...values.map((value, index) => [String(index), value]), ["otherValue", otherValue]]);
912
912
  }
913
+ export function readLiveFormCheckboxValues(value) {
914
+ if (Array.isArray(value)) {
915
+ return value.filter((entry) => typeof entry === "string");
916
+ }
917
+ if (!value || typeof value !== "object")
918
+ return [];
919
+ const record = value;
920
+ if (Array.isArray(record.values)) {
921
+ return record.values.filter((entry) => typeof entry === "string");
922
+ }
923
+ return Object.entries(record)
924
+ .filter(([key, entry]) => /^\d+$/.test(key) && typeof entry === "string" && entry.length > 0)
925
+ .sort(([left], [right]) => Number(left) - Number(right))
926
+ .map(([, entry]) => entry);
927
+ }
928
+ export function normalizeLiveFormCheckboxValue(value, otherValue = null) {
929
+ const explicitOtherValue = value && typeof value === "object" && !Array.isArray(value) && "otherValue" in value
930
+ ? value.otherValue
931
+ : otherValue;
932
+ return buildLiveFormCheckboxValue(readLiveFormCheckboxValues(value), typeof explicitOtherValue === "string" ? explicitOtherValue : explicitOtherValue === null ? null : otherValue);
933
+ }
913
934
  export function buildLiveFormDateTimeValue(value, current = {}) {
914
935
  return {
915
936
  ...(current && typeof current === "object" ? current : {}),
@@ -1696,6 +1717,104 @@ export async function waitForLiveFormDomFieldState(page, options) {
1696
1717
  }
1697
1718
  return latest;
1698
1719
  }
1720
+ export async function readLiveFormCheckboxState(page, options) {
1721
+ if (!hasEvaluate(page)) {
1722
+ throw new Error("readLiveFormCheckboxState requires a Playwright page with evaluate().");
1723
+ }
1724
+ return page.evaluate((stateOptions) => {
1725
+ const controlSnapshot = (control) => ({
1726
+ tagName: control.tagName.toLowerCase(),
1727
+ type: control.getAttribute("type"),
1728
+ id: control.getAttribute("id"),
1729
+ name: control.getAttribute("name"),
1730
+ value: control.getAttribute("value"),
1731
+ currentValue: control.value,
1732
+ checked: control.checked,
1733
+ });
1734
+ const root = stateOptions.selector != null
1735
+ ? document.querySelector(stateOptions.selector)
1736
+ : stateOptions.internalLabel != null
1737
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
1738
+ : stateOptions.fieldId == null
1739
+ ? null
1740
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
1741
+ const controls = root
1742
+ ? Array.from(root.querySelectorAll("input[type=\"checkbox\"]")).map(controlSnapshot)
1743
+ : [];
1744
+ const selectedValues = controls
1745
+ .filter((control) => control.checked === true)
1746
+ .map((control) => control.currentValue ?? control.value ?? "")
1747
+ .filter((value) => value.length > 0);
1748
+ const allValues = controls
1749
+ .map((control) => control.currentValue ?? control.value ?? "")
1750
+ .filter((value) => value.length > 0);
1751
+ const expectedValues = [...(stateOptions.expectedValues ?? [])];
1752
+ const missingValues = expectedValues.filter((value) => !selectedValues.includes(value));
1753
+ const unexpectedValues = selectedValues.filter((value) => !expectedValues.includes(value));
1754
+ const exact = stateOptions.exact !== false;
1755
+ return {
1756
+ fieldMatched: Boolean(root),
1757
+ selectedValues,
1758
+ allValues,
1759
+ expectedValues,
1760
+ missingValues,
1761
+ unexpectedValues,
1762
+ exact,
1763
+ matched: Boolean(root) && missingValues.length === 0 && (!exact || unexpectedValues.length === 0),
1764
+ controls,
1765
+ };
1766
+ }, options);
1767
+ }
1768
+ export async function waitForLiveFormCheckboxState(page, options) {
1769
+ const timeoutMs = options.timeoutMs ?? 10_000;
1770
+ const intervalMs = options.intervalMs ?? 100;
1771
+ const startedAt = Date.now();
1772
+ let latest = await readLiveFormCheckboxState(page, options);
1773
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
1774
+ await delay(intervalMs);
1775
+ latest = await readLiveFormCheckboxState(page, options);
1776
+ }
1777
+ return latest;
1778
+ }
1779
+ export async function diagnoseLiveFormCommitControl(page, options) {
1780
+ if (!hasEvaluate(page)) {
1781
+ throw new Error("diagnoseLiveFormCommitControl requires a Playwright page with evaluate().");
1782
+ }
1783
+ return page.evaluate((commitOptions) => {
1784
+ const element = document.querySelector(commitOptions.selector);
1785
+ if (!(element instanceof HTMLElement)) {
1786
+ return {
1787
+ selector: commitOptions.selector,
1788
+ exists: false,
1789
+ visible: null,
1790
+ disabled: null,
1791
+ ariaDisabled: null,
1792
+ text: null,
1793
+ reason: "missing",
1794
+ ready: false,
1795
+ };
1796
+ }
1797
+ const style = getComputedStyle(element);
1798
+ const rect = element.getBoundingClientRect();
1799
+ const disabled = "disabled" in element ? Boolean(element.disabled) : element.getAttribute("disabled") !== null;
1800
+ const ariaDisabled = element.getAttribute("aria-disabled") === "true";
1801
+ const visible = style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity) !== 0 && rect.width > 0 && rect.height > 0;
1802
+ const reason = !visible ? (style.display === "none" ? "display-none" : style.visibility === "hidden" ? "visibility-hidden" : rect.width <= 0 || rect.height <= 0 ? "zero-size" : "transparent")
1803
+ : disabled ? "disabled"
1804
+ : ariaDisabled ? "aria-disabled"
1805
+ : null;
1806
+ return {
1807
+ selector: commitOptions.selector,
1808
+ exists: true,
1809
+ visible,
1810
+ disabled,
1811
+ ariaDisabled,
1812
+ text: element.textContent?.replace(/\s+/g, " ").trim() ?? "",
1813
+ reason,
1814
+ ready: visible && !disabled && !ariaDisabled,
1815
+ };
1816
+ }, options);
1817
+ }
1699
1818
  export async function dispatchLiveFormFieldDomEvents(page, options) {
1700
1819
  if (!hasEvaluate(page)) {
1701
1820
  throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
@@ -3231,21 +3350,89 @@ export async function performLiveFormFieldTransaction(page, options) {
3231
3350
  afterQuiet: options.afterQuiet ?? true,
3232
3351
  });
3233
3352
  }
3234
- export async function answerProgressiveLiveFormField(page, options) {
3235
- const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
3353
+ export async function setLiveFormCheckboxValues(page, options) {
3354
+ const payload = normalizeLiveFormCheckboxValue(options.values);
3355
+ const values = readLiveFormCheckboxValues(payload);
3236
3356
  const transaction = await performLiveFormFieldTransaction(page, {
3237
3357
  write: {
3238
3358
  ...(options.write ?? {}),
3239
3359
  target: options.target,
3240
- value: options.value,
3360
+ value: payload,
3241
3361
  notify: options.write?.notify ?? true,
3242
3362
  },
3243
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3363
+ fieldKind: "checkbox",
3244
3364
  waitForEvent: options.waitForEvent ?? true,
3245
3365
  dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3246
- afterQuiet: options.afterQuiet ?? true,
3247
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
3366
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
3367
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
3248
3368
  });
3369
+ const dom = await waitForLiveFormCheckboxState(page, {
3370
+ ...options.target,
3371
+ expectedValues: values,
3372
+ exact: options.exactDomSelection ?? true,
3373
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
3374
+ });
3375
+ let commitControl;
3376
+ let clickedCommit = false;
3377
+ if (options.commitSelector) {
3378
+ const deadline = Date.now() + (options.commitTimeoutMs ?? 5_000);
3379
+ commitControl = await diagnoseLiveFormCommitControl(page, { selector: options.commitSelector });
3380
+ while (!commitControl.ready && Date.now() < deadline) {
3381
+ await delay(100);
3382
+ commitControl = await diagnoseLiveFormCommitControl(page, { selector: options.commitSelector });
3383
+ }
3384
+ if (options.clickCommit === true) {
3385
+ if (!commitControl.ready) {
3386
+ throw new Error(`Checkbox commit control "${options.commitSelector}" is not ready after selecting ${values.length} value(s): ${commitControl.reason ?? "unknown"}.`);
3387
+ }
3388
+ await page.locator(options.commitSelector).first().click({ timeout: options.commitTimeoutMs ?? 5_000 });
3389
+ clickedCommit = true;
3390
+ }
3391
+ }
3392
+ return {
3393
+ payload,
3394
+ values,
3395
+ transaction,
3396
+ dom,
3397
+ ...(commitControl ? { commitControl } : {}),
3398
+ clickedCommit,
3399
+ ok: transaction.write.valueMatched &&
3400
+ (transaction.event?.matched ?? true) &&
3401
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= values.length : true) &&
3402
+ (transaction.quiet?.quiet ?? true) &&
3403
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
3404
+ dom.matched &&
3405
+ (commitControl?.ready ?? true),
3406
+ };
3407
+ }
3408
+ export async function answerProgressiveLiveFormField(page, options) {
3409
+ const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
3410
+ const checkboxTransaction = options.fieldKind === "checkbox"
3411
+ ? await setLiveFormCheckboxValues(page, {
3412
+ target: options.target,
3413
+ values: options.value,
3414
+ ...(options.write === undefined ? {} : { write: options.write }),
3415
+ waitForEvent: options.waitForEvent ?? true,
3416
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3417
+ afterQuiet: options.afterQuiet ?? true,
3418
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3419
+ })
3420
+ : undefined;
3421
+ const transaction = checkboxTransaction
3422
+ ? checkboxTransaction.transaction
3423
+ : await performLiveFormFieldTransaction(page, {
3424
+ write: {
3425
+ ...(options.write ?? {}),
3426
+ target: options.target,
3427
+ value: options.value,
3428
+ notify: options.write?.notify ?? true,
3429
+ },
3430
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
3431
+ waitForEvent: options.waitForEvent ?? true,
3432
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
3433
+ afterQuiet: options.afterQuiet ?? true,
3434
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
3435
+ });
3249
3436
  const visibilityTargets = [
3250
3437
  ...(options.expectVisible ?? []),
3251
3438
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -3450,11 +3637,8 @@ export function normalizeLiveFormSampleValue(kind = "unknown", value) {
3450
3637
  if (["radio", "rating", "select"].includes(kind) && typeof value === "string") {
3451
3638
  return { value };
3452
3639
  }
3453
- if (kind === "checkbox" && Array.isArray(value)) {
3454
- return buildLiveFormCheckboxValue(value.filter((entry) => typeof entry === "string"));
3455
- }
3456
- if (kind === "checkbox" && value && typeof value === "object" && Array.isArray(value.values)) {
3457
- return buildLiveFormCheckboxValue(value.values.filter((entry) => typeof entry === "string"));
3640
+ if (kind === "checkbox" && (Array.isArray(value) || (value && typeof value === "object"))) {
3641
+ return normalizeLiveFormCheckboxValue(value);
3458
3642
  }
3459
3643
  if (kind === "checkbox" && typeof value === "boolean") {
3460
3644
  return { checked: value };
@@ -4672,7 +4856,19 @@ export const liveFormFieldRecipes = {
4672
4856
  setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
4673
4857
  setChoiceApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormChoiceValue(value), options),
4674
4858
  setSelectApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormSelectValue(value), options),
4675
- setCheckboxesApi: (name, internalLabel, values, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormCheckboxValue(values), options),
4859
+ setCheckboxesApi: (name, internalLabel, values, options = {}) => ({
4860
+ name,
4861
+ run: async (harness) => {
4862
+ await setLiveFormCheckboxValues(harness.page, {
4863
+ target: { internalLabel },
4864
+ values,
4865
+ write: { notify: options.notify ?? true },
4866
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
4867
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4868
+ afterQuiet: options.waitForQuiet ?? true,
4869
+ });
4870
+ },
4871
+ }),
4676
4872
  setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
4677
4873
  setMatrixApi: (name, internalLabel, selections, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormMatrixValue(selections), options),
4678
4874
  setRatingApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormRatingValue(value), options),