@redrockswebdevelopment/formstack-form-testing 0.1.30 → 0.1.32
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 +49 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ export const liveFormFieldTypeCoverage = {
|
|
|
42
42
|
domInteraction: true,
|
|
43
43
|
domAssertion: true,
|
|
44
44
|
reason: "Date API state can be set reliably, but split/calendar renderer variants should be verified through DOM assertions.",
|
|
45
|
-
recommendedHelpers: ["buildLiveFormDateTimeValue", "
|
|
45
|
+
recommendedHelpers: ["buildLiveFormDateTimeValue", "setLiveFormDateTimeValue", "readLiveFormDateTimeState", "fillDateTime"],
|
|
46
46
|
},
|
|
47
47
|
datetime: {
|
|
48
48
|
kind: "datetime",
|
|
@@ -51,7 +51,7 @@ export const liveFormFieldTypeCoverage = {
|
|
|
51
51
|
domInteraction: true,
|
|
52
52
|
domAssertion: true,
|
|
53
53
|
reason: "Date/time API state can be set, but visible split controls may lag or vary by renderer.",
|
|
54
|
-
recommendedHelpers: ["buildLiveFormDateTimeValue", "
|
|
54
|
+
recommendedHelpers: ["buildLiveFormDateTimeValue", "setLiveFormDateTimeValue", "readLiveFormDateTimeState", "fillDateTime"],
|
|
55
55
|
},
|
|
56
56
|
description: {
|
|
57
57
|
kind: "description",
|
|
@@ -87,7 +87,7 @@ export const liveFormFieldTypeCoverage = {
|
|
|
87
87
|
domInteraction: true,
|
|
88
88
|
domAssertion: true,
|
|
89
89
|
reason: "File fields must be tested through browser file upload controls, not generic Live Form API setValue payloads.",
|
|
90
|
-
recommendedHelpers: ["uploadFile", "liveFormFieldRecipes.uploadFileDom"
|
|
90
|
+
recommendedHelpers: ["uploadLiveFormFile", "readLiveFormFileState", "uploadFile", "liveFormFieldRecipes.uploadFileDom"],
|
|
91
91
|
},
|
|
92
92
|
matrix: {
|
|
93
93
|
kind: "matrix",
|
|
@@ -2309,16 +2309,24 @@ export async function readLiveFormDateTimeState(page, options) {
|
|
|
2309
2309
|
}
|
|
2310
2310
|
const expected = stateOptions.expectedValue ?? {};
|
|
2311
2311
|
const expectedEntries = Object.entries(expected).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
|
|
2312
|
-
const
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2312
|
+
const renderedValueAvailable = Object.keys(selectedParts).length > 0;
|
|
2313
|
+
const unavailableParts = renderedValueAvailable ? [] : expectedEntries.map(([part]) => part);
|
|
2314
|
+
const missingParts = renderedValueAvailable
|
|
2315
|
+
? expectedEntries
|
|
2316
|
+
.filter(([part]) => selectedParts[part] === undefined || selectedParts[part] === "")
|
|
2317
|
+
.map(([part]) => part)
|
|
2318
|
+
: [];
|
|
2319
|
+
const mismatchedParts = renderedValueAvailable
|
|
2320
|
+
? expectedEntries
|
|
2321
|
+
.filter(([part, value]) => selectedParts[part] !== undefined && selectedParts[part] !== "" && selectedParts[part] !== value)
|
|
2322
|
+
.map(([part]) => part)
|
|
2323
|
+
: [];
|
|
2318
2324
|
return {
|
|
2319
2325
|
fieldMatched: Boolean(root),
|
|
2320
2326
|
selectedParts,
|
|
2321
2327
|
...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
|
|
2328
|
+
renderedValueAvailable,
|
|
2329
|
+
unavailableParts,
|
|
2322
2330
|
missingParts,
|
|
2323
2331
|
mismatchedParts,
|
|
2324
2332
|
matched: Boolean(root) && missingParts.length === 0 && mismatchedParts.length === 0,
|
|
@@ -5245,6 +5253,109 @@ function productPartLocator(page, internalLabel, part) {
|
|
|
5245
5253
|
function productQuantityLocator(page, internalLabel) {
|
|
5246
5254
|
return page.locator(`${formstackSelectors.fieldByInternalLabel(internalLabel)} input[name$="[quantity]"], ${formstackSelectors.fieldByInternalLabel(internalLabel)} select[name$="[quantity]"]`).first();
|
|
5247
5255
|
}
|
|
5256
|
+
export function normalizeLiveFormFileNames(files) {
|
|
5257
|
+
const values = Array.isArray(files) ? files : [files];
|
|
5258
|
+
return values.map((file) => {
|
|
5259
|
+
const parts = String(file).split(/[\\/]/).filter(Boolean);
|
|
5260
|
+
return parts.length > 0 ? parts[parts.length - 1] ?? String(file) : String(file);
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
function fileInputLocator(page, target) {
|
|
5264
|
+
if (target.internalLabel != null) {
|
|
5265
|
+
return page.locator(formstackSelectors.fieldByInternalLabel(target.internalLabel)).locator("input[type=\"file\"]").first();
|
|
5266
|
+
}
|
|
5267
|
+
if (target.fieldId != null) {
|
|
5268
|
+
return page.locator(formstackSelectors.fieldById(target.fieldId)).locator("input[type=\"file\"]").first();
|
|
5269
|
+
}
|
|
5270
|
+
throw new Error("File upload target requires internalLabel or fieldId.");
|
|
5271
|
+
}
|
|
5272
|
+
export async function readLiveFormFileState(page, options) {
|
|
5273
|
+
if (!hasEvaluate(page)) {
|
|
5274
|
+
throw new Error("readLiveFormFileState requires a Playwright page with evaluate().");
|
|
5275
|
+
}
|
|
5276
|
+
return page.evaluate((stateOptions) => {
|
|
5277
|
+
const root = stateOptions.selector != null
|
|
5278
|
+
? document.querySelector(stateOptions.selector)
|
|
5279
|
+
: stateOptions.internalLabel != null
|
|
5280
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
|
|
5281
|
+
: stateOptions.fieldId == null
|
|
5282
|
+
? null
|
|
5283
|
+
: document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
|
|
5284
|
+
const controls = root
|
|
5285
|
+
? Array.from(root.querySelectorAll("input[type=\"file\"]")).map((control) => {
|
|
5286
|
+
const files = Array.from(control.files ?? []).map((file) => file.name);
|
|
5287
|
+
return {
|
|
5288
|
+
tagName: control.tagName.toLowerCase(),
|
|
5289
|
+
type: control.getAttribute("type"),
|
|
5290
|
+
id: control.getAttribute("id"),
|
|
5291
|
+
name: control.getAttribute("name"),
|
|
5292
|
+
value: control.getAttribute("value"),
|
|
5293
|
+
currentValue: control.value,
|
|
5294
|
+
checked: null,
|
|
5295
|
+
files,
|
|
5296
|
+
accept: control.getAttribute("accept"),
|
|
5297
|
+
multiple: control.multiple,
|
|
5298
|
+
disabled: control.disabled,
|
|
5299
|
+
required: control.required,
|
|
5300
|
+
};
|
|
5301
|
+
})
|
|
5302
|
+
: [];
|
|
5303
|
+
const selectedFileNames = controls.flatMap((control) => control.files);
|
|
5304
|
+
const expectedFileNames = [...(stateOptions.expectedFileNames ?? [])];
|
|
5305
|
+
const missingFileNames = expectedFileNames.filter((name) => !selectedFileNames.includes(name));
|
|
5306
|
+
const unexpectedFileNames = selectedFileNames.filter((name) => !expectedFileNames.includes(name));
|
|
5307
|
+
const exact = stateOptions.exact !== false;
|
|
5308
|
+
return {
|
|
5309
|
+
fieldMatched: Boolean(root),
|
|
5310
|
+
controlMatched: controls.length > 0,
|
|
5311
|
+
selectedFileNames,
|
|
5312
|
+
expectedFileNames,
|
|
5313
|
+
missingFileNames,
|
|
5314
|
+
unexpectedFileNames,
|
|
5315
|
+
exact,
|
|
5316
|
+
matched: Boolean(root) &&
|
|
5317
|
+
controls.length > 0 &&
|
|
5318
|
+
missingFileNames.length === 0 &&
|
|
5319
|
+
(expectedFileNames.length === 0 || !exact || unexpectedFileNames.length === 0),
|
|
5320
|
+
controls,
|
|
5321
|
+
};
|
|
5322
|
+
}, options);
|
|
5323
|
+
}
|
|
5324
|
+
export async function waitForLiveFormFileState(page, options) {
|
|
5325
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
5326
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
5327
|
+
const startedAt = Date.now();
|
|
5328
|
+
let latest = await readLiveFormFileState(page, options);
|
|
5329
|
+
while (!latest.matched && Date.now() - startedAt < timeoutMs) {
|
|
5330
|
+
await delay(intervalMs);
|
|
5331
|
+
latest = await readLiveFormFileState(page, options);
|
|
5332
|
+
}
|
|
5333
|
+
return latest;
|
|
5334
|
+
}
|
|
5335
|
+
export async function uploadLiveFormFile(page, options) {
|
|
5336
|
+
const fileNames = options.expectedFileNames ?? normalizeLiveFormFileNames(options.files);
|
|
5337
|
+
await fileInputLocator(page, options.target).setInputFiles(options.files);
|
|
5338
|
+
const quiet = options.afterQuiet === false
|
|
5339
|
+
? undefined
|
|
5340
|
+
: await waitForLiveFormQuiet(page, typeof options.afterQuiet === "object" ? options.afterQuiet : {});
|
|
5341
|
+
const dom = await waitForLiveFormFileState(page, {
|
|
5342
|
+
...options.target,
|
|
5343
|
+
expectedFileNames: fileNames,
|
|
5344
|
+
...(options.exact === undefined ? {} : { exact: options.exact }),
|
|
5345
|
+
});
|
|
5346
|
+
const validation = options.afterValidation === undefined
|
|
5347
|
+
? undefined
|
|
5348
|
+
: await waitForLiveFormValidationState(page, typeof options.afterValidation === "object" ? options.afterValidation : {});
|
|
5349
|
+
return {
|
|
5350
|
+
fileNames,
|
|
5351
|
+
dom,
|
|
5352
|
+
...(quiet ? { quiet } : {}),
|
|
5353
|
+
...(validation ? { validation } : {}),
|
|
5354
|
+
ok: dom.matched &&
|
|
5355
|
+
(quiet?.quiet ?? true) &&
|
|
5356
|
+
((validation?.inlineErrors.length ?? 0) === 0),
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5248
5359
|
export async function drawSignature(page, internalLabel, options = {}) {
|
|
5249
5360
|
if (!page.mouse) {
|
|
5250
5361
|
throw new Error("drawSignature requires a Playwright page with mouse support.");
|
|
@@ -5661,6 +5772,19 @@ export const liveFormFieldRecipes = {
|
|
|
5661
5772
|
name,
|
|
5662
5773
|
run: (harness) => harness.uploadFile(internalLabel, files),
|
|
5663
5774
|
}),
|
|
5775
|
+
uploadFileAndAssertDom: (name, internalLabel, files, options = {}) => ({
|
|
5776
|
+
name,
|
|
5777
|
+
run: async (harness) => {
|
|
5778
|
+
const result = await uploadLiveFormFile(harness.page, {
|
|
5779
|
+
...options,
|
|
5780
|
+
target: { internalLabel },
|
|
5781
|
+
files,
|
|
5782
|
+
});
|
|
5783
|
+
if (!result.ok) {
|
|
5784
|
+
throw new Error(`File upload "${internalLabel}" did not match expected selected files.`);
|
|
5785
|
+
}
|
|
5786
|
+
},
|
|
5787
|
+
}),
|
|
5664
5788
|
clearSignatureDom: (name, internalLabel) => ({
|
|
5665
5789
|
name,
|
|
5666
5790
|
run: (harness) => harness.clearSignature(internalLabel),
|