@redrockswebdevelopment/formstack-form-testing 0.1.34 → 0.1.35

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
@@ -23,8 +23,8 @@ export const liveFormFieldTypeCoverage = {
23
23
  apiWrite: true,
24
24
  domInteraction: true,
25
25
  domAssertion: true,
26
- reason: "Compound address values can be written through Live Form API, while layout and subfield UX should be asserted through DOM controls.",
27
- recommendedHelpers: ["buildLiveFormAddressValue", "setLiveFormFieldValueTransaction", "fillAddress", "readLiveFormDomFieldState"],
26
+ reason: "Compound address values can be written through Live Form API, while subfield UX should be asserted for address line, city, state, ZIP, and country controls.",
27
+ recommendedHelpers: ["buildLiveFormAddressValue", "setLiveFormAddressValue", "readLiveFormAddressState", "fillAddress"],
28
28
  },
29
29
  checkbox: {
30
30
  kind: "checkbox",
@@ -901,6 +901,16 @@ export function buildLiveFormAddressValue(value, current = {}) {
901
901
  ...value,
902
902
  };
903
903
  }
904
+ export function normalizeLiveFormAddressValue(value, current = {}) {
905
+ return buildLiveFormAddressValue({
906
+ ...(value.address === undefined ? {} : { address: String(value.address) }),
907
+ ...(value.address2 === undefined ? {} : { address2: String(value.address2) }),
908
+ ...(value.city === undefined ? {} : { city: String(value.city) }),
909
+ ...(value.state === undefined ? {} : { state: String(value.state) }),
910
+ ...(value.zip === undefined ? {} : { zip: String(value.zip) }),
911
+ ...(value.country === undefined ? {} : { country: String(value.country) }),
912
+ }, current);
913
+ }
904
914
  export function buildLiveFormChoiceValue(value, otherValue = "") {
905
915
  return { value, otherValue };
906
916
  }
@@ -1947,6 +1957,104 @@ export async function waitForLiveFormProductState(page, options) {
1947
1957
  }
1948
1958
  return latest;
1949
1959
  }
