@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.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 and amount UI can diverge by product configuration.",
154
- recommendedHelpers: ["buildLiveFormProductValue", "setLiveFormFieldValueTransaction", "fillProduct", "readLiveFormDomFieldState"],
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; 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",
@@ -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 ratingTransaction = !checkboxTransaction && !choiceTransaction && !selectTransaction && !matrixTransaction && options.fieldKind === "rating"
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
- : ratingTransaction
4245
- ? ratingTransaction.transaction
4246
- : dateTimeTransaction
4247
- ? dateTimeTransaction.transaction
4248
- : await performLiveFormFieldTransaction(page, {
4249
- write: {
4250
- ...(options.write ?? {}),
4251
- target: options.target,
4252
- value: options.value,
4253
- notify: options.write?.notify ?? true,
4254
- },
4255
- ...(options.fieldKind ? { fieldKind: options.fieldKind } : {}),
4256
- waitForEvent: options.waitForEvent ?? true,
4257
- dispatchDomEvents: options.dispatchDomEvents ?? "auto",
4258
- afterQuiet: options.afterQuiet ?? true,
4259
- ...(validationOptions ? { afterValidation: validationOptions } : {}),
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) => apiRecipeStep(name, { internalLabel }, buildLiveFormProductValue(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" }),