@redrockswebdevelopment/formstack-form-testing 0.1.35 → 0.1.36

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
@@ -104,8 +104,8 @@ export const liveFormFieldTypeCoverage = {
104
104
  apiWrite: true,
105
105
  domInteraction: true,
106
106
  domAssertion: true,
107
- reason: "Name compound fields support direct API values and stable subfield DOM automation.",
108
- recommendedHelpers: ["buildLiveFormNameValue", "setLiveFormFieldValueTransaction", "fillName", "readLiveFormReadiness"],
107
+ reason: "Name compound fields support direct API values and should be asserted by prefix, first, middle, last, and suffix subfields.",
108
+ recommendedHelpers: ["buildLiveFormNameValue", "setLiveFormNameValue", "readLiveFormNameState", "fillName"],
109
109
  },
110
110
  number: {
111
111
  kind: "number",
@@ -895,6 +895,15 @@ export function buildLiveFormNameValue(value, current = {}) {
895
895
  ...value,
896
896
  };
897
897
  }
898
+ export function normalizeLiveFormNameValue(value, current = {}) {
899
+ return buildLiveFormNameValue({
900
+ ...(value.prefix === undefined ? {} : { prefix: String(value.prefix) }),
901
+ ...(value.first === undefined ? {} : { first: String(value.first) }),
902
+ ...(value.middle === undefined ? {} : { middle: String(value.middle) }),
903
+ ...(value.last === undefined ? {} : { last: String(value.last) }),
904
+ ...(value.suffix === undefined ? {} : { suffix: String(value.suffix) }),
905
+ }, current);
906
+ }
898
907
  export function buildLiveFormAddressValue(value, current = {}) {
899
908
  return {
900
909
  ...(current && typeof current === "object" ? current : {}),
@@ -2055,6 +2064,102 @@ export async function waitForLiveFormAddressState(page, options) {
2055
2064
  }
2056
2065
  return latest;
2057
2066
  }
2067
+ export async function readLiveFormNameState(page, options) {
2068
+ if (!hasEvaluate(page)) {
2069
+ throw new Error("readLiveFormNameState requires a Playwright page with evaluate().");
2070
+ }
2071
+ return page.evaluate((stateOptions) => {
2072
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
2073
+ const inferPart = (control) => {
2074
+ const label = control.id ? document.querySelector(`label[for="${CSS.escape(control.id)}"]`) : null;
2075
+ const haystack = [
2076
+ control.name,
2077
+ control.id,
2078
+ control.className,
2079
+ control.getAttribute("placeholder"),
2080
+ control.getAttribute("aria-label"),
2081
+ label?.textContent,
2082
+ ].map(normalizeText).join(" ").toLowerCase();
2083
+ if (/\b(prefix|title|\[prefix\]|-prefix$|_prefix$)/i.test(haystack))
2084
+ return "prefix";
2085
+ if (/\b(first name|given name|first|\[first\]|-first$|_first$)/i.test(haystack))
2086
+ return "first";
2087
+ if (/\b(middle name|middle initial|middle|\[middle\]|-middle$|_middle$)/i.test(haystack))
2088
+ return "middle";
2089
+ if (/\b(last name|family name|surname|last|\[last\]|-last$|_last$)/i.test(haystack))
2090
+ return "last";
2091
+ if (/\b(suffix|\[suffix\]|-suffix$|_suffix$)/i.test(haystack))
2092
+ return "suffix";
2093
+ return null;
2094
+ };
2095
+ const root = stateOptions.selector != null
2096
+ ? document.querySelector(stateOptions.selector)
2097
+ : stateOptions.internalLabel != null
2098
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
2099
+ : stateOptions.fieldId == null
2100
+ ? null
2101
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
2102
+ const nativeControls = root
2103
+ ? Array.from(root.querySelectorAll("input, textarea, select"))
2104
+ : [];
2105
+ const controls = nativeControls.map((control) => ({
2106
+ tagName: control.tagName.toLowerCase(),
2107
+ type: control.getAttribute("type"),
2108
+ id: control.getAttribute("id"),
2109
+ name: control.getAttribute("name"),
2110
+ value: control.getAttribute("value"),
2111
+ currentValue: "value" in control ? control.value : "",
2112
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
2113
+ part: inferPart(control),
2114
+ text: control instanceof HTMLSelectElement
2115
+ ? normalizeText(control.selectedOptions.item(0)?.textContent)
2116
+ : normalizeText(control.textContent),
2117
+ disabled: "disabled" in control ? Boolean(control.disabled) : null,
2118
+ }));
2119
+ const selectedParts = {};
2120
+ for (const control of controls) {
2121
+ if (control.part && control.disabled !== true && typeof control.currentValue === "string" && control.currentValue.length > 0) {
2122
+ selectedParts[control.part] = control.currentValue;
2123
+ }
2124
+ }
2125
+ const expected = stateOptions.expectedValue ?? {};
2126
+ const expectedEntries = Object.entries(expected).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
2127
+ const availableParts = new Set(controls.filter((control) => control.part && control.disabled !== true).map((control) => control.part));
2128
+ const renderedExpectedEntries = expectedEntries.filter(([part]) => availableParts.has(part));
2129
+ const unavailableParts = expectedEntries
2130
+ .filter(([part]) => !availableParts.has(part))
2131
+ .map(([part]) => part);
2132
+ const missingParts = renderedExpectedEntries
2133
+ .filter(([part]) => selectedParts[part] === undefined || selectedParts[part] === "")
2134
+ .map(([part]) => part);
2135
+ const mismatchedParts = renderedExpectedEntries
2136
+ .filter(([part, value]) => selectedParts[part] !== undefined && selectedParts[part] !== "" && selectedParts[part] !== value)
2137
+ .map(([part]) => part);
2138
+ const renderedValueAvailable = Object.keys(selectedParts).length > 0;
2139
+ return {
2140
+ fieldMatched: Boolean(root),
2141
+ selectedParts,
2142
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
2143
+ renderedValueAvailable,
2144
+ unavailableParts,
2145
+ missingParts,
2146
+ mismatchedParts,
2147
+ matched: Boolean(root) && missingParts.length === 0 && mismatchedParts.length === 0,
2148
+ controls,
2149
+ };
2150
+ }, options);
2151
+ }
2152
+ export async function waitForLiveFormNameState(page, options) {
2153
+ const timeoutMs = options.timeoutMs ?? 10_000;
2154
+ const intervalMs = options.intervalMs ?? 100;
2155
+ const startedAt = Date.now();
2156
+ let latest = await readLiveFormNameState(page, options);
2157
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
2158
+ await delay(intervalMs);
2159
+ latest = await readLiveFormNameState(page, options);
2160
+ }
2161
+ return latest;
2162
+ }
2058
2163
  export async function readLiveFormCheckboxState(page, options) {
2059
2164
  if (!hasEvaluate(page)) {
2060
2165
  throw new Error("readLiveFormCheckboxState requires a Playwright page with evaluate().");
@@ -4391,6 +4496,47 @@ export async function setLiveFormAddressValue(page, options) {
4391
4496
  (visibility?.matched ?? true),
4392
4497
  };
4393
4498
  }
4499
+ export async function setLiveFormNameValue(page, options) {
4500
+ const payload = normalizeLiveFormNameValue(options.value, options.current);
4501
+ const transaction = await performLiveFormFieldTransaction(page, {
4502
+ write: {
4503
+ ...(options.write ?? {}),
4504
+ target: options.target,
4505
+ value: payload,
4506
+ notify: options.write?.notify ?? true,
4507
+ },
4508
+ fieldKind: "name",
4509
+ waitForEvent: options.waitForEvent ?? true,
4510
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4511
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
4512
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
4513
+ });
4514
+ const dom = await waitForLiveFormNameState(page, {
4515
+ ...options.target,
4516
+ expectedValue: payload,
4517
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
4518
+ });
4519
+ const visibilityTargets = [
4520
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
4521
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
4522
+ ];
4523
+ const visibility = visibilityTargets.length > 0
4524
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
4525
+ : undefined;
4526
+ return {
4527
+ payload,
4528
+ transaction,
4529
+ dom,
4530
+ ...(visibility ? { visibility } : {}),
4531
+ ok: transaction.write.valueMatched &&
4532
+ (transaction.event?.matched ?? true) &&
4533
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
4534
+ (transaction.quiet?.quiet ?? true) &&
4535
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
4536
+ dom.matched &&
4537
+ (visibility?.matched ?? true),
4538
+ };
4539
+ }
4394
4540
  export async function setLiveFormRatingValue(page, options) {
4395
4541
  const payload = normalizeLiveFormRatingValue(options.value);
4396
4542
  const value = payload.value;
@@ -4521,7 +4667,18 @@ export async function answerProgressiveLiveFormField(page, options) {
4521
4667
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
4522
4668
  })
4523
4669
  : undefined;
4524
- const addressTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "address"
4670
+ const nameTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "name"
4671
+ ? await setLiveFormNameValue(page, {
4672
+ target: options.target,
4673
+ value: options.value,
4674
+ ...(options.write === undefined ? {} : { write: options.write }),
4675
+ waitForEvent: options.waitForEvent ?? true,
4676
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4677
+ afterQuiet: options.afterQuiet ?? true,
4678
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4679
+ })
4680
+ : undefined;
4681
+ const addressTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !nameTransaction && options.fieldKind === "address"
4525
4682
  ? await setLiveFormAddressValue(page, {
4526
4683
  target: options.target,
4527
4684
  value: options.value,
@@ -4532,7 +4689,7 @@ export async function answerProgressiveLiveFormField(page, options) {
4532
4689
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
4533
4690
  })
4534
4691
  : undefined;
4535
- const productTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !addressTransaction && options.fieldKind === "product"
4692
+ const productTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !nameTransaction && !addressTransaction && options.fieldKind === "product"
4536
4693
  ? await setLiveFormProductValue(page, {
4537
4694
  target: options.target,
4538
4695
  value: options.value,
@@ -4543,7 +4700,7 @@ export async function answerProgressiveLiveFormField(page, options) {
4543
4700
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
4544
4701
  })
4545
4702
  : undefined;
4546
- const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !addressTransaction && !productTransaction && options.fieldKind === "rating"
4703
+ const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !nameTransaction && !addressTransaction && !productTransaction && options.fieldKind === "rating"
4547
4704
  ? await setLiveFormRatingValue(page, {
4548
4705
  target: options.target,
4549
4706
  value: options.value,
@@ -4573,27 +4730,29 @@ export async function answerProgressiveLiveFormField(page, options) {
4573
4730
  ? selectTransaction.transaction
4574
4731
  : matrixTransaction
4575
4732
  ? matrixTransaction.transaction
4576
- : addressTransaction
4577
- ? addressTransaction.transaction
4578
- : productTransaction
4579
- ? productTransaction.transaction
4580
- : ratingTransaction
4581
- ? ratingTransaction.transaction
4582
- : dateTimeTransaction
4583
- ? dateTimeTransaction.transaction
4584
- : await performLiveFormFieldTransaction(page, {
4585
- write: {
4586
- ...(options.write ?? {}),
4587
- target: options.target,
4588
- value: options.value,
4589
- notify: options.write?.notify ?? true,
4590
- },
4591
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4592
- waitForEvent: options.waitForEvent ?? true,
4593
- dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4594
- afterQuiet: options.afterQuiet ?? true,
4595
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
4596
- });
4733
+ : nameTransaction
4734
+ ? nameTransaction.transaction
4735
+ : addressTransaction
4736
+ ? addressTransaction.transaction
4737
+ : productTransaction
4738
+ ? productTransaction.transaction
4739
+ : ratingTransaction
4740
+ ? ratingTransaction.transaction
4741
+ : dateTimeTransaction
4742
+ ? dateTimeTransaction.transaction
4743
+ : await performLiveFormFieldTransaction(page, {
4744
+ write: {
4745
+ ...(options.write ?? {}),
4746
+ target: options.target,
4747
+ value: options.value,
4748
+ notify: options.write?.notify ?? true,
4749
+ },
4750
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4751
+ waitForEvent: options.waitForEvent ?? true,
4752
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4753
+ afterQuiet: options.afterQuiet ?? true,
4754
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4755
+ });
4597
4756
  const visibilityTargets = [
4598
4757
  ...(options.expectVisible ?? []),
4599
4758
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -6297,7 +6456,19 @@ export const liveFormFieldRecipes = {
6297
6456
  }
6298
6457
  },
6299
6458
  }),
6300
- setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
6459
+ setNameApi: (name, internalLabel, value, options = {}) => ({
6460
+ name,
6461
+ run: async (harness) => {
6462
+ await setLiveFormNameValue(harness.page, {
6463
+ target: { internalLabel },
6464
+ value,
6465
+ write: { notify: options.notify ?? true },
6466
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
6467
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
6468
+ afterQuiet: options.waitForQuiet ?? true,
6469
+ });
6470
+ },
6471
+ }),
6301
6472
  setAddressApi: (name, internalLabel, value, options = {}) => ({
6302
6473
  name,
6303
6474
  run: async (harness) => {