@redrockswebdevelopment/formstack-form-testing 0.1.32 → 0.1.33

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
@@ -195,8 +195,8 @@ export const liveFormFieldTypeCoverage = {
195
195
  apiWrite: false,
196
196
  domInteraction: true,
197
197
  domAssertion: true,
198
- reason: "Signature fields require specialized canvas/user-event automation; the portable helper safely supports clearing and DOM assertions.",
199
- recommendedHelpers: ["clearSignature", "liveFormFieldRecipes.clearSignatureDom", "readLiveFormDomFieldState"],
198
+ reason: "Signature fields require specialized canvas/user-event automation; strict helpers verify hidden signature values or canvas ink after draw/clear.",
199
+ recommendedHelpers: ["drawLiveFormSignature", "clearLiveFormSignature", "readLiveFormSignatureState", "liveFormFieldRecipes.drawSignatureAndAssertDom"],
200
200
  },
201
201
  textarea: {
202
202
  kind: "textarea",
@@ -5269,6 +5269,12 @@ function fileInputLocator(page, target) {
5269
5269
  }
5270
5270
  throw new Error("File upload target requires internalLabel or fieldId.");
5271
5271
  }
5272
+ function liveFormSignatureInternalLabel(target) {
5273
+ if (target.internalLabel == null) {
5274
+ throw new Error("Signature target requires internalLabel.");
5275
+ }
5276
+ return target.internalLabel;
5277
+ }
5272
5278
  export async function readLiveFormFileState(page, options) {
5273
5279
  if (!hasEvaluate(page)) {
5274
5280
  throw new Error("readLiveFormFileState requires a Playwright page with evaluate().");
@@ -5356,6 +5362,101 @@ export async function uploadLiveFormFile(page, options) {
5356
5362
  ((validation?.inlineErrors.length ?? 0) === 0),
5357
5363
  };
5358
5364
  }
5365
+ export async function readLiveFormSignatureState(page, options) {
5366
+ if (!hasEvaluate(page)) {
5367
+ throw new Error("readLiveFormSignatureState requires a Playwright page with evaluate().");
5368
+ }
5369
+ return page.evaluate((stateOptions) => {
5370
+ const root = stateOptions.selector != null
5371
+ ? document.querySelector(stateOptions.selector)
5372
+ : stateOptions.internalLabel != null
5373
+ ? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
5374
+ : stateOptions.fieldId == null
5375
+ ? null
5376
+ : document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
5377
+ const hiddenValues = root
5378
+ ? Array.from(root.querySelectorAll("input[type=\"hidden\"], textarea"))
5379
+ .map((control) => control.value)
5380
+ .filter((value) => value.trim().length > 0)
5381
+ : [];
5382
+ const canvases = root
5383
+ ? Array.from(root.querySelectorAll("canvas")).map((canvas) => {
5384
+ try {
5385
+ const context = canvas.getContext("2d", { willReadFrequently: true });
5386
+ if (!context || canvas.width <= 0 || canvas.height <= 0) {
5387
+ return {
5388
+ width: canvas.width,
5389
+ height: canvas.height,
5390
+ readable: false,
5391
+ inkPixelCount: null,
5392
+ dataUrlLength: null,
5393
+ error: context ? "empty-canvas" : "missing-2d-context",
5394
+ };
5395
+ }
5396
+ const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
5397
+ let inkPixelCount = 0;
5398
+ for (let index = 0; index < pixels.length; index += 4) {
5399
+ const alpha = pixels[index + 3] ?? 0;
5400
+ const red = pixels[index] ?? 255;
5401
+ const green = pixels[index + 1] ?? 255;
5402
+ const blue = pixels[index + 2] ?? 255;
5403
+ if (alpha > 0 && (red < 245 || green < 245 || blue < 245))
5404
+ inkPixelCount += 1;
5405
+ }
5406
+ let dataUrlLength = null;
5407
+ try {
5408
+ dataUrlLength = canvas.toDataURL("image/png").length;
5409
+ }
5410
+ catch {
5411
+ dataUrlLength = null;
5412
+ }
5413
+ return {
5414
+ width: canvas.width,
5415
+ height: canvas.height,
5416
+ readable: true,
5417
+ inkPixelCount,
5418
+ dataUrlLength,
5419
+ error: null,
5420
+ };
5421
+ }
5422
+ catch (error) {
5423
+ return {
5424
+ width: canvas.width,
5425
+ height: canvas.height,
5426
+ readable: false,
5427
+ inkPixelCount: null,
5428
+ dataUrlLength: null,
5429
+ error: error instanceof Error ? error.message : String(error),
5430
+ };
5431
+ }
5432
+ })
5433
+ : [];
5434
+ const surfaceMatched = Boolean(root?.querySelector(".konvajs-content, canvas"));
5435
+ const canvasMatched = canvases.length > 0;
5436
+ const signed = hiddenValues.length > 0 || canvases.some((canvas) => (canvas.inkPixelCount ?? 0) > 0);
5437
+ return {
5438
+ fieldMatched: Boolean(root),
5439
+ surfaceMatched,
5440
+ canvasMatched,
5441
+ signed,
5442
+ ...(stateOptions.expectSigned === undefined ? {} : { expectSigned: stateOptions.expectSigned }),
5443
+ matched: Boolean(root) && surfaceMatched && (stateOptions.expectSigned === undefined || signed === stateOptions.expectSigned),
5444
+ hiddenValues,
5445
+ canvases,
5446
+ };
5447
+ }, options);
5448
+ }
5449
+ export async function waitForLiveFormSignatureState(page, options) {
5450
+ const timeoutMs = options.timeoutMs ?? 10_000;
5451
+ const intervalMs = options.intervalMs ?? 100;
5452
+ const startedAt = Date.now();
5453
+ let latest = await readLiveFormSignatureState(page, options);
5454
+ while (!latest.matched && Date.now() - startedAt < timeoutMs) {
5455
+ await delay(intervalMs);
5456
+ latest = await readLiveFormSignatureState(page, options);
5457
+ }
5458
+ return latest;
5459
+ }
5359
5460
  export async function drawSignature(page, internalLabel, options = {}) {
5360
5461
  if (!page.mouse) {
5361
5462
  throw new Error("drawSignature requires a Playwright page with mouse support.");
@@ -5393,6 +5494,49 @@ export async function drawSignature(page, internalLabel, options = {}) {
5393
5494
  }
5394
5495
  await page.mouse.up();
5395
5496
  }
5497
+ export async function drawLiveFormSignature(page, options) {
5498
+ await drawSignature(page, liveFormSignatureInternalLabel(options.target), options);
5499
+ const quiet = options.afterQuiet === false
5500
+ ? undefined
5501
+ : await waitForLiveFormQuiet(page, typeof options.afterQuiet === "object" ? options.afterQuiet : {});
5502
+ const dom = await waitForLiveFormSignatureState(page, {
5503
+ ...options.target,
5504
+ expectSigned: true,
5505
+ });
5506
+ const validation = options.afterValidation === undefined
5507
+ ? undefined
5508
+ : await waitForLiveFormValidationState(page, typeof options.afterValidation === "object" ? options.afterValidation : {});
5509
+ return {
5510
+ dom,
5511
+ ...(quiet ? { quiet } : {}),
5512
+ ...(validation ? { validation } : {}),
5513
+ ok: dom.matched &&
5514
+ (quiet?.quiet ?? true) &&
5515
+ ((validation?.inlineErrors.length ?? 0) === 0),
5516
+ };
5517
+ }
5518
+ export async function clearLiveFormSignature(page, options) {
5519
+ const internalLabel = liveFormSignatureInternalLabel(options.target);
5520
+ await page.locator(formstackSelectors.signatureClearButtonByInternalLabel(internalLabel)).first().click();
5521
+ const quiet = options.afterQuiet === false
5522
+ ? undefined
5523
+ : await waitForLiveFormQuiet(page, typeof options.afterQuiet === "object" ? options.afterQuiet : {});
5524
+ const dom = await waitForLiveFormSignatureState(page, {
5525
+ ...options.target,
5526
+ expectSigned: false,
5527
+ });
5528
+ const validation = options.afterValidation === undefined
5529
+ ? undefined
5530
+ : await waitForLiveFormValidationState(page, typeof options.afterValidation === "object" ? options.afterValidation : {});
5531
+ return {
5532
+ dom,
5533
+ ...(quiet ? { quiet } : {}),
5534
+ ...(validation ? { validation } : {}),
5535
+ ok: dom.matched &&
5536
+ (quiet?.quiet ?? true) &&
5537
+ ((validation?.inlineErrors.length ?? 0) === 0),
5538
+ };
5539
+ }
5396
5540
  export function createFormTestHarness(page, options = {}) {
5397
5541
  return {
5398
5542
  page,
@@ -5793,6 +5937,30 @@ export const liveFormFieldRecipes = {
5793
5937
  name,
5794
5938
  run: (harness) => harness.drawSignature(internalLabel, options),
5795
5939
  }),
5940
+ drawSignatureAndAssertDom: (name, internalLabel, options = {}) => ({
5941
+ name,
5942
+ run: async (harness) => {
5943
+ const result = await drawLiveFormSignature(harness.page, {
5944
+ ...options,
5945
+ target: { internalLabel },
5946
+ });
5947
+ if (!result.ok) {
5948
+ throw new Error(`Signature "${internalLabel}" did not produce a signed state.`);
5949
+ }
5950
+ },
5951
+ }),
5952
+ clearSignatureAndAssertDom: (name, internalLabel, options = {}) => ({
5953
+ name,
5954
+ run: async (harness) => {
5955
+ const result = await clearLiveFormSignature(harness.page, {
5956
+ ...options,
5957
+ target: { internalLabel },
5958
+ });
5959
+ if (!result.ok) {
5960
+ throw new Error(`Signature "${internalLabel}" did not clear to an unsigned state.`);
5961
+ }
5962
+ },
5963
+ }),
5796
5964
  setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
5797
5965
  setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
5798
5966
  setChoiceApi: (name, internalLabel, value, options = {}) => ({