ff-dom 3.0.3-beta.2 → 3.0.3-beta.4
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 +1091 -280
- package/dist/types/browser/browser/xpath.d.ts +11 -3
- package/dist/types/browser/utils/cssSelector.d.ts +1 -0
- 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/cssSelector.d.ts +1 -0
- 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
|
@@ -1875,16 +1875,21 @@
|
|
|
1875
1875
|
if (!xpaths1?.length) {
|
|
1876
1876
|
const xpaths = addAttributeSplitCombineXpaths(domNode.attributes, domNode, docmt, isTarget);
|
|
1877
1877
|
if (xpaths?.length) {
|
|
1878
|
-
xpaths1.
|
|
1878
|
+
xpaths1.push(...xpaths);
|
|
1879
1879
|
}
|
|
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;
|
|
@@ -3781,7 +4198,9 @@
|
|
|
3781
4198
|
// Share target candidate work across all relation attempts in this run.
|
|
3782
4199
|
const targetCandidateContext = createTargetCandidateContext(element2, docmt, xpaths2);
|
|
3783
4200
|
let finalXpaths = [];
|
|
3784
|
-
const relations = allowedRelations
|
|
4201
|
+
const relations = allowedRelations?.length ?
|
|
4202
|
+
allowedRelations
|
|
4203
|
+
: getSmartRelations(element1, element2);
|
|
3785
4204
|
if (!isIndex) {
|
|
3786
4205
|
// Fresh non-index runs should start from a clean candidate list for this
|
|
3787
4206
|
// reference/target pair; only candidates found in this pass are reusable.
|
|
@@ -4943,7 +5362,18 @@
|
|
|
4943
5362
|
};
|
|
4944
5363
|
const getReferenceElementXpath = (element) => {
|
|
4945
5364
|
let xpaths1 = [];
|
|
4946
|
-
|
|
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
|
+
: [];
|
|
4947
5377
|
const referenceElementXpaths = getReferenceElementsXpath(element, element.ownerDocument, false);
|
|
4948
5378
|
if (referenceElementXpaths?.length) {
|
|
4949
5379
|
xpaths1 = xpaths1?.length
|
|
@@ -4951,41 +5381,53 @@
|
|
|
4951
5381
|
: referenceElementXpaths;
|
|
4952
5382
|
}
|
|
4953
5383
|
if (!xpaths1?.length) {
|
|
4954
|
-
xpaths1 = parseDOM(element, element.ownerDocument, true, false);
|
|
5384
|
+
xpaths1 = normalizeXpaths(parseDOM(element, element.ownerDocument, true, false));
|
|
4955
5385
|
xpaths1 = xpaths1?.map((x) => x.value.charAt(0) == "(" &&
|
|
4956
5386
|
findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
|
|
4957
5387
|
? { key: "", value: removeParenthesis(x.value) }
|
|
4958
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);
|
|
4959
5397
|
}
|
|
4960
5398
|
else {
|
|
4961
|
-
let xpaths = parseDOM(element, element.ownerDocument, true, false);
|
|
4962
|
-
if (xpaths
|
|
4963
|
-
xpaths = xpaths
|
|
4964
|
-
findMatchingParenthesis(x.value, 0) + 1 ===
|
|
4965
|
-
|
|
4966
|
-
|
|
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);
|
|
4967
5409
|
xpaths1 = xpaths1.concat(xpaths);
|
|
4968
5410
|
}
|
|
4969
5411
|
}
|
|
4970
|
-
if (!xpaths1
|
|
4971
|
-
xpaths1 = [
|
|
4972
|
-
{
|
|
5412
|
+
if (!xpaths1.length) {
|
|
5413
|
+
xpaths1 = [{
|
|
4973
5414
|
key: "",
|
|
4974
5415
|
value: getRelativeXPath(element, element.ownerDocument, false, false, Array.from(element.attributes))
|
|
4975
|
-
}
|
|
4976
|
-
];
|
|
5416
|
+
}];
|
|
4977
5417
|
}
|
|
4978
|
-
if (!xpaths1
|
|
4979
|
-
xpaths1 = [
|
|
4980
|
-
{
|
|
5418
|
+
if (!xpaths1.length) {
|
|
5419
|
+
xpaths1 = [{
|
|
4981
5420
|
key: "",
|
|
4982
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)
|
|
4983
5429
|
}
|
|
4984
|
-
|
|
4985
|
-
xpaths1 = xpaths1?.map((x) => x.value.charAt(0) == "(" &&
|
|
4986
|
-
findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
|
|
4987
|
-
? { key: "", value: removeParenthesis(x.value) }
|
|
4988
|
-
: { key: "", value: x.value });
|
|
5430
|
+
: x);
|
|
4989
5431
|
}
|
|
4990
5432
|
const childAnchorXpaths = Array.from(element.children || [])
|
|
4991
5433
|
.map((child) => {
|
|
@@ -5242,6 +5684,19 @@
|
|
|
5242
5684
|
const trimmed = value.trim();
|
|
5243
5685
|
return Boolean(trimmed) && trimmed.length <= 120 && !/^\d+$/.test(trimmed);
|
|
5244
5686
|
};
|
|
5687
|
+
const isStableSemanticId = (el, idAttribute) => {
|
|
5688
|
+
const value = idAttribute.value.trim();
|
|
5689
|
+
if (!isStableAttributeValue(value))
|
|
5690
|
+
return false;
|
|
5691
|
+
const wasModified = modifiedElementAttributes.some((modifiedAttribute) => modifiedAttribute.doc === el.ownerDocument &&
|
|
5692
|
+
modifiedAttribute.element === el &&
|
|
5693
|
+
modifiedAttribute.attributeName === "id");
|
|
5694
|
+
if (wasModified)
|
|
5695
|
+
return false;
|
|
5696
|
+
// Keep semantic levels/versions such as "level2-shadow-host", but reject
|
|
5697
|
+
// IDs containing long numeric runs, which are more likely generated.
|
|
5698
|
+
return !/\d{4,}/.test(value);
|
|
5699
|
+
};
|
|
5245
5700
|
const isCssSelectorUnique = (selector, el, root) => {
|
|
5246
5701
|
try {
|
|
5247
5702
|
const matches = root.querySelectorAll(selector);
|
|
@@ -5299,8 +5754,8 @@
|
|
|
5299
5754
|
return;
|
|
5300
5755
|
const idAttribute = el.getAttributeNode("id");
|
|
5301
5756
|
if (!idAttribute ||
|
|
5302
|
-
!
|
|
5303
|
-
|
|
5757
|
+
(!checkBlockedAttributes(idAttribute, el, true) &&
|
|
5758
|
+
!isStableSemanticId(el, idAttribute))) {
|
|
5304
5759
|
return;
|
|
5305
5760
|
}
|
|
5306
5761
|
return `#${escapeCssIdentifier(el.id, el)}`;
|
|
@@ -5420,6 +5875,59 @@
|
|
|
5420
5875
|
}
|
|
5421
5876
|
return;
|
|
5422
5877
|
};
|
|
5878
|
+
const getNthOfType = (el) => {
|
|
5879
|
+
let index = 1;
|
|
5880
|
+
let sibling = el.previousElementSibling;
|
|
5881
|
+
while (sibling) {
|
|
5882
|
+
if (sibling.tagName === el.tagName)
|
|
5883
|
+
index++;
|
|
5884
|
+
sibling = sibling.previousElementSibling;
|
|
5885
|
+
}
|
|
5886
|
+
return index;
|
|
5887
|
+
};
|
|
5888
|
+
const getRelativeCssSegment = (el) => {
|
|
5889
|
+
const parent = el.parentElement;
|
|
5890
|
+
const tag = el.tagName.toLowerCase();
|
|
5891
|
+
if (parent) {
|
|
5892
|
+
const stableSelectors = getDirectStableSelectors(el, {
|
|
5893
|
+
includeTag: true,
|
|
5894
|
+
preferredAttributesOnly: true
|
|
5895
|
+
})
|
|
5896
|
+
.filter((selector) => isCssSelectorUnique(selector, el, parent))
|
|
5897
|
+
.sort((a, b) => a.length - b.length);
|
|
5898
|
+
if (stableSelectors.length)
|
|
5899
|
+
return stableSelectors[0];
|
|
5900
|
+
const sameTagSiblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
5901
|
+
if (sameTagSiblings.length > 1) {
|
|
5902
|
+
return `${tag}:nth-of-type(${getNthOfType(el)})`;
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
return tag;
|
|
5906
|
+
};
|
|
5907
|
+
const getShortestPositionalCssPath = (el) => {
|
|
5908
|
+
if (!isElementNode(el) || isIgnoredTag(el))
|
|
5909
|
+
return;
|
|
5910
|
+
const root = el.getRootNode();
|
|
5911
|
+
const descendantSegments = [];
|
|
5912
|
+
let current = el;
|
|
5913
|
+
while (current && isElementNode(current) && !isIgnoredTag(current)) {
|
|
5914
|
+
const anchorSelectors = getDirectStableSelectors(current, {
|
|
5915
|
+
includeTag: true,
|
|
5916
|
+
preferredAttributesOnly: true
|
|
5917
|
+
});
|
|
5918
|
+
for (const anchorSelector of anchorSelectors) {
|
|
5919
|
+
if (!isCssSelectorUnique(anchorSelector, current, root))
|
|
5920
|
+
continue;
|
|
5921
|
+
const candidate = [anchorSelector, ...descendantSegments].join(" > ");
|
|
5922
|
+
if (isCssSelectorUnique(candidate, el, root)) {
|
|
5923
|
+
return candidate;
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
descendantSegments.unshift(getRelativeCssSegment(current));
|
|
5927
|
+
current = current.parentElement;
|
|
5928
|
+
}
|
|
5929
|
+
return getAbsoluteCssPath(el);
|
|
5930
|
+
};
|
|
5423
5931
|
const getAbsoluteCssPath = (el) => {
|
|
5424
5932
|
const view = el.ownerDocument?.defaultView;
|
|
5425
5933
|
if (!view || !(el instanceof view.Element))
|
|
@@ -5447,6 +5955,7 @@
|
|
|
5447
5955
|
getIdCssPath,
|
|
5448
5956
|
getAttributeCssPath,
|
|
5449
5957
|
getClassCssPath,
|
|
5958
|
+
getShortestPositionalCssPath,
|
|
5450
5959
|
getAbsoluteCssPath
|
|
5451
5960
|
};
|
|
5452
5961
|
|
|
@@ -5514,10 +6023,30 @@
|
|
|
5514
6023
|
}
|
|
5515
6024
|
return null;
|
|
5516
6025
|
};
|
|
5517
|
-
const getId = (element) => {
|
|
5518
|
-
|
|
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;
|
|
5519
6039
|
};
|
|
5520
|
-
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
|
+
}
|
|
5521
6050
|
return element.className || null;
|
|
5522
6051
|
};
|
|
5523
6052
|
const getVisibleText = (element) => {
|
|
@@ -5545,12 +6074,25 @@
|
|
|
5545
6074
|
"/following"
|
|
5546
6075
|
];
|
|
5547
6076
|
function getElementFromXPath(docmt, xpath) {
|
|
5548
|
-
|
|
5549
|
-
|
|
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);
|
|
5550
6094
|
return null;
|
|
5551
|
-
|
|
5552
|
-
const xpathResult = xpathEvaluator.evaluate(xpath, docmt, null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
5553
|
-
return xpathResult.singleNodeValue;
|
|
6095
|
+
}
|
|
5554
6096
|
}
|
|
5555
6097
|
function checkReferenceElementIsValid(locator, relation, docmt) {
|
|
5556
6098
|
if (locator.includes(relation)) {
|
|
@@ -5584,7 +6126,7 @@
|
|
|
5584
6126
|
}
|
|
5585
6127
|
return null;
|
|
5586
6128
|
}
|
|
5587
|
-
const getElementsFromHTML = (record, docmt) => {
|
|
6129
|
+
const getElementsFromHTML = (record, docmt, platform) => {
|
|
5588
6130
|
clearXPathEvalCache(docmt);
|
|
5589
6131
|
const elementsToRemove = docmt.querySelectorAll("script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg");
|
|
5590
6132
|
if (elementsToRemove) {
|
|
@@ -5613,45 +6155,330 @@
|
|
|
5613
6155
|
: resolveIsSelfHealed(newLocator.name, oldValue, newValue);
|
|
5614
6156
|
pushUniqueLocator(newLocator);
|
|
5615
6157
|
}
|
|
5616
|
-
|
|
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
|
+
}
|
|
5617
6452
|
if (isCssSelectorLocator(locator)) {
|
|
5618
6453
|
return getElementFromCssSelector(ctx, selector);
|
|
5619
6454
|
}
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
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);
|
|
5625
6467
|
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
return ctx.querySelector(`[name="${safeName}"]`);
|
|
6468
|
+
if (strategy === "name") {
|
|
6469
|
+
return ctx.querySelector(`[name="${escapeAttrValue(selector)}"]`);
|
|
5629
6470
|
}
|
|
5630
|
-
|
|
6471
|
+
if (strategy === "tagName") {
|
|
5631
6472
|
return ctx.querySelector(selector);
|
|
5632
6473
|
}
|
|
5633
|
-
|
|
6474
|
+
if (strategy === "linkText") {
|
|
5634
6475
|
return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.trim() === selector) || null);
|
|
5635
6476
|
}
|
|
5636
|
-
|
|
6477
|
+
if (strategy === "partialLinkText") {
|
|
5637
6478
|
return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.includes(selector)) || null);
|
|
5638
6479
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
const normalizedXPath = normalizeXPath(selector);
|
|
5642
|
-
const el = getElementFromXPath(ctx, normalizedXPath);
|
|
5643
|
-
if (el) {
|
|
5644
|
-
createLocator(locator, {
|
|
5645
|
-
value: selector,
|
|
5646
|
-
isRecorded: String(locator.isRecorded).includes("N") ? "N" : "Y"
|
|
5647
|
-
});
|
|
5648
|
-
}
|
|
5649
|
-
return el;
|
|
5650
|
-
}
|
|
5651
|
-
else {
|
|
5652
|
-
return ctx.querySelector(selector);
|
|
5653
|
-
}
|
|
5654
|
-
}
|
|
6480
|
+
return null;
|
|
6481
|
+
};
|
|
5655
6482
|
function findInIframes(docmt, locator, selector) {
|
|
5656
6483
|
const iframes = docmt.querySelectorAll("iframe");
|
|
5657
6484
|
for (const iframe of iframes) {
|
|
@@ -5659,7 +6486,11 @@
|
|
|
5659
6486
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
5660
6487
|
if (!iframeDoc)
|
|
5661
6488
|
continue;
|
|
5662
|
-
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);
|
|
5663
6494
|
if (el)
|
|
5664
6495
|
return el;
|
|
5665
6496
|
}
|
|
@@ -5677,9 +6508,12 @@
|
|
|
5677
6508
|
}
|
|
5678
6509
|
}
|
|
5679
6510
|
/** Locator Value Cleaner (Handles Special Scenarios) **/
|
|
5680
|
-
const cleanLocatorValue = (val, type, isRecorded) => {
|
|
5681
|
-
if (
|
|
6511
|
+
const cleanLocatorValue = (val, type, isRecorded, platform) => {
|
|
6512
|
+
if (val == null)
|
|
5682
6513
|
return null;
|
|
6514
|
+
if (typeof val !== "string") {
|
|
6515
|
+
val = String(val);
|
|
6516
|
+
}
|
|
5683
6517
|
let cleaned = val.trim();
|
|
5684
6518
|
// Return null for empty or literal "null"
|
|
5685
6519
|
if (!cleaned || cleaned.toLowerCase() === "null")
|
|
@@ -5696,8 +6530,15 @@
|
|
|
5696
6530
|
if (type === "id" || type === "name") {
|
|
5697
6531
|
cleaned = cleaned.replace(/['"]/g, "").trim();
|
|
5698
6532
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
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
|
+
}
|
|
5701
6542
|
// Final check for empty strings
|
|
5702
6543
|
if (!cleaned || /^['"]{2}$/.test(cleaned))
|
|
5703
6544
|
return null;
|
|
@@ -5733,13 +6574,18 @@
|
|
|
5733
6574
|
}
|
|
5734
6575
|
const trimmedSelector = selector.trim();
|
|
5735
6576
|
//normal DOM
|
|
5736
|
-
|
|
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);
|
|
5737
6583
|
//iframe (if not found)
|
|
5738
6584
|
if (!targetElement) {
|
|
5739
6585
|
targetElement = findInIframes(docmt, locator, trimmedSelector);
|
|
5740
6586
|
}
|
|
5741
6587
|
//shadow DOM (if still not found)
|
|
5742
|
-
if (!targetElement) {
|
|
6588
|
+
if (!targetElement && !isMobilePlatform(platform) && docmt.body) {
|
|
5743
6589
|
targetElement = getElementFromShadowRoot(docmt.body, trimmedSelector);
|
|
5744
6590
|
}
|
|
5745
6591
|
if (!targetElement) {
|
|
@@ -5752,7 +6598,7 @@
|
|
|
5752
6598
|
return finalLocatorsSet.has(key);
|
|
5753
6599
|
};
|
|
5754
6600
|
if (targetElement) {
|
|
5755
|
-
const payloadXPaths = record.locators.filter((l) => l.name === "xpath" && l.value);
|
|
6601
|
+
const payloadXPaths = record.locators.filter((l) => getLocatorStrategy(l.name) === "xpath" && l.value);
|
|
5756
6602
|
const payloadCssSelectors = record.locators.filter((l) => isCssSelectorLocator(l) && l.value);
|
|
5757
6603
|
for (const px of payloadXPaths) {
|
|
5758
6604
|
if (isSameXPathStillValid(px.value, docmt, targetElement))
|
|
@@ -5766,17 +6612,19 @@
|
|
|
5766
6612
|
isSelfHealed: null
|
|
5767
6613
|
});
|
|
5768
6614
|
}
|
|
5769
|
-
const existingXPaths = finalLocators.filter((l) => l.name === "xpath" && l.value);
|
|
5770
|
-
|
|
5771
|
-
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)));
|
|
5772
6617
|
const excludedAttributes = [];
|
|
5773
|
-
const idValue = getId(targetElement);
|
|
5774
|
-
if (
|
|
6618
|
+
const idValue = getId(targetElement, platform);
|
|
6619
|
+
if (!isIOSPlatform(platform) &&
|
|
6620
|
+
idValue &&
|
|
5775
6621
|
!locatorExists("id", idValue) &&
|
|
5776
6622
|
!isNumberExist(idValue)) {
|
|
5777
|
-
const prevId =
|
|
5778
|
-
if (isUniqueInDOM(docmt, "id", idValue, targetElement)) {
|
|
5779
|
-
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");
|
|
5780
6628
|
createLocator(prevId, {
|
|
5781
6629
|
name: "id",
|
|
5782
6630
|
type: "static",
|
|
@@ -5785,26 +6633,56 @@
|
|
|
5785
6633
|
});
|
|
5786
6634
|
}
|
|
5787
6635
|
}
|
|
5788
|
-
const
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
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",
|
|
5795
6654
|
type: "static",
|
|
5796
6655
|
isRecorded: "Y",
|
|
5797
|
-
value:
|
|
6656
|
+
value: accessibilityId
|
|
5798
6657
|
});
|
|
5799
6658
|
}
|
|
5800
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
|
+
}
|
|
5801
6675
|
const textValue = getVisibleText(targetElement);
|
|
5802
6676
|
if (textValue && !isNumberExist(textValue)) {
|
|
5803
|
-
const prevLinkText =
|
|
5804
|
-
if (isUniqueInDOM(docmt, "linkText", textValue, targetElement)) {
|
|
5805
|
-
excludedAttributes.push(
|
|
6677
|
+
const prevLinkText = findLocatorByStrategy("linkText");
|
|
6678
|
+
if (isUniqueInDOM(docmt, "linkText", textValue, targetElement, platform)) {
|
|
6679
|
+
excludedAttributes.push(isAndroidPlatform(platform)
|
|
6680
|
+
? "text"
|
|
6681
|
+
: "linkText");
|
|
5806
6682
|
createLocator(prevLinkText, {
|
|
5807
|
-
name:
|
|
6683
|
+
name: isAndroidPlatform(platform)
|
|
6684
|
+
? "name"
|
|
6685
|
+
: "linkText",
|
|
5808
6686
|
type: "static",
|
|
5809
6687
|
isRecorded: "Y",
|
|
5810
6688
|
value: textValue
|
|
@@ -5812,11 +6690,12 @@
|
|
|
5812
6690
|
}
|
|
5813
6691
|
}
|
|
5814
6692
|
const nameLocator = getName(targetElement);
|
|
5815
|
-
if (
|
|
6693
|
+
if (!isIOSPlatform(platform) &&
|
|
6694
|
+
nameLocator &&
|
|
5816
6695
|
!locatorExists("name", nameLocator) &&
|
|
5817
6696
|
!isNumberExist(nameLocator)) {
|
|
5818
|
-
const prevName =
|
|
5819
|
-
if (isUniqueInDOM(docmt, "name", nameLocator, targetElement)) {
|
|
6697
|
+
const prevName = findLocatorByStrategy("name");
|
|
6698
|
+
if (isUniqueInDOM(docmt, "name", nameLocator, targetElement, platform)) {
|
|
5820
6699
|
excludedAttributes.push("name");
|
|
5821
6700
|
createLocator(prevName, {
|
|
5822
6701
|
name: "name",
|
|
@@ -5826,42 +6705,68 @@
|
|
|
5826
6705
|
});
|
|
5827
6706
|
}
|
|
5828
6707
|
}
|
|
5829
|
-
const classValue = getClassName(targetElement);
|
|
6708
|
+
const classValue = getClassName(targetElement, platform);
|
|
6709
|
+
const prevClassLocator = findLocatorByStrategy("className");
|
|
5830
6710
|
if (classValue &&
|
|
6711
|
+
(!isIOSPlatform(platform) || prevClassLocator) &&
|
|
5831
6712
|
classValue.trim() !== "" &&
|
|
5832
6713
|
!classValue.includes(" ") &&
|
|
5833
6714
|
!locatorExists("className", classValue) &&
|
|
5834
6715
|
!isNumberExist(classValue)) {
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
excludedAttributes.push("className");
|
|
6716
|
+
if (isUniqueInDOM(docmt, "className", classValue, targetElement, platform)) {
|
|
6717
|
+
excludedAttributes.push(isIOSPlatform(platform) ? "type" : "className");
|
|
5838
6718
|
createLocator(prevClassLocator, {
|
|
5839
|
-
name: "className",
|
|
6719
|
+
name: prevClassLocator?.name || (isIOSPlatform(platform) ? "class name" : "className"),
|
|
5840
6720
|
type: "static",
|
|
5841
6721
|
isRecorded: "Y",
|
|
5842
6722
|
value: classValue
|
|
5843
6723
|
});
|
|
5844
6724
|
}
|
|
5845
6725
|
}
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
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,
|
|
5852
6733
|
type: "static",
|
|
5853
|
-
isRecorded: "Y"
|
|
6734
|
+
isRecorded: "Y",
|
|
6735
|
+
value: predicateString
|
|
5854
6736
|
});
|
|
5855
6737
|
}
|
|
5856
|
-
|
|
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
|
+
}
|
|
5857
6762
|
const allAttributes = Array.from(targetElement.attributes);
|
|
5858
|
-
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"));
|
|
5859
6765
|
//If any direct locator is broken then we consider it as broken xpath
|
|
5860
6766
|
let xpathResults = [];
|
|
5861
6767
|
try {
|
|
5862
6768
|
xpathResults =
|
|
5863
|
-
parseDOM(targetElement, docmt, false, true, includedAttributes) ??
|
|
5864
|
-
[];
|
|
6769
|
+
parseDOM(targetElement, docmt, false, true, includedAttributes, [], platform) ?? [];
|
|
5865
6770
|
}
|
|
5866
6771
|
catch (error) {
|
|
5867
6772
|
console.error("Error generating XPath candidates:", error);
|
|
@@ -5872,10 +6777,12 @@
|
|
|
5872
6777
|
for (const brokenPx of brokenPayloadXPaths) {
|
|
5873
6778
|
if (xpathAdded >= brokenPayloadXPaths.length)
|
|
5874
6779
|
break;
|
|
5875
|
-
const originalPattern = getXPathPattern(brokenPx.value);
|
|
6780
|
+
const originalPattern = getXPathPattern(brokenPx.value, platform);
|
|
5876
6781
|
if (usedXPathPatterns.has(originalPattern))
|
|
5877
6782
|
continue;
|
|
5878
|
-
const
|
|
6783
|
+
const flatXpaths = getXPathCandidates(xpathResults);
|
|
6784
|
+
const match = flatXpaths.find((r) => typeof r.value === "string" &&
|
|
6785
|
+
getXPathPattern(r.value, platform) === originalPattern);
|
|
5879
6786
|
if (match?.value) {
|
|
5880
6787
|
createLocator(brokenPx, {
|
|
5881
6788
|
name: "xpath",
|
|
@@ -5889,23 +6796,34 @@
|
|
|
5889
6796
|
}
|
|
5890
6797
|
}
|
|
5891
6798
|
if (xpathAdded < brokenPayloadXPaths.length) {
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
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
|
+
}
|
|
5909
6827
|
}
|
|
5910
6828
|
}
|
|
5911
6829
|
}
|
|
@@ -5936,12 +6854,20 @@
|
|
|
5936
6854
|
console.error("Error processing locator:", locator, error);
|
|
5937
6855
|
}
|
|
5938
6856
|
}
|
|
5939
|
-
if (finalLocators.length < 5) {
|
|
6857
|
+
if (!isIOSPlatform(platform) && finalLocators.length < 5) {
|
|
5940
6858
|
const fallbackCandidates = [
|
|
5941
|
-
{ name: "id", value: getId(targetElement) },
|
|
6859
|
+
{ name: "id", value: getId(targetElement, platform) },
|
|
5942
6860
|
{ name: "name", value: getName(targetElement) },
|
|
5943
|
-
{ name: "className", value: getClassName(targetElement) },
|
|
5944
|
-
|
|
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
|
+
}
|
|
5945
6871
|
];
|
|
5946
6872
|
for (const candidate of fallbackCandidates) {
|
|
5947
6873
|
if (finalLocators.length > 4)
|
|
@@ -5955,7 +6881,7 @@
|
|
|
5955
6881
|
continue;
|
|
5956
6882
|
if (name === "className" && value.includes(" "))
|
|
5957
6883
|
continue;
|
|
5958
|
-
if (isUniqueInDOM(docmt, name, value, targetElement)) {
|
|
6884
|
+
if (isUniqueInDOM(docmt, name, value, targetElement, platform)) {
|
|
5959
6885
|
createLocator(undefined, {
|
|
5960
6886
|
name,
|
|
5961
6887
|
type: "static",
|
|
@@ -5965,7 +6891,7 @@
|
|
|
5965
6891
|
});
|
|
5966
6892
|
}
|
|
5967
6893
|
}
|
|
5968
|
-
if (finalLocators.length < 5) {
|
|
6894
|
+
if (finalLocators.length < 5 && !isMobilePlatform(platform)) {
|
|
5969
6895
|
parseCssSelectors(targetElement, "multiple").forEach((cssSelector) => {
|
|
5970
6896
|
if (finalLocators.length > 4)
|
|
5971
6897
|
return;
|
|
@@ -5973,7 +6899,7 @@
|
|
|
5973
6899
|
return;
|
|
5974
6900
|
if (locatorExists("cssSelector", cssSelector.value))
|
|
5975
6901
|
return;
|
|
5976
|
-
if (!isUniqueInDOM(docmt, "cssSelector", cssSelector.value, targetElement)) {
|
|
6902
|
+
if (!isUniqueInDOM(docmt, "cssSelector", cssSelector.value, targetElement, platform)) {
|
|
5977
6903
|
return;
|
|
5978
6904
|
}
|
|
5979
6905
|
createLocator(undefined, {
|
|
@@ -5988,7 +6914,7 @@
|
|
|
5988
6914
|
}
|
|
5989
6915
|
const finalAutoHealedLocators = finalLocators.map((obj) => ({
|
|
5990
6916
|
...obj,
|
|
5991
|
-
value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded)
|
|
6917
|
+
value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded, platform)
|
|
5992
6918
|
}));
|
|
5993
6919
|
const jsonResult = [
|
|
5994
6920
|
{
|
|
@@ -6025,121 +6951,6 @@
|
|
|
6025
6951
|
return null;
|
|
6026
6952
|
};
|
|
6027
6953
|
|
|
6028
|
-
function getOptimalClassChain(doc, domNode, uniqueAttributes) {
|
|
6029
|
-
try {
|
|
6030
|
-
if (!domNode ||
|
|
6031
|
-
domNode.nodeType !== 1 ||
|
|
6032
|
-
!domNode.tagName ||
|
|
6033
|
-
domNode.tagName === "XCUIElementTypeApplication") {
|
|
6034
|
-
return "";
|
|
6035
|
-
}
|
|
6036
|
-
const tag = domNode.tagName;
|
|
6037
|
-
// Priority attributes
|
|
6038
|
-
const priorityAttrs = ["name", "label", "value"];
|
|
6039
|
-
// ---------- STEP 1 : TRY GOOD ATTRIBUTES ----------
|
|
6040
|
-
for (const attrName of priorityAttrs) {
|
|
6041
|
-
const attr = uniqueAttributes.find(a => a.name === attrName);
|
|
6042
|
-
if (!attr)
|
|
6043
|
-
continue;
|
|
6044
|
-
let attrValue = attr.value;
|
|
6045
|
-
if (!attrValue)
|
|
6046
|
-
continue;
|
|
6047
|
-
attrValue = attrValue.trim().replace(/\s+/g, " ");
|
|
6048
|
-
if (isNumberExist(attrValue))
|
|
6049
|
-
continue;
|
|
6050
|
-
const xpath = `//${tag}[@${attrName}="${attrValue}"]`;
|
|
6051
|
-
let count = 0;
|
|
6052
|
-
try {
|
|
6053
|
-
count = getCountOfXPath(xpath, domNode, doc);
|
|
6054
|
-
}
|
|
6055
|
-
catch (err) {
|
|
6056
|
-
console.log(err);
|
|
6057
|
-
continue;
|
|
6058
|
-
}
|
|
6059
|
-
const hasSpace = /\s/.test(attrValue);
|
|
6060
|
-
if (count === 1) {
|
|
6061
|
-
if (hasSpace) {
|
|
6062
|
-
return `/${tag}[\`${attrName} CONTAINS "${attrValue}"\`]`;
|
|
6063
|
-
}
|
|
6064
|
-
return `/${tag}[\`${attrName} == "${attrValue}"\`]`;
|
|
6065
|
-
}
|
|
6066
|
-
if (count > 1 && domNode.parentElement) {
|
|
6067
|
-
const siblings = Array.from(domNode.parentElement.children)
|
|
6068
|
-
.filter(el => el.tagName === tag);
|
|
6069
|
-
const index = siblings.indexOf(domNode) + 1;
|
|
6070
|
-
if (hasSpace) {
|
|
6071
|
-
return `/${tag}[\`${attrName} CONTAINS "${attrValue}"\`][${index}]`;
|
|
6072
|
-
}
|
|
6073
|
-
return `/${tag}[\`${attrName} == "${attrValue}"\`][${index}]`;
|
|
6074
|
-
}
|
|
6075
|
-
}
|
|
6076
|
-
// ---------- STEP 2 : FALLBACK USING TAG INDEX ----------
|
|
6077
|
-
let classChain = `/${tag}`;
|
|
6078
|
-
if (domNode.parentElement) {
|
|
6079
|
-
const siblings = Array.from(domNode.parentElement.children)
|
|
6080
|
-
.filter(el => el.tagName === tag);
|
|
6081
|
-
if (siblings.length > 1) {
|
|
6082
|
-
const index = siblings.indexOf(domNode) + 1;
|
|
6083
|
-
classChain += `[${index}]`;
|
|
6084
|
-
}
|
|
6085
|
-
}
|
|
6086
|
-
const parentChain = getOptimalClassChain(doc, domNode.parentElement, uniqueAttributes);
|
|
6087
|
-
return parentChain + classChain;
|
|
6088
|
-
}
|
|
6089
|
-
catch (error) {
|
|
6090
|
-
console.log(`Unable to generate optimal -ios class chain : ${JSON.stringify(error)}`);
|
|
6091
|
-
return null;
|
|
6092
|
-
}
|
|
6093
|
-
}
|
|
6094
|
-
function getOptimalPredicateString(doc, domNode, uniqueAttributes) {
|
|
6095
|
-
try {
|
|
6096
|
-
// BASE CASE #1: If this isn't an element, we're above the root, or this is `XCUIElementTypeApplication`,
|
|
6097
|
-
// which is not an official XCUITest element, return empty string
|
|
6098
|
-
if (!domNode?.tagName ||
|
|
6099
|
-
domNode?.nodeType !== 1 ||
|
|
6100
|
-
domNode?.tagName === 'XCUIElementTypeApplication') {
|
|
6101
|
-
return '';
|
|
6102
|
-
}
|
|
6103
|
-
// BASE CASE #2: Check all attributes and try to find the best way
|
|
6104
|
-
let xpathAttributes = [];
|
|
6105
|
-
let predicateString = [];
|
|
6106
|
-
for (let attr of uniqueAttributes) {
|
|
6107
|
-
const attrValue = attr.value;
|
|
6108
|
-
const attrName = attr.name;
|
|
6109
|
-
if (attrValue.length === 0) {
|
|
6110
|
-
continue;
|
|
6111
|
-
}
|
|
6112
|
-
if (attrValue && !isNumberExist(attrValue)) {
|
|
6113
|
-
xpathAttributes.push(`@${attrName}="${attrValue}"`);
|
|
6114
|
-
const xpathe = `//*[${xpathAttributes.join(' and ')}]`;
|
|
6115
|
-
predicateString.push(`${attrName} == "${attrValue}"`);
|
|
6116
|
-
let othersWithAttr;
|
|
6117
|
-
// If the XPath does not parse, move to the next unique attribute
|
|
6118
|
-
try {
|
|
6119
|
-
othersWithAttr = getCountOfXPath(xpathe, domNode, doc);
|
|
6120
|
-
}
|
|
6121
|
-
catch (ign) {
|
|
6122
|
-
console.log(ign);
|
|
6123
|
-
continue;
|
|
6124
|
-
}
|
|
6125
|
-
// If the attribute isn't actually unique, get it's index too
|
|
6126
|
-
if (othersWithAttr === 1) {
|
|
6127
|
-
return predicateString.join(' AND ');
|
|
6128
|
-
}
|
|
6129
|
-
}
|
|
6130
|
-
}
|
|
6131
|
-
}
|
|
6132
|
-
catch (error) {
|
|
6133
|
-
// If there's an unexpected exception, abort and don't get an XPath
|
|
6134
|
-
console.log(`The most optimal '-ios predicate string' could not be determined because an error was thrown: '${JSON.stringify(error, null, 2)}'`);
|
|
6135
|
-
}
|
|
6136
|
-
return null;
|
|
6137
|
-
}
|
|
6138
|
-
const iosSelectors = {
|
|
6139
|
-
getOptimalClassChain,
|
|
6140
|
-
getOptimalPredicateString
|
|
6141
|
-
};
|
|
6142
|
-
|
|
6143
6954
|
const createXPathAPI = () => ({
|
|
6144
6955
|
xpath,
|
|
6145
6956
|
referenceXpaths: referenceXpath,
|