@redrockswebdevelopment/formstack-form-testing 0.1.29 → 0.1.30

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
@@ -969,6 +969,61 @@ export function buildLiveFormDateTimeValue(value, current = {}) {
969
969
  ...(value.ampm !== undefined ? { A: value.ampm } : {}),
970
970
  };
971
971
  }
972
+ export function normalizeLiveFormDateTimeValue(value) {
973
+ if (value instanceof Date) {
974
+ return {
975
+ M: String(value.getUTCMonth() + 1).padStart(2, "0"),
976
+ D: String(value.getUTCDate()).padStart(2, "0"),
977
+ Y: String(value.getUTCFullYear()),
978
+ H: String(value.getUTCHours()).padStart(2, "0"),
979
+ I: String(value.getUTCMinutes()).padStart(2, "0"),
980
+ S: String(value.getUTCSeconds()).padStart(2, "0"),
981
+ };
982
+ }
983
+ if (typeof value === "string") {
984
+ const iso = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2})(?::(\d{2}))?)?/);
985
+ if (iso) {
986
+ const [, year, month, day, hour, minutes, seconds] = iso;
987
+ return {
988
+ ...(year === undefined ? {} : { Y: year }),
989
+ ...(month === undefined ? {} : { M: month }),
990
+ ...(day === undefined ? {} : { D: day }),
991
+ ...(hour === undefined ? {} : { H: hour }),
992
+ ...(minutes === undefined ? {} : { I: minutes }),
993
+ ...(seconds === undefined ? {} : { S: seconds }),
994
+ };
995
+ }
996
+ const us = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)?)?/i);
997
+ if (us) {
998
+ const [, month, day, year, hour, minutes, seconds, ampm] = us;
999
+ return {
1000
+ ...(month === undefined ? {} : { M: month.padStart(2, "0") }),
1001
+ ...(day === undefined ? {} : { D: day.padStart(2, "0") }),
1002
+ ...(year === undefined ? {} : { Y: year }),
1003
+ ...(hour === undefined ? {} : { H: hour.padStart(2, "0") }),
1004
+ ...(minutes === undefined ? {} : { I: minutes }),
1005
+ ...(seconds === undefined ? {} : { S: seconds }),
1006
+ ...(ampm === undefined ? {} : { A: ampm.toUpperCase() }),
1007
+ };
1008
+ }
1009
+ return { Y: value };
1010
+ }
1011
+ if ("month" in value || "day" in value || "year" in value || "hour" in value || "minutes" in value || "seconds" in value || "ampm" in value) {
1012
+ return buildLiveFormDateTimeValue(value);
1013
+ }
1014
+ if ("M" in value || "D" in value || "Y" in value || "H" in value || "I" in value || "S" in value || "A" in value) {
1015
+ return {
1016
+ ...(value.M === undefined ? {} : { M: String(value.M) }),
1017
+ ...(value.D === undefined ? {} : { D: String(value.D) }),
1018
+ ...(value.Y === undefined ? {} : { Y: String(value.Y) }),
1019
+ ...(value.H === undefined ? {} : { H: String(value.H) }),
1020
+ ...(value.I === undefined ? {} : { I: String(value.I) }),
1021
+ ...(value.S === undefined ? {} : { S: String(value.S) }),
1022
+ ...(value.A === undefined ? {} : { A: String(value.A) }),
1023
+ };
1024
+ }
1025
+ return {};
1026
+ }
972
1027
  export function buildLiveFormProductValue(value, current = {}) {
973
1028
  return {
974
1029
  ...(current && typeof current === "object" ? current : {}),
@@ -2172,6 +2227,116 @@ export async function waitForLiveFormRatingState(page, options) {
2172
2227
  }
2173
2228
  return latest;
2174
2229
  }
2230
+ export async function readLiveFormDateTimeState(page, options) {
2231
+ if (!hasEvaluate(page)) {
2232
+ throw new Error("readLiveFormDateTimeState requires a Playwright page with evaluate().");
2233
+ }
2234
+ return page.evaluate((stateOptions) => {
2235
+ const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
2236
+ const inferPart = (control) => {
2237
+ const label = control.id ? document.querySelector(`label[for="${CSS.escape(control.id)}"]`) : null;
2238
+ const haystack = [
2239
+ control.name,
2240
+ control.id,
2241
+ control.className,
2242
+ control.getAttribute("placeholder"),
2243
+ control.getAttribute("aria-label"),
2244
+ label?.textContent,
2245
+ ].map(normalizeText).join(" ").toLowerCase();
2246
+ if (/\b(month|mm|\[m\]|-m$|_m$)/i.test(haystack))
2247
+ return "M";
2248
+ if (/\b(day|dd|\[d\]|-d$|_d$)/i.test(haystack))
2249
+ return "D";
2250
+ if (/\b(year|yyyy|\[y\]|-y$|_y$)/i.test(haystack))
2251
+ return "Y";
2252
+ if (/\b(hour|hh|\[h\]|-h$|_h$)/i.test(haystack))
2253
+ return "H";
2254
+ if (/\b(minute|minutes|mmn|\[i\]|-i$|_i$)/i.test(haystack))
2255
+ return "I";
2256
+ if (/\b(second|seconds|\[s\]|-s$|_s$)/i.test(haystack))
2257
+ return "S";
2258
+ if (/\b(am\/pm|ampm|am pm|\[a\]|-a$|_a$)/i.test(haystack))
2259
+ return "A";
2260
+ return null;
2261
+ };
2262
+ const root = stateOptions.selector != null
2263
+ ? document.querySelector(stateOptions.selector)
2264
+ : stateOptions.internalLabel != null
2265
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
2266
+ : stateOptions.fieldId == null
2267
+ ? null
2268
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
2269
+ const nativeControls = root
2270
+ ? Array.from(root.querySelectorAll("input, textarea, select"))
2271
+ : [];
2272
+ const controls = nativeControls.map((control) => {
2273
+ const currentValue = "value" in control ? control.value : "";
2274
+ return {
2275
+ tagName: control.tagName.toLowerCase(),
2276
+ type: control.getAttribute("type"),
2277
+ id: control.getAttribute("id"),
2278
+ name: control.getAttribute("name"),
2279
+ value: control.getAttribute("value"),
2280
+ currentValue,
2281
+ checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
2282
+ part: inferPart(control),
2283
+ text: control instanceof HTMLSelectElement
2284
+ ? normalizeText(control.selectedOptions.item(0)?.textContent)
2285
+ : normalizeText(control.textContent),
2286
+ };
2287
+ });
2288
+ const selectedParts = {};
2289
+ for (const control of controls) {
2290
+ if (control.part && typeof control.currentValue === "string" && control.currentValue.length > 0) {
2291
+ selectedParts[control.part] = control.currentValue;
2292
+ }
2293
+ }
2294
+ const unparted = controls.filter((control) => control.part === null && typeof control.currentValue === "string" && control.currentValue.length > 0);
2295
+ if (Object.keys(selectedParts).length === 0 && unparted.length === 1) {
2296
+ const parsed = unparted[0]?.currentValue?.match(/^(\d{4})-(\d{2})-(\d{2})/) ?? unparted[0]?.currentValue?.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
2297
+ if (parsed) {
2298
+ if (parsed[0].includes("-")) {
2299
+ selectedParts.Y = parsed[1] ?? "";
2300
+ selectedParts.M = parsed[2] ?? "";
2301
+ selectedParts.D = parsed[3] ?? "";
2302
+ }
2303
+ else {
2304
+ selectedParts.M = (parsed[1] ?? "").padStart(2, "0");
2305
+ selectedParts.D = (parsed[2] ?? "").padStart(2, "0");
2306
+ selectedParts.Y = parsed[3] ?? "";
2307
+ }
2308
+ }
2309
+ }
2310
+ const expected = stateOptions.expectedValue ?? {};
2311
+ const expectedEntries = Object.entries(expected).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
2312
+ const missingParts = expectedEntries
2313
+ .filter(([part]) => selectedParts[part] === undefined || selectedParts[part] === "")
2314
+ .map(([part]) => part);
2315
+ const mismatchedParts = expectedEntries
2316
+ .filter(([part, value]) => selectedParts[part] !== undefined && selectedParts[part] !== "" && selectedParts[part] !== value)
2317
+ .map(([part]) => part);
2318
+ return {
2319
+ fieldMatched: Boolean(root),
2320
+ selectedParts,
2321
+ ...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
2322
+ missingParts,
2323
+ mismatchedParts,
2324
+ matched: Boolean(root) && missingParts.length === 0 && mismatchedParts.length === 0,
2325
+ controls,
2326
+ };
2327
+ }, options);
2328
+ }
2329
+ export async function waitForLiveFormDateTimeState(page, options) {
2330
+ const timeoutMs = options.timeoutMs ?? 10_000;
2331
+ const intervalMs = options.intervalMs ?? 100;
2332
+ const startedAt = Date.now();
2333
+ let latest = await readLiveFormDateTimeState(page, options);
2334
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
2335
+ await delay(intervalMs);
2336
+ latest = await readLiveFormDateTimeState(page, options);
2337
+ }
2338
+ return latest;
2339
+ }
2175
2340
  export async function dispatchLiveFormFieldDomEvents(page, options) {
2176
2341
  if (!hasEvaluate(page)) {
2177
2342
  throw new Error("dispatchLiveFormFieldDomEvents requires a Playwright page with evaluate().");
@@ -3951,6 +4116,47 @@ export async function setLiveFormRatingValue(page, options) {
3951
4116
  (visibility?.matched ?? true),
3952
4117
  };
3953
4118
  }
4119
+ export async function setLiveFormDateTimeValue(page, options) {
4120
+ const payload = normalizeLiveFormDateTimeValue(options.value);
4121
+ const transaction = await performLiveFormFieldTransaction(page, {
4122
+ write: {
4123
+ ...(options.write ?? {}),
4124
+ target: options.target,
4125
+ value: payload,
4126
+ notify: options.write?.notify ?? true,
4127
+ },
4128
+ fieldKind: "datetime",
4129
+ waitForEvent: options.waitForEvent ?? true,
4130
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4131
+ afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
4132
+ ...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
4133
+ });
4134
+ const dom = await waitForLiveFormDateTimeState(page, {
4135
+ ...options.target,
4136
+ expectedValue: payload,
4137
+ ...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
4138
+ });
4139
+ const visibilityTargets = [
4140
+ ...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
4141
+ ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
4142
+ ];
4143
+ const visibility = visibilityTargets.length > 0
4144
+ ? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
4145
+ : undefined;
4146
+ return {
4147
+ payload,
4148
+ transaction,
4149
+ dom,
4150
+ ...(visibility ? { visibility } : {}),
4151
+ ok: transaction.write.valueMatched &&
4152
+ (transaction.event?.matched ?? true) &&
4153
+ (transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
4154
+ (transaction.quiet?.quiet ?? true) &&
4155
+ ((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
4156
+ dom.matched &&
4157
+ (visibility?.matched ?? true),
4158
+ };
4159
+ }
3954
4160
  export async function answerProgressiveLiveFormField(page, options) {
3955
4161
  const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
3956
4162
  const checkboxTransaction = options.fieldKind === "checkbox"
@@ -4008,6 +4214,17 @@ export async function answerProgressiveLiveFormField(page, options) {
4008
4214
  ...(validationOptions ? { afterValidation: validationOptions } : {}),
4009
4215
  })
4010
4216
  : undefined;
4217
+ const dateTimeTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !ratingTransaction && ["date", "datetime"].includes(options.fieldKind ?? "")
4218
+ ? await setLiveFormDateTimeValue(page, {
4219
+ target: options.target,
4220
+ value: options.value,
4221
+ ...(options.write === undefined ? {} : { write: options.write }),
4222
+ waitForEvent: options.waitForEvent ?? true,
4223
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4224
+ afterQuiet: options.afterQuiet ?? true,
4225
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4226
+ })
4227
+ : undefined;
4011
4228
  const transaction = checkboxTransaction
4012
4229
  ? checkboxTransaction.transaction
4013
4230
  : choiceTransaction
@@ -4018,19 +4235,21 @@ export async function answerProgressiveLiveFormField(page, options) {
4018
4235
  ? matrixTransaction.transaction
4019
4236
  : ratingTransaction
4020
4237
  ? 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
- });
4238
+ : dateTimeTransaction
4239
+ ? dateTimeTransaction.transaction
4240
+ : await performLiveFormFieldTransaction(page, {
4241
+ write: {
4242
+ ...(options.write ?? {}),
4243
+ target: options.target,
4244
+ value: options.value,
4245
+ notify: options.write?.notify ?? true,
4246
+ },
4247
+ ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4248
+ waitForEvent: options.waitForEvent ?? true,
4249
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4250
+ afterQuiet: options.afterQuiet ?? true,
4251
+ ...(validationOptions ? { afterValidation: validationOptions } : {}),
4252
+ });
4034
4253
  const visibilityTargets = [
4035
4254
  ...(options.expectVisible ?? []),
4036
4255
  ...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
@@ -5491,7 +5710,19 @@ export const liveFormFieldRecipes = {
5491
5710
  });
5492
5711
  },
5493
5712
  }),
5494
- setDateTimeApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormDateTimeValue(value), options),
5713
+ setDateTimeApi: (name, internalLabel, value, options = {}) => ({
5714
+ name,
5715
+ run: async (harness) => {
5716
+ await setLiveFormDateTimeValue(harness.page, {
5717
+ target: { internalLabel },
5718
+ value,
5719
+ write: { notify: options.notify ?? true },
5720
+ waitForEvent: options.waitForEvent ?? (options.notify ?? true),
5721
+ dispatchDomEvents: options.dispatchDomEvents ?? "auto",
5722
+ afterQuiet: options.waitForQuiet ?? true,
5723
+ });
5724
+ },
5725
+ }),
5495
5726
  setMatrixApi: (name, internalLabel, selections, options = {}) => ({
5496
5727
  name,
5497
5728
  run: async (harness) => {