ff-dom 3.0.3-beta.3 → 3.0.3
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.browser.cjs +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.cdn.js +1018 -276
- package/dist/types/browser/browser/xpath.d.ts +10 -3
- package/dist/types/browser/utils/getElementsFromHTML.d.ts +1 -1
- package/dist/types/browser/utils/xpath.d.ts +32 -2
- package/dist/types/browser/utils/xpathHelpers.d.ts +11 -9
- package/dist/types/node/node/xpath.d.ts +2 -2
- package/dist/types/node/utils/getElementsFromHTML.d.ts +1 -1
- package/dist/types/node/utils/iosSelector.d.ts +7 -0
- package/dist/types/node/utils/xpath.d.ts +32 -2
- package/dist/types/node/utils/xpathHelpers.d.ts +11 -9
- package/dist/xpath.mjs +1 -1
- package/package.json +1 -1
package/dist/index.cdn.js
CHANGED
|
@@ -1880,11 +1880,16 @@
|
|
|
1880
1880
|
}
|
|
1881
1881
|
return xpaths1;
|
|
1882
1882
|
};
|
|
1883
|
-
const parseXml = (xmlStr, type) => {
|
|
1884
|
-
if (
|
|
1885
|
-
return
|
|
1883
|
+
const parseXml = (xmlStr, type, DOMParserImpl) => {
|
|
1884
|
+
if (!DOMParserImpl) {
|
|
1885
|
+
return null;
|
|
1886
1886
|
}
|
|
1887
|
-
|
|
1887
|
+
const doc = new DOMParserImpl().parseFromString(xmlStr, type);
|
|
1888
|
+
// optional validation
|
|
1889
|
+
if (doc.querySelector("parsererror")) {
|
|
1890
|
+
throw new Error("Invalid XML");
|
|
1891
|
+
}
|
|
1892
|
+
return doc;
|
|
1888
1893
|
};
|
|
1889
1894
|
const normalizeXPath = (xpath) => {
|
|
1890
1895
|
// Replace text() = "value" or text()='value'
|
|
@@ -1907,29 +1912,70 @@
|
|
|
1907
1912
|
}
|
|
1908
1913
|
return closePos;
|
|
1909
1914
|
};
|
|
1910
|
-
function
|
|
1911
|
-
|
|
1915
|
+
function getXPathString(xpath) {
|
|
1916
|
+
// ✅ Normal web xpath
|
|
1917
|
+
if (typeof xpath === "string") {
|
|
1918
|
+
return xpath;
|
|
1919
|
+
}
|
|
1920
|
+
// ✅ Android/object xpath
|
|
1921
|
+
if (xpath &&
|
|
1922
|
+
typeof xpath === "object" &&
|
|
1923
|
+
typeof xpath.value === "string") {
|
|
1924
|
+
return xpath.value;
|
|
1925
|
+
}
|
|
1926
|
+
return "";
|
|
1927
|
+
}
|
|
1928
|
+
function canonicalizeXPath(xpath, platform) {
|
|
1929
|
+
const xpathValue = getXPathString(xpath);
|
|
1930
|
+
// Android flow
|
|
1931
|
+
if (String(platform).toLowerCase() === "android") {
|
|
1932
|
+
return xpathValue
|
|
1933
|
+
.toLowerCase()
|
|
1934
|
+
.replace(/@text="[^"]*"/g, '@text="?"')
|
|
1935
|
+
.replace(/@text='[^']*'/g, "@text='?'")
|
|
1936
|
+
.replace(/"[^"]*"/g, "?")
|
|
1937
|
+
.replace(/'[^']*'/g, "?")
|
|
1938
|
+
.replace(/\/\/android\.[a-z0-9._-]+/g, "//android_tag")
|
|
1939
|
+
.replace(/\/\/\*/g, "//*");
|
|
1940
|
+
}
|
|
1941
|
+
// Web flow
|
|
1942
|
+
return xpathValue
|
|
1912
1943
|
.toLowerCase()
|
|
1913
|
-
// replace quoted values
|
|
1914
1944
|
.replace(/'[^']*'/g, "?")
|
|
1915
1945
|
.replace(/"[^"]*"/g, "?")
|
|
1916
|
-
// replace numbers in predicates
|
|
1917
1946
|
.replace(/\[\d+\]/g, "[?]")
|
|
1918
|
-
// normalize node names
|
|
1919
1947
|
.replace(/\/\/[a-z0-9_-]+/g, "//node")
|
|
1920
|
-
.replace(/\/[a-z0-9_-]+/g, "/node")
|
|
1948
|
+
.replace(/\/[a-z0-9_-]+/g, "/node");
|
|
1921
1949
|
}
|
|
1922
1950
|
function extractXPathSignatureParts(xpath) {
|
|
1923
|
-
const
|
|
1951
|
+
const xpathValue = typeof xpath === "string"
|
|
1952
|
+
? xpath
|
|
1953
|
+
: xpath?.value || "";
|
|
1954
|
+
if (!xpathValue) {
|
|
1955
|
+
return {
|
|
1956
|
+
axis: "none",
|
|
1957
|
+
attribute: "none",
|
|
1958
|
+
usesNormalize: false
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
const xp = xpathValue.toLowerCase();
|
|
1924
1962
|
const axisMatch = xp.match(/(ancestor-or-self|ancestor|descendant-or-self|descendant|following-sibling|preceding-sibling|following|preceding|parent|child|self)::/);
|
|
1925
1963
|
const axis = axisMatch?.[1] ?? "none";
|
|
1926
1964
|
const attrMatch = xp.match(/@([a-z0-9:-]+)/);
|
|
1927
1965
|
const attribute = attrMatch?.[1] ?? "none";
|
|
1928
1966
|
const usesNormalize = xp.includes("normalize-space");
|
|
1929
|
-
return {
|
|
1967
|
+
return {
|
|
1968
|
+
axis,
|
|
1969
|
+
attribute,
|
|
1970
|
+
usesNormalize
|
|
1971
|
+
};
|
|
1930
1972
|
}
|
|
1931
|
-
function getXPathPattern(xpath) {
|
|
1932
|
-
const canonical = canonicalizeXPath(xpath);
|
|
1973
|
+
function getXPathPattern(xpath, platform) {
|
|
1974
|
+
const canonical = canonicalizeXPath(xpath, platform);
|
|
1975
|
+
// Android: use full canonical xpath as pattern
|
|
1976
|
+
if (platform?.toLowerCase().includes("android")) {
|
|
1977
|
+
return `XPATH|shape:${canonical}`;
|
|
1978
|
+
}
|
|
1933
1979
|
const parts = extractXPathSignatureParts(xpath);
|
|
1934
1980
|
return [
|
|
1935
1981
|
"XPATH",
|
|
@@ -1945,8 +1991,13 @@
|
|
|
1945
1991
|
function shouldUseSnapshot(xpath) {
|
|
1946
1992
|
return /\[(?:\s*\.|\s*contains\s*\(\s*\.|\s*normalize-space\s*\(\s*\.)/.test(xpath);
|
|
1947
1993
|
}
|
|
1948
|
-
function isUniqueInDOM(docmt, name, value, element) {
|
|
1949
|
-
|
|
1994
|
+
function isUniqueInDOM(docmt, name, value, element, platform) {
|
|
1995
|
+
// Scope uniqueness checks to the element's actual root, including ShadowRoot.
|
|
1996
|
+
const root = (element?.getRootNode?.() ?? docmt);
|
|
1997
|
+
const isAndroid = String(platform).toLowerCase().includes("android");
|
|
1998
|
+
const isIOS = String(platform).toLowerCase().includes("ios");
|
|
1999
|
+
const isMobile = isAndroid || isIOS;
|
|
2000
|
+
// ✅ Fix 2: explicitly type the return as Element[]
|
|
1950
2001
|
const queryAll = (selector) => {
|
|
1951
2002
|
try {
|
|
1952
2003
|
return Array.from(root.querySelectorAll(selector));
|
|
@@ -1955,24 +2006,83 @@
|
|
|
1955
2006
|
return [];
|
|
1956
2007
|
}
|
|
1957
2008
|
};
|
|
2009
|
+
// ✅ Fix 3: explicitly type the filter result as Element[]
|
|
2010
|
+
const walkAttrAll = (attr, val) => {
|
|
2011
|
+
try {
|
|
2012
|
+
return Array.from(root.querySelectorAll("*")).filter((el) => el.getAttribute(attr) === val);
|
|
2013
|
+
}
|
|
2014
|
+
catch {
|
|
2015
|
+
return [];
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
// ✅ Fix 4: partial match walk also needs explicit type guard
|
|
2019
|
+
const walkAttrIncludes = (attr, val) => {
|
|
2020
|
+
try {
|
|
2021
|
+
return Array.from(root.querySelectorAll("*")).filter((el) => el.getAttribute(attr)?.includes(val) ?? false);
|
|
2022
|
+
}
|
|
2023
|
+
catch {
|
|
2024
|
+
return [];
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
1958
2027
|
try {
|
|
1959
2028
|
switch (name) {
|
|
1960
|
-
case "id":
|
|
2029
|
+
case "id": {
|
|
2030
|
+
if (isAndroid)
|
|
2031
|
+
return walkAttrAll("resource-id", value).length === 1;
|
|
2032
|
+
if (isIOS)
|
|
2033
|
+
return walkAttrAll("name", value).length === 1;
|
|
1961
2034
|
return queryAll(`#${escapeAttrValue(value)}`).length === 1;
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2035
|
+
}
|
|
2036
|
+
case "accessibilityId": {
|
|
2037
|
+
if (isAndroid)
|
|
2038
|
+
return walkAttrAll("content-desc", value).length === 1;
|
|
2039
|
+
if (isIOS)
|
|
2040
|
+
return walkAttrAll("name", value).length === 1;
|
|
2041
|
+
return walkAttrAll("aria-label", value).length === 1;
|
|
2042
|
+
}
|
|
2043
|
+
case "className": {
|
|
2044
|
+
if (isAndroid)
|
|
2045
|
+
return walkAttrAll("class", value).length === 1;
|
|
2046
|
+
if (isIOS)
|
|
2047
|
+
return walkAttrAll("type", value).length === 1;
|
|
1965
2048
|
return queryAll(`.${value}`).length === 1;
|
|
1966
|
-
|
|
2049
|
+
}
|
|
2050
|
+
case "tagName": {
|
|
2051
|
+
if (isAndroid)
|
|
2052
|
+
return walkAttrAll("class", value).length === 1;
|
|
2053
|
+
if (isIOS) {
|
|
2054
|
+
return queryAll("*").filter((candidate) => (candidate.getAttribute("type") || candidate.tagName) === value).length === 1;
|
|
2055
|
+
}
|
|
1967
2056
|
return queryAll(value).length === 1;
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2057
|
+
}
|
|
2058
|
+
case "name": {
|
|
2059
|
+
if (isAndroid) {
|
|
2060
|
+
return (walkAttrAll("text", value).length === 1 ||
|
|
2061
|
+
walkAttrAll("content-desc", value).length === 1);
|
|
2062
|
+
}
|
|
2063
|
+
if (isIOS)
|
|
2064
|
+
return walkAttrAll("name", value).length === 1;
|
|
2065
|
+
return queryAll(`[name="${escapeAttrValue(value)}"]`).length === 1;
|
|
2066
|
+
}
|
|
2067
|
+
case "linkText": {
|
|
2068
|
+
if (isAndroid)
|
|
2069
|
+
return walkAttrAll("text", value).length === 1;
|
|
2070
|
+
if (isIOS)
|
|
2071
|
+
return walkAttrAll("label", value).length === 1;
|
|
2072
|
+
return (queryAll("a").filter((a) => a.textContent?.trim() === value).length === 1);
|
|
2073
|
+
}
|
|
2074
|
+
case "partialLinkText": {
|
|
2075
|
+
if (isAndroid)
|
|
2076
|
+
return walkAttrIncludes("text", value).length === 1;
|
|
2077
|
+
if (isIOS)
|
|
2078
|
+
return walkAttrIncludes("label", value).length === 1;
|
|
2079
|
+
return (queryAll("a").filter((a) => a.textContent?.includes(value)).length === 1);
|
|
2080
|
+
}
|
|
2081
|
+
case "cssSelector": {
|
|
2082
|
+
if (isMobile)
|
|
2083
|
+
return false;
|
|
1975
2084
|
return queryAll(value).length === 1;
|
|
2085
|
+
}
|
|
1976
2086
|
default:
|
|
1977
2087
|
return false;
|
|
1978
2088
|
}
|
|
@@ -2035,9 +2145,132 @@
|
|
|
2035
2145
|
getContainerTextCondition,
|
|
2036
2146
|
};
|
|
2037
2147
|
|
|
2148
|
+
function getOptimalClassChain(doc, domNode, uniqueAttributes) {
|
|
2149
|
+
try {
|
|
2150
|
+
if (!domNode ||
|
|
2151
|
+
domNode.nodeType !== 1 ||
|
|
2152
|
+
!domNode.tagName ||
|
|
2153
|
+
domNode.tagName === "XCUIElementTypeApplication") {
|
|
2154
|
+
return "";
|
|
2155
|
+
}
|
|
2156
|
+
const tag = domNode.tagName;
|
|
2157
|
+
// Priority attributes
|
|
2158
|
+
const priorityAttrs = ["name", "label", "value"];
|
|
2159
|
+
// ---------- STEP 1 : TRY GOOD ATTRIBUTES ----------
|
|
2160
|
+
for (const attrName of priorityAttrs) {
|
|
2161
|
+
const attr = uniqueAttributes.find(a => a.name === attrName);
|
|
2162
|
+
if (!attr)
|
|
2163
|
+
continue;
|
|
2164
|
+
let attrValue = attr.value;
|
|
2165
|
+
if (!attrValue)
|
|
2166
|
+
continue;
|
|
2167
|
+
attrValue = attrValue.trim().replace(/\s+/g, " ");
|
|
2168
|
+
if (isNumberExist(attrValue))
|
|
2169
|
+
continue;
|
|
2170
|
+
const xpath = `//${tag}[@${attrName}="${attrValue}"]`;
|
|
2171
|
+
let count = 0;
|
|
2172
|
+
try {
|
|
2173
|
+
count = getCountOfXPath(xpath, domNode, doc);
|
|
2174
|
+
}
|
|
2175
|
+
catch (err) {
|
|
2176
|
+
console.log(err);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const hasSpace = /\s/.test(attrValue);
|
|
2180
|
+
if (count === 1) {
|
|
2181
|
+
if (hasSpace) {
|
|
2182
|
+
return `**/${tag}[\`${attrName} CONTAINS "${attrValue}"\`]`;
|
|
2183
|
+
}
|
|
2184
|
+
return `**/${tag}[\`${attrName} == "${attrValue}"\`]`;
|
|
2185
|
+
}
|
|
2186
|
+
if (count > 1 && domNode.parentElement) {
|
|
2187
|
+
const siblings = Array.from(domNode.parentElement.children)
|
|
2188
|
+
.filter(el => el.tagName === tag);
|
|
2189
|
+
const index = siblings.indexOf(domNode) + 1;
|
|
2190
|
+
if (hasSpace) {
|
|
2191
|
+
return `**/${tag}[\`${attrName} CONTAINS "${attrValue}"\`][${index}]`;
|
|
2192
|
+
}
|
|
2193
|
+
return `**/${tag}[\`${attrName} == "${attrValue}"\`][${index}]`;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
// ---------- STEP 2 : FALLBACK USING TAG INDEX ----------
|
|
2197
|
+
let classChain = `/${tag}`;
|
|
2198
|
+
if (domNode.parentElement) {
|
|
2199
|
+
const siblings = Array.from(domNode.parentElement.children)
|
|
2200
|
+
.filter(el => el.tagName === tag);
|
|
2201
|
+
if (siblings.length > 1) {
|
|
2202
|
+
const index = siblings.indexOf(domNode) + 1;
|
|
2203
|
+
classChain += `[${index}]`;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
const parentChain = getOptimalClassChain(doc, domNode.parentElement, uniqueAttributes);
|
|
2207
|
+
return parentChain + classChain;
|
|
2208
|
+
}
|
|
2209
|
+
catch (error) {
|
|
2210
|
+
console.log(`Unable to generate optimal -ios class chain : ${JSON.stringify(error)}`);
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function getOptimalPredicateString(doc, domNode, uniqueAttributes) {
|
|
2215
|
+
try {
|
|
2216
|
+
// BASE CASE #1: If this isn't an element, we're above the root, or this is `XCUIElementTypeApplication`,
|
|
2217
|
+
// which is not an official XCUITest element, return empty string
|
|
2218
|
+
if (!domNode?.tagName ||
|
|
2219
|
+
domNode?.nodeType !== 1 ||
|
|
2220
|
+
domNode?.tagName === 'XCUIElementTypeApplication') {
|
|
2221
|
+
return '';
|
|
2222
|
+
}
|
|
2223
|
+
// BASE CASE #2: Check attributes in iOS locator priority order.
|
|
2224
|
+
// Prefer stable XCTest identifiers (`name`) over weak type-only predicates.
|
|
2225
|
+
let xpathAttributes = [];
|
|
2226
|
+
let predicateString = [];
|
|
2227
|
+
const priorityAttrs = ["name", "label", "value", "type"];
|
|
2228
|
+
const orderedAttributes = priorityAttrs
|
|
2229
|
+
.map((attrName) => uniqueAttributes.find((attr) => attr.name === attrName))
|
|
2230
|
+
.filter((attr) => Boolean(attr));
|
|
2231
|
+
for (let attr of orderedAttributes) {
|
|
2232
|
+
const attrValue = attr.value;
|
|
2233
|
+
const attrName = attr.name;
|
|
2234
|
+
if (attrValue.length === 0) {
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
if (attrValue && !isNumberExist(attrValue)) {
|
|
2238
|
+
xpathAttributes.push(`@${attrName}="${attrValue}"`);
|
|
2239
|
+
const xpathe = `//*[${xpathAttributes.join(' and ')}]`;
|
|
2240
|
+
predicateString.push(`${attrName} == "${attrValue}"`);
|
|
2241
|
+
let othersWithAttr;
|
|
2242
|
+
// If the XPath does not parse, move to the next unique attribute
|
|
2243
|
+
try {
|
|
2244
|
+
othersWithAttr = getCountOfXPath(xpathe, domNode, doc);
|
|
2245
|
+
}
|
|
2246
|
+
catch (ign) {
|
|
2247
|
+
console.log(ign);
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
if (othersWithAttr === 1) {
|
|
2251
|
+
if (attrName === "type" && predicateString.length === 1) {
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
return predicateString.join(' AND ');
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
catch (error) {
|
|
2260
|
+
// If there's an unexpected exception, abort and don't get an XPath
|
|
2261
|
+
console.log(`The most optimal '-ios predicate string' could not be determined because an error was thrown: '${JSON.stringify(error, null, 2)}'`);
|
|
2262
|
+
}
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
const iosSelectors = {
|
|
2266
|
+
getOptimalClassChain,
|
|
2267
|
+
getOptimalPredicateString
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2038
2270
|
let xpathData$1 = [];
|
|
2039
2271
|
let xpathDataWithIndex$1 = [];
|
|
2040
2272
|
let referenceElementMode = false;
|
|
2273
|
+
let locatorData = [];
|
|
2041
2274
|
let parentXpathCache = new WeakMap();
|
|
2042
2275
|
let childRelativeXpathCache = new WeakMap();
|
|
2043
2276
|
const getElementSignature = (element) => element ?
|
|
@@ -3573,7 +3806,191 @@
|
|
|
3573
3806
|
console.log(error);
|
|
3574
3807
|
}
|
|
3575
3808
|
};
|
|
3576
|
-
const
|
|
3809
|
+
const parseDOMAndroid = (element, doc, isIndex, isTarget, includedAttributes = []) => {
|
|
3810
|
+
xpathData$1 = [];
|
|
3811
|
+
locatorData = [];
|
|
3812
|
+
const res = {};
|
|
3813
|
+
const keys = {};
|
|
3814
|
+
const targetElemt = element;
|
|
3815
|
+
const docmt = targetElemt?.ownerDocument || doc;
|
|
3816
|
+
const tag = targetElemt?.tagName;
|
|
3817
|
+
const { attributes } = targetElemt;
|
|
3818
|
+
[...attributes].forEach((m) => {
|
|
3819
|
+
if (m.name === 'resource-id') {
|
|
3820
|
+
res['id'] = m.value;
|
|
3821
|
+
}
|
|
3822
|
+
else if (m.name === 'content-desc') {
|
|
3823
|
+
res['content'] = m.value;
|
|
3824
|
+
}
|
|
3825
|
+
else {
|
|
3826
|
+
res[m.name] = m.value;
|
|
3827
|
+
}
|
|
3828
|
+
});
|
|
3829
|
+
// resource-id locator
|
|
3830
|
+
if (res?.id && !isNumberExist(res.id)) {
|
|
3831
|
+
const idXpath = `//*[@resource-id=${escapeCharacters(res.id)}]`;
|
|
3832
|
+
const count = getCountOfXPath(idXpath, targetElemt, docmt);
|
|
3833
|
+
if (count === 1) {
|
|
3834
|
+
locatorData.push({
|
|
3835
|
+
key: 'id',
|
|
3836
|
+
value: [{ key: 'resource-id', value: res.id }],
|
|
3837
|
+
viewkey: 'Id',
|
|
3838
|
+
});
|
|
3839
|
+
keys['resource-id'] = 1;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
// accessibility id locator
|
|
3843
|
+
if (res?.content && !isNumberExist(res.content)) {
|
|
3844
|
+
const contentXpath = `//*[@content-desc=${escapeCharacters(res.content)}]`;
|
|
3845
|
+
const count = getCountOfXPath(contentXpath, targetElemt, docmt);
|
|
3846
|
+
if (count === 1) {
|
|
3847
|
+
locatorData.push({
|
|
3848
|
+
key: 'accessibilityId',
|
|
3849
|
+
value: [
|
|
3850
|
+
{
|
|
3851
|
+
key: 'accessibilityID',
|
|
3852
|
+
value: res.content,
|
|
3853
|
+
},
|
|
3854
|
+
],
|
|
3855
|
+
viewkey: 'accessibilityId',
|
|
3856
|
+
});
|
|
3857
|
+
keys['content-desc'] = 1;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
// class locator
|
|
3861
|
+
if (res?.class && !isNumberExist(res.class)) {
|
|
3862
|
+
const classXpath = `//*[@class=${escapeCharacters(res.class)}]`;
|
|
3863
|
+
const count = getCountOfXPath(classXpath, targetElemt, docmt);
|
|
3864
|
+
if (count === 1) {
|
|
3865
|
+
locatorData.push({
|
|
3866
|
+
key: 'className',
|
|
3867
|
+
viewkey: 'className',
|
|
3868
|
+
value: [{ key: 'class', value: res.class }],
|
|
3869
|
+
});
|
|
3870
|
+
keys['class'] = 1;
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
const attributesToUse = includedAttributes.length > 0
|
|
3874
|
+
? includedAttributes
|
|
3875
|
+
: Array.from(attributes);
|
|
3876
|
+
const relevantAttributes = attributesToUse.filter((x) => !keys[x.name]);
|
|
3877
|
+
addAllXPathAttributes(relevantAttributes, targetElemt, docmt, isIndex, isTarget);
|
|
3878
|
+
// regex xpath generation
|
|
3879
|
+
if (xpathData$1.length) {
|
|
3880
|
+
const len = xpathData$1.length;
|
|
3881
|
+
for (let i = 0; i < len; i++) {
|
|
3882
|
+
let xpth = xpathData$1[i].value;
|
|
3883
|
+
xpth =
|
|
3884
|
+
'//*' +
|
|
3885
|
+
xpth.substring(xpth.indexOf('//') + 2 + tag.length);
|
|
3886
|
+
const count = getCountOfXPath(xpth, element, docmt);
|
|
3887
|
+
if (count === 1) {
|
|
3888
|
+
xpathData$1.push({
|
|
3889
|
+
key: `${xpathData$1[i].key} regex`,
|
|
3890
|
+
value: xpth,
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
// relative xpath fallback
|
|
3896
|
+
if (!xpathData$1.length) {
|
|
3897
|
+
addRelativeXpaths(targetElemt, docmt, isIndex, isTarget, relevantAttributes);
|
|
3898
|
+
}
|
|
3899
|
+
// merge xpath locators
|
|
3900
|
+
for (const x of xpathData$1) {
|
|
3901
|
+
locatorData.push({
|
|
3902
|
+
key: 'xpath',
|
|
3903
|
+
viewkey: 'xpath',
|
|
3904
|
+
value: [x],
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
return locatorData;
|
|
3908
|
+
};
|
|
3909
|
+
const parseDOMIOS = (element, doc, isIndex, isTarget, includedAttributes = []) => {
|
|
3910
|
+
xpathData$1 = [];
|
|
3911
|
+
locatorData = [];
|
|
3912
|
+
const keys = {};
|
|
3913
|
+
const targetElemt = element;
|
|
3914
|
+
const docmt = targetElemt?.ownerDocument || doc;
|
|
3915
|
+
const tag = targetElemt?.tagName;
|
|
3916
|
+
const { attributes } = targetElemt;
|
|
3917
|
+
const addUniqueIOSLocator = (locatorKey, viewkey, attributeName) => {
|
|
3918
|
+
const attributeValue = targetElemt.getAttribute(attributeName);
|
|
3919
|
+
if (!attributeValue || isNumberExist(attributeValue)) {
|
|
3920
|
+
return;
|
|
3921
|
+
}
|
|
3922
|
+
const locatorXpath = `//*[@${attributeName}=${escapeCharacters(attributeValue)}]`;
|
|
3923
|
+
const count = getCountOfXPath(locatorXpath, targetElemt, docmt);
|
|
3924
|
+
if (count !== 1) {
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3927
|
+
locatorData.push({
|
|
3928
|
+
key: locatorKey,
|
|
3929
|
+
viewkey,
|
|
3930
|
+
value: [{ key: attributeName, value: attributeValue }]
|
|
3931
|
+
});
|
|
3932
|
+
keys[attributeName] = 1;
|
|
3933
|
+
};
|
|
3934
|
+
addUniqueIOSLocator("accessibilityId", "accessibilityId", "name");
|
|
3935
|
+
addUniqueIOSLocator("className", "className", "type");
|
|
3936
|
+
const attributesToUse = includedAttributes.length > 0
|
|
3937
|
+
? includedAttributes
|
|
3938
|
+
: Array.from(attributes);
|
|
3939
|
+
const selectorAttributes = attributesToUse.filter((attribute) => ["label", "name", "value", "type"].includes(attribute.name));
|
|
3940
|
+
const classChain = iosSelectors.getOptimalClassChain(docmt, targetElemt, selectorAttributes);
|
|
3941
|
+
if (classChain) {
|
|
3942
|
+
locatorData.push({
|
|
3943
|
+
viewkey: "Class Chain",
|
|
3944
|
+
key: "iOSClassChain",
|
|
3945
|
+
value: [{ key: "classChain", value: classChain }]
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
const predicateString = iosSelectors.getOptimalPredicateString(docmt, targetElemt, selectorAttributes);
|
|
3949
|
+
if (predicateString) {
|
|
3950
|
+
locatorData.push({
|
|
3951
|
+
viewkey: "Predicate String",
|
|
3952
|
+
key: "iOSNsPredicateString",
|
|
3953
|
+
value: [{ key: "predicateString", value: predicateString }]
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
const relevantAttributes = attributesToUse.filter((attribute) => (attribute.name === "name" || !keys[attribute.name]) &&
|
|
3957
|
+
["name", "label", "value"].includes(attribute.name));
|
|
3958
|
+
addAllXPathAttributes(relevantAttributes, targetElemt, docmt, isIndex, isTarget);
|
|
3959
|
+
if (xpathData$1.length) {
|
|
3960
|
+
const len = xpathData$1.length;
|
|
3961
|
+
for (let i = 0; i < len; i++) {
|
|
3962
|
+
let xpth = xpathData$1[i].value;
|
|
3963
|
+
xpth =
|
|
3964
|
+
"//*" +
|
|
3965
|
+
xpth.substring(xpth.indexOf("//") + 2 + tag.length);
|
|
3966
|
+
const count = getCountOfXPath(xpth, element, docmt);
|
|
3967
|
+
if (count === 1) {
|
|
3968
|
+
xpathData$1.push({
|
|
3969
|
+
key: `${xpathData$1[i].key} regex`,
|
|
3970
|
+
value: xpth
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
if (!xpathData$1.length) {
|
|
3976
|
+
addRelativeXpaths(targetElemt, docmt, isIndex, isTarget, relevantAttributes);
|
|
3977
|
+
}
|
|
3978
|
+
for (const xpath of xpathData$1) {
|
|
3979
|
+
locatorData.push({
|
|
3980
|
+
key: "xpath",
|
|
3981
|
+
viewkey: "xpath",
|
|
3982
|
+
value: [xpath]
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
return locatorData;
|
|
3986
|
+
};
|
|
3987
|
+
const parseDOM = (element, doc, isIndex, isTarget, includedAttributes = [], strategies = [], platform) => {
|
|
3988
|
+
if (platform === 'Android') {
|
|
3989
|
+
return parseDOMAndroid(element, doc, isIndex, isTarget, includedAttributes);
|
|
3990
|
+
}
|
|
3991
|
+
if (platform === 'iOS') {
|
|
3992
|
+
return parseDOMIOS(element, doc, isIndex, isTarget, includedAttributes);
|
|
3993
|
+
}
|
|
3577
3994
|
xpathData$1 = [];
|
|
3578
3995
|
console.log(element);
|
|
3579
3996
|
const targetElemt = element;
|
|
@@ -4945,7 +5362,18 @@
|
|
|
4945
5362
|
};
|
|
4946
5363
|
const getReferenceElementXpath = (element) => {
|
|
4947
5364
|
let xpaths1 = [];
|
|
4948
|
-
|
|
5365
|
+
const normalizeXpaths = (data) => Array.isArray(data)
|
|
5366
|
+
? data.flatMap((item) => typeof item?.value === "string"
|
|
5367
|
+
? [{ key: item.key || "", value: item.value }]
|
|
5368
|
+
: Array.isArray(item?.value)
|
|
5369
|
+
? item.value
|
|
5370
|
+
.filter((entry) => typeof entry?.value === "string")
|
|
5371
|
+
.map((entry) => ({
|
|
5372
|
+
key: entry.key || item.key || "",
|
|
5373
|
+
value: entry.value
|
|
5374
|
+
}))
|
|
5375
|
+
: [])
|
|
5376
|
+
: [];
|
|
4949
5377
|
const referenceElementXpaths = getReferenceElementsXpath(element, element.ownerDocument, false);
|
|
4950
5378
|
if (referenceElementXpaths?.length) {
|
|
4951
5379
|
xpaths1 = xpaths1?.length
|
|
@@ -4953,41 +5381,53 @@
|
|
|
4953
5381
|
: referenceElementXpaths;
|
|
4954
5382
|
}
|
|
4955
5383
|
if (!xpaths1?.length) {
|
|
4956
|
-
xpaths1 = parseDOM(element, element.ownerDocument, true, false);
|
|
5384
|
+
xpaths1 = normalizeXpaths(parseDOM(element, element.ownerDocument, true, false));
|
|
4957
5385
|
xpaths1 = xpaths1?.map((x) => x.value.charAt(0) == "(" &&
|
|
4958
5386
|
findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
|
|
4959
5387
|
? { key: "", value: removeParenthesis(x.value) }
|
|
4960
5388
|
: { key: "", value: x.value });
|
|
5389
|
+
xpaths1 = xpaths1.map((x) => x.value.charAt(0) === "(" &&
|
|
5390
|
+
findMatchingParenthesis(x.value, 0) + 1 ===
|
|
5391
|
+
x.value.lastIndexOf("[")
|
|
5392
|
+
? {
|
|
5393
|
+
key: "",
|
|
5394
|
+
value: removeParenthesis(x.value)
|
|
5395
|
+
}
|
|
5396
|
+
: x);
|
|
4961
5397
|
}
|
|
4962
5398
|
else {
|
|
4963
|
-
let xpaths = parseDOM(element, element.ownerDocument, true, false);
|
|
4964
|
-
if (xpaths
|
|
4965
|
-
xpaths = xpaths
|
|
4966
|
-
findMatchingParenthesis(x.value, 0) + 1 ===
|
|
4967
|
-
|
|
4968
|
-
|
|
5399
|
+
let xpaths = normalizeXpaths(parseDOM(element, element.ownerDocument, true, false));
|
|
5400
|
+
if (xpaths.length) {
|
|
5401
|
+
xpaths = xpaths.map((x) => x.value.charAt(0) === "(" &&
|
|
5402
|
+
findMatchingParenthesis(x.value, 0) + 1 ===
|
|
5403
|
+
x.value.lastIndexOf("[")
|
|
5404
|
+
? {
|
|
5405
|
+
key: "",
|
|
5406
|
+
value: removeParenthesis(x.value)
|
|
5407
|
+
}
|
|
5408
|
+
: x);
|
|
4969
5409
|
xpaths1 = xpaths1.concat(xpaths);
|
|
4970
5410
|
}
|
|
4971
5411
|
}
|
|
4972
|
-
if (!xpaths1
|
|
4973
|
-
xpaths1 = [
|
|
4974
|
-
{
|
|
5412
|
+
if (!xpaths1.length) {
|
|
5413
|
+
xpaths1 = [{
|
|
4975
5414
|
key: "",
|
|
4976
5415
|
value: getRelativeXPath(element, element.ownerDocument, false, false, Array.from(element.attributes))
|
|
4977
|
-
}
|
|
4978
|
-
];
|
|
5416
|
+
}];
|
|
4979
5417
|
}
|
|
4980
|
-
if (!xpaths1
|
|
4981
|
-
xpaths1 = [
|
|
4982
|
-
{
|
|
5418
|
+
if (!xpaths1.length) {
|
|
5419
|
+
xpaths1 = [{
|
|
4983
5420
|
key: "",
|
|
4984
5421
|
value: getRelativeXPath(element, element.ownerDocument, true, false, Array.from(element.attributes))
|
|
5422
|
+
}];
|
|
5423
|
+
xpaths1 = xpaths1.map((x) => x.value.charAt(0) === "(" &&
|
|
5424
|
+
findMatchingParenthesis(x.value, 0) + 1 ===
|
|
5425
|
+
x.value.lastIndexOf("[")
|
|
5426
|
+
? {
|
|
5427
|
+
key: "",
|
|
5428
|
+
value: removeParenthesis(x.value)
|
|
4985
5429
|
}
|
|
4986
|
-
|
|
4987
|
-
xpaths1 = xpaths1?.map((x) => x.value.charAt(0) == "(" &&
|
|
4988
|
-
findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
|
|
4989
|
-
? { key: "", value: removeParenthesis(x.value) }
|
|
4990
|
-
: { key: "", value: x.value });
|
|
5430
|
+
: x);
|
|
4991
5431
|
}
|
|
4992
5432
|
const childAnchorXpaths = Array.from(element.children || [])
|
|
4993
5433
|
.map((child) => {
|
|
@@ -5583,10 +6023,30 @@
|
|
|
5583
6023
|
}
|
|
5584
6024
|
return null;
|
|
5585
6025
|
};
|
|
5586
|
-
const getId = (element) => {
|
|
5587
|
-
|
|
6026
|
+
const getId = (element, platform) => {
|
|
6027
|
+
if (!element)
|
|
6028
|
+
return null;
|
|
6029
|
+
const normalizedPlatform = String(platform).toLowerCase();
|
|
6030
|
+
if (normalizedPlatform.includes("android")) {
|
|
6031
|
+
return (element.getAttribute("resource-id") ||
|
|
6032
|
+
element.getAttribute("resourceId") ||
|
|
6033
|
+
null);
|
|
6034
|
+
}
|
|
6035
|
+
if (normalizedPlatform.includes("ios")) {
|
|
6036
|
+
return element.getAttribute("name") || null;
|
|
6037
|
+
}
|
|
6038
|
+
return element.id || null;
|
|
5588
6039
|
};
|
|
5589
|
-
const getClassName = (element) => {
|
|
6040
|
+
const getClassName = (element, platform) => {
|
|
6041
|
+
const normalizedPlatform = String(platform).toLowerCase();
|
|
6042
|
+
if (normalizedPlatform.includes("android")) {
|
|
6043
|
+
return (element.getAttribute("class") ||
|
|
6044
|
+
element.getAttribute("className") ||
|
|
6045
|
+
null);
|
|
6046
|
+
}
|
|
6047
|
+
if (normalizedPlatform.includes("ios")) {
|
|
6048
|
+
return element.getAttribute("type") || null;
|
|
6049
|
+
}
|
|
5590
6050
|
return element.className || null;
|
|
5591
6051
|
};
|
|
5592
6052
|
const getVisibleText = (element) => {
|
|
@@ -5614,12 +6074,25 @@
|
|
|
5614
6074
|
"/following"
|
|
5615
6075
|
];
|
|
5616
6076
|
function getElementFromXPath(docmt, xpath) {
|
|
5617
|
-
|
|
5618
|
-
|
|
6077
|
+
try {
|
|
6078
|
+
const xpathResultType = docmt.defaultView?.XPathResult?.FIRST_ORDERED_NODE_TYPE ??
|
|
6079
|
+
(typeof XPathResult !== "undefined"
|
|
6080
|
+
? XPathResult.FIRST_ORDERED_NODE_TYPE
|
|
6081
|
+
: 9);
|
|
6082
|
+
const xpathEvaluator = docmt.defaultView?.XPathEvaluator
|
|
6083
|
+
? new docmt.defaultView.XPathEvaluator()
|
|
6084
|
+
: typeof XPathEvaluator !== "undefined"
|
|
6085
|
+
? new XPathEvaluator()
|
|
6086
|
+
: null;
|
|
6087
|
+
const xpathResult = xpathEvaluator
|
|
6088
|
+
? xpathEvaluator.evaluate(xpath, docmt, null, xpathResultType, null)
|
|
6089
|
+
: docmt.evaluate(xpath, docmt, null, xpathResultType, null);
|
|
6090
|
+
return xpathResult.singleNodeValue;
|
|
6091
|
+
}
|
|
6092
|
+
catch (error) {
|
|
6093
|
+
console.error("Invalid XPath:", xpath, error);
|
|
5619
6094
|
return null;
|
|
5620
|
-
|
|
5621
|
-
const xpathResult = xpathEvaluator.evaluate(xpath, docmt, null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
5622
|
-
return xpathResult.singleNodeValue;
|
|
6095
|
+
}
|
|
5623
6096
|
}
|
|
5624
6097
|
function checkReferenceElementIsValid(locator, relation, docmt) {
|
|
5625
6098
|
if (locator.includes(relation)) {
|
|
@@ -5653,7 +6126,7 @@
|
|
|
5653
6126
|
}
|
|
5654
6127
|
return null;
|
|
5655
6128
|
}
|
|
5656
|
-
const getElementsFromHTML = (record, docmt) => {
|
|
6129
|
+
const getElementsFromHTML = (record, docmt, platform) => {
|
|
5657
6130
|
clearXPathEvalCache(docmt);
|
|
5658
6131
|
const elementsToRemove = docmt.querySelectorAll("script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg");
|
|
5659
6132
|
if (elementsToRemove) {
|
|
@@ -5682,45 +6155,330 @@
|
|
|
5682
6155
|
: resolveIsSelfHealed(newLocator.name, oldValue, newValue);
|
|
5683
6156
|
pushUniqueLocator(newLocator);
|
|
5684
6157
|
}
|
|
5685
|
-
|
|
6158
|
+
const walkAttr = (ctx, attr, value) => {
|
|
6159
|
+
for (const el of Array.from(ctx.querySelectorAll("*"))) {
|
|
6160
|
+
if (el.getAttribute(attr) === value)
|
|
6161
|
+
return el;
|
|
6162
|
+
}
|
|
6163
|
+
return null;
|
|
6164
|
+
};
|
|
6165
|
+
const isAndroidPlatform = (platform) => String(platform).toLowerCase().includes("android");
|
|
6166
|
+
const isMobilePlatform = (platform) => isAndroidPlatform(platform) ||
|
|
6167
|
+
String(platform).toLowerCase().includes("ios");
|
|
6168
|
+
const isIOSPlatform = (platform) => String(platform).toLowerCase().includes("ios");
|
|
6169
|
+
const getLocatorStrategy = (name) => {
|
|
6170
|
+
const normalized = String(name || "")
|
|
6171
|
+
.trim()
|
|
6172
|
+
.toLowerCase()
|
|
6173
|
+
.replace(/^-/, "")
|
|
6174
|
+
.replace(/[\s_-]+/g, "");
|
|
6175
|
+
switch (normalized) {
|
|
6176
|
+
case "id":
|
|
6177
|
+
return "id";
|
|
6178
|
+
case "name":
|
|
6179
|
+
return "name";
|
|
6180
|
+
case "xpath":
|
|
6181
|
+
return "xpath";
|
|
6182
|
+
case "classname":
|
|
6183
|
+
return "className";
|
|
6184
|
+
case "accessibilityid":
|
|
6185
|
+
return "accessibilityId";
|
|
6186
|
+
case "iospredicatestring":
|
|
6187
|
+
case "iosnspredicatestring":
|
|
6188
|
+
return "iosPredicateString";
|
|
6189
|
+
case "iosclasschain":
|
|
6190
|
+
return "iosClassChain";
|
|
6191
|
+
default:
|
|
6192
|
+
return String(name || "");
|
|
6193
|
+
}
|
|
6194
|
+
};
|
|
6195
|
+
const findLocatorByStrategy = (strategy) => record.locators.find((candidate) => getLocatorStrategy(candidate.name) === strategy);
|
|
6196
|
+
const getAttributeValue = (element, attribute) => {
|
|
6197
|
+
if (attribute.toLowerCase() === "type") {
|
|
6198
|
+
return element.getAttribute("type") || element.tagName || "";
|
|
6199
|
+
}
|
|
6200
|
+
return element.getAttribute(attribute) || "";
|
|
6201
|
+
};
|
|
6202
|
+
const splitIOSExpression = (expression) => {
|
|
6203
|
+
const clauses = [];
|
|
6204
|
+
const operators = [];
|
|
6205
|
+
let quote = "";
|
|
6206
|
+
let start = 0;
|
|
6207
|
+
for (let index = 0; index < expression.length; index++) {
|
|
6208
|
+
const char = expression[index];
|
|
6209
|
+
if ((char === '"' || char === "'") && expression[index - 1] !== "\\") {
|
|
6210
|
+
quote = quote === char ? "" : quote || char;
|
|
6211
|
+
continue;
|
|
6212
|
+
}
|
|
6213
|
+
if (quote)
|
|
6214
|
+
continue;
|
|
6215
|
+
const remaining = expression.slice(index);
|
|
6216
|
+
const operatorMatch = remaining.match(/^\s+(AND|OR)\s+/i);
|
|
6217
|
+
if (!operatorMatch)
|
|
6218
|
+
continue;
|
|
6219
|
+
clauses.push(expression.slice(start, index).trim());
|
|
6220
|
+
operators.push(operatorMatch[1].toUpperCase());
|
|
6221
|
+
index += operatorMatch[0].length - 1;
|
|
6222
|
+
start = index + 1;
|
|
6223
|
+
}
|
|
6224
|
+
clauses.push(expression.slice(start).trim());
|
|
6225
|
+
return { clauses, operators };
|
|
6226
|
+
};
|
|
6227
|
+
const matchesIOSPredicate = (element, predicate) => {
|
|
6228
|
+
const expression = predicate.trim().replace(/^\((.*)\)$/s, "$1");
|
|
6229
|
+
if (/^(TRUEPREDICATE|1\s*==\s*1)$/i.test(expression))
|
|
6230
|
+
return true;
|
|
6231
|
+
const { clauses, operators } = splitIOSExpression(expression);
|
|
6232
|
+
const results = clauses.map((clause) => {
|
|
6233
|
+
const match = clause.match(/^([\w-]+)\s*(==|=|!=|CONTAINS|BEGINSWITH|ENDSWITH|MATCHES)(\[[cd]+\])?\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s]+))$/i);
|
|
6234
|
+
if (!match)
|
|
6235
|
+
return false;
|
|
6236
|
+
const [, attribute, rawOperator, modifiers, doubleQuoted, singleQuoted, bare] = match;
|
|
6237
|
+
const expected = (doubleQuoted ?? singleQuoted ?? bare ?? "")
|
|
6238
|
+
.replace(/\\([\\"'])/g, "$1");
|
|
6239
|
+
let actual = getAttributeValue(element, attribute);
|
|
6240
|
+
let comparableExpected = expected;
|
|
6241
|
+
if (comparableExpected === "1" && actual.toLowerCase() === "true") {
|
|
6242
|
+
actual = "1";
|
|
6243
|
+
}
|
|
6244
|
+
else if (comparableExpected === "0" &&
|
|
6245
|
+
actual.toLowerCase() === "false") {
|
|
6246
|
+
actual = "0";
|
|
6247
|
+
}
|
|
6248
|
+
if (modifiers?.toLowerCase().includes("c")) {
|
|
6249
|
+
actual = actual.toLocaleLowerCase();
|
|
6250
|
+
comparableExpected = comparableExpected.toLocaleLowerCase();
|
|
6251
|
+
}
|
|
6252
|
+
switch (rawOperator.toUpperCase()) {
|
|
6253
|
+
case "=":
|
|
6254
|
+
case "==":
|
|
6255
|
+
return actual === comparableExpected;
|
|
6256
|
+
case "!=":
|
|
6257
|
+
return actual !== comparableExpected;
|
|
6258
|
+
case "CONTAINS":
|
|
6259
|
+
return actual.includes(comparableExpected);
|
|
6260
|
+
case "BEGINSWITH":
|
|
6261
|
+
return actual.startsWith(comparableExpected);
|
|
6262
|
+
case "ENDSWITH":
|
|
6263
|
+
return actual.endsWith(comparableExpected);
|
|
6264
|
+
case "MATCHES":
|
|
6265
|
+
try {
|
|
6266
|
+
return new RegExp(comparableExpected).test(actual);
|
|
6267
|
+
}
|
|
6268
|
+
catch {
|
|
6269
|
+
return false;
|
|
6270
|
+
}
|
|
6271
|
+
default:
|
|
6272
|
+
return false;
|
|
6273
|
+
}
|
|
6274
|
+
});
|
|
6275
|
+
let result = results[0] ?? false;
|
|
6276
|
+
operators.forEach((operator, index) => {
|
|
6277
|
+
result = operator === "AND"
|
|
6278
|
+
? result && results[index + 1]
|
|
6279
|
+
: result || results[index + 1];
|
|
6280
|
+
});
|
|
6281
|
+
return result;
|
|
6282
|
+
};
|
|
6283
|
+
const resolveIOSPredicate = (ctx, predicate) => Array.from(ctx.querySelectorAll("*")).find((element) => matchesIOSPredicate(element, predicate)) || null;
|
|
6284
|
+
const splitIOSClassChain = (classChain) => {
|
|
6285
|
+
const segments = [];
|
|
6286
|
+
let current = "";
|
|
6287
|
+
let quote = "";
|
|
6288
|
+
let fence = "";
|
|
6289
|
+
let bracketDepth = 0;
|
|
6290
|
+
for (let index = 0; index < classChain.length; index++) {
|
|
6291
|
+
const char = classChain[index];
|
|
6292
|
+
if ((char === '"' || char === "'") && classChain[index - 1] !== "\\") {
|
|
6293
|
+
quote = quote === char ? "" : quote || char;
|
|
6294
|
+
}
|
|
6295
|
+
else if (!quote && (char === "`" || char === "$")) {
|
|
6296
|
+
fence = fence === char ? "" : fence || char;
|
|
6297
|
+
}
|
|
6298
|
+
else if (!quote && !fence && char === "[") {
|
|
6299
|
+
bracketDepth++;
|
|
6300
|
+
}
|
|
6301
|
+
else if (!quote && !fence && char === "]") {
|
|
6302
|
+
bracketDepth--;
|
|
6303
|
+
}
|
|
6304
|
+
if (char === "/" && !quote && !fence && bracketDepth === 0) {
|
|
6305
|
+
if (current)
|
|
6306
|
+
segments.push(current);
|
|
6307
|
+
current = "";
|
|
6308
|
+
}
|
|
6309
|
+
else {
|
|
6310
|
+
current += char;
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
if (current)
|
|
6314
|
+
segments.push(current);
|
|
6315
|
+
return segments.filter(Boolean);
|
|
6316
|
+
};
|
|
6317
|
+
const resolveIOSClassChain = (ctx, classChain) => {
|
|
6318
|
+
const segments = splitIOSClassChain(classChain.trim());
|
|
6319
|
+
let current = [ctx];
|
|
6320
|
+
let useDescendants = false;
|
|
6321
|
+
for (const segment of segments) {
|
|
6322
|
+
if (segment === "**") {
|
|
6323
|
+
useDescendants = true;
|
|
6324
|
+
continue;
|
|
6325
|
+
}
|
|
6326
|
+
const type = segment.match(/^([^[]+)/)?.[1]?.trim() || "*";
|
|
6327
|
+
const predicates = Array.from(segment.matchAll(/\[(?:`([^`]*)`|\$([^$]*)\$)\]/g)).map((match) => match[1] ?? match[2]);
|
|
6328
|
+
const indexMatch = segment.match(/\[(-?\d+)\]\s*$/);
|
|
6329
|
+
const candidates = [];
|
|
6330
|
+
for (const parent of current) {
|
|
6331
|
+
const descendants = useDescendants
|
|
6332
|
+
? Array.from(parent.querySelectorAll("*"))
|
|
6333
|
+
: parent.nodeType === 9
|
|
6334
|
+
? [parent.documentElement]
|
|
6335
|
+
: Array.from(parent.children);
|
|
6336
|
+
candidates.push(...descendants.filter((element) => {
|
|
6337
|
+
const elementType = getAttributeValue(element, "type");
|
|
6338
|
+
return (type === "*" || elementType === type) &&
|
|
6339
|
+
predicates.every((predicate) => matchesIOSPredicate(element, predicate));
|
|
6340
|
+
}));
|
|
6341
|
+
}
|
|
6342
|
+
if (indexMatch) {
|
|
6343
|
+
const requestedIndex = Number(indexMatch[1]);
|
|
6344
|
+
const resolvedIndex = requestedIndex < 0
|
|
6345
|
+
? candidates.length + requestedIndex
|
|
6346
|
+
: requestedIndex - 1;
|
|
6347
|
+
current = candidates[resolvedIndex] ? [candidates[resolvedIndex]] : [];
|
|
6348
|
+
}
|
|
6349
|
+
else {
|
|
6350
|
+
current = candidates;
|
|
6351
|
+
}
|
|
6352
|
+
useDescendants = false;
|
|
6353
|
+
if (!current.length)
|
|
6354
|
+
return null;
|
|
6355
|
+
}
|
|
6356
|
+
return current[0] || null;
|
|
6357
|
+
};
|
|
6358
|
+
const isXPathLocator = (locator, selector) => (getLocatorStrategy(locator.name) === "xpath" ||
|
|
6359
|
+
locator.name?.toLowerCase().includes("xpath") ||
|
|
6360
|
+
selector.startsWith("//")) &&
|
|
6361
|
+
!String(locator.type || "").includes("dynamic");
|
|
6362
|
+
const getXPathCandidates = (results) => {
|
|
6363
|
+
return results.flatMap((group) => {
|
|
6364
|
+
if (!group)
|
|
6365
|
+
return [];
|
|
6366
|
+
if (Array.isArray(group.value)) {
|
|
6367
|
+
if (group.key !== "xpath" && group.viewkey !== "xpath") {
|
|
6368
|
+
return [];
|
|
6369
|
+
}
|
|
6370
|
+
return group.value;
|
|
6371
|
+
}
|
|
6372
|
+
return [group];
|
|
6373
|
+
}).filter((result) => typeof result?.value === "string" &&
|
|
6374
|
+
result.value.trim().startsWith("//"));
|
|
6375
|
+
};
|
|
6376
|
+
const resolveAndroidElement = (ctx, locator, selector) => {
|
|
6377
|
+
const strategy = getLocatorStrategy(locator.name);
|
|
6378
|
+
if (isXPathLocator(locator, selector)) {
|
|
6379
|
+
return getElementFromXPath(ctx, normalizeXPath(selector));
|
|
6380
|
+
}
|
|
6381
|
+
// ID
|
|
6382
|
+
if (strategy === "id" ||
|
|
6383
|
+
(strategy !== "accessibilityId" && locator.name.includes("id")) ||
|
|
6384
|
+
selector.startsWith("#")) {
|
|
6385
|
+
const clean = selector.startsWith("#")
|
|
6386
|
+
? selector.slice(1)
|
|
6387
|
+
: selector;
|
|
6388
|
+
return (walkAttr(ctx, "resource-id", clean) ||
|
|
6389
|
+
walkAttr(ctx, "content-desc", clean));
|
|
6390
|
+
}
|
|
6391
|
+
// accessibilityId
|
|
6392
|
+
if (strategy === "accessibilityId") {
|
|
6393
|
+
return walkAttr(ctx, "content-desc", selector);
|
|
6394
|
+
}
|
|
6395
|
+
// className
|
|
6396
|
+
if (strategy === "className" ||
|
|
6397
|
+
locator.name.includes("className") ||
|
|
6398
|
+
selector.startsWith(".")) {
|
|
6399
|
+
return (ctx.querySelector(`[class="${selector}"]`) ||
|
|
6400
|
+
null);
|
|
6401
|
+
}
|
|
6402
|
+
// name
|
|
6403
|
+
if (strategy === "name") {
|
|
6404
|
+
return (walkAttr(ctx, "text", selector) ||
|
|
6405
|
+
walkAttr(ctx, "content-desc", selector));
|
|
6406
|
+
}
|
|
6407
|
+
// tagName
|
|
6408
|
+
if (strategy === "tagName") {
|
|
6409
|
+
return (ctx.querySelector(`[class="${selector}"]`) ||
|
|
6410
|
+
null);
|
|
6411
|
+
}
|
|
6412
|
+
// linkText
|
|
6413
|
+
if (strategy === "linkText") {
|
|
6414
|
+
return walkAttr(ctx, "text", selector);
|
|
6415
|
+
}
|
|
6416
|
+
// partialLinkText
|
|
6417
|
+
if (strategy === "partialLinkText") {
|
|
6418
|
+
return (Array.from(ctx.querySelectorAll("*")).find((el) => el.getAttribute("text")?.includes(selector)) || null);
|
|
6419
|
+
}
|
|
6420
|
+
return null;
|
|
6421
|
+
};
|
|
6422
|
+
const resolveIOSElement = (ctx, locator, selector) => {
|
|
6423
|
+
const strategy = getLocatorStrategy(locator.name);
|
|
6424
|
+
if (isXPathLocator(locator, selector)) {
|
|
6425
|
+
return getElementFromXPath(ctx, normalizeXPath(selector));
|
|
6426
|
+
}
|
|
6427
|
+
if (strategy === "id") {
|
|
6428
|
+
return walkAttr(ctx, "name", selector);
|
|
6429
|
+
}
|
|
6430
|
+
if (strategy === "accessibilityId") {
|
|
6431
|
+
return walkAttr(ctx, "name", selector);
|
|
6432
|
+
}
|
|
6433
|
+
if (strategy === "name") {
|
|
6434
|
+
return walkAttr(ctx, "name", selector);
|
|
6435
|
+
}
|
|
6436
|
+
if (strategy === "className") {
|
|
6437
|
+
return walkAttr(ctx, "type", selector);
|
|
6438
|
+
}
|
|
6439
|
+
if (strategy === "iosPredicateString") {
|
|
6440
|
+
return resolveIOSPredicate(ctx, selector);
|
|
6441
|
+
}
|
|
6442
|
+
if (strategy === "iosClassChain") {
|
|
6443
|
+
return resolveIOSClassChain(ctx, selector);
|
|
6444
|
+
}
|
|
6445
|
+
return null;
|
|
6446
|
+
};
|
|
6447
|
+
const resolveWebElement = (ctx, locator, selector) => {
|
|
6448
|
+
const strategy = getLocatorStrategy(locator.name);
|
|
6449
|
+
if (isXPathLocator(locator, selector)) {
|
|
6450
|
+
return getElementFromXPath(ctx, normalizeXPath(selector));
|
|
6451
|
+
}
|
|
5686
6452
|
if (isCssSelectorLocator(locator)) {
|
|
5687
6453
|
return getElementFromCssSelector(ctx, selector);
|
|
5688
6454
|
}
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
6455
|
+
if (strategy === "id" ||
|
|
6456
|
+
locator.name.includes("id") ||
|
|
6457
|
+
selector.startsWith("#")) {
|
|
6458
|
+
const clean = selector.startsWith("#")
|
|
6459
|
+
? selector.slice(1)
|
|
6460
|
+
: selector;
|
|
6461
|
+
return ctx.querySelector("#" + escapeAttrValue(clean));
|
|
6462
|
+
}
|
|
6463
|
+
if (strategy === "className" ||
|
|
6464
|
+
locator.name.includes("className") ||
|
|
6465
|
+
selector.startsWith(".")) {
|
|
6466
|
+
return ctx.querySelector(selector);
|
|
5694
6467
|
}
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
return ctx.querySelector(`[name="${safeName}"]`);
|
|
6468
|
+
if (strategy === "name") {
|
|
6469
|
+
return ctx.querySelector(`[name="${escapeAttrValue(selector)}"]`);
|
|
5698
6470
|
}
|
|
5699
|
-
|
|
6471
|
+
if (strategy === "tagName") {
|
|
5700
6472
|
return ctx.querySelector(selector);
|
|
5701
6473
|
}
|
|
5702
|
-
|
|
6474
|
+
if (strategy === "linkText") {
|
|
5703
6475
|
return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.trim() === selector) || null);
|
|
5704
6476
|
}
|
|
5705
|
-
|
|
6477
|
+
if (strategy === "partialLinkText") {
|
|
5706
6478
|
return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.includes(selector)) || null);
|
|
5707
6479
|
}
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
const normalizedXPath = normalizeXPath(selector);
|
|
5711
|
-
const el = getElementFromXPath(ctx, normalizedXPath);
|
|
5712
|
-
if (el) {
|
|
5713
|
-
createLocator(locator, {
|
|
5714
|
-
value: selector,
|
|
5715
|
-
isRecorded: String(locator.isRecorded).includes("N") ? "N" : "Y"
|
|
5716
|
-
});
|
|
5717
|
-
}
|
|
5718
|
-
return el;
|
|
5719
|
-
}
|
|
5720
|
-
else {
|
|
5721
|
-
return ctx.querySelector(selector);
|
|
5722
|
-
}
|
|
5723
|
-
}
|
|
6480
|
+
return null;
|
|
6481
|
+
};
|
|
5724
6482
|
function findInIframes(docmt, locator, selector) {
|
|
5725
6483
|
const iframes = docmt.querySelectorAll("iframe");
|
|
5726
6484
|
for (const iframe of iframes) {
|
|
@@ -5728,7 +6486,11 @@
|
|
|
5728
6486
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
5729
6487
|
if (!iframeDoc)
|
|
5730
6488
|
continue;
|
|
5731
|
-
const el =
|
|
6489
|
+
const el = isAndroidPlatform(platform)
|
|
6490
|
+
? resolveAndroidElement(iframeDoc, locator, selector)
|
|
6491
|
+
: isIOSPlatform(platform)
|
|
6492
|
+
? resolveIOSElement(iframeDoc, locator, selector)
|
|
6493
|
+
: resolveWebElement(iframeDoc, locator, selector);
|
|
5732
6494
|
if (el)
|
|
5733
6495
|
return el;
|
|
5734
6496
|
}
|
|
@@ -5746,9 +6508,12 @@
|
|
|
5746
6508
|
}
|
|
5747
6509
|
}
|
|
5748
6510
|
/** Locator Value Cleaner (Handles Special Scenarios) **/
|
|
5749
|
-
const cleanLocatorValue = (val, type, isRecorded) => {
|
|
5750
|
-
if (
|
|
6511
|
+
const cleanLocatorValue = (val, type, isRecorded, platform) => {
|
|
6512
|
+
if (val == null)
|
|
5751
6513
|
return null;
|
|
6514
|
+
if (typeof val !== "string") {
|
|
6515
|
+
val = String(val);
|
|
6516
|
+
}
|
|
5752
6517
|
let cleaned = val.trim();
|
|
5753
6518
|
// Return null for empty or literal "null"
|
|
5754
6519
|
if (!cleaned || cleaned.toLowerCase() === "null")
|
|
@@ -5765,8 +6530,15 @@
|
|
|
5765
6530
|
if (type === "id" || type === "name") {
|
|
5766
6531
|
cleaned = cleaned.replace(/['"]/g, "").trim();
|
|
5767
6532
|
}
|
|
5768
|
-
|
|
5769
|
-
|
|
6533
|
+
const isAndroid = String(platform).toLowerCase().includes("android");
|
|
6534
|
+
const isAndroidResourceId = /^[a-zA-Z0-9._]+:id\/[a-zA-Z0-9_]+$/.test(cleaned);
|
|
6535
|
+
if (type === "xpath" &&
|
|
6536
|
+
isRecorded === "Y" &&
|
|
6537
|
+
!cleaned.startsWith("//")) {
|
|
6538
|
+
if (!(isAndroid && isAndroidResourceId)) {
|
|
6539
|
+
return null;
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
5770
6542
|
// Final check for empty strings
|
|
5771
6543
|
if (!cleaned || /^['"]{2}$/.test(cleaned))
|
|
5772
6544
|
return null;
|
|
@@ -5802,13 +6574,18 @@
|
|
|
5802
6574
|
}
|
|
5803
6575
|
const trimmedSelector = selector.trim();
|
|
5804
6576
|
//normal DOM
|
|
5805
|
-
|
|
6577
|
+
// normal DOM
|
|
6578
|
+
targetElement = isAndroidPlatform(platform)
|
|
6579
|
+
? resolveAndroidElement(docmt, locator, trimmedSelector)
|
|
6580
|
+
: isIOSPlatform(platform)
|
|
6581
|
+
? resolveIOSElement(docmt, locator, trimmedSelector)
|
|
6582
|
+
: resolveWebElement(docmt, locator, trimmedSelector);
|
|
5806
6583
|
//iframe (if not found)
|
|
5807
6584
|
if (!targetElement) {
|
|
5808
6585
|
targetElement = findInIframes(docmt, locator, trimmedSelector);
|
|
5809
6586
|
}
|
|
5810
6587
|
//shadow DOM (if still not found)
|
|
5811
|
-
if (!targetElement) {
|
|
6588
|
+
if (!targetElement && !isMobilePlatform(platform) && docmt.body) {
|
|
5812
6589
|
targetElement = getElementFromShadowRoot(docmt.body, trimmedSelector);
|
|
5813
6590
|
}
|
|
5814
6591
|
if (!targetElement) {
|
|
@@ -5821,7 +6598,7 @@
|
|
|
5821
6598
|
return finalLocatorsSet.has(key);
|
|
5822
6599
|
};
|
|
5823
6600
|
if (targetElement) {
|
|
5824
|
-
const payloadXPaths = record.locators.filter((l) => l.name === "xpath" && l.value);
|
|
6601
|
+
const payloadXPaths = record.locators.filter((l) => getLocatorStrategy(l.name) === "xpath" && l.value);
|
|
5825
6602
|
const payloadCssSelectors = record.locators.filter((l) => isCssSelectorLocator(l) && l.value);
|
|
5826
6603
|
for (const px of payloadXPaths) {
|
|
5827
6604
|
if (isSameXPathStillValid(px.value, docmt, targetElement))
|
|
@@ -5835,17 +6612,19 @@
|
|
|
5835
6612
|
isSelfHealed: null
|
|
5836
6613
|
});
|
|
5837
6614
|
}
|
|
5838
|
-
const existingXPaths = finalLocators.filter((l) => l.name === "xpath" && l.value);
|
|
5839
|
-
|
|
5840
|
-
const usedXPathPatterns = new Set(existingXPaths.map((x) => getXPathPattern(x.value)));
|
|
6615
|
+
const existingXPaths = finalLocators.filter((l) => getLocatorStrategy(l.name) === "xpath" && l.value);
|
|
6616
|
+
const usedXPathPatterns = new Set(existingXPaths.map((x) => getXPathPattern(x.value, platform)));
|
|
5841
6617
|
const excludedAttributes = [];
|
|
5842
|
-
const idValue = getId(targetElement);
|
|
5843
|
-
if (
|
|
6618
|
+
const idValue = getId(targetElement, platform);
|
|
6619
|
+
if (!isIOSPlatform(platform) &&
|
|
6620
|
+
idValue &&
|
|
5844
6621
|
!locatorExists("id", idValue) &&
|
|
5845
6622
|
!isNumberExist(idValue)) {
|
|
5846
|
-
const prevId =
|
|
5847
|
-
if (isUniqueInDOM(docmt, "id", idValue, targetElement)) {
|
|
5848
|
-
excludedAttributes.push(
|
|
6623
|
+
const prevId = findLocatorByStrategy("id");
|
|
6624
|
+
if (isUniqueInDOM(docmt, "id", idValue, targetElement, platform)) {
|
|
6625
|
+
excludedAttributes.push(isAndroidPlatform(platform)
|
|
6626
|
+
? "resource-id"
|
|
6627
|
+
: "id");
|
|
5849
6628
|
createLocator(prevId, {
|
|
5850
6629
|
name: "id",
|
|
5851
6630
|
type: "static",
|
|
@@ -5854,26 +6633,56 @@
|
|
|
5854
6633
|
});
|
|
5855
6634
|
}
|
|
5856
6635
|
}
|
|
5857
|
-
const
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
6636
|
+
const accessibilityId = isAndroidPlatform(platform)
|
|
6637
|
+
? targetElement.getAttribute("content-desc")
|
|
6638
|
+
: isIOSPlatform(platform)
|
|
6639
|
+
? targetElement.getAttribute("name")
|
|
6640
|
+
: targetElement.getAttribute("aria-label");
|
|
6641
|
+
const prevAccessibility = findLocatorByStrategy("accessibilityId");
|
|
6642
|
+
if (accessibilityId &&
|
|
6643
|
+
(!isIOSPlatform(platform) || prevAccessibility) &&
|
|
6644
|
+
!locatorExists("accessibilityId", accessibilityId) &&
|
|
6645
|
+
!isNumberExist(accessibilityId)) {
|
|
6646
|
+
if (isUniqueInDOM(docmt, "accessibilityId", accessibilityId, targetElement, platform)) {
|
|
6647
|
+
excludedAttributes.push(isAndroidPlatform(platform)
|
|
6648
|
+
? "content-desc"
|
|
6649
|
+
: isIOSPlatform(platform)
|
|
6650
|
+
? "name"
|
|
6651
|
+
: "aria-label");
|
|
6652
|
+
createLocator(prevAccessibility, {
|
|
6653
|
+
name: prevAccessibility?.name || "accessibilityId",
|
|
5864
6654
|
type: "static",
|
|
5865
6655
|
isRecorded: "Y",
|
|
5866
|
-
value:
|
|
6656
|
+
value: accessibilityId
|
|
5867
6657
|
});
|
|
5868
6658
|
}
|
|
5869
6659
|
}
|
|
6660
|
+
if (!isMobilePlatform(platform)) {
|
|
6661
|
+
const tagName = targetElement.tagName;
|
|
6662
|
+
if (tagName && !locatorExists("tagName", tagName)) {
|
|
6663
|
+
const prevTag = findLocatorByStrategy("tagName");
|
|
6664
|
+
if (isUniqueInDOM(docmt, "tagName", tagName, targetElement, platform)) {
|
|
6665
|
+
excludedAttributes.push("tagName");
|
|
6666
|
+
createLocator(prevTag, {
|
|
6667
|
+
name: "tagName",
|
|
6668
|
+
type: "static",
|
|
6669
|
+
isRecorded: "Y",
|
|
6670
|
+
value: tagName
|
|
6671
|
+
});
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
5870
6675
|
const textValue = getVisibleText(targetElement);
|
|
5871
6676
|
if (textValue && !isNumberExist(textValue)) {
|
|
5872
|
-
const prevLinkText =
|
|
5873
|
-
if (isUniqueInDOM(docmt, "linkText", textValue, targetElement)) {
|
|
5874
|
-
excludedAttributes.push(
|
|
6677
|
+
const prevLinkText = findLocatorByStrategy("linkText");
|
|
6678
|
+
if (isUniqueInDOM(docmt, "linkText", textValue, targetElement, platform)) {
|
|
6679
|
+
excludedAttributes.push(isAndroidPlatform(platform)
|
|
6680
|
+
? "text"
|
|
6681
|
+
: "linkText");
|
|
5875
6682
|
createLocator(prevLinkText, {
|
|
5876
|
-
name:
|
|
6683
|
+
name: isAndroidPlatform(platform)
|
|
6684
|
+
? "name"
|
|
6685
|
+
: "linkText",
|
|
5877
6686
|
type: "static",
|
|
5878
6687
|
isRecorded: "Y",
|
|
5879
6688
|
value: textValue
|
|
@@ -5881,11 +6690,12 @@
|
|
|
5881
6690
|
}
|
|
5882
6691
|
}
|
|
5883
6692
|
const nameLocator = getName(targetElement);
|
|
5884
|
-
if (
|
|
6693
|
+
if (!isIOSPlatform(platform) &&
|
|
6694
|
+
nameLocator &&
|
|
5885
6695
|
!locatorExists("name", nameLocator) &&
|
|
5886
6696
|
!isNumberExist(nameLocator)) {
|
|
5887
|
-
const prevName =
|
|
5888
|
-
if (isUniqueInDOM(docmt, "name", nameLocator, targetElement)) {
|
|
6697
|
+
const prevName = findLocatorByStrategy("name");
|
|
6698
|
+
if (isUniqueInDOM(docmt, "name", nameLocator, targetElement, platform)) {
|
|
5889
6699
|
excludedAttributes.push("name");
|
|
5890
6700
|
createLocator(prevName, {
|
|
5891
6701
|
name: "name",
|
|
@@ -5895,42 +6705,68 @@
|
|
|
5895
6705
|
});
|
|
5896
6706
|
}
|
|
5897
6707
|
}
|
|
5898
|
-
const classValue = getClassName(targetElement);
|
|
6708
|
+
const classValue = getClassName(targetElement, platform);
|
|
6709
|
+
const prevClassLocator = findLocatorByStrategy("className");
|
|
5899
6710
|
if (classValue &&
|
|
6711
|
+
(!isIOSPlatform(platform) || prevClassLocator) &&
|
|
5900
6712
|
classValue.trim() !== "" &&
|
|
5901
6713
|
!classValue.includes(" ") &&
|
|
5902
6714
|
!locatorExists("className", classValue) &&
|
|
5903
6715
|
!isNumberExist(classValue)) {
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
excludedAttributes.push("className");
|
|
6716
|
+
if (isUniqueInDOM(docmt, "className", classValue, targetElement, platform)) {
|
|
6717
|
+
excludedAttributes.push(isIOSPlatform(platform) ? "type" : "className");
|
|
5907
6718
|
createLocator(prevClassLocator, {
|
|
5908
|
-
name: "className",
|
|
6719
|
+
name: prevClassLocator?.name || (isIOSPlatform(platform) ? "class name" : "className"),
|
|
5909
6720
|
type: "static",
|
|
5910
6721
|
isRecorded: "Y",
|
|
5911
6722
|
value: classValue
|
|
5912
6723
|
});
|
|
5913
6724
|
}
|
|
5914
6725
|
}
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
6726
|
+
if (isIOSPlatform(platform)) {
|
|
6727
|
+
const selectorAttributes = Array.from(targetElement.attributes);
|
|
6728
|
+
const predicateString = iosSelectors.getOptimalPredicateString(docmt, targetElement, selectorAttributes);
|
|
6729
|
+
const prevPredicate = findLocatorByStrategy("iosPredicateString");
|
|
6730
|
+
if (prevPredicate && predicateString && !locatorExists(prevPredicate.name, predicateString)) {
|
|
6731
|
+
createLocator(prevPredicate, {
|
|
6732
|
+
name: prevPredicate.name,
|
|
5921
6733
|
type: "static",
|
|
5922
|
-
isRecorded: "Y"
|
|
6734
|
+
isRecorded: "Y",
|
|
6735
|
+
value: predicateString
|
|
5923
6736
|
});
|
|
5924
6737
|
}
|
|
5925
|
-
|
|
6738
|
+
const classChain = iosSelectors.getOptimalClassChain(docmt, targetElement, selectorAttributes);
|
|
6739
|
+
const prevClassChain = findLocatorByStrategy("iosClassChain");
|
|
6740
|
+
if (prevClassChain && classChain && !locatorExists(prevClassChain.name, classChain)) {
|
|
6741
|
+
createLocator(prevClassChain, {
|
|
6742
|
+
name: prevClassChain.name,
|
|
6743
|
+
type: "static",
|
|
6744
|
+
isRecorded: "Y",
|
|
6745
|
+
value: classChain
|
|
6746
|
+
});
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
if (!isMobilePlatform(platform)) {
|
|
6750
|
+
parseCssSelectors(targetElement, "single").forEach((cssSelector) => {
|
|
6751
|
+
if (cssSelector.value &&
|
|
6752
|
+
!locatorExists("cssSelector", cssSelector.value)) {
|
|
6753
|
+
createLocator(undefined, {
|
|
6754
|
+
name: "cssSelector",
|
|
6755
|
+
value: cssSelector.value,
|
|
6756
|
+
type: "static",
|
|
6757
|
+
isRecorded: "Y"
|
|
6758
|
+
});
|
|
6759
|
+
}
|
|
6760
|
+
});
|
|
6761
|
+
}
|
|
5926
6762
|
const allAttributes = Array.from(targetElement.attributes);
|
|
5927
|
-
const includedAttributes = allAttributes.filter((attr) => !excludedAttributes.includes(attr.name)
|
|
6763
|
+
const includedAttributes = allAttributes.filter((attr) => !excludedAttributes.includes(attr.name) ||
|
|
6764
|
+
(isIOSPlatform(platform) && attr.name === "name"));
|
|
5928
6765
|
//If any direct locator is broken then we consider it as broken xpath
|
|
5929
6766
|
let xpathResults = [];
|
|
5930
6767
|
try {
|
|
5931
6768
|
xpathResults =
|
|
5932
|
-
parseDOM(targetElement, docmt, false, true, includedAttributes) ??
|
|
5933
|
-
[];
|
|
6769
|
+
parseDOM(targetElement, docmt, false, true, includedAttributes, [], platform) ?? [];
|
|
5934
6770
|
}
|
|
5935
6771
|
catch (error) {
|
|
5936
6772
|
console.error("Error generating XPath candidates:", error);
|
|
@@ -5941,10 +6777,12 @@
|
|
|
5941
6777
|
for (const brokenPx of brokenPayloadXPaths) {
|
|
5942
6778
|
if (xpathAdded >= brokenPayloadXPaths.length)
|
|
5943
6779
|
break;
|
|
5944
|
-
const originalPattern = getXPathPattern(brokenPx.value);
|
|
6780
|
+
const originalPattern = getXPathPattern(brokenPx.value, platform);
|
|
5945
6781
|
if (usedXPathPatterns.has(originalPattern))
|
|
5946
6782
|
continue;
|
|
5947
|
-
const
|
|
6783
|
+
const flatXpaths = getXPathCandidates(xpathResults);
|
|
6784
|
+
const match = flatXpaths.find((r) => typeof r.value === "string" &&
|
|
6785
|
+
getXPathPattern(r.value, platform) === originalPattern);
|
|
5948
6786
|
if (match?.value) {
|
|
5949
6787
|
createLocator(brokenPx, {
|
|
5950
6788
|
name: "xpath",
|
|
@@ -5958,23 +6796,34 @@
|
|
|
5958
6796
|
}
|
|
5959
6797
|
}
|
|
5960
6798
|
if (xpathAdded < brokenPayloadXPaths.length) {
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
6799
|
+
console.log("xpathResults =", JSON.stringify(xpathResults, null, 2));
|
|
6800
|
+
for (const group of xpathResults) {
|
|
6801
|
+
const values = getXPathCandidates([group]);
|
|
6802
|
+
for (const result of values) {
|
|
6803
|
+
if (xpathAdded >= brokenPayloadXPaths.length)
|
|
6804
|
+
break;
|
|
6805
|
+
const xpathValue = typeof result.value === "string"
|
|
6806
|
+
? result.value
|
|
6807
|
+
: "";
|
|
6808
|
+
if (!xpathValue)
|
|
6809
|
+
continue;
|
|
6810
|
+
const pattern = getXPathPattern(xpathValue, platform);
|
|
6811
|
+
if (usedXPathPatterns.has(pattern))
|
|
6812
|
+
continue;
|
|
6813
|
+
const baseLocator = brokenPayloadXPaths[xpathAdded] ??
|
|
6814
|
+
payloadXPaths[xpathAdded];
|
|
6815
|
+
if (!baseLocator)
|
|
6816
|
+
continue;
|
|
6817
|
+
createLocator(baseLocator, {
|
|
6818
|
+
name: "xpath",
|
|
6819
|
+
value: xpathValue,
|
|
6820
|
+
type: "static",
|
|
6821
|
+
isRecorded: "Y",
|
|
6822
|
+
isSelfHealed: "Y"
|
|
6823
|
+
});
|
|
6824
|
+
usedXPathPatterns.add(pattern);
|
|
6825
|
+
xpathAdded++;
|
|
6826
|
+
}
|
|
5978
6827
|
}
|
|
5979
6828
|
}
|
|
5980
6829
|
}
|
|
@@ -6005,12 +6854,20 @@
|
|
|
6005
6854
|
console.error("Error processing locator:", locator, error);
|
|
6006
6855
|
}
|
|
6007
6856
|
}
|
|
6008
|
-
if (finalLocators.length < 5) {
|
|
6857
|
+
if (!isIOSPlatform(platform) && finalLocators.length < 5) {
|
|
6009
6858
|
const fallbackCandidates = [
|
|
6010
|
-
{ name: "id", value: getId(targetElement) },
|
|
6859
|
+
{ name: "id", value: getId(targetElement, platform) },
|
|
6011
6860
|
{ name: "name", value: getName(targetElement) },
|
|
6012
|
-
{ name: "className", value: getClassName(targetElement) },
|
|
6013
|
-
|
|
6861
|
+
{ name: "className", value: getClassName(targetElement, platform) },
|
|
6862
|
+
...(!isMobilePlatform(platform)
|
|
6863
|
+
? [{ name: "tagName", value: targetElement.tagName }]
|
|
6864
|
+
: []),
|
|
6865
|
+
{
|
|
6866
|
+
name: isAndroidPlatform(platform)
|
|
6867
|
+
? "name"
|
|
6868
|
+
: "linkText",
|
|
6869
|
+
value: getVisibleText(targetElement)
|
|
6870
|
+
}
|
|
6014
6871
|
];
|
|
6015
6872
|
for (const candidate of fallbackCandidates) {
|
|
6016
6873
|
if (finalLocators.length > 4)
|
|
@@ -6024,7 +6881,7 @@
|
|
|
6024
6881
|
continue;
|
|
6025
6882
|
if (name === "className" && value.includes(" "))
|
|
6026
6883
|
continue;
|
|
6027
|
-
if (isUniqueInDOM(docmt, name, value, targetElement)) {
|
|
6884
|
+
if (isUniqueInDOM(docmt, name, value, targetElement, platform)) {
|
|
6028
6885
|
createLocator(undefined, {
|
|
6029
6886
|
name,
|
|
6030
6887
|
type: "static",
|
|
@@ -6034,7 +6891,7 @@
|
|
|
6034
6891
|
});
|
|
6035
6892
|
}
|
|
6036
6893
|
}
|
|
6037
|
-
if (finalLocators.length < 5) {
|
|
6894
|
+
if (finalLocators.length < 5 && !isMobilePlatform(platform)) {
|
|
6038
6895
|
parseCssSelectors(targetElement, "multiple").forEach((cssSelector) => {
|
|
6039
6896
|
if (finalLocators.length > 4)
|
|
6040
6897
|
return;
|
|
@@ -6042,7 +6899,7 @@
|
|
|
6042
6899
|
return;
|
|
6043
6900
|
if (locatorExists("cssSelector", cssSelector.value))
|
|
6044
6901
|
return;
|
|
6045
|
-
if (!isUniqueInDOM(docmt, "cssSelector", cssSelector.value, targetElement)) {
|
|
6902
|
+
if (!isUniqueInDOM(docmt, "cssSelector", cssSelector.value, targetElement, platform)) {
|
|
6046
6903
|
return;
|
|
6047
6904
|
}
|
|
6048
6905
|
createLocator(undefined, {
|
|
@@ -6057,7 +6914,7 @@
|
|
|
6057
6914
|
}
|
|
6058
6915
|
const finalAutoHealedLocators = finalLocators.map((obj) => ({
|
|
6059
6916
|
...obj,
|
|
6060
|
-
value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded)
|
|
6917
|
+
value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded, platform)
|
|
6061
6918
|
}));
|
|
6062
6919
|
const jsonResult = [
|
|
6063
6920
|
{
|
|
@@ -6094,121 +6951,6 @@
|
|
|
6094
6951
|
return null;
|
|
6095
6952
|
};
|
|
6096
6953
|
|
|
6097
|
-
function getOptimalClassChain(doc, domNode, uniqueAttributes) {
|
|
6098
|
-
try {
|
|
6099
|
-
if (!domNode ||
|
|
6100
|
-
domNode.nodeType !== 1 ||
|
|
6101
|
-
!domNode.tagName ||
|
|
6102
|
-
domNode.tagName === "XCUIElementTypeApplication") {
|
|
6103
|
-
return "";
|
|
6104
|
-
}
|
|
6105
|
-
const tag = domNode.tagName;
|
|
6106
|
-
// Priority attributes
|
|
6107
|
-
const priorityAttrs = ["name", "label", "value"];
|
|
6108
|
-
// ---------- STEP 1 : TRY GOOD ATTRIBUTES ----------
|
|
6109
|
-
for (const attrName of priorityAttrs) {
|
|
6110
|
-
const attr = uniqueAttributes.find(a => a.name === attrName);
|
|
6111
|
-
if (!attr)
|
|
6112
|
-
continue;
|
|
6113
|
-
let attrValue = attr.value;
|
|
6114
|
-
if (!attrValue)
|
|
6115
|
-
continue;
|
|
6116
|
-
attrValue = attrValue.trim().replace(/\s+/g, " ");
|
|
6117
|
-
if (isNumberExist(attrValue))
|
|
6118
|
-
continue;
|
|
6119
|
-
const xpath = `//${tag}[@${attrName}="${attrValue}"]`;
|
|
6120
|
-
let count = 0;
|
|
6121
|
-
try {
|
|
6122
|
-
count = getCountOfXPath(xpath, domNode, doc);
|
|
6123
|
-
}
|
|
6124
|
-
catch (err) {
|
|
6125
|
-
console.log(err);
|
|
6126
|
-
continue;
|
|
6127
|
-
}
|
|
6128
|
-
const hasSpace = /\s/.test(attrValue);
|
|
6129
|
-
if (count === 1) {
|
|
6130
|
-
if (hasSpace) {
|
|
6131
|
-
return `/${tag}[\`${attrName} CONTAINS "${attrValue}"\`]`;
|
|
6132
|
-
}
|
|
6133
|
-
return `/${tag}[\`${attrName} == "${attrValue}"\`]`;
|
|
6134
|
-
}
|
|
6135
|
-
if (count > 1 && domNode.parentElement) {
|
|
6136
|
-
const siblings = Array.from(domNode.parentElement.children)
|
|
6137
|
-
.filter(el => el.tagName === tag);
|
|
6138
|
-
const index = siblings.indexOf(domNode) + 1;
|
|
6139
|
-
if (hasSpace) {
|
|
6140
|
-
return `/${tag}[\`${attrName} CONTAINS "${attrValue}"\`][${index}]`;
|
|
6141
|
-
}
|
|
6142
|
-
return `/${tag}[\`${attrName} == "${attrValue}"\`][${index}]`;
|
|
6143
|
-
}
|
|
6144
|
-
}
|
|
6145
|
-
// ---------- STEP 2 : FALLBACK USING TAG INDEX ----------
|
|
6146
|
-
let classChain = `/${tag}`;
|
|
6147
|
-
if (domNode.parentElement) {
|
|
6148
|
-
const siblings = Array.from(domNode.parentElement.children)
|
|
6149
|
-
.filter(el => el.tagName === tag);
|
|
6150
|
-
if (siblings.length > 1) {
|
|
6151
|
-
const index = siblings.indexOf(domNode) + 1;
|
|
6152
|
-
classChain += `[${index}]`;
|
|
6153
|
-
}
|
|
6154
|
-
}
|
|
6155
|
-
const parentChain = getOptimalClassChain(doc, domNode.parentElement, uniqueAttributes);
|
|
6156
|
-
return parentChain + classChain;
|
|
6157
|
-
}
|
|
6158
|
-
catch (error) {
|
|
6159
|
-
console.log(`Unable to generate optimal -ios class chain : ${JSON.stringify(error)}`);
|
|
6160
|
-
return null;
|
|
6161
|
-
}
|
|
6162
|
-
}
|
|
6163
|
-
function getOptimalPredicateString(doc, domNode, uniqueAttributes) {
|
|
6164
|
-
try {
|
|
6165
|
-
// BASE CASE #1: If this isn't an element, we're above the root, or this is `XCUIElementTypeApplication`,
|
|
6166
|
-
// which is not an official XCUITest element, return empty string
|
|
6167
|
-
if (!domNode?.tagName ||
|
|
6168
|
-
domNode?.nodeType !== 1 ||
|
|
6169
|
-
domNode?.tagName === 'XCUIElementTypeApplication') {
|
|
6170
|
-
return '';
|
|
6171
|
-
}
|
|
6172
|
-
// BASE CASE #2: Check all attributes and try to find the best way
|
|
6173
|
-
let xpathAttributes = [];
|
|
6174
|
-
let predicateString = [];
|
|
6175
|
-
for (let attr of uniqueAttributes) {
|
|
6176
|
-
const attrValue = attr.value;
|
|
6177
|
-
const attrName = attr.name;
|
|
6178
|
-
if (attrValue.length === 0) {
|
|
6179
|
-
continue;
|
|
6180
|
-
}
|
|
6181
|
-
if (attrValue && !isNumberExist(attrValue)) {
|
|
6182
|
-
xpathAttributes.push(`@${attrName}="${attrValue}"`);
|
|
6183
|
-
const xpathe = `//*[${xpathAttributes.join(' and ')}]`;
|
|
6184
|
-
predicateString.push(`${attrName} == "${attrValue}"`);
|
|
6185
|
-
let othersWithAttr;
|
|
6186
|
-
// If the XPath does not parse, move to the next unique attribute
|
|
6187
|
-
try {
|
|
6188
|
-
othersWithAttr = getCountOfXPath(xpathe, domNode, doc);
|
|
6189
|
-
}
|
|
6190
|
-
catch (ign) {
|
|
6191
|
-
console.log(ign);
|
|
6192
|
-
continue;
|
|
6193
|
-
}
|
|
6194
|
-
// If the attribute isn't actually unique, get it's index too
|
|
6195
|
-
if (othersWithAttr === 1) {
|
|
6196
|
-
return predicateString.join(' AND ');
|
|
6197
|
-
}
|
|
6198
|
-
}
|
|
6199
|
-
}
|
|
6200
|
-
}
|
|
6201
|
-
catch (error) {
|
|
6202
|
-
// If there's an unexpected exception, abort and don't get an XPath
|
|
6203
|
-
console.log(`The most optimal '-ios predicate string' could not be determined because an error was thrown: '${JSON.stringify(error, null, 2)}'`);
|
|
6204
|
-
}
|
|
6205
|
-
return null;
|
|
6206
|
-
}
|
|
6207
|
-
const iosSelectors = {
|
|
6208
|
-
getOptimalClassChain,
|
|
6209
|
-
getOptimalPredicateString
|
|
6210
|
-
};
|
|
6211
|
-
|
|
6212
6954
|
const createXPathAPI = () => ({
|
|
6213
6955
|
xpath,
|
|
6214
6956
|
referenceXpaths: referenceXpath,
|