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.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.concat(xpaths);
1878
+ xpaths1.push(...xpaths);
1879
1879
  }
1880
1880
  }
1881
1881
  return xpaths1;
1882
1882
  };
1883
- const parseXml = (xmlStr, type) => {
1884
- if (window.DOMParser) {
1885
- return new window.DOMParser().parseFromString(xmlStr, type);
1883
+ const parseXml = (xmlStr, type, DOMParserImpl) => {
1884
+ if (!DOMParserImpl) {
1885
+ return null;
1886
1886
  }
1887
- return null;
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 canonicalizeXPath(xpath) {
1911
- return (xpath
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 xp = xpath.toLowerCase();
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 { axis, attribute, usesNormalize };
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
- const root = element?.getRootNode?.() ?? docmt;
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
- case "name":
1963
- return queryAll(`[name="${escapeAttrValue(value)}"]`).length === 1;
1964
- case "className":
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
- case "tagName":
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
- case "linkText":
1969
- return (queryAll("a").filter((a) => a.textContent?.trim() === value)
1970
- .length === 1);
1971
- case "partialLinkText":
1972
- return (queryAll("a").filter((a) => a.textContent?.includes(value)).length ===
1973
- 1);
1974
- case "cssSelector":
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 parseDOM = (element, doc, isIndex, isTarget, includedAttributes = [], strategies = []) => {
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
- xpaths1 = parseDOM(element, element.ownerDocument, false, false);
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?.length) {
4963
- xpaths = xpaths?.map((x) => x.value.charAt(0) == "(" &&
4964
- findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
4965
- ? { key: "", value: removeParenthesis(x.value) }
4966
- : { key: "", value: x.value });
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?.length) {
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?.length) {
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
- !isStableAttributeValue(idAttribute.value) ||
5303
- !checkBlockedAttributes(idAttribute, el, true)) {
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
- return element?.id || null;
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
- const window = docmt.defaultView;
5549
- if (!window)
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
- const xpathEvaluator = new window.XPathEvaluator();
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
- function resolveElement(ctx, locator, selector) {
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
- else if (locator.name.includes("id") || selector.startsWith("#")) {
5621
- return ctx.querySelector("#" + escapeAttrValue(selector));
5622
- }
5623
- else if (locator.name.includes("className") || selector.startsWith(".")) {
5624
- return ctx.querySelector("." + selector);
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
- else if (locator.name === "name") {
5627
- const safeName = escapeAttrValue(selector);
5628
- return ctx.querySelector(`[name="${safeName}"]`);
6468
+ if (strategy === "name") {
6469
+ return ctx.querySelector(`[name="${escapeAttrValue(selector)}"]`);
5629
6470
  }
5630
- else if (locator.name === "tagName") {
6471
+ if (strategy === "tagName") {
5631
6472
  return ctx.querySelector(selector);
5632
6473
  }
5633
- else if (locator.name === "linkText") {
6474
+ if (strategy === "linkText") {
5634
6475
  return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.trim() === selector) || null);
5635
6476
  }
5636
- else if (locator.name === "partialLinkText") {
6477
+ if (strategy === "partialLinkText") {
5637
6478
  return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.includes(selector)) || null);
5638
6479
  }
5639
- else if ((locator.name.includes("xpath") || selector.startsWith("//")) &&
5640
- !locator.type.match("dynamic")) {
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 = resolveElement(iframeDoc, locator, selector);
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 (!val)
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
- if (type === "xpath" && isRecorded === "Y" && !val.startsWith("//"))
5700
- return null;
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
- targetElement = resolveElement(docmt, locator, trimmedSelector);
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
- // Track XPath patterns already used
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 (idValue &&
6618
+ const idValue = getId(targetElement, platform);
6619
+ if (!isIOSPlatform(platform) &&
6620
+ idValue &&
5775
6621
  !locatorExists("id", idValue) &&
5776
6622
  !isNumberExist(idValue)) {
5777
- const prevId = record.locators.find((l) => l.name === "id");
5778
- if (isUniqueInDOM(docmt, "id", idValue, targetElement)) {
5779
- excludedAttributes.push("id");
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 tagName = targetElement.tagName;
5789
- if (tagName && !locatorExists("tagName", tagName)) {
5790
- const prevTag = record.locators.find((l) => l.name === "tagName");
5791
- if (isUniqueInDOM(docmt, "tagName", tagName, targetElement)) {
5792
- excludedAttributes.push("tagName");
5793
- createLocator(prevTag, {
5794
- name: "tagName",
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: tagName
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 = record.locators.find((l) => l.name === "linkText");
5804
- if (isUniqueInDOM(docmt, "linkText", textValue, targetElement)) {
5805
- excludedAttributes.push("linkText");
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: "linkText",
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 (nameLocator &&
6693
+ if (!isIOSPlatform(platform) &&
6694
+ nameLocator &&
5816
6695
  !locatorExists("name", nameLocator) &&
5817
6696
  !isNumberExist(nameLocator)) {
5818
- const prevName = record.locators.find((l) => l.name === "name");
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
- const prevClassLocator = record.locators.find((l) => l.name === "className");
5836
- if (isUniqueInDOM(docmt, "className", classValue, targetElement)) {
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
- parseCssSelectors(targetElement, "single").forEach((cssSelector) => {
5847
- if (cssSelector.value &&
5848
- !locatorExists("cssSelector", cssSelector.value)) {
5849
- createLocator(undefined, {
5850
- name: "cssSelector",
5851
- value: cssSelector.value,
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 match = xpathResults.find((r) => r.value && getXPathPattern(r.value) === originalPattern);
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
- for (const result of xpathResults) {
5893
- if (xpathAdded >= brokenPayloadXPaths.length)
5894
- break;
5895
- if (!result.value)
5896
- continue;
5897
- const pattern = getXPathPattern(result.value);
5898
- if (usedXPathPatterns.has(pattern))
5899
- continue;
5900
- createLocator(result, {
5901
- name: "xpath",
5902
- value: result.value,
5903
- type: "static",
5904
- isRecorded: "Y",
5905
- isSelfHealed: "Y"
5906
- });
5907
- usedXPathPatterns.add(pattern);
5908
- xpathAdded++;
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
- { name: "linkText", value: getVisibleText(targetElement) }
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,