ff-dom 3.0.3-beta.3 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cdn.js CHANGED
@@ -1880,11 +1880,16 @@
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;
@@ -4945,7 +5362,18 @@
4945
5362
  };
4946
5363
  const getReferenceElementXpath = (element) => {
4947
5364
  let xpaths1 = [];
4948
- 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
+ : [];
4949
5377
  const referenceElementXpaths = getReferenceElementsXpath(element, element.ownerDocument, false);
4950
5378
  if (referenceElementXpaths?.length) {
4951
5379
  xpaths1 = xpaths1?.length
@@ -4953,41 +5381,53 @@
4953
5381
  : referenceElementXpaths;
4954
5382
  }
4955
5383
  if (!xpaths1?.length) {
4956
- xpaths1 = parseDOM(element, element.ownerDocument, true, false);
5384
+ xpaths1 = normalizeXpaths(parseDOM(element, element.ownerDocument, true, false));
4957
5385
  xpaths1 = xpaths1?.map((x) => x.value.charAt(0) == "(" &&
4958
5386
  findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
4959
5387
  ? { key: "", value: removeParenthesis(x.value) }
4960
5388
  : { key: "", value: x.value });
5389
+ xpaths1 = xpaths1.map((x) => x.value.charAt(0) === "(" &&
5390
+ findMatchingParenthesis(x.value, 0) + 1 ===
5391
+ x.value.lastIndexOf("[")
5392
+ ? {
5393
+ key: "",
5394
+ value: removeParenthesis(x.value)
5395
+ }
5396
+ : x);
4961
5397
  }
4962
5398
  else {
4963
- let xpaths = parseDOM(element, element.ownerDocument, true, false);
4964
- if (xpaths?.length) {
4965
- xpaths = xpaths?.map((x) => x.value.charAt(0) == "(" &&
4966
- findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
4967
- ? { key: "", value: removeParenthesis(x.value) }
4968
- : { 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);
4969
5409
  xpaths1 = xpaths1.concat(xpaths);
4970
5410
  }
4971
5411
  }
4972
- if (!xpaths1?.length) {
4973
- xpaths1 = [
4974
- {
5412
+ if (!xpaths1.length) {
5413
+ xpaths1 = [{
4975
5414
  key: "",
4976
5415
  value: getRelativeXPath(element, element.ownerDocument, false, false, Array.from(element.attributes))
4977
- }
4978
- ];
5416
+ }];
4979
5417
  }
4980
- if (!xpaths1?.length) {
4981
- xpaths1 = [
4982
- {
5418
+ if (!xpaths1.length) {
5419
+ xpaths1 = [{
4983
5420
  key: "",
4984
5421
  value: getRelativeXPath(element, element.ownerDocument, true, false, Array.from(element.attributes))
5422
+ }];
5423
+ xpaths1 = xpaths1.map((x) => x.value.charAt(0) === "(" &&
5424
+ findMatchingParenthesis(x.value, 0) + 1 ===
5425
+ x.value.lastIndexOf("[")
5426
+ ? {
5427
+ key: "",
5428
+ value: removeParenthesis(x.value)
4985
5429
  }
4986
- ];
4987
- xpaths1 = xpaths1?.map((x) => x.value.charAt(0) == "(" &&
4988
- findMatchingParenthesis(x.value, 0) + 1 === x.value.lastIndexOf("[")
4989
- ? { key: "", value: removeParenthesis(x.value) }
4990
- : { key: "", value: x.value });
5430
+ : x);
4991
5431
  }
4992
5432
  const childAnchorXpaths = Array.from(element.children || [])
4993
5433
  .map((child) => {
@@ -5583,10 +6023,30 @@
5583
6023
  }
5584
6024
  return null;
5585
6025
  };
5586
- const getId = (element) => {
5587
- 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;
5588
6039
  };
5589
- const getClassName = (element) => {
6040
+ const getClassName = (element, platform) => {
6041
+ const normalizedPlatform = String(platform).toLowerCase();
6042
+ if (normalizedPlatform.includes("android")) {
6043
+ return (element.getAttribute("class") ||
6044
+ element.getAttribute("className") ||
6045
+ null);
6046
+ }
6047
+ if (normalizedPlatform.includes("ios")) {
6048
+ return element.getAttribute("type") || null;
6049
+ }
5590
6050
  return element.className || null;
5591
6051
  };
5592
6052
  const getVisibleText = (element) => {
@@ -5614,12 +6074,25 @@
5614
6074
  "/following"
5615
6075
  ];
5616
6076
  function getElementFromXPath(docmt, xpath) {
5617
- const window = docmt.defaultView;
5618
- 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);
5619
6094
  return null;
5620
- const xpathEvaluator = new window.XPathEvaluator();
5621
- const xpathResult = xpathEvaluator.evaluate(xpath, docmt, null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
5622
- return xpathResult.singleNodeValue;
6095
+ }
5623
6096
  }
5624
6097
  function checkReferenceElementIsValid(locator, relation, docmt) {
5625
6098
  if (locator.includes(relation)) {
@@ -5653,7 +6126,7 @@
5653
6126
  }
5654
6127
  return null;
5655
6128
  }
5656
- const getElementsFromHTML = (record, docmt) => {
6129
+ const getElementsFromHTML = (record, docmt, platform) => {
5657
6130
  clearXPathEvalCache(docmt);
5658
6131
  const elementsToRemove = docmt.querySelectorAll("script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg");
5659
6132
  if (elementsToRemove) {
@@ -5682,45 +6155,330 @@
5682
6155
  : resolveIsSelfHealed(newLocator.name, oldValue, newValue);
5683
6156
  pushUniqueLocator(newLocator);
5684
6157
  }
5685
- 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
+ }
5686
6452
  if (isCssSelectorLocator(locator)) {
5687
6453
  return getElementFromCssSelector(ctx, selector);
5688
6454
  }
5689
- else if (locator.name.includes("id") || selector.startsWith("#")) {
5690
- return ctx.querySelector("#" + escapeAttrValue(selector));
5691
- }
5692
- else if (locator.name.includes("className") || selector.startsWith(".")) {
5693
- 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);
5694
6467
  }
5695
- else if (locator.name === "name") {
5696
- const safeName = escapeAttrValue(selector);
5697
- return ctx.querySelector(`[name="${safeName}"]`);
6468
+ if (strategy === "name") {
6469
+ return ctx.querySelector(`[name="${escapeAttrValue(selector)}"]`);
5698
6470
  }
5699
- else if (locator.name === "tagName") {
6471
+ if (strategy === "tagName") {
5700
6472
  return ctx.querySelector(selector);
5701
6473
  }
5702
- else if (locator.name === "linkText") {
6474
+ if (strategy === "linkText") {
5703
6475
  return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.trim() === selector) || null);
5704
6476
  }
5705
- else if (locator.name === "partialLinkText") {
6477
+ if (strategy === "partialLinkText") {
5706
6478
  return (Array.from(ctx.querySelectorAll("a")).find((a) => a.textContent?.includes(selector)) || null);
5707
6479
  }
5708
- else if ((locator.name.includes("xpath") || selector.startsWith("//")) &&
5709
- !locator.type.match("dynamic")) {
5710
- const normalizedXPath = normalizeXPath(selector);
5711
- const el = getElementFromXPath(ctx, normalizedXPath);
5712
- if (el) {
5713
- createLocator(locator, {
5714
- value: selector,
5715
- isRecorded: String(locator.isRecorded).includes("N") ? "N" : "Y"
5716
- });
5717
- }
5718
- return el;
5719
- }
5720
- else {
5721
- return ctx.querySelector(selector);
5722
- }
5723
- }
6480
+ return null;
6481
+ };
5724
6482
  function findInIframes(docmt, locator, selector) {
5725
6483
  const iframes = docmt.querySelectorAll("iframe");
5726
6484
  for (const iframe of iframes) {
@@ -5728,7 +6486,11 @@
5728
6486
  const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
5729
6487
  if (!iframeDoc)
5730
6488
  continue;
5731
- const el = 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);
5732
6494
  if (el)
5733
6495
  return el;
5734
6496
  }
@@ -5746,9 +6508,12 @@
5746
6508
  }
5747
6509
  }
5748
6510
  /** Locator Value Cleaner (Handles Special Scenarios) **/
5749
- const cleanLocatorValue = (val, type, isRecorded) => {
5750
- if (!val)
6511
+ const cleanLocatorValue = (val, type, isRecorded, platform) => {
6512
+ if (val == null)
5751
6513
  return null;
6514
+ if (typeof val !== "string") {
6515
+ val = String(val);
6516
+ }
5752
6517
  let cleaned = val.trim();
5753
6518
  // Return null for empty or literal "null"
5754
6519
  if (!cleaned || cleaned.toLowerCase() === "null")
@@ -5765,8 +6530,15 @@
5765
6530
  if (type === "id" || type === "name") {
5766
6531
  cleaned = cleaned.replace(/['"]/g, "").trim();
5767
6532
  }
5768
- if (type === "xpath" && isRecorded === "Y" && !val.startsWith("//"))
5769
- 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
+ }
5770
6542
  // Final check for empty strings
5771
6543
  if (!cleaned || /^['"]{2}$/.test(cleaned))
5772
6544
  return null;
@@ -5802,13 +6574,18 @@
5802
6574
  }
5803
6575
  const trimmedSelector = selector.trim();
5804
6576
  //normal DOM
5805
- 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);
5806
6583
  //iframe (if not found)
5807
6584
  if (!targetElement) {
5808
6585
  targetElement = findInIframes(docmt, locator, trimmedSelector);
5809
6586
  }
5810
6587
  //shadow DOM (if still not found)
5811
- if (!targetElement) {
6588
+ if (!targetElement && !isMobilePlatform(platform) && docmt.body) {
5812
6589
  targetElement = getElementFromShadowRoot(docmt.body, trimmedSelector);
5813
6590
  }
5814
6591
  if (!targetElement) {
@@ -5821,7 +6598,7 @@
5821
6598
  return finalLocatorsSet.has(key);
5822
6599
  };
5823
6600
  if (targetElement) {
5824
- const payloadXPaths = record.locators.filter((l) => l.name === "xpath" && l.value);
6601
+ const payloadXPaths = record.locators.filter((l) => getLocatorStrategy(l.name) === "xpath" && l.value);
5825
6602
  const payloadCssSelectors = record.locators.filter((l) => isCssSelectorLocator(l) && l.value);
5826
6603
  for (const px of payloadXPaths) {
5827
6604
  if (isSameXPathStillValid(px.value, docmt, targetElement))
@@ -5835,17 +6612,19 @@
5835
6612
  isSelfHealed: null
5836
6613
  });
5837
6614
  }
5838
- const existingXPaths = finalLocators.filter((l) => l.name === "xpath" && l.value);
5839
- // Track XPath patterns already used
5840
- const usedXPathPatterns = new Set(existingXPaths.map((x) => getXPathPattern(x.value)));
6615
+ const existingXPaths = finalLocators.filter((l) => getLocatorStrategy(l.name) === "xpath" && l.value);
6616
+ const usedXPathPatterns = new Set(existingXPaths.map((x) => getXPathPattern(x.value, platform)));
5841
6617
  const excludedAttributes = [];
5842
- const idValue = getId(targetElement);
5843
- if (idValue &&
6618
+ const idValue = getId(targetElement, platform);
6619
+ if (!isIOSPlatform(platform) &&
6620
+ idValue &&
5844
6621
  !locatorExists("id", idValue) &&
5845
6622
  !isNumberExist(idValue)) {
5846
- const prevId = record.locators.find((l) => l.name === "id");
5847
- if (isUniqueInDOM(docmt, "id", idValue, targetElement)) {
5848
- 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");
5849
6628
  createLocator(prevId, {
5850
6629
  name: "id",
5851
6630
  type: "static",
@@ -5854,26 +6633,56 @@
5854
6633
  });
5855
6634
  }
5856
6635
  }
5857
- const tagName = targetElement.tagName;
5858
- if (tagName && !locatorExists("tagName", tagName)) {
5859
- const prevTag = record.locators.find((l) => l.name === "tagName");
5860
- if (isUniqueInDOM(docmt, "tagName", tagName, targetElement)) {
5861
- excludedAttributes.push("tagName");
5862
- createLocator(prevTag, {
5863
- 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",
5864
6654
  type: "static",
5865
6655
  isRecorded: "Y",
5866
- value: tagName
6656
+ value: accessibilityId
5867
6657
  });
5868
6658
  }
5869
6659
  }
6660
+ if (!isMobilePlatform(platform)) {
6661
+ const tagName = targetElement.tagName;
6662
+ if (tagName && !locatorExists("tagName", tagName)) {
6663
+ const prevTag = findLocatorByStrategy("tagName");
6664
+ if (isUniqueInDOM(docmt, "tagName", tagName, targetElement, platform)) {
6665
+ excludedAttributes.push("tagName");
6666
+ createLocator(prevTag, {
6667
+ name: "tagName",
6668
+ type: "static",
6669
+ isRecorded: "Y",
6670
+ value: tagName
6671
+ });
6672
+ }
6673
+ }
6674
+ }
5870
6675
  const textValue = getVisibleText(targetElement);
5871
6676
  if (textValue && !isNumberExist(textValue)) {
5872
- const prevLinkText = record.locators.find((l) => l.name === "linkText");
5873
- if (isUniqueInDOM(docmt, "linkText", textValue, targetElement)) {
5874
- 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");
5875
6682
  createLocator(prevLinkText, {
5876
- name: "linkText",
6683
+ name: isAndroidPlatform(platform)
6684
+ ? "name"
6685
+ : "linkText",
5877
6686
  type: "static",
5878
6687
  isRecorded: "Y",
5879
6688
  value: textValue
@@ -5881,11 +6690,12 @@
5881
6690
  }
5882
6691
  }
5883
6692
  const nameLocator = getName(targetElement);
5884
- if (nameLocator &&
6693
+ if (!isIOSPlatform(platform) &&
6694
+ nameLocator &&
5885
6695
  !locatorExists("name", nameLocator) &&
5886
6696
  !isNumberExist(nameLocator)) {
5887
- const prevName = record.locators.find((l) => l.name === "name");
5888
- if (isUniqueInDOM(docmt, "name", nameLocator, targetElement)) {
6697
+ const prevName = findLocatorByStrategy("name");
6698
+ if (isUniqueInDOM(docmt, "name", nameLocator, targetElement, platform)) {
5889
6699
  excludedAttributes.push("name");
5890
6700
  createLocator(prevName, {
5891
6701
  name: "name",
@@ -5895,42 +6705,68 @@
5895
6705
  });
5896
6706
  }
5897
6707
  }
5898
- const classValue = getClassName(targetElement);
6708
+ const classValue = getClassName(targetElement, platform);
6709
+ const prevClassLocator = findLocatorByStrategy("className");
5899
6710
  if (classValue &&
6711
+ (!isIOSPlatform(platform) || prevClassLocator) &&
5900
6712
  classValue.trim() !== "" &&
5901
6713
  !classValue.includes(" ") &&
5902
6714
  !locatorExists("className", classValue) &&
5903
6715
  !isNumberExist(classValue)) {
5904
- const prevClassLocator = record.locators.find((l) => l.name === "className");
5905
- if (isUniqueInDOM(docmt, "className", classValue, targetElement)) {
5906
- excludedAttributes.push("className");
6716
+ if (isUniqueInDOM(docmt, "className", classValue, targetElement, platform)) {
6717
+ excludedAttributes.push(isIOSPlatform(platform) ? "type" : "className");
5907
6718
  createLocator(prevClassLocator, {
5908
- name: "className",
6719
+ name: prevClassLocator?.name || (isIOSPlatform(platform) ? "class name" : "className"),
5909
6720
  type: "static",
5910
6721
  isRecorded: "Y",
5911
6722
  value: classValue
5912
6723
  });
5913
6724
  }
5914
6725
  }
5915
- parseCssSelectors(targetElement, "single").forEach((cssSelector) => {
5916
- if (cssSelector.value &&
5917
- !locatorExists("cssSelector", cssSelector.value)) {
5918
- createLocator(undefined, {
5919
- name: "cssSelector",
5920
- 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,
5921
6733
  type: "static",
5922
- isRecorded: "Y"
6734
+ isRecorded: "Y",
6735
+ value: predicateString
5923
6736
  });
5924
6737
  }
5925
- });
6738
+ const classChain = iosSelectors.getOptimalClassChain(docmt, targetElement, selectorAttributes);
6739
+ const prevClassChain = findLocatorByStrategy("iosClassChain");
6740
+ if (prevClassChain && classChain && !locatorExists(prevClassChain.name, classChain)) {
6741
+ createLocator(prevClassChain, {
6742
+ name: prevClassChain.name,
6743
+ type: "static",
6744
+ isRecorded: "Y",
6745
+ value: classChain
6746
+ });
6747
+ }
6748
+ }
6749
+ if (!isMobilePlatform(platform)) {
6750
+ parseCssSelectors(targetElement, "single").forEach((cssSelector) => {
6751
+ if (cssSelector.value &&
6752
+ !locatorExists("cssSelector", cssSelector.value)) {
6753
+ createLocator(undefined, {
6754
+ name: "cssSelector",
6755
+ value: cssSelector.value,
6756
+ type: "static",
6757
+ isRecorded: "Y"
6758
+ });
6759
+ }
6760
+ });
6761
+ }
5926
6762
  const allAttributes = Array.from(targetElement.attributes);
5927
- const includedAttributes = allAttributes.filter((attr) => !excludedAttributes.includes(attr.name));
6763
+ const includedAttributes = allAttributes.filter((attr) => !excludedAttributes.includes(attr.name) ||
6764
+ (isIOSPlatform(platform) && attr.name === "name"));
5928
6765
  //If any direct locator is broken then we consider it as broken xpath
5929
6766
  let xpathResults = [];
5930
6767
  try {
5931
6768
  xpathResults =
5932
- parseDOM(targetElement, docmt, false, true, includedAttributes) ??
5933
- [];
6769
+ parseDOM(targetElement, docmt, false, true, includedAttributes, [], platform) ?? [];
5934
6770
  }
5935
6771
  catch (error) {
5936
6772
  console.error("Error generating XPath candidates:", error);
@@ -5941,10 +6777,12 @@
5941
6777
  for (const brokenPx of brokenPayloadXPaths) {
5942
6778
  if (xpathAdded >= brokenPayloadXPaths.length)
5943
6779
  break;
5944
- const originalPattern = getXPathPattern(brokenPx.value);
6780
+ const originalPattern = getXPathPattern(brokenPx.value, platform);
5945
6781
  if (usedXPathPatterns.has(originalPattern))
5946
6782
  continue;
5947
- const 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);
5948
6786
  if (match?.value) {
5949
6787
  createLocator(brokenPx, {
5950
6788
  name: "xpath",
@@ -5958,23 +6796,34 @@
5958
6796
  }
5959
6797
  }
5960
6798
  if (xpathAdded < brokenPayloadXPaths.length) {
5961
- for (const result of xpathResults) {
5962
- if (xpathAdded >= brokenPayloadXPaths.length)
5963
- break;
5964
- if (!result.value)
5965
- continue;
5966
- const pattern = getXPathPattern(result.value);
5967
- if (usedXPathPatterns.has(pattern))
5968
- continue;
5969
- createLocator(result, {
5970
- name: "xpath",
5971
- value: result.value,
5972
- type: "static",
5973
- isRecorded: "Y",
5974
- isSelfHealed: "Y"
5975
- });
5976
- usedXPathPatterns.add(pattern);
5977
- 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
+ }
5978
6827
  }
5979
6828
  }
5980
6829
  }
@@ -6005,12 +6854,20 @@
6005
6854
  console.error("Error processing locator:", locator, error);
6006
6855
  }
6007
6856
  }
6008
- if (finalLocators.length < 5) {
6857
+ if (!isIOSPlatform(platform) && finalLocators.length < 5) {
6009
6858
  const fallbackCandidates = [
6010
- { name: "id", value: getId(targetElement) },
6859
+ { name: "id", value: getId(targetElement, platform) },
6011
6860
  { name: "name", value: getName(targetElement) },
6012
- { name: "className", value: getClassName(targetElement) },
6013
- { 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
+ }
6014
6871
  ];
6015
6872
  for (const candidate of fallbackCandidates) {
6016
6873
  if (finalLocators.length > 4)
@@ -6024,7 +6881,7 @@
6024
6881
  continue;
6025
6882
  if (name === "className" && value.includes(" "))
6026
6883
  continue;
6027
- if (isUniqueInDOM(docmt, name, value, targetElement)) {
6884
+ if (isUniqueInDOM(docmt, name, value, targetElement, platform)) {
6028
6885
  createLocator(undefined, {
6029
6886
  name,
6030
6887
  type: "static",
@@ -6034,7 +6891,7 @@
6034
6891
  });
6035
6892
  }
6036
6893
  }
6037
- if (finalLocators.length < 5) {
6894
+ if (finalLocators.length < 5 && !isMobilePlatform(platform)) {
6038
6895
  parseCssSelectors(targetElement, "multiple").forEach((cssSelector) => {
6039
6896
  if (finalLocators.length > 4)
6040
6897
  return;
@@ -6042,7 +6899,7 @@
6042
6899
  return;
6043
6900
  if (locatorExists("cssSelector", cssSelector.value))
6044
6901
  return;
6045
- if (!isUniqueInDOM(docmt, "cssSelector", cssSelector.value, targetElement)) {
6902
+ if (!isUniqueInDOM(docmt, "cssSelector", cssSelector.value, targetElement, platform)) {
6046
6903
  return;
6047
6904
  }
6048
6905
  createLocator(undefined, {
@@ -6057,7 +6914,7 @@
6057
6914
  }
6058
6915
  const finalAutoHealedLocators = finalLocators.map((obj) => ({
6059
6916
  ...obj,
6060
- value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded)
6917
+ value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded, platform)
6061
6918
  }));
6062
6919
  const jsonResult = [
6063
6920
  {
@@ -6094,121 +6951,6 @@
6094
6951
  return null;
6095
6952
  };
6096
6953
 
6097
- function getOptimalClassChain(doc, domNode, uniqueAttributes) {
6098
- try {
6099
- if (!domNode ||
6100
- domNode.nodeType !== 1 ||
6101
- !domNode.tagName ||
6102
- domNode.tagName === "XCUIElementTypeApplication") {
6103
- return "";
6104
- }
6105
- const tag = domNode.tagName;
6106
- // Priority attributes
6107
- const priorityAttrs = ["name", "label", "value"];
6108
- // ---------- STEP 1 : TRY GOOD ATTRIBUTES ----------
6109
- for (const attrName of priorityAttrs) {
6110
- const attr = uniqueAttributes.find(a => a.name === attrName);
6111
- if (!attr)
6112
- continue;
6113
- let attrValue = attr.value;
6114
- if (!attrValue)
6115
- continue;
6116
- attrValue = attrValue.trim().replace(/\s+/g, " ");
6117
- if (isNumberExist(attrValue))
6118
- continue;
6119
- const xpath = `//${tag}[@${attrName}="${attrValue}"]`;
6120
- let count = 0;
6121
- try {
6122
- count = getCountOfXPath(xpath, domNode, doc);
6123
- }
6124
- catch (err) {
6125
- console.log(err);
6126
- continue;
6127
- }
6128
- const hasSpace = /\s/.test(attrValue);
6129
- if (count === 1) {
6130
- if (hasSpace) {
6131
- return `/${tag}[\`${attrName} CONTAINS "${attrValue}"\`]`;
6132
- }
6133
- return `/${tag}[\`${attrName} == "${attrValue}"\`]`;
6134
- }
6135
- if (count > 1 && domNode.parentElement) {
6136
- const siblings = Array.from(domNode.parentElement.children)
6137
- .filter(el => el.tagName === tag);
6138
- const index = siblings.indexOf(domNode) + 1;
6139
- if (hasSpace) {
6140
- return `/${tag}[\`${attrName} CONTAINS "${attrValue}"\`][${index}]`;
6141
- }
6142
- return `/${tag}[\`${attrName} == "${attrValue}"\`][${index}]`;
6143
- }
6144
- }
6145
- // ---------- STEP 2 : FALLBACK USING TAG INDEX ----------
6146
- let classChain = `/${tag}`;
6147
- if (domNode.parentElement) {
6148
- const siblings = Array.from(domNode.parentElement.children)
6149
- .filter(el => el.tagName === tag);
6150
- if (siblings.length > 1) {
6151
- const index = siblings.indexOf(domNode) + 1;
6152
- classChain += `[${index}]`;
6153
- }
6154
- }
6155
- const parentChain = getOptimalClassChain(doc, domNode.parentElement, uniqueAttributes);
6156
- return parentChain + classChain;
6157
- }
6158
- catch (error) {
6159
- console.log(`Unable to generate optimal -ios class chain : ${JSON.stringify(error)}`);
6160
- return null;
6161
- }
6162
- }
6163
- function getOptimalPredicateString(doc, domNode, uniqueAttributes) {
6164
- try {
6165
- // BASE CASE #1: If this isn't an element, we're above the root, or this is `XCUIElementTypeApplication`,
6166
- // which is not an official XCUITest element, return empty string
6167
- if (!domNode?.tagName ||
6168
- domNode?.nodeType !== 1 ||
6169
- domNode?.tagName === 'XCUIElementTypeApplication') {
6170
- return '';
6171
- }
6172
- // BASE CASE #2: Check all attributes and try to find the best way
6173
- let xpathAttributes = [];
6174
- let predicateString = [];
6175
- for (let attr of uniqueAttributes) {
6176
- const attrValue = attr.value;
6177
- const attrName = attr.name;
6178
- if (attrValue.length === 0) {
6179
- continue;
6180
- }
6181
- if (attrValue && !isNumberExist(attrValue)) {
6182
- xpathAttributes.push(`@${attrName}="${attrValue}"`);
6183
- const xpathe = `//*[${xpathAttributes.join(' and ')}]`;
6184
- predicateString.push(`${attrName} == "${attrValue}"`);
6185
- let othersWithAttr;
6186
- // If the XPath does not parse, move to the next unique attribute
6187
- try {
6188
- othersWithAttr = getCountOfXPath(xpathe, domNode, doc);
6189
- }
6190
- catch (ign) {
6191
- console.log(ign);
6192
- continue;
6193
- }
6194
- // If the attribute isn't actually unique, get it's index too
6195
- if (othersWithAttr === 1) {
6196
- return predicateString.join(' AND ');
6197
- }
6198
- }
6199
- }
6200
- }
6201
- catch (error) {
6202
- // If there's an unexpected exception, abort and don't get an XPath
6203
- console.log(`The most optimal '-ios predicate string' could not be determined because an error was thrown: '${JSON.stringify(error, null, 2)}'`);
6204
- }
6205
- return null;
6206
- }
6207
- const iosSelectors = {
6208
- getOptimalClassChain,
6209
- getOptimalPredicateString
6210
- };
6211
-
6212
6954
  const createXPathAPI = () => ({
6213
6955
  xpath,
6214
6956
  referenceXpaths: referenceXpath,