@redrockswebdevelopment/formstack-form-testing 0.1.40 → 0.1.42
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 +91 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +373 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,15 @@ export const liveFormFieldTypeCoverage = {
|
|
|
26
26
|
reason: "Compound address values can be written through Live Form API, while subfield UX should be asserted for address line, city, state, ZIP, and country controls.",
|
|
27
27
|
recommendedHelpers: ["buildLiveFormAddressValue", "setLiveFormAddressValue", "readLiveFormAddressState", "fillAddress"],
|
|
28
28
|
},
|
|
29
|
+
calculation: {
|
|
30
|
+
kind: "calculation",
|
|
31
|
+
strategy: "assertion-only",
|
|
32
|
+
apiWrite: false,
|
|
33
|
+
domInteraction: false,
|
|
34
|
+
domAssertion: true,
|
|
35
|
+
reason: "Calculation fields are derived by Formstack. Test their lifecycle after source changes, numeric/date normalization, visibility, and submission inclusion instead of writing them directly.",
|
|
36
|
+
recommendedHelpers: ["readLiveFormCalculationState", "waitForLiveFormCalculationState", "traceLiveFormCalculationLifecycle", "readLiveFormSubmissionInclusion"],
|
|
37
|
+
},
|
|
29
38
|
checkbox: {
|
|
30
39
|
kind: "checkbox",
|
|
31
40
|
strategy: "api-safe",
|
|
@@ -89,6 +98,15 @@ export const liveFormFieldTypeCoverage = {
|
|
|
89
98
|
reason: "File fields must be tested through browser file upload controls, not generic Live Form API setValue payloads.",
|
|
90
99
|
recommendedHelpers: ["uploadLiveFormFile", "readLiveFormFileState", "uploadFile", "liveFormFieldRecipes.uploadFileDom"],
|
|
91
100
|
},
|
|
101
|
+
hidden: {
|
|
102
|
+
kind: "hidden",
|
|
103
|
+
strategy: "assertion-only",
|
|
104
|
+
apiWrite: true,
|
|
105
|
+
domInteraction: false,
|
|
106
|
+
domAssertion: true,
|
|
107
|
+
reason: "Hidden fields can be populated through API or scripts, but tests should prove visibility state and whether the populated control is included in FormData/submission surfaces.",
|
|
108
|
+
recommendedHelpers: ["setLiveFormFieldValueTransaction", "readLiveFormFieldValue", "readLiveFormSubmissionInclusion", "readLiveFormStateDrift"],
|
|
109
|
+
},
|
|
92
110
|
matrix: {
|
|
93
111
|
kind: "matrix",
|
|
94
112
|
strategy: "api-safe",
|
|
@@ -1883,7 +1901,12 @@ export async function readLiveFormStructuralState(page, options) {
|
|
|
1883
1901
|
return "embed";
|
|
1884
1902
|
if (classes.includes("pagebreak") || classes.includes("page-break") || dataTestId.includes("pagebreak") || id.includes("pagebreak"))
|
|
1885
1903
|
return "pagebreak";
|
|
1886
|
-
if (classes.includes("description") ||
|
|
1904
|
+
if (classes.includes("description") ||
|
|
1905
|
+
classes.includes("richtext") ||
|
|
1906
|
+
internalLabel.includes("description") ||
|
|
1907
|
+
internalLabel.includes("richtext") ||
|
|
1908
|
+
dataTestId.includes("description") ||
|
|
1909
|
+
dataTestId.includes("richtext"))
|
|
1887
1910
|
return "description";
|
|
1888
1911
|
if (classes.includes("section") || id.includes("section") || element.querySelector(".fsSectionHeader, [class*='SectionHeader']"))
|
|
1889
1912
|
return "section";
|
|
@@ -1920,7 +1943,9 @@ export async function readLiveFormStructuralState(page, options) {
|
|
|
1920
1943
|
const visible = isVisible(root);
|
|
1921
1944
|
const visibilityMatched = expectedVisible === undefined || visible === expectedVisible;
|
|
1922
1945
|
const kindMatched = stateOptions.expectedKind === undefined || inferredKind === stateOptions.expectedKind;
|
|
1923
|
-
const controlsMatched = stateOptions.allowControls === true ||
|
|
1946
|
+
const controlsMatched = stateOptions.allowControls === true ||
|
|
1947
|
+
(stateOptions.allowControls !== false && inferredKind === "section") ||
|
|
1948
|
+
controls.length === 0;
|
|
1924
1949
|
return {
|
|
1925
1950
|
matched: Boolean(root) && kindMatched && textMatched && visibilityMatched && controlsMatched,
|
|
1926
1951
|
elementMatched: Boolean(root),
|
|
@@ -1952,6 +1977,327 @@ export async function waitForLiveFormStructuralState(page, options) {
|
|
|
1952
1977
|
}
|
|
1953
1978
|
return latest;
|
|
1954
1979
|
}
|
|
1980
|
+
export async function readLiveFormCalculationState(page, options) {
|
|
1981
|
+
if (!hasEvaluate(page)) {
|
|
1982
|
+
throw new Error("readLiveFormCalculationState requires a Playwright page with evaluate().");
|
|
1983
|
+
}
|
|
1984
|
+
return page.evaluate((stateOptions) => {
|
|
1985
|
+
const readPath = (value, path) => {
|
|
1986
|
+
if (!path)
|
|
1987
|
+
return value;
|
|
1988
|
+
return path.split(".").reduce((current, segment) => {
|
|
1989
|
+
if (current === null || current === undefined || typeof current !== "object")
|
|
1990
|
+
return undefined;
|
|
1991
|
+
return Reflect.get(current, segment);
|
|
1992
|
+
}, value);
|
|
1993
|
+
};
|
|
1994
|
+
const valuesEqual = (left, right) => {
|
|
1995
|
+
if (Object.is(left, right))
|
|
1996
|
+
return true;
|
|
1997
|
+
try {
|
|
1998
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1999
|
+
}
|
|
2000
|
+
catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
const stringifyValue = (value) => {
|
|
2005
|
+
if (value === null || value === undefined)
|
|
2006
|
+
return "";
|
|
2007
|
+
if (typeof value === "string")
|
|
2008
|
+
return value;
|
|
2009
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
2010
|
+
return String(value);
|
|
2011
|
+
try {
|
|
2012
|
+
return JSON.stringify(value);
|
|
2013
|
+
}
|
|
2014
|
+
catch {
|
|
2015
|
+
return String(value);
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
const normalizeNumber = (value) => {
|
|
2019
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2020
|
+
return value;
|
|
2021
|
+
const text = stringifyValue(value).replace(/[$,%\s,]/g, "");
|
|
2022
|
+
if (!text)
|
|
2023
|
+
return null;
|
|
2024
|
+
const parsed = Number(text);
|
|
2025
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2026
|
+
};
|
|
2027
|
+
const normalizeDate = (value) => {
|
|
2028
|
+
const text = stringifyValue(value).trim();
|
|
2029
|
+
if (!text)
|
|
2030
|
+
return null;
|
|
2031
|
+
const timestamp = Date.parse(text);
|
|
2032
|
+
if (Number.isNaN(timestamp))
|
|
2033
|
+
return null;
|
|
2034
|
+
return new Date(timestamp).toISOString().slice(0, 10);
|
|
2035
|
+
};
|
|
2036
|
+
const expectedDateText = stateOptions.expectedDate === undefined ? undefined : normalizeDate(stateOptions.expectedDate) ?? undefined;
|
|
2037
|
+
const isVisible = (element) => {
|
|
2038
|
+
if (!element)
|
|
2039
|
+
return null;
|
|
2040
|
+
if (!(element instanceof HTMLElement))
|
|
2041
|
+
return false;
|
|
2042
|
+
for (let current = element; current; current = current.parentElement) {
|
|
2043
|
+
const style = getComputedStyle(current);
|
|
2044
|
+
const rect = current.getBoundingClientRect();
|
|
2045
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
|
|
2046
|
+
return false;
|
|
2047
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
return element.getClientRects().length > 0;
|
|
2051
|
+
};
|
|
2052
|
+
const readApi = () => {
|
|
2053
|
+
const factory = Reflect.get(window, "fsApi");
|
|
2054
|
+
const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
|
|
2055
|
+
const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
|
|
2056
|
+
const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
|
|
2057
|
+
return Array.isArray(forms) ? forms[0] ?? null : null;
|
|
2058
|
+
};
|
|
2059
|
+
const form = readApi();
|
|
2060
|
+
const apiAvailable = Boolean(form && typeof form === "object");
|
|
2061
|
+
const getField = form && typeof form === "object" ? Reflect.get(form, "getField") : null;
|
|
2062
|
+
const getFieldByInternalLabel = form && typeof form === "object" ? Reflect.get(form, "getFieldByInternalLabel") : null;
|
|
2063
|
+
const field = stateOptions.fieldId != null && typeof getField === "function"
|
|
2064
|
+
? Reflect.apply(getField, form, [String(stateOptions.fieldId)])
|
|
2065
|
+
: stateOptions.internalLabel && typeof getFieldByInternalLabel === "function"
|
|
2066
|
+
? Reflect.apply(getFieldByInternalLabel, form, [stateOptions.internalLabel])
|
|
2067
|
+
: null;
|
|
2068
|
+
const fieldAvailable = Boolean(field && typeof field === "object");
|
|
2069
|
+
const getId = field && typeof field === "object" ? Reflect.get(field, "getId") : null;
|
|
2070
|
+
const getValue = field && typeof field === "object" ? Reflect.get(field, "getValue") : null;
|
|
2071
|
+
const getGeneralAttribute = field && typeof field === "object" ? Reflect.get(field, "getGeneralAttribute") : null;
|
|
2072
|
+
const id = fieldAvailable
|
|
2073
|
+
? typeof getId === "function"
|
|
2074
|
+
? Reflect.apply(getId, field, [])
|
|
2075
|
+
: Reflect.get(field, "id") ?? null
|
|
2076
|
+
: stateOptions.fieldId ?? null;
|
|
2077
|
+
const value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
|
|
2078
|
+
const selectedValue = readPath(value, stateOptions.valuePath);
|
|
2079
|
+
const root = stateOptions.selector != null
|
|
2080
|
+
? document.querySelector(stateOptions.selector)
|
|
2081
|
+
: stateOptions.internalLabel != null
|
|
2082
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(stateOptions.internalLabel)}"]`)
|
|
2083
|
+
: id == null
|
|
2084
|
+
? null
|
|
2085
|
+
: document.querySelector(`#fsCell${CSS.escape(String(id))}, #fsFieldCell${CSS.escape(String(id))}, [data-testid="field-${CSS.escape(String(id))}"]`);
|
|
2086
|
+
const visible = isVisible(root);
|
|
2087
|
+
const hidden = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["hidden"]) === true : visible === null ? null : !visible;
|
|
2088
|
+
const textValue = stringifyValue(selectedValue);
|
|
2089
|
+
const numberValue = normalizeNumber(selectedValue);
|
|
2090
|
+
const dateValue = normalizeDate(selectedValue);
|
|
2091
|
+
const valueMatched = stateOptions.expectedValue === undefined || valuesEqual(selectedValue, stateOptions.expectedValue);
|
|
2092
|
+
const numberMatched = stateOptions.expectedNumber === undefined ||
|
|
2093
|
+
(numberValue !== null && Math.abs(numberValue - stateOptions.expectedNumber) <= (stateOptions.numberTolerance ?? 0));
|
|
2094
|
+
const dateMatched = expectedDateText === undefined || dateValue === expectedDateText;
|
|
2095
|
+
const visibilityMatched = stateOptions.expectedVisible === undefined || visible === stateOptions.expectedVisible;
|
|
2096
|
+
return {
|
|
2097
|
+
matched: apiAvailable && fieldAvailable && valueMatched && numberMatched && dateMatched && visibilityMatched,
|
|
2098
|
+
apiAvailable,
|
|
2099
|
+
fieldAvailable,
|
|
2100
|
+
id,
|
|
2101
|
+
internalLabel: root?.getAttribute("data-internal-label") ?? stateOptions.internalLabel ?? null,
|
|
2102
|
+
value,
|
|
2103
|
+
selectedValue,
|
|
2104
|
+
textValue,
|
|
2105
|
+
numberValue,
|
|
2106
|
+
dateValue,
|
|
2107
|
+
visible,
|
|
2108
|
+
hidden,
|
|
2109
|
+
...(stateOptions.expectedValue === undefined ? {} : { expectedValue: stateOptions.expectedValue }),
|
|
2110
|
+
...(stateOptions.expectedNumber === undefined ? {} : { expectedNumber: stateOptions.expectedNumber }),
|
|
2111
|
+
...(expectedDateText === undefined ? {} : { expectedDate: expectedDateText }),
|
|
2112
|
+
valueMatched,
|
|
2113
|
+
numberMatched,
|
|
2114
|
+
dateMatched,
|
|
2115
|
+
visibilityMatched,
|
|
2116
|
+
};
|
|
2117
|
+
}, options);
|
|
2118
|
+
}
|
|
2119
|
+
export async function waitForLiveFormCalculationState(page, options) {
|
|
2120
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
2121
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
2122
|
+
const startedAt = Date.now();
|
|
2123
|
+
let latest = await readLiveFormCalculationState(page, options);
|
|
2124
|
+
while (!latest.matched && Date.now() - startedAt < timeoutMs) {
|
|
2125
|
+
await delay(intervalMs);
|
|
2126
|
+
latest = await readLiveFormCalculationState(page, options);
|
|
2127
|
+
}
|
|
2128
|
+
return latest;
|
|
2129
|
+
}
|
|
2130
|
+
export async function readLiveFormSubmissionInclusion(page, options) {
|
|
2131
|
+
if (!hasEvaluate(page)) {
|
|
2132
|
+
throw new Error("readLiveFormSubmissionInclusion requires a Playwright page with evaluate().");
|
|
2133
|
+
}
|
|
2134
|
+
return page.evaluate((inclusionOptions) => {
|
|
2135
|
+
const isEmpty = (value) => {
|
|
2136
|
+
if (value === null || value === undefined)
|
|
2137
|
+
return true;
|
|
2138
|
+
if (typeof value === "string")
|
|
2139
|
+
return value.trim().length === 0;
|
|
2140
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
2141
|
+
return false;
|
|
2142
|
+
if (Array.isArray(value))
|
|
2143
|
+
return value.length === 0 || value.every((item) => isEmpty(item));
|
|
2144
|
+
if (typeof value === "object")
|
|
2145
|
+
return Object.values(value).every((item) => isEmpty(item));
|
|
2146
|
+
return false;
|
|
2147
|
+
};
|
|
2148
|
+
const isVisible = (element) => {
|
|
2149
|
+
if (!element)
|
|
2150
|
+
return null;
|
|
2151
|
+
if (!(element instanceof HTMLElement))
|
|
2152
|
+
return false;
|
|
2153
|
+
for (let current = element; current; current = current.parentElement) {
|
|
2154
|
+
const style = getComputedStyle(current);
|
|
2155
|
+
const rect = current.getBoundingClientRect();
|
|
2156
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
|
|
2157
|
+
return false;
|
|
2158
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
return element.getClientRects().length > 0;
|
|
2162
|
+
};
|
|
2163
|
+
const controlSnapshot = (control, formData) => {
|
|
2164
|
+
const input = control;
|
|
2165
|
+
const name = control.getAttribute("name");
|
|
2166
|
+
const type = control.getAttribute("type");
|
|
2167
|
+
const disabled = "disabled" in input ? input.disabled : null;
|
|
2168
|
+
const visible = isVisible(control) === true;
|
|
2169
|
+
const checked = control instanceof HTMLInputElement && ["checkbox", "radio"].includes(control.type) ? control.checked : null;
|
|
2170
|
+
const successful = Boolean(name) &&
|
|
2171
|
+
disabled !== true &&
|
|
2172
|
+
(!(control instanceof HTMLInputElement) || !["checkbox", "radio"].includes(control.type) || control.checked);
|
|
2173
|
+
const formDataIncluded = Boolean(name && formData?.has(name));
|
|
2174
|
+
return {
|
|
2175
|
+
tagName: control.tagName.toLowerCase(),
|
|
2176
|
+
type,
|
|
2177
|
+
id: control.getAttribute("id"),
|
|
2178
|
+
name,
|
|
2179
|
+
value: control.getAttribute("value"),
|
|
2180
|
+
currentValue: "value" in input ? input.value : null,
|
|
2181
|
+
checked,
|
|
2182
|
+
disabled,
|
|
2183
|
+
visible,
|
|
2184
|
+
successful,
|
|
2185
|
+
formDataIncluded,
|
|
2186
|
+
};
|
|
2187
|
+
};
|
|
2188
|
+
const readApi = () => {
|
|
2189
|
+
const factory = Reflect.get(window, "fsApi");
|
|
2190
|
+
const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
|
|
2191
|
+
const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
|
|
2192
|
+
const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
|
|
2193
|
+
return Array.isArray(forms) ? forms[0] ?? null : null;
|
|
2194
|
+
};
|
|
2195
|
+
const formApi = readApi();
|
|
2196
|
+
const apiAvailable = Boolean(formApi && typeof formApi === "object");
|
|
2197
|
+
const getField = formApi && typeof formApi === "object" ? Reflect.get(formApi, "getField") : null;
|
|
2198
|
+
const getFieldByInternalLabel = formApi && typeof formApi === "object" ? Reflect.get(formApi, "getFieldByInternalLabel") : null;
|
|
2199
|
+
const field = inclusionOptions.fieldId != null && typeof getField === "function"
|
|
2200
|
+
? Reflect.apply(getField, formApi, [String(inclusionOptions.fieldId)])
|
|
2201
|
+
: inclusionOptions.internalLabel && typeof getFieldByInternalLabel === "function"
|
|
2202
|
+
? Reflect.apply(getFieldByInternalLabel, formApi, [inclusionOptions.internalLabel])
|
|
2203
|
+
: null;
|
|
2204
|
+
const fieldAvailable = Boolean(field && typeof field === "object");
|
|
2205
|
+
const getId = field && typeof field === "object" ? Reflect.get(field, "getId") : null;
|
|
2206
|
+
const getValue = field && typeof field === "object" ? Reflect.get(field, "getValue") : null;
|
|
2207
|
+
const getGeneralAttribute = field && typeof field === "object" ? Reflect.get(field, "getGeneralAttribute") : null;
|
|
2208
|
+
const id = fieldAvailable
|
|
2209
|
+
? typeof getId === "function"
|
|
2210
|
+
? Reflect.apply(getId, field, [])
|
|
2211
|
+
: Reflect.get(field, "id") ?? null
|
|
2212
|
+
: inclusionOptions.fieldId ?? null;
|
|
2213
|
+
const value = typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
|
|
2214
|
+
const root = inclusionOptions.selector != null
|
|
2215
|
+
? document.querySelector(inclusionOptions.selector)
|
|
2216
|
+
: inclusionOptions.internalLabel != null
|
|
2217
|
+
? document.querySelector(`[data-internal-label="${CSS.escape(inclusionOptions.internalLabel)}"]`)
|
|
2218
|
+
: id == null
|
|
2219
|
+
? null
|
|
2220
|
+
: document.querySelector(`#fsCell${CSS.escape(String(id))}, #fsFieldCell${CSS.escape(String(id))}, [data-testid="field-${CSS.escape(String(id))}"]`);
|
|
2221
|
+
const htmlForm = root?.closest("form") ?? document.querySelector("form");
|
|
2222
|
+
const formData = htmlForm instanceof HTMLFormElement ? new FormData(htmlForm) : null;
|
|
2223
|
+
const controls = root
|
|
2224
|
+
? Array.from(root.querySelectorAll("input, textarea, select"))
|
|
2225
|
+
.filter((control) => inclusionOptions.includeHiddenControls !== false || control.getAttribute("type") !== "hidden")
|
|
2226
|
+
.map((control) => controlSnapshot(control, formData))
|
|
2227
|
+
: [];
|
|
2228
|
+
const includedNames = [...new Set(controls.filter((control) => control.formDataIncluded).map((control) => control.name).filter((name) => Boolean(name)))];
|
|
2229
|
+
const included = includedNames.length > 0;
|
|
2230
|
+
const domVisible = isVisible(root);
|
|
2231
|
+
const apiHidden = typeof getGeneralAttribute === "function" ? Reflect.apply(getGeneralAttribute, field, ["hidden"]) === true : null;
|
|
2232
|
+
const matched = Boolean(root) && (inclusionOptions.expectedIncluded === undefined || included === inclusionOptions.expectedIncluded);
|
|
2233
|
+
const reason = !root
|
|
2234
|
+
? "field-root-missing"
|
|
2235
|
+
: included
|
|
2236
|
+
? domVisible === false ? "included-while-dom-hidden" : "included"
|
|
2237
|
+
: isEmpty(value) ? "not-included-empty-value" : domVisible === false || apiHidden === true ? "not-included-hidden-by-logic-or-disabled" : "not-included-no-successful-controls";
|
|
2238
|
+
return {
|
|
2239
|
+
matched,
|
|
2240
|
+
fieldMatched: Boolean(root),
|
|
2241
|
+
apiAvailable,
|
|
2242
|
+
fieldAvailable,
|
|
2243
|
+
id,
|
|
2244
|
+
internalLabel: root?.getAttribute("data-internal-label") ?? inclusionOptions.internalLabel ?? null,
|
|
2245
|
+
value,
|
|
2246
|
+
isEmpty: isEmpty(value),
|
|
2247
|
+
apiHidden,
|
|
2248
|
+
domVisible,
|
|
2249
|
+
controls,
|
|
2250
|
+
included,
|
|
2251
|
+
includedNames,
|
|
2252
|
+
...(inclusionOptions.expectedIncluded === undefined ? {} : { expectedIncluded: inclusionOptions.expectedIncluded }),
|
|
2253
|
+
reason,
|
|
2254
|
+
};
|
|
2255
|
+
}, options);
|
|
2256
|
+
}
|
|
2257
|
+
export async function traceLiveFormCalculationLifecycle(page, options) {
|
|
2258
|
+
const totalStartedAt = Date.now();
|
|
2259
|
+
const phases = [];
|
|
2260
|
+
const beforeStartedAt = Date.now();
|
|
2261
|
+
const before = await readLiveFormCalculationState(page, options.calculation);
|
|
2262
|
+
phases.push(endCascadePhase("before-calculation", beforeStartedAt, before.fieldAvailable));
|
|
2263
|
+
const writeStartedAt = Date.now();
|
|
2264
|
+
const transaction = await performLiveFormFieldTransaction(page, {
|
|
2265
|
+
...(options.trigger.transaction ?? {}),
|
|
2266
|
+
write: {
|
|
2267
|
+
...(options.trigger.write ?? {}),
|
|
2268
|
+
target: options.trigger.target,
|
|
2269
|
+
value: options.trigger.value,
|
|
2270
|
+
notify: options.trigger.write?.notify ?? true,
|
|
2271
|
+
},
|
|
2272
|
+
...(options.trigger.fieldKind === undefined ? {} : { fieldKind: options.trigger.fieldKind }),
|
|
2273
|
+
});
|
|
2274
|
+
phases.push(endCascadePhase("write", writeStartedAt, transaction.write.valueMatched !== false));
|
|
2275
|
+
const quietOptions = options.quiet === true ? {} : options.quiet === false ? undefined : options.quiet;
|
|
2276
|
+
const quietStartedAt = Date.now();
|
|
2277
|
+
const quiet = quietOptions ? await waitForLiveFormQuiet(page, quietOptions) : undefined;
|
|
2278
|
+
if (quiet)
|
|
2279
|
+
phases.push(endCascadePhase("quiet", quietStartedAt, quiet.quiet));
|
|
2280
|
+
const afterStartedAt = Date.now();
|
|
2281
|
+
const after = await waitForLiveFormCalculationState(page, options.calculation);
|
|
2282
|
+
phases.push(endCascadePhase("after-calculation", afterStartedAt, after.matched));
|
|
2283
|
+
const visibilityStartedAt = Date.now();
|
|
2284
|
+
const visibility = options.visibility ? await waitForLiveFormVisibility(page, options.visibility) : undefined;
|
|
2285
|
+
if (visibility)
|
|
2286
|
+
phases.push(endCascadePhase("visibility", visibilityStartedAt, visibility.matched));
|
|
2287
|
+
const endedAt = Date.now();
|
|
2288
|
+
const ok = transaction.write.valueMatched !== false && (quiet?.quiet ?? true) && after.matched && (visibility?.matched ?? true);
|
|
2289
|
+
return {
|
|
2290
|
+
label: options.label ?? "live-form-calculation-lifecycle",
|
|
2291
|
+
before,
|
|
2292
|
+
transaction,
|
|
2293
|
+
after,
|
|
2294
|
+
...(quiet ? { quiet } : {}),
|
|
2295
|
+
...(visibility ? { visibility } : {}),
|
|
2296
|
+
phases,
|
|
2297
|
+
totalMs: endedAt - totalStartedAt,
|
|
2298
|
+
ok,
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
1955
2301
|
export async function readLiveFormProductState(page, options) {
|
|
1956
2302
|
if (!hasEvaluate(page)) {
|
|
1957
2303
|
throw new Error("readLiveFormProductState requires a Playwright page with evaluate().");
|
|
@@ -3432,16 +3778,39 @@ export async function discoverLiveFormFieldShapes(page, options = {}) {
|
|
|
3432
3778
|
controls.some((control) => control.name?.toLowerCase().includes("payment") || control.name?.toLowerCase().includes("credit"))) {
|
|
3433
3779
|
return "payment";
|
|
3434
3780
|
}
|
|
3781
|
+
if (internalLabel.includes("calculation") ||
|
|
3782
|
+
internalLabel.includes("calculated") ||
|
|
3783
|
+
internalLabel.includes("calc_") ||
|
|
3784
|
+
internalLabel.endsWith("_calc") ||
|
|
3785
|
+
classes.includes("calculation") ||
|
|
3786
|
+
classes.includes("calculated") ||
|
|
3787
|
+
dataTestId.includes("calculation") ||
|
|
3788
|
+
text.includes("calculation")) {
|
|
3789
|
+
return "calculation";
|
|
3790
|
+
}
|
|
3435
3791
|
if (internalLabel.includes("code_embed") || text.startsWith(":root{") || text.includes("<script"))
|
|
3436
3792
|
return "embed";
|
|
3437
3793
|
if (controls.length === 0) {
|
|
3438
3794
|
if (classes.includes("pagebreak") || classes.includes("page-break") || dataTestId.includes("pagebreak") || id.includes("pagebreak")) {
|
|
3439
3795
|
return "pagebreak";
|
|
3440
3796
|
}
|
|
3441
|
-
if (classes.includes("description") ||
|
|
3797
|
+
if (classes.includes("description") ||
|
|
3798
|
+
classes.includes("richtext") ||
|
|
3799
|
+
internalLabel.includes("description") ||
|
|
3800
|
+
internalLabel.includes("richtext") ||
|
|
3801
|
+
dataTestId.includes("description") ||
|
|
3802
|
+
dataTestId.includes("richtext")) {
|
|
3442
3803
|
return "description";
|
|
3443
3804
|
}
|
|
3444
|
-
|
|
3805
|
+
if (classes.includes("section") || id.includes("section") || element.querySelector(".fsSectionHeader, [class*='SectionHeader']")) {
|
|
3806
|
+
return "section";
|
|
3807
|
+
}
|
|
3808
|
+
return "description";
|
|
3809
|
+
}
|
|
3810
|
+
if (internalLabel.includes("hidden") ||
|
|
3811
|
+
classes.includes("hidden-field") ||
|
|
3812
|
+
controls.every((control) => control.type === "hidden")) {
|
|
3813
|
+
return "hidden";
|
|
3445
3814
|
}
|
|
3446
3815
|
if (classes.includes("matrix") || internalLabel.includes("matrix") || element.querySelector("[id^='matrix-row-'], table"))
|
|
3447
3816
|
return "matrix";
|