@redrockswebdevelopment/formstack-form-testing 0.1.28 → 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.d.ts +44 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +268 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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().");
|
|
@@ -2183,14 +2348,30 @@ export async function dispatchLiveFormFieldDomEvents(page, options) {
|
|
|
2183
2348
|
: dispatchOptions.target.fieldId == null
|
|
2184
2349
|
? null
|
|
2185
2350
|
: document.querySelector(`#fsCell${CSS.escape(String(dispatchOptions.target.fieldId))}, [data-testid="field-${CSS.escape(String(dispatchOptions.target.fieldId))}"]`);
|
|
2186
|
-
const
|
|
2351
|
+
const isSelectedControl = (control) => {
|
|
2352
|
+
if (control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type))
|
|
2353
|
+
return control.checked;
|
|
2354
|
+
if (control instanceof HTMLInputElement && control.type === "hidden")
|
|
2355
|
+
return false;
|
|
2356
|
+
const ariaChecked = control.getAttribute("aria-checked");
|
|
2357
|
+
const ariaPressed = control.getAttribute("aria-pressed");
|
|
2358
|
+
const ariaSelected = control.getAttribute("aria-selected");
|
|
2359
|
+
const classes = Array.from(control.classList).join(" ");
|
|
2360
|
+
return ariaChecked === "true" ||
|
|
2361
|
+
ariaPressed === "true" ||
|
|
2362
|
+
ariaSelected === "true" ||
|
|
2363
|
+
/\b(active|checked|selected|filled|on)\b/i.test(classes);
|
|
2364
|
+
};
|
|
2365
|
+
const allControls = root
|
|
2366
|
+
? Array.from(root.querySelectorAll("input, textarea, select, button, [role=\"radio\"], [role=\"button\"], [aria-checked], [aria-pressed], [aria-selected]"))
|
|
2367
|
+
: [];
|
|
2187
2368
|
const controls = dispatchOptions.onlyCheckedChoices ?? true
|
|
2188
2369
|
? allControls.filter((control) => {
|
|
2189
|
-
if (control instanceof HTMLInputElement
|
|
2190
|
-
return control
|
|
2191
|
-
if (control
|
|
2192
|
-
return
|
|
2193
|
-
return
|
|
2370
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLButtonElement)
|
|
2371
|
+
return isSelectedControl(control);
|
|
2372
|
+
if (control.matches("[role=\"radio\"], [role=\"button\"], [aria-checked], [aria-pressed], [aria-selected]"))
|
|
2373
|
+
return isSelectedControl(control);
|
|
2374
|
+
return !(control instanceof HTMLInputElement && control.type === "hidden");
|
|
2194
2375
|
})
|
|
2195
2376
|
: allControls;
|
|
2196
2377
|
for (const control of controls) {
|
|
@@ -2207,7 +2388,7 @@ export async function dispatchLiveFormFieldDomEvents(page, options) {
|
|
|
2207
2388
|
id: control.getAttribute("id"),
|
|
2208
2389
|
name: control.getAttribute("name"),
|
|
2209
2390
|
value: control.getAttribute("value"),
|
|
2210
|
-
currentValue: "value" in control ? control.value : null,
|
|
2391
|
+
currentValue: "value" in control ? String(control.value) : null,
|
|
2211
2392
|
checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
|
|
2212
2393
|
})),
|
|
2213
2394
|
};
|
|
@@ -3935,6 +4116,47 @@ export async function setLiveFormRatingValue(page, options) {
|
|
|
3935
4116
|
(visibility?.matched ?? true),
|
|
3936
4117
|
};
|
|
3937
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
|
+
}
|
|
3938
4160
|
export async function answerProgressiveLiveFormField(page, options) {
|
|
3939
4161
|
const validationOptions = options.afterValidation === undefined ? undefined : typeof options.afterValidation === "object" ? options.afterValidation : {};
|
|
3940
4162
|
const checkboxTransaction = options.fieldKind === "checkbox"
|
|
@@ -3992,6 +4214,17 @@ export async function answerProgressiveLiveFormField(page, options) {
|
|
|
3992
4214
|
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
3993
4215
|
})
|
|
3994
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;
|
|
3995
4228
|
const transaction = checkboxTransaction
|
|
3996
4229
|
? checkboxTransaction.transaction
|
|
3997
4230
|
: choiceTransaction
|
|
@@ -4002,19 +4235,21 @@ export async function answerProgressiveLiveFormField(page, options) {
|
|
|
4002
4235
|
? matrixTransaction.transaction
|
|
4003
4236
|
: ratingTransaction
|
|
4004
4237
|
? ratingTransaction.transaction
|
|
4005
|
-
:
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
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
|
+
});
|
|
4018
4253
|
const visibilityTargets = [
|
|
4019
4254
|
...(options.expectVisible ?? []),
|
|
4020
4255
|
...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
|
|
@@ -5475,7 +5710,19 @@ export const liveFormFieldRecipes = {
|
|
|
5475
5710
|
});
|
|
5476
5711
|
},
|
|
5477
5712
|
}),
|
|
5478
|
-
setDateTimeApi: (name, internalLabel, 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
|
+
}),
|
|
5479
5726
|
setMatrixApi: (name, internalLabel, selections, options = {}) => ({
|
|
5480
5727
|
name,
|
|
5481
5728
|
run: async (harness) => {
|