@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.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", "setLiveFormFieldValueTransaction", "fillDateTime", "readLiveFormDomFieldState"],
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", "setLiveFormFieldValueTransaction", "fillDateTime", "readLiveFormDomFieldState"],
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", "readLiveFormDomFieldState"],
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 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);
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),