@redrockswebdevelopment/formstack-form-testing 0.1.32 → 0.1.34
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 +105 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +377 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -150,8 +150,8 @@ export const liveFormFieldTypeCoverage = {
|
|
|
150
150
|
apiWrite: true,
|
|
151
151
|
domInteraction: true,
|
|
152
152
|
domAssertion: true,
|
|
153
|
-
reason: "Product API state can be set, but visible quantity
|
|
154
|
-
recommendedHelpers: ["buildLiveFormProductValue", "
|
|
153
|
+
reason: "Product API state can be set, but visible quantity, fixed amount, and selected product UI can diverge by product configuration.",
|
|
154
|
+
recommendedHelpers: ["buildLiveFormProductValue", "setLiveFormProductValue", "readLiveFormProductState", "fillProduct"],
|
|
155
155
|
},
|
|
156
156
|
radio: {
|
|
157
157
|
kind: "radio",
|
|
@@ -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;
|
|
199
|
-
recommendedHelpers: ["
|
|
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",
|
|
@@ -1030,6 +1030,13 @@ export function buildLiveFormProductValue(value, current = {}) {
|
|
|
1030
1030
|
...value,
|
|
1031
1031
|
};
|
|
1032
1032
|
}
|
|
1033
|
+
export function normalizeLiveFormProductValue(value, current = {}) {
|
|
1034
|
+
return buildLiveFormProductValue({
|
|
1035
|
+
...(value.fixedAmount === undefined ? {} : { fixedAmount: String(value.fixedAmount) }),
|
|
1036
|
+
...(value.quantity === undefined ? {} : { quantity: String(value.quantity) }),
|
|
1037
|
+
...("value" in value && value.value !== undefined ? { value: String(value.value) } : {}),
|
|
1038
|
+
}, current);
|
|
1039
|
+
}
|
|
1033
1040
|
export function buildLiveFormRatingValue(value) {
|
|
1034
1041
|
return { value: String(value) };
|
|
1035
1042
|
}
|
|
@@ -1827,6 +1834,119 @@ export async function waitForLiveFormDomFieldState(page, options) {
|
|
|
1827
1834
|
}
|
|
1828
1835
|
return latest;
|
|
1829
1836
|
}
|
|
1837
|
+
export async function readLiveFormProductState(page, options) {
|
|
1838
|
+
if (!hasEvaluate(page)) {
|
|
1839
|
+
throw new Error("readLiveFormProductState requires a Playwright page with evaluate().");
|
|
1840
|
+
}
|
|
1841
|
+
return page.evaluate((stateOptions) => {
|
|
1842
|
+
const normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
1843
|
+
const inferPart = (control) => {
|
|
1844
|
+
const label = control.id ? document.querySelector(`label[for="${CSS.escape(control.id)}"]`) : null;
|
|
1845
|
+
const haystack = [
|
|
1846
|
+
control.name,
|
|
1847
|
+
control.id,
|
|
1848
|
+
control.className,
|
|
1849
|
+
control.getAttribute("placeholder"),
|
|
1850
|
+
control.getAttribute("aria-label"),
|
|
1851
|
+
label?.textContent,
|
|
1852
|
+
].map(normalizeText).join(" ").toLowerCase();
|
|
1853
|
+
if (/\b(quantity|qty|\[quantity\]|-quantity$|_quantity$)/i.test(haystack))
|
|
1854
|
+
return "quantity";
|
|
1855
|
+
if (/\b(fixed amount|fixedamount|amount|price|\[fixedamount\]|-fixedamount$|_fixedamount$)/i.test(haystack))
|
|
1856
|
+
return "fixedAmount";
|
|
1857
|
+
if (/\b(product|option|value|\[value\]|-value$|_value$)/i.test(haystack))
|
|
1858
|
+
return "value";
|
|
1859
|
+
return null;
|
|
1860
|
+
};
|
|
1861
|
+
const root = stateOptions.selector != null
|
|
1862
|
+
? document.querySelector(stateOptions.selector)
|
|
1863
|
+
: stateOptions.internalLabel != null
|
|
1864
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
|
|
1865
|
+
: stateOptions.fieldId == null
|
|
1866
|
+
? null
|
|
1867
|
+
: document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
|
|
1868
|
+
const nativeControls = root
|
|
1869
|
+
? Array.from(root.querySelectorAll("input, textarea, select"))
|
|
1870
|
+
: [];
|
|
1871
|
+
const controls = nativeControls.map((control) => {
|
|
1872
|
+
const selected = control instanceof HTMLSelectElement
|
|
1873
|
+
? control.selectedIndex >= 0
|
|
1874
|
+
: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type)
|
|
1875
|
+
? control.checked
|
|
1876
|
+
: null;
|
|
1877
|
+
return {
|
|
1878
|
+
tagName: control.tagName.toLowerCase(),
|
|
1879
|
+
type: control.getAttribute("type"),
|
|
1880
|
+
id: control.getAttribute("id"),
|
|
1881
|
+
name: control.getAttribute("name"),
|
|
1882
|
+
value: control.getAttribute("value"),
|
|
1883
|
+
currentValue: "value" in control ? control.value : "",
|
|
1884
|
+
checked: control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null,
|
|
1885
|
+
part: inferPart(control),
|
|
1886
|
+
text: control instanceof HTMLSelectElement
|
|
1887
|
+
? normalizeText(control.selectedOptions.item(0)?.textContent)
|
|
1888
|
+
: normalizeText(control.textContent),
|
|
1889
|
+
selected,
|
|
1890
|
+
disabled: "disabled" in control ? Boolean(control.disabled) : null,
|
|
1891
|
+
};
|
|
1892
|
+
});
|
|
1893
|
+
const activeControls = controls.filter((control) => control.disabled !== true &&
|
|
1894
|
+
((control.tagName === "select" && (control.currentValue ?? "").length > 0) ||
|
|
1895
|
+
(["checkbox", "radio"].includes(control.type ?? "") ? control.checked === true : (control.currentValue ?? "").length > 0)));
|
|
1896
|
+
const findPart = (part) => activeControls.find((control) => control.part === part)?.currentValue ?? null;
|
|
1897
|
+
const selectedValue = findPart("value") ?? activeControls.find((control) => ["checkbox", "radio"].includes(control.type ?? "") && control.checked === true)?.currentValue ?? null;
|
|
1898
|
+
const quantity = findPart("quantity");
|
|
1899
|
+
const fixedAmount = findPart("fixedAmount");
|
|
1900
|
+
const amountText = root
|
|
1901
|
+
? Array.from(root.querySelectorAll("[class*=\"amount\"], [class*=\"price\"], [data-testid*=\"amount\"], [data-testid*=\"price\"]"))
|
|
1902
|
+
.map((element) => normalizeText(element.textContent))
|
|
1903
|
+
.find((text) => text.length > 0 && /[$€£]|\d/.test(text)) ?? null
|
|
1904
|
+
: null;
|
|
1905
|
+
const expected = stateOptions.expectedValue ?? {};
|
|
1906
|
+
const expectedEntries = Object.entries(expected).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
|
|
1907
|
+
const selectedParts = {
|
|
1908
|
+
value: selectedValue,
|
|
1909
|
+
quantity,
|
|
1910
|
+
fixedAmount,
|
|
1911
|
+
};
|
|
1912
|
+
const renderedExpectedEntries = expectedEntries.filter(([part]) => selectedParts[part] !== null);
|
|
1913
|
+
const unavailableParts = expectedEntries
|
|
1914
|
+
.filter(([part]) => selectedParts[part] === null)
|
|
1915
|
+
.map(([part]) => part);
|
|
1916
|
+
const missingParts = renderedExpectedEntries
|
|
1917
|
+
.filter(([part]) => selectedParts[part] === "")
|
|
1918
|
+
.map(([part]) => part);
|
|
1919
|
+
const mismatchedParts = renderedExpectedEntries
|
|
1920
|
+
.filter(([part, value]) => selectedParts[part] !== "" && selectedParts[part] !== value)
|
|
1921
|
+
.map(([part]) => part);
|
|
1922
|
+
const renderedValueAvailable = renderedExpectedEntries.length > 0 || activeControls.length > 0 || amountText !== null;
|
|
1923
|
+
return {
|
|
1924
|
+
fieldMatched: Boolean(root),
|
|
1925
|
+
selectedValue,
|
|
1926
|
+
quantity,
|
|
1927
|
+
fixedAmount,
|
|
1928
|
+
amountText,
|
|
1929
|
+
...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
|
|
1930
|
+
renderedValueAvailable,
|
|
1931
|
+
unavailableParts,
|
|
1932
|
+
missingParts,
|
|
1933
|
+
mismatchedParts,
|
|
1934
|
+
matched: Boolean(root) && missingParts.length === 0 && mismatchedParts.length === 0,
|
|
1935
|
+
controls,
|
|
1936
|
+
};
|
|
1937
|
+
}, options);
|
|
1938
|
+
}
|
|
1939
|
+
export async function waitForLiveFormProductState(page, options) {
|
|
1940
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
1941
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
1942
|
+
const startedAt = Date.now();
|
|
1943
|
+
let latest = await readLiveFormProductState(page, options);
|
|
1944
|
+
while (!latest.matched && Date.now() - startedAt < timeoutMs) {
|
|
1945
|
+
await delay(intervalMs);
|
|
1946
|
+
latest = await readLiveFormProductState(page, options);
|
|
1947
|
+
}
|
|
1948
|
+
return latest;
|
|
1949
|
+
}
|
|
1830
1950
|
export async function readLiveFormCheckboxState(page, options) {
|
|
1831
1951
|
if (!hasEvaluate(page)) {
|
|
1832
1952
|
throw new Error("readLiveFormCheckboxState requires a Playwright page with evaluate().");
|
|
@@ -4081,6 +4201,47 @@ export async function setLiveFormMatrixValues(page, options) {
|
|
|
4081
4201
|
(visibility?.matched ?? true),
|
|
4082
4202
|
};
|
|
4083
4203
|
}
|
|
4204
|
+
export async function setLiveFormProductValue(page, options) {
|
|
4205
|
+
const payload = normalizeLiveFormProductValue(options.value, options.current);
|
|
4206
|
+
const transaction = await performLiveFormFieldTransaction(page, {
|
|
4207
|
+
write: {
|
|
4208
|
+
...(options.write ?? {}),
|
|
4209
|
+
target: options.target,
|
|
4210
|
+
value: payload,
|
|
4211
|
+
notify: options.write?.notify ?? true,
|
|
4212
|
+
},
|
|
4213
|
+
fieldKind: "product",
|
|
4214
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
4215
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4216
|
+
afterQuiet: options.afterQuiet ?? { quietMs: 200, timeoutMs: 5_000 },
|
|
4217
|
+
...(options.afterValidation === undefined ? {} : { afterValidation: options.afterValidation }),
|
|
4218
|
+
});
|
|
4219
|
+
const dom = await waitForLiveFormProductState(page, {
|
|
4220
|
+
...options.target,
|
|
4221
|
+
expectedValue: payload,
|
|
4222
|
+
...(options.write?.timeoutMs === undefined ? {} : { timeoutMs: options.write.timeoutMs }),
|
|
4223
|
+
});
|
|
4224
|
+
const visibilityTargets = [
|
|
4225
|
+
...(options.expectVisible ?? []).map((target) => ({ ...target, visible: true })),
|
|
4226
|
+
...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
|
|
4227
|
+
];
|
|
4228
|
+
const visibility = visibilityTargets.length > 0
|
|
4229
|
+
? await waitForLiveFormVisibility(page, { targets: visibilityTargets })
|
|
4230
|
+
: undefined;
|
|
4231
|
+
return {
|
|
4232
|
+
payload,
|
|
4233
|
+
transaction,
|
|
4234
|
+
dom,
|
|
4235
|
+
...(visibility ? { visibility } : {}),
|
|
4236
|
+
ok: transaction.write.valueMatched &&
|
|
4237
|
+
(transaction.event?.matched ?? true) &&
|
|
4238
|
+
(transaction.domDispatch ? transaction.domDispatch.matchedControlCount >= 1 : true) &&
|
|
4239
|
+
(transaction.quiet?.quiet ?? true) &&
|
|
4240
|
+
((transaction.validation?.inlineErrors.length ?? 0) === 0) &&
|
|
4241
|
+
dom.matched &&
|
|
4242
|
+
(visibility?.matched ?? true),
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4084
4245
|
export async function setLiveFormRatingValue(page, options) {
|
|
4085
4246
|
const payload = normalizeLiveFormRatingValue(options.value);
|
|
4086
4247
|
const value = payload.value;
|
|
@@ -4211,7 +4372,18 @@ export async function answerProgressiveLiveFormField(page, options) {
|
|
|
4211
4372
|
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
4212
4373
|
})
|
|
4213
4374
|
: undefined;
|
|
4214
|
-
const
|
|
4375
|
+
const productTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "product"
|
|
4376
|
+
? await setLiveFormProductValue(page, {
|
|
4377
|
+
target: options.target,
|
|
4378
|
+
value: options.value,
|
|
4379
|
+
...(options.write === undefined ? {} : { write: options.write }),
|
|
4380
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
4381
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4382
|
+
afterQuiet: options.afterQuiet ?? true,
|
|
4383
|
+
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
4384
|
+
})
|
|
4385
|
+
: undefined;
|
|
4386
|
+
const ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && !productTransaction && options.fieldKind === "rating"
|
|
4215
4387
|
? await setLiveFormRatingValue(page, {
|
|
4216
4388
|
target: options.target,
|
|
4217
4389
|
value: options.value,
|
|
@@ -4241,23 +4413,25 @@ export async function answerProgressiveLiveFormField(page, options) {
|
|
|
4241
4413
|
? selectTransaction.transaction
|
|
4242
4414
|
: matrixTransaction
|
|
4243
4415
|
? matrixTransaction.transaction
|
|
4244
|
-
:
|
|
4245
|
-
?
|
|
4246
|
-
:
|
|
4247
|
-
?
|
|
4248
|
-
:
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4416
|
+
: productTransaction
|
|
4417
|
+
? productTransaction.transaction
|
|
4418
|
+
: ratingTransaction
|
|
4419
|
+
? ratingTransaction.transaction
|
|
4420
|
+
: dateTimeTransaction
|
|
4421
|
+
? dateTimeTransaction.transaction
|
|
4422
|
+
: await performLiveFormFieldTransaction(page, {
|
|
4423
|
+
write: {
|
|
4424
|
+
...(options.write ?? {}),
|
|
4425
|
+
target: options.target,
|
|
4426
|
+
value: options.value,
|
|
4427
|
+
notify: options.write?.notify ?? true,
|
|
4428
|
+
},
|
|
4429
|
+
...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
|
|
4430
|
+
waitForEvent: options.waitForEvent ?? true,
|
|
4431
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
4432
|
+
afterQuiet: options.afterQuiet ?? true,
|
|
4433
|
+
...(validationOptions ? { afterValidation: validationOptions } : {}),
|
|
4434
|
+
});
|
|
4261
4435
|
const visibilityTargets = [
|
|
4262
4436
|
...(options.expectVisible ?? []),
|
|
4263
4437
|
...(options.expectHidden ?? []).map((target) => ({ ...target, visible: false })),
|
|
@@ -5269,6 +5443,12 @@ function fileInputLocator(page, target) {
|
|
|
5269
5443
|
}
|
|
5270
5444
|
throw new Error("File upload target requires internalLabel or fieldId.");
|
|
5271
5445
|
}
|
|
5446
|
+
function liveFormSignatureInternalLabel(target) {
|
|
5447
|
+
if (target.internalLabel == null) {
|
|
5448
|
+
throw new Error("Signature target requires internalLabel.");
|
|
5449
|
+
}
|
|
5450
|
+
return target.internalLabel;
|
|
5451
|
+
}
|
|
5272
5452
|
export async function readLiveFormFileState(page, options) {
|
|
5273
5453
|
if (!hasEvaluate(page)) {
|
|
5274
5454
|
throw new Error("readLiveFormFileState requires a Playwright page with evaluate().");
|
|
@@ -5356,6 +5536,101 @@ export async function uploadLiveFormFile(page, options) {
|
|
|
5356
5536
|
((validation?.inlineErrors.length ?? 0) === 0),
|
|
5357
5537
|
};
|
|
5358
5538
|
}
|
|
5539
|
+
export async function readLiveFormSignatureState(page, options) {
|
|
5540
|
+
if (!hasEvaluate(page)) {
|
|
5541
|
+
throw new Error("readLiveFormSignatureState requires a Playwright page with evaluate().");
|
|
5542
|
+
}
|
|
5543
|
+
return page.evaluate((stateOptions) => {
|
|
5544
|
+
const root = stateOptions.selector != null
|
|
5545
|
+
? document.querySelector(stateOptions.selector)
|
|
5546
|
+
: stateOptions.internalLabel != null
|
|
5547
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
|
|
5548
|
+
: stateOptions.fieldId == null
|
|
5549
|
+
? null
|
|
5550
|
+
: document.querySelector(`#fsCell${CSS.escape(String(stateOptions.fieldId))}, #fsFieldCell${CSS.escape(String(stateOptions.fieldId))}, [data-testid="field-${CSS.escape(String(stateOptions.fieldId))}"]`);
|
|
5551
|
+
const hiddenValues = root
|
|
5552
|
+
? Array.from(root.querySelectorAll("input[type=\"hidden\"], textarea"))
|
|
5553
|
+
.map((control) => control.value)
|
|
5554
|
+
.filter((value) => value.trim().length > 0)
|
|
5555
|
+
: [];
|
|
5556
|
+
const canvases = root
|
|
5557
|
+
? Array.from(root.querySelectorAll("canvas")).map((canvas) => {
|
|
5558
|
+
try {
|
|
5559
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
5560
|
+
if (!context || canvas.width <= 0 || canvas.height <= 0) {
|
|
5561
|
+
return {
|
|
5562
|
+
width: canvas.width,
|
|
5563
|
+
height: canvas.height,
|
|
5564
|
+
readable: false,
|
|
5565
|
+
inkPixelCount: null,
|
|
5566
|
+
dataUrlLength: null,
|
|
5567
|
+
error: context ? "empty-canvas" : "missing-2d-context",
|
|
5568
|
+
};
|
|
5569
|
+
}
|
|
5570
|
+
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
5571
|
+
let inkPixelCount = 0;
|
|
5572
|
+
for (let index = 0; index < pixels.length; index += 4) {
|
|
5573
|
+
const alpha = pixels[index + 3] ?? 0;
|
|
5574
|
+
const red = pixels[index] ?? 255;
|
|
5575
|
+
const green = pixels[index + 1] ?? 255;
|
|
5576
|
+
const blue = pixels[index + 2] ?? 255;
|
|
5577
|
+
if (alpha > 0 && (red < 245 || green < 245 || blue < 245))
|
|
5578
|
+
inkPixelCount += 1;
|
|
5579
|
+
}
|
|
5580
|
+
let dataUrlLength = null;
|
|
5581
|
+
try {
|
|
5582
|
+
dataUrlLength = canvas.toDataURL("image/png").length;
|
|
5583
|
+
}
|
|
5584
|
+
catch {
|
|
5585
|
+
dataUrlLength = null;
|
|
5586
|
+
}
|
|
5587
|
+
return {
|
|
5588
|
+
width: canvas.width,
|
|
5589
|
+
height: canvas.height,
|
|
5590
|
+
readable: true,
|
|
5591
|
+
inkPixelCount,
|
|
5592
|
+
dataUrlLength,
|
|
5593
|
+
error: null,
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
catch (error) {
|
|
5597
|
+
return {
|
|
5598
|
+
width: canvas.width,
|
|
5599
|
+
height: canvas.height,
|
|
5600
|
+
readable: false,
|
|
5601
|
+
inkPixelCount: null,
|
|
5602
|
+
dataUrlLength: null,
|
|
5603
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5606
|
+
})
|
|
5607
|
+
: [];
|
|
5608
|
+
const surfaceMatched = Boolean(root?.querySelector(".konvajs-content, canvas"));
|
|
5609
|
+
const canvasMatched = canvases.length > 0;
|
|
5610
|
+
const signed = hiddenValues.length > 0 || canvases.some((canvas) => (canvas.inkPixelCount ?? 0) > 0);
|
|
5611
|
+
return {
|
|
5612
|
+
fieldMatched: Boolean(root),
|
|
5613
|
+
surfaceMatched,
|
|
5614
|
+
canvasMatched,
|
|
5615
|
+
signed,
|
|
5616
|
+
...(stateOptions.expectSigned === undefined ? {} : { expectSigned: stateOptions.expectSigned }),
|
|
5617
|
+
matched: Boolean(root) && surfaceMatched && (stateOptions.expectSigned === undefined || signed === stateOptions.expectSigned),
|
|
5618
|
+
hiddenValues,
|
|
5619
|
+
canvases,
|
|
5620
|
+
};
|
|
5621
|
+
}, options);
|
|
5622
|
+
}
|
|
5623
|
+
export async function waitForLiveFormSignatureState(page, options) {
|
|
5624
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
5625
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
5626
|
+
const startedAt = Date.now();
|
|
5627
|
+
let latest = await readLiveFormSignatureState(page, options);
|
|
5628
|
+
while (!latest.matched && Date.now() - startedAt < timeoutMs) {
|
|
5629
|
+
await delay(intervalMs);
|
|
5630
|
+
latest = await readLiveFormSignatureState(page, options);
|
|
5631
|
+
}
|
|
5632
|
+
return latest;
|
|
5633
|
+
}
|
|
5359
5634
|
export async function drawSignature(page, internalLabel, options = {}) {
|
|
5360
5635
|
if (!page.mouse) {
|
|
5361
5636
|
throw new Error("drawSignature requires a Playwright page with mouse support.");
|
|
@@ -5393,6 +5668,49 @@ export async function drawSignature(page, internalLabel, options = {}) {
|
|
|
5393
5668
|
}
|
|
5394
5669
|
await page.mouse.up();
|
|
5395
5670
|
}
|
|
5671
|
+
export async function drawLiveFormSignature(page, options) {
|
|
5672
|
+
await drawSignature(page, liveFormSignatureInternalLabel(options.target), options);
|
|
5673
|
+
const quiet = options.afterQuiet === false
|
|
5674
|
+
? undefined
|
|
5675
|
+
: await waitForLiveFormQuiet(page, typeof options.afterQuiet === "object" ? options.afterQuiet : {});
|
|
5676
|
+
const dom = await waitForLiveFormSignatureState(page, {
|
|
5677
|
+
...options.target,
|
|
5678
|
+
expectSigned: true,
|
|
5679
|
+
});
|
|
5680
|
+
const validation = options.afterValidation === undefined
|
|
5681
|
+
? undefined
|
|
5682
|
+
: await waitForLiveFormValidationState(page, typeof options.afterValidation === "object" ? options.afterValidation : {});
|
|
5683
|
+
return {
|
|
5684
|
+
dom,
|
|
5685
|
+
...(quiet ? { quiet } : {}),
|
|
5686
|
+
...(validation ? { validation } : {}),
|
|
5687
|
+
ok: dom.matched &&
|
|
5688
|
+
(quiet?.quiet ?? true) &&
|
|
5689
|
+
((validation?.inlineErrors.length ?? 0) === 0),
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
export async function clearLiveFormSignature(page, options) {
|
|
5693
|
+
const internalLabel = liveFormSignatureInternalLabel(options.target);
|
|
5694
|
+
await page.locator(formstackSelectors.signatureClearButtonByInternalLabel(internalLabel)).first().click();
|
|
5695
|
+
const quiet = options.afterQuiet === false
|
|
5696
|
+
? undefined
|
|
5697
|
+
: await waitForLiveFormQuiet(page, typeof options.afterQuiet === "object" ? options.afterQuiet : {});
|
|
5698
|
+
const dom = await waitForLiveFormSignatureState(page, {
|
|
5699
|
+
...options.target,
|
|
5700
|
+
expectSigned: false,
|
|
5701
|
+
});
|
|
5702
|
+
const validation = options.afterValidation === undefined
|
|
5703
|
+
? undefined
|
|
5704
|
+
: await waitForLiveFormValidationState(page, typeof options.afterValidation === "object" ? options.afterValidation : {});
|
|
5705
|
+
return {
|
|
5706
|
+
dom,
|
|
5707
|
+
...(quiet ? { quiet } : {}),
|
|
5708
|
+
...(validation ? { validation } : {}),
|
|
5709
|
+
ok: dom.matched &&
|
|
5710
|
+
(quiet?.quiet ?? true) &&
|
|
5711
|
+
((validation?.inlineErrors.length ?? 0) === 0),
|
|
5712
|
+
};
|
|
5713
|
+
}
|
|
5396
5714
|
export function createFormTestHarness(page, options = {}) {
|
|
5397
5715
|
return {
|
|
5398
5716
|
page,
|
|
@@ -5793,6 +6111,30 @@ export const liveFormFieldRecipes = {
|
|
|
5793
6111
|
name,
|
|
5794
6112
|
run: (harness) => harness.drawSignature(internalLabel, options),
|
|
5795
6113
|
}),
|
|
6114
|
+
drawSignatureAndAssertDom: (name, internalLabel, options = {}) => ({
|
|
6115
|
+
name,
|
|
6116
|
+
run: async (harness) => {
|
|
6117
|
+
const result = await drawLiveFormSignature(harness.page, {
|
|
6118
|
+
...options,
|
|
6119
|
+
target: { internalLabel },
|
|
6120
|
+
});
|
|
6121
|
+
if (!result.ok) {
|
|
6122
|
+
throw new Error(`Signature "${internalLabel}" did not produce a signed state.`);
|
|
6123
|
+
}
|
|
6124
|
+
},
|
|
6125
|
+
}),
|
|
6126
|
+
clearSignatureAndAssertDom: (name, internalLabel, options = {}) => ({
|
|
6127
|
+
name,
|
|
6128
|
+
run: async (harness) => {
|
|
6129
|
+
const result = await clearLiveFormSignature(harness.page, {
|
|
6130
|
+
...options,
|
|
6131
|
+
target: { internalLabel },
|
|
6132
|
+
});
|
|
6133
|
+
if (!result.ok) {
|
|
6134
|
+
throw new Error(`Signature "${internalLabel}" did not clear to an unsigned state.`);
|
|
6135
|
+
}
|
|
6136
|
+
},
|
|
6137
|
+
}),
|
|
5796
6138
|
setNameApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormNameValue(value), options),
|
|
5797
6139
|
setAddressApi: (name, internalLabel, value, options) => apiRecipeStep(name, { internalLabel }, buildLiveFormAddressValue(value), options),
|
|
5798
6140
|
setChoiceApi: (name, internalLabel, value, options = {}) => ({
|
|
@@ -5873,7 +6215,19 @@ export const liveFormFieldRecipes = {
|
|
|
5873
6215
|
});
|
|
5874
6216
|
},
|
|
5875
6217
|
}),
|
|
5876
|
-
setProductApi: (name, internalLabel, value, options) =>
|
|
6218
|
+
setProductApi: (name, internalLabel, value, options = {}) => ({
|
|
6219
|
+
name,
|
|
6220
|
+
run: async (harness) => {
|
|
6221
|
+
await setLiveFormProductValue(harness.page, {
|
|
6222
|
+
target: { internalLabel },
|
|
6223
|
+
value,
|
|
6224
|
+
write: { notify: options.notify ?? true },
|
|
6225
|
+
waitForEvent: options.waitForEvent ?? (options.notify ?? true),
|
|
6226
|
+
dispatchDomEvents: options.dispatchDomEvents ?? "auto",
|
|
6227
|
+
afterQuiet: options.waitForQuiet ?? true,
|
|
6228
|
+
});
|
|
6229
|
+
},
|
|
6230
|
+
}),
|
|
5877
6231
|
setFieldApiAuto: (name, internalLabel, value, fieldKind, options = {}) => apiRecipeStep(name, { internalLabel }, value, { ...options, fieldKind, dispatchDomEvents: "auto" }),
|
|
5878
6232
|
setNameApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setNameApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "name", dispatchDomEvents: "auto" }),
|
|
5879
6233
|
setChoiceApiAndDispatch: (name, internalLabel, value, options = {}) => liveFormFieldRecipes.setChoiceApi(name, internalLabel, value, { ...options, fieldKind: options.fieldKind ?? "radio", dispatchDomEvents: "auto" }),
|