@redrockswebdevelopment/formstack-form-testing 0.1.43 → 0.1.44
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 +93 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,6 +16,17 @@ export class FormScenarioRunError extends Error {
|
|
|
16
16
|
this.result = result;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
export class LiveFormTestingError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
detail;
|
|
22
|
+
constructor(code, message, detail) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "LiveFormTestingError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
if (detail)
|
|
27
|
+
this.detail = detail;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
export const liveFormFieldTypeCoverage = {
|
|
20
31
|
address: {
|
|
21
32
|
kind: "address",
|
|
@@ -247,6 +258,70 @@ export const liveFormFieldTypeCoverage = {
|
|
|
247
258
|
export function getLiveFormFieldTypeCoverage(kind) {
|
|
248
259
|
return liveFormFieldTypeCoverage[kind];
|
|
249
260
|
}
|
|
261
|
+
export function createLiveFormTestingError(code, message, detail) {
|
|
262
|
+
return new LiveFormTestingError(code, message, detail);
|
|
263
|
+
}
|
|
264
|
+
export const liveFormCiProfiles = {
|
|
265
|
+
"fast-smoke": {
|
|
266
|
+
name: "fast-smoke",
|
|
267
|
+
projects: ["chromium"],
|
|
268
|
+
recommendedRetries: 1,
|
|
269
|
+
includeVisualBaselines: false,
|
|
270
|
+
includeHostedSubmissions: false,
|
|
271
|
+
includePaymentFields: false,
|
|
272
|
+
defaultTimeoutMs: 30_000,
|
|
273
|
+
description: "Fast deterministic checks for package and client CI on every commit.",
|
|
274
|
+
},
|
|
275
|
+
"hosted-regression": {
|
|
276
|
+
name: "hosted-regression",
|
|
277
|
+
projects: ["chromium", "mobile-chrome"],
|
|
278
|
+
recommendedRetries: 2,
|
|
279
|
+
includeVisualBaselines: false,
|
|
280
|
+
includeHostedSubmissions: true,
|
|
281
|
+
includePaymentFields: false,
|
|
282
|
+
defaultTimeoutMs: 60_000,
|
|
283
|
+
description: "Hosted Formstack checks for helper compatibility, custom code behavior, and submission payload probes.",
|
|
284
|
+
},
|
|
285
|
+
"full-browser": {
|
|
286
|
+
name: "full-browser",
|
|
287
|
+
projects: ["chromium", "mobile-chrome", "webkit"],
|
|
288
|
+
recommendedRetries: 2,
|
|
289
|
+
includeVisualBaselines: true,
|
|
290
|
+
includeHostedSubmissions: true,
|
|
291
|
+
includePaymentFields: false,
|
|
292
|
+
defaultTimeoutMs: 90_000,
|
|
293
|
+
description: "Full browser regression profile for release candidates and project acceptance.",
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
export function getLiveFormCiProfile(name) {
|
|
297
|
+
return liveFormCiProfiles[name];
|
|
298
|
+
}
|
|
299
|
+
export function createLiveFormFixtureRegistry(fixtures) {
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
for (const fixture of fixtures) {
|
|
302
|
+
if (seen.has(fixture.key)) {
|
|
303
|
+
throw createLiveFormTestingError("fixture-not-found", `Duplicate live form fixture key "${fixture.key}".`, { key: fixture.key });
|
|
304
|
+
}
|
|
305
|
+
seen.add(fixture.key);
|
|
306
|
+
}
|
|
307
|
+
return { fixtures: [...fixtures] };
|
|
308
|
+
}
|
|
309
|
+
export function selectLiveFormFixture(registry, options) {
|
|
310
|
+
const tags = new Set(options.tags ?? []);
|
|
311
|
+
const fixture = registry.fixtures.find((entry) => {
|
|
312
|
+
if (options.key !== undefined && entry.key !== options.key)
|
|
313
|
+
return false;
|
|
314
|
+
if (options.environment !== undefined && entry.environment !== options.environment)
|
|
315
|
+
return false;
|
|
316
|
+
if (tags.size > 0 && ![...tags].every((tag) => entry.tags?.includes(tag)))
|
|
317
|
+
return false;
|
|
318
|
+
return true;
|
|
319
|
+
});
|
|
320
|
+
if (!fixture) {
|
|
321
|
+
throw createLiveFormTestingError("fixture-not-found", "No live form fixture matched the requested selector.", { options });
|
|
322
|
+
}
|
|
323
|
+
return fixture;
|
|
324
|
+
}
|
|
250
325
|
export function summarizeLiveFormFieldCoverage(reportOrFields) {
|
|
251
326
|
const fields = "fields" in reportOrFields ? reportOrFields.fields : reportOrFields;
|
|
252
327
|
const countsByKind = {};
|
|
@@ -268,6 +343,62 @@ export function summarizeLiveFormFieldCoverage(reportOrFields) {
|
|
|
268
343
|
export async function readLiveFormFieldCoverage(page, options = {}) {
|
|
269
344
|
return summarizeLiveFormFieldCoverage(await discoverLiveFormFieldShapes(page, options));
|
|
270
345
|
}
|
|
346
|
+
export async function diagnoseLiveFormUnknownFields(page, options = {}) {
|
|
347
|
+
const coverage = await readLiveFormFieldCoverage(page, options);
|
|
348
|
+
const unknownFields = coverage.fields.filter((field) => field.inferredKind === "unknown");
|
|
349
|
+
const metadata = options.includeMetadata === false || unknownFields.length === 0
|
|
350
|
+
? undefined
|
|
351
|
+
: await readLiveFormApiMetadataSnapshot(page, {
|
|
352
|
+
fieldTargets: unknownFields.map((field) => ({
|
|
353
|
+
...(field.fieldId == null ? {} : { fieldId: field.fieldId }),
|
|
354
|
+
...(field.internalLabel == null ? {} : { internalLabel: field.internalLabel }),
|
|
355
|
+
})),
|
|
356
|
+
...(options.includeValues === undefined ? {} : { includeValues: options.includeValues }),
|
|
357
|
+
});
|
|
358
|
+
const metadataByLabel = new Map((metadata?.fields ?? []).map((field) => [field.internalLabel, field]));
|
|
359
|
+
const suggestionsFor = (field) => {
|
|
360
|
+
const text = field.labelText.toLowerCase();
|
|
361
|
+
const types = new Set(field.controlTypes);
|
|
362
|
+
const label = field.internalLabel?.toLowerCase() ?? "";
|
|
363
|
+
const suggestions = [];
|
|
364
|
+
if (types.has("hidden") && field.controlTypes.length === 1) {
|
|
365
|
+
suggestions.push({ kind: "hidden", reason: "Field exposes only hidden controls.", confidence: "medium" });
|
|
366
|
+
}
|
|
367
|
+
if (label.includes("calc") || text.includes("calculation") || text.includes("total")) {
|
|
368
|
+
suggestions.push({ kind: "calculation", reason: "Field label or internal label suggests a derived calculation.", confidence: "medium" });
|
|
369
|
+
}
|
|
370
|
+
if (types.has("date"))
|
|
371
|
+
suggestions.push({ kind: "date", reason: "Field contains a native date input.", confidence: "high" });
|
|
372
|
+
if (types.has("number"))
|
|
373
|
+
suggestions.push({ kind: "number", reason: "Field contains a native number input.", confidence: "high" });
|
|
374
|
+
if (types.has("text"))
|
|
375
|
+
suggestions.push({ kind: "text", reason: "Field contains text controls but no stronger field-specific markers.", confidence: "low" });
|
|
376
|
+
if (field.controlCount === 0)
|
|
377
|
+
suggestions.push({ kind: "description", reason: "Field has no controls and may be display-only content.", confidence: "low" });
|
|
378
|
+
return suggestions;
|
|
379
|
+
};
|
|
380
|
+
const diagnostics = unknownFields.map((field) => {
|
|
381
|
+
const suggestions = suggestionsFor(field);
|
|
382
|
+
const fieldMetadata = field.internalLabel === null ? undefined : metadataByLabel.get(field.internalLabel);
|
|
383
|
+
return {
|
|
384
|
+
field,
|
|
385
|
+
suggestions,
|
|
386
|
+
...(fieldMetadata === undefined ? {} : { metadata: fieldMetadata }),
|
|
387
|
+
recommendedNextSteps: [
|
|
388
|
+
"Attach the field diagnostic JSON and screenshot to the failing test artifact.",
|
|
389
|
+
"Inspect stable data-internal-label, controls, API methods, and Formstack field settings.",
|
|
390
|
+
suggestions.length > 0
|
|
391
|
+
? `Try an inferred-kind override or helper for ${suggestions[0]?.kind}.`
|
|
392
|
+
: "Add a package-level inferred kind or a project-specific helper before automating this field.",
|
|
393
|
+
],
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
unknownFields: diagnostics,
|
|
398
|
+
hasUnknownFields: diagnostics.length > 0,
|
|
399
|
+
...(options.screenshotPath === undefined ? {} : { screenshotPath: options.screenshotPath }),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
271
402
|
function formatCoverageField(field) {
|
|
272
403
|
return `${field.inferredKind}:${field.internalLabel ?? field.domId ?? "unlabeled"}`;
|
|
273
404
|
}
|
|
@@ -299,7 +430,12 @@ export function assertLiveFormFieldCoverage(report, options = {}) {
|
|
|
299
430
|
}
|
|
300
431
|
}
|
|
301
432
|
if (failures.length > 0) {
|
|
302
|
-
throw
|
|
433
|
+
throw createLiveFormTestingError("field-coverage-failed", `Live form field coverage assertion failed: ${failures.join("; ")}`, {
|
|
434
|
+
failures,
|
|
435
|
+
countsByKind: report.countsByKind,
|
|
436
|
+
unknownFields: report.uncovered.map(formatCoverageField),
|
|
437
|
+
paymentFields: report.paymentFields.map(formatCoverageField),
|
|
438
|
+
});
|
|
303
439
|
}
|
|
304
440
|
}
|
|
305
441
|
function matchesPattern(value, pattern) {
|
|
@@ -3730,6 +3866,121 @@ export async function readLiveFormStateDrift(page, options = {}) {
|
|
|
3730
3866
|
hasDrift: issues.length > 0,
|
|
3731
3867
|
};
|
|
3732
3868
|
}
|
|
3869
|
+
export async function readLiveFormApiMetadataSnapshot(page, options = {}) {
|
|
3870
|
+
if (!hasEvaluate(page)) {
|
|
3871
|
+
throw new Error("readLiveFormApiMetadataSnapshot requires a Playwright page with evaluate().");
|
|
3872
|
+
}
|
|
3873
|
+
return page.evaluate((metadataOptions) => {
|
|
3874
|
+
const methodNames = metadataOptions.methodNames ?? [
|
|
3875
|
+
"getId",
|
|
3876
|
+
"getValue",
|
|
3877
|
+
"setValue",
|
|
3878
|
+
"getInfo",
|
|
3879
|
+
"getGeneralAttribute",
|
|
3880
|
+
"setGeneralAttribute",
|
|
3881
|
+
"validate",
|
|
3882
|
+
"setSkipValidation",
|
|
3883
|
+
];
|
|
3884
|
+
const domSelector = metadataOptions.snapshot?.selector ?? "[data-internal-label]";
|
|
3885
|
+
const textMaxLength = metadataOptions.snapshot?.textMaxLength ?? 160;
|
|
3886
|
+
const controlSnapshot = (control) => {
|
|
3887
|
+
const input = control;
|
|
3888
|
+
return {
|
|
3889
|
+
tagName: control.tagName.toLowerCase(),
|
|
3890
|
+
type: control.getAttribute("type"),
|
|
3891
|
+
id: control.getAttribute("id"),
|
|
3892
|
+
name: control.getAttribute("name"),
|
|
3893
|
+
value: control.getAttribute("value"),
|
|
3894
|
+
currentValue: "value" in input ? input.value : null,
|
|
3895
|
+
checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(input.type) ? control.checked : null,
|
|
3896
|
+
};
|
|
3897
|
+
};
|
|
3898
|
+
const domFields = Array.from(document.querySelectorAll(domSelector)).map((element) => ({
|
|
3899
|
+
internalLabel: element.getAttribute("data-internal-label"),
|
|
3900
|
+
id: element.id,
|
|
3901
|
+
text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, textMaxLength) ?? "",
|
|
3902
|
+
controls: Array.from(element.querySelectorAll("input, textarea, select")).map(controlSnapshot),
|
|
3903
|
+
}));
|
|
3904
|
+
const domByInternalLabel = new Map(domFields.map((field) => [field.internalLabel, field]));
|
|
3905
|
+
const readApi = () => {
|
|
3906
|
+
const factory = Reflect.get(window, "fsApi");
|
|
3907
|
+
const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
|
|
3908
|
+
const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
|
|
3909
|
+
const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
|
|
3910
|
+
const form = Array.isArray(forms) ? forms[0] ?? null : null;
|
|
3911
|
+
return { form, apiAvailable: Boolean(api && typeof api === "object") };
|
|
3912
|
+
};
|
|
3913
|
+
const { form, apiAvailable } = readApi();
|
|
3914
|
+
const formAvailable = Boolean(form && typeof form === "object");
|
|
3915
|
+
const formId = formAvailable ? Reflect.get(form, "id") ?? Reflect.get(form, "formId") ?? null : null;
|
|
3916
|
+
const formMethodNames = formAvailable
|
|
3917
|
+
? [...new Set([
|
|
3918
|
+
...Object.keys(form),
|
|
3919
|
+
...Object.getOwnPropertyNames(Object.getPrototypeOf(form) ?? {}),
|
|
3920
|
+
])].filter((name) => typeof Reflect.get(form, name) === "function").sort()
|
|
3921
|
+
: [];
|
|
3922
|
+
const getField = formAvailable ? Reflect.get(form, "getField") : null;
|
|
3923
|
+
const getFieldByInternalLabel = formAvailable ? Reflect.get(form, "getFieldByInternalLabel") : null;
|
|
3924
|
+
const targets = metadataOptions.fieldTargets ?? (metadataOptions.includeAllDomFields === false
|
|
3925
|
+
? []
|
|
3926
|
+
: domFields
|
|
3927
|
+
.map((field) => field.internalLabel)
|
|
3928
|
+
.filter((internalLabel) => Boolean(internalLabel))
|
|
3929
|
+
.map((internalLabel) => ({ internalLabel })));
|
|
3930
|
+
const resolveField = (target) => target.fieldId != null && typeof getField === "function"
|
|
3931
|
+
? Reflect.apply(getField, form, [String(target.fieldId)])
|
|
3932
|
+
: target.internalLabel && typeof getFieldByInternalLabel === "function"
|
|
3933
|
+
? Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel])
|
|
3934
|
+
: null;
|
|
3935
|
+
const readAttribute = (field, name) => {
|
|
3936
|
+
if (!field || typeof field !== "object")
|
|
3937
|
+
return undefined;
|
|
3938
|
+
const getGeneralAttribute = Reflect.get(field, "getGeneralAttribute");
|
|
3939
|
+
return typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, [name]) : undefined;
|
|
3940
|
+
};
|
|
3941
|
+
const fields = targets.map((target) => {
|
|
3942
|
+
const field = resolveField(target);
|
|
3943
|
+
const fieldAvailable = Boolean(field && typeof field === "object");
|
|
3944
|
+
const getId = fieldAvailable ? Reflect.get(field, "getId") : null;
|
|
3945
|
+
const getValue = fieldAvailable ? Reflect.get(field, "getValue") : null;
|
|
3946
|
+
const getInfo = fieldAvailable ? Reflect.get(field, "getInfo") : null;
|
|
3947
|
+
const id = fieldAvailable
|
|
3948
|
+
? typeof getId === "function"
|
|
3949
|
+
? Reflect.apply(getId, field, [])
|
|
3950
|
+
: Reflect.get(field, "id") ?? target.fieldId ?? null
|
|
3951
|
+
: target.fieldId ?? null;
|
|
3952
|
+
const internalLabel = target.internalLabel ?? (fieldAvailable ? Reflect.get(field, "internalLabel") ?? null : null);
|
|
3953
|
+
const fieldMethodNames = fieldAvailable
|
|
3954
|
+
? [...new Set([
|
|
3955
|
+
...Object.keys(field),
|
|
3956
|
+
...Object.getOwnPropertyNames(Object.getPrototypeOf(field) ?? {}),
|
|
3957
|
+
])].filter((name) => typeof Reflect.get(field, name) === "function").sort()
|
|
3958
|
+
: [];
|
|
3959
|
+
const dom = internalLabel ? domByInternalLabel.get(String(internalLabel)) ?? null : null;
|
|
3960
|
+
return {
|
|
3961
|
+
target,
|
|
3962
|
+
fieldAvailable,
|
|
3963
|
+
id,
|
|
3964
|
+
internalLabel: internalLabel == null ? null : String(internalLabel),
|
|
3965
|
+
...(metadataOptions.includeValues === false || typeof getValue !== "function" ? {} : { value: Reflect.apply(getValue, field, []) }),
|
|
3966
|
+
...(typeof getInfo !== "function" ? {} : { info: Reflect.apply(getInfo, field, []) }),
|
|
3967
|
+
hidden: readAttribute(field, "hidden"),
|
|
3968
|
+
required: readAttribute(field, "required"),
|
|
3969
|
+
skipValidation: readAttribute(field, "skipValidation"),
|
|
3970
|
+
methodNames: fieldMethodNames,
|
|
3971
|
+
methods: methodNames.map((name) => ({ name, available: fieldAvailable && typeof Reflect.get(field, name) === "function" })),
|
|
3972
|
+
dom,
|
|
3973
|
+
};
|
|
3974
|
+
});
|
|
3975
|
+
return {
|
|
3976
|
+
apiAvailable,
|
|
3977
|
+
formAvailable,
|
|
3978
|
+
formId,
|
|
3979
|
+
formMethodNames,
|
|
3980
|
+
fields,
|
|
3981
|
+
};
|
|
3982
|
+
}, options);
|
|
3983
|
+
}
|
|
3733
3984
|
export async function discoverLiveFormFieldShapes(page, options = {}) {
|
|
3734
3985
|
if (!hasEvaluate(page)) {
|
|
3735
3986
|
throw new Error("discoverLiveFormFieldShapes requires a Playwright page with evaluate().");
|