1960
+ export async function readLiveFormAddressState(page, options) {
1961
+ if (!hasEvaluate(page)) {
1962
+ throw new Error("readLiveFormAddressState requires a Playwright page with evaluate().");
1963
+ }
1964
+ return page.evaluate((stateOptions) => {
1965
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
1966
+ const inferPart = (control) => {
1967
+ const label = control.id ? document.querySelector(`label[for="${CSS.escape(control.id)}"]`) : null;
1968
+ const haystack = [
1969
+ control.name,
1970
+ control.id,
1971
+ control.className,
1972
+ control.getAttribute("placeholder"),
1973
+ control.getAttribute("aria-label"),
1974
+ label?.textContent,
1975
+ ].map(normalizeText).join(" ").toLowerCase();
1976
+ if (/\b(address line 2|address 2|address2|line 2|street 2|\[address2\]|-address2$|_address2$)/i.test(haystack))
1977
+ return "address2";
1978
+ if (/\b(address line 1|address 1|address|street|line 1|\[address\]|-address$|_address$)/i.test(haystack))
1979
+ return "address";
1980
+ if (/\b(city|\[city\]|-city$|_city$)/i.test(haystack))
1981
+ return "city";
1982
+ if (/\b(state|province|region|\[state\]|-state$|_state$)/i.test(haystack))
1983
+ return "state";
1984
+ if (/\b(zip|postal|postcode|post code|\[zip\]|-zip$|_zip$)/i.test(haystack))
1985
+ return "zip";
1986
+ if (/\b(country|\[country\]|-country$|_country$)/i.test(haystack))
1987
+ return "country";
1988
+ return null;
1989
+ };
1990
+ const root = stateOptions.selector != null
1991
+ ? document.querySelector(stateOptions.selector)
1992
+ : stateOptions.internalLabel != null
1993
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
1994
+ : stateOptions.fieldId == null
1995
+ ? null
1996
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
1997
+ const nativeControls = root
1998
+ ? Array.from(root.querySelectorAll("input, textarea, select"))
1999
+ : [];
2000
+ const controls = nativeControls.map((control) => ({
2001
+ tagName: control.tagName.toLowerCase(),
2002
+ type: control.getAttribute("type"),
2003
+ id: control.getAttribute("id"),
2004
+ name: control.getAttribute("name"),
2005
+ value: control.getAttribute("value"),
2006
+ currentValue: "value" in control ? control.value : "",
2007
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
2008
+ part: inferPart(control),
2009
+ text: control instanceof HTMLSelectElement
2010
+ ? normalizeText(control.selectedOptions.item(0)?.textContent)
2011
+ : normalizeText(control.textContent),
2012
+ disabled: "disabled" in control ? Boolean(control.disabled) : null,
2013
+ }));
2014
+ const selectedParts = {};
2015
+ for (const control of controls) {
2016
+ if (control.part && control.disabled !== true && typeof control.currentValue === "string" && control.currentValue.length > 0) {
2017
+ selectedParts[control.part] = control.currentValue;
2018
+ }
2019
+ }
2020
+ const expected = stateOptions.expectedValue ?? {};
2021
+ const expectedEntries = Object.entries(expected).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
2022
+ const availableParts = new Set(controls.filter((control) => control.part && control.disabled !== true).map((control) => control.part));
2023
+ const renderedExpectedEntries = expectedEntries.filter(([part]) => availableParts.has(part));
2024
+ const unavailableParts = expectedEntries
2025
+ .filter(([part]) => !availableParts.has(part))
2026
+ .map(([part]) => part);
2027
+ const missingParts = renderedExpectedEntries
2028
+ .filter(([part]) => selectedParts[part] === undefined || selectedParts[part] === "")
2029
+ .map(([part]) => part);
2030
+ const mismatchedParts = renderedExpectedEntries
2031
+ .filter(([part, value]) => selectedParts[part] !== undefined && selectedParts[part] !== "" && selectedParts[part] !== value)
2032
+ .map(([part]) => part);
2033
+ const renderedValueAvailable = Object.keys(selectedParts).length > 0;
2034
+ return {
2035
+ fieldMatched: Boolean(root),
2036
+ selectedParts,
2037
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
2038
+ renderedValueAvailable,
2039
+ unavailableParts,
2040
+ missingParts,
2041
+ mismatchedParts,
2042
+ matched: Boolean(root) && missingParts.length === 0 && mismatchedParts.length === 0,
2043
+ controls,
2044
+ };
2045
+ }, options);
2046
+ }
2047
+ export async function waitForLiveFormAddressState(page, options) {
2048
+ const timeoutMs = options.timeoutMs ?? 10_000;
2049
+ const intervalMs = options.intervalMs ?? 100;
2050
+ const startedAt = Date.now();
2051
+ let latest = await readLiveFormAddressState(page, options);
2052
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
2053
+ await delay(intervalMs);
2054
+ latest = await readLiveFormAddressState(page, options);
2055
+ }
2056
+ return latest;
2057
+ }
1950
2058
  export async function readLiveFormCheckboxState(page, options) {
1951
2059
  if (!hasEvaluate(page)) {
1952
2060
  throw new Error("readLiveFormCheckboxState requires a Playwright page with evaluate().");
@@ -4242,6 +4350,47 @@ export async function setLiveFormProductValue(page, options) {
4242
4350
  (visibility?.matched ?? true),
4243
4351
  };
4244
4352
  }
4353
+ export async function setLiveFormAddressValue(page, options) {
4354
+ const payload = normalizeLiveFormAddressValue(options.value, options.current);
4355
+ const transaction = await performLiveFormFieldTransaction(page, {
4356
+ write: {
4357
+ ...(options.write ?? {}),
4358
+ target: options.target,
4359
+ value: payload,
4360
+ notify: options.write?.notify ?? true,
4361
+ },
4362
+ fieldKind: "address",
4363
+ waitForEvent: options.waitForEvent ?? true,
4364
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4365
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
4366
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
4367
+ });
4368
+ const dom = await waitForLiveFormAddressState(page, {
4369
+ ...options.target,
4370
+ expectedValue: payload,
4371
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
4372
+ });
4373
+ const visibilityTargets = [
4374
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
4375
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
4376
+ ];
4377
+ const visibility = visibilityTargets.length > 0
4378
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
4379
+ : undefined;
4380
+ return {
4381
+ payload,
4382
+ transaction,
4383
+ dom,
4384
+ ...(visibility ? { visibility } : {}),
4385
+ ok: transaction.write.valueMatched &&
4386
+ (transaction.event?.matched ?? true) &&
4387
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
4388
+ (transaction.quiet?.quiet ?? true) &&
4389
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
4390
+ dom.matched &&
4391
+ (visibility?.matched ?? true),
4392
+ };
4393
+ }
4245
4394
  export async function setLiveFormRatingValue(page, options) {
4246
4395
  const payload = normalizeLiveFormRatingValue(options.value);
4247
4396
  const value = payload.value;
@@ -4372,7 +4521,18 @@ export async function answerProgressiveLiveFormField(page, options) {
4372
4521
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
4373
4522
  })
4374
4523
  : undefined;
4375
- const productTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "product"
4524
+ const addressTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "address"
4525
+ ? await setLiveFormAddressValue(page, {
4526
+ target: options.target,
4527
+ value: options.value,
4528
+ ...(options.write === undefined ? {} : { write: options.write }),
4529
+ waitForEvent: options.waitForEvent ?? true,
4530
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4531
+ afterQuiet: options.afterQuiet ?? true,
4532
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4533
+ })
4534
+ : undefined;
4535
+ const productTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !addressTransaction && options.fieldKind === "product"
4376
4536
  ? await setLiveFormProductValue(page, {
4377
4537
  target: options.target,
4378
4538
  value: options.value,
@@ -4383,7 +4543,7 @@ export async function answerProgressiveLiveFormField(page, options) {
4383
4543
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
4384
4544
  })
4385
4545
  : undefined;
4386
- const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !productTransaction && options.fieldKind === "rating"
4546
+ const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !addressTransaction && !productTransaction && options.fieldKind === "rating"
4387
4547
  ? await setLiveFormRatingValue(page, {
4388
4548
  target: options.target,
4389
4549
  value: options.value,
@@ -4413,25 +4573,27 @@ export async function answerProgressiveLiveFormField(page, options) {
4413
4573
  ? selectTransaction.transaction
4414
4574
  : matrixTransaction
4415
4575
  ? matrixTransaction.transaction
4416
- : productTransaction
4417
- ? productTransaction.transaction
4418
- : ratingTransaction
4419
- ? ratingTransaction.transaction
4420
- : dateTimeTransaction
4421
- ? dateTimeTransaction.transaction
4422
- : await performLiveFormFieldTransaction(page, {
4423
- write: {
4424
- ...(options.write ?? {}),
4425
- target: options.target,
4426
- value: options.value,
4427
- notify: options.write?.notify ?? true,
4428
- },
4429
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4430
- waitForEvent: options.waitForEvent ?? true,
4431
- dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4432
- afterQuiet: options.afterQuiet ?? true,
4433
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
4434
- });
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
+ });
4435
4597
  const visibilityTargets = [
4436
4598
  ...(options.expectVisible ?? []),
4437
4599
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -6136,7 +6298,19 @@ export const liveFormFieldRecipes = {
6136
6298
  },
6137
6299
  }),
6138
6300
  setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
6139
- setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
6301
+ setAddressApi: (name, internalLabel, value, options = {}) => ({
6302
+ name,
6303
+ run: async (harness) => {
6304
+ await setLiveFormAddressValue(harness.page, {
6305
+ target: { internalLabel },
6306
+ value,
6307
+ write: { notify: options.notify ?? true },
6308
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
6309
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
6310
+ afterQuiet: options.waitForQuiet ?? true,
6311
+ });
6312
+ },
6313
+ }),
6140
6314
  setChoiceApi: (name, internalLabel, value, options = {}) => ({
6141
6315
  name,
6142
6316
  run: async (harness) => {