From 7b4b291d40464971c26f764627647eb3aa7f7250 Mon Sep 17 00:00:00 2001
From: Mykola Mokhnach <mokhnach@gmail.com>
Date: Wed, 12 Mar 2025 23:47:25 +0100
Subject: [PATCH 1/4] feat: Add 'limitXpathContextScope' setting

---
 .../Commands/FBSessionCommands.m              |   4 +
 WebDriverAgentLib/Utilities/FBConfiguration.h |   8 ++
 WebDriverAgentLib/Utilities/FBConfiguration.m |  12 ++
 WebDriverAgentLib/Utilities/FBSettings.h      |   1 +
 WebDriverAgentLib/Utilities/FBSettings.m      |   1 +
 WebDriverAgentLib/Utilities/FBXPath-Private.h |   7 +-
 WebDriverAgentLib/Utilities/FBXPath.m         | 119 ++++++++++++++----
 .../FBXPathIntegrationTests.m                 |  23 ++++
 WebDriverAgentTests/UnitTests/FBXPathTests.m  |  16 +--
 9 files changed, 154 insertions(+), 37 deletions(-)

diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m
index 8f84d7afe..67b6b81e9 100644
--- a/WebDriverAgentLib/Commands/FBSessionCommands.m
+++ b/WebDriverAgentLib/Commands/FBSessionCommands.m
@@ -351,6 +351,7 @@ + (NSArray *)routes
       FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]),
       FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]),
       FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]),
+      FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]),
 #if !TARGET_OS_TV
       FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation],
 #endif
@@ -445,6 +446,9 @@ + (NSArray *)routes
   if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) {
     [FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]];
   }
+  if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) {
+    [FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]];
+  }
 
 #if !TARGET_OS_TV
   if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) {
diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h
index ad9687b19..cb0a67dcf 100644
--- a/WebDriverAgentLib/Utilities/FBConfiguration.h
+++ b/WebDriverAgentLib/Utilities/FBConfiguration.h
@@ -103,6 +103,14 @@ extern NSString *const FBSnapshotMaxDepthKey;
 + (NSUInteger)mjpegServerFramerate;
 + (void)setMjpegServerFramerate:(NSUInteger)framerate;
 
+/**
+ Whether to limit the XPath scope to descendant items only while performing a lookup
+ in an element context. Enabled by default. Being disabled, allows to use XPath locators
+ like ".." in order to match parent items of the current context root.
+ */
++ (BOOL)limitXpathContextScope;
++ (void)setLimitXpathContextScope:(BOOL)enabled;
+
 /**
  The quality of display screenshots. The higher quality you set is the bigger screenshot size is.
  The highest quality value is 0 (lossless PNG) or 3 (lossless HEIC). The lowest quality is 2 (highly compressed JPEG).
diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m
index 1065b892e..f3cf8e42f 100644
--- a/WebDriverAgentLib/Utilities/FBConfiguration.m
+++ b/WebDriverAgentLib/Utilities/FBConfiguration.m
@@ -56,6 +56,7 @@
 static BOOL FBShouldUseCompactResponses;
 static NSString *FBElementResponseAttributes;
 static BOOL FBUseClearTextShortcut;
+static BOOL FBLimitXpathContextScope = YES;
 #if !TARGET_OS_TV
 static UIInterfaceOrientation FBScreenshotOrientation;
 #endif
@@ -438,6 +439,16 @@ + (BOOL)useClearTextShortcut
   return FBUseClearTextShortcut;
 }
 
++ (BOOL)limitXpathContextScope
+{
+  return FBLimitXpathContextScope;
+}
+
++ (void)setLimitXpathContextScope:(BOOL)enabled
+{
+  FBLimitXpathContextScope = enabled;
+}
+
 #if !TARGET_OS_TV
 + (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error
 {
@@ -503,6 +514,7 @@ + (void)resetSessionSettings
   // 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100.
   FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50);
   FBUseClearTextShortcut = YES;
+  FBLimitXpathContextScope = YES;
 #if !TARGET_OS_TV
   FBScreenshotOrientation = UIInterfaceOrientationUnknown;
 #endif
diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h
index 1a551c63f..7f6d57970 100644
--- a/WebDriverAgentLib/Utilities/FBSettings.h
+++ b/WebDriverAgentLib/Utilities/FBSettings.h
@@ -38,6 +38,7 @@ extern NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT;
 extern NSString* const FB_SETTING_MAX_TYPING_FREQUENCY;
 extern NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS;
 extern NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT;
+extern NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE;
 
 
 NS_ASSUME_NONNULL_END
diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m
index f5a61a8a8..c19889db3 100644
--- a/WebDriverAgentLib/Utilities/FBSettings.m
+++ b/WebDriverAgentLib/Utilities/FBSettings.m
@@ -34,3 +34,4 @@
 NSString* const FB_SETTING_MAX_TYPING_FREQUENCY = @"maxTypingFrequency";
 NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS = @"respectSystemAlerts";
 NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT = @"useClearTextShortcut";
+NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE = @"limitXpathContextScope";
diff --git a/WebDriverAgentLib/Utilities/FBXPath-Private.h b/WebDriverAgentLib/Utilities/FBXPath-Private.h
index 31bb403bf..7346b2c3f 100644
--- a/WebDriverAgentLib/Utilities/FBXPath-Private.h
+++ b/WebDriverAgentLib/Utilities/FBXPath-Private.h
@@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
  If `query` argument is assigned then `excludedAttributes` argument is effectively ignored.
  @return zero if the method has completed successfully
  */
-+ (int)xmlRepresentationWithRootElement:(id<FBElement>)root
++ (int)xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
                                  writer:(xmlTextWriterPtr)writer
                            elementStore:(nullable NSMutableDictionary *)elementStore
                                   query:(nullable NSString*)query
@@ -45,9 +45,12 @@ NS_ASSUME_NONNULL_BEGIN
  
  @param xpathQuery actual query. Should be valid XPath 1.0-compatible expression
  @param document libxml2-compatible document pointer
+ @param contextNode Optonal context node instance
  @return pointer to a libxml2-compatible structure with set of matched nodes or NULL in case of failure
  */
-+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc;
++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
+                     document:(xmlDocPtr)doc
+                  contextNode:(nullable xmlNodePtr)contextNode;
 
 @end
 
diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m
index d36291fdd..18793041f 100644
--- a/WebDriverAgentLib/Utilities/FBXPath.m
+++ b/WebDriverAgentLib/Utilities/FBXPath.m
@@ -11,6 +11,7 @@
 
 #import "FBConfiguration.h"
 #import "FBExceptions.h"
+#import "FBElementUtils.h"
 #import "FBLogger.h"
 #import "FBMacros.h"
 #import "FBXMLGenerationOptions.h"
@@ -145,7 +146,7 @@ + (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
     }
 
     if (rc >= 0) {
-      rc = [self xmlRepresentationWithRootElement:root
+      rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root]
                                            writer:writer
                                      elementStore:nil
                                             query:nil
@@ -193,10 +194,29 @@ + (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
   }
   NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
   int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
+  id<FBXCElementSnapshot> lookupScopeSnapshot = nil;
+  id<FBXCElementSnapshot> contextRootSnapshot = nil;
   if (rc < 0) {
     [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
   } else {
-    rc = [self xmlRepresentationWithRootElement:root
+    if (FBConfiguration.limitXpathContextScope) {
+      lookupScopeSnapshot = [self snapshotWithRoot:root];
+    } else {
+      if ([root isKindOfClass:XCUIElement.class]) {
+        lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application]];
+        contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
+          ? nil
+          : [(XCUIElement *)root fb_takeSnapshot:YES];
+      } else {
+        lookupScopeSnapshot = (id<FBXCElementSnapshot>)root;
+        contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id<FBXCElementSnapshot>)root;
+        while (nil != lookupScopeSnapshot.parent) {
+          lookupScopeSnapshot = lookupScopeSnapshot.parent;
+        }
+      }
+    }
+
+    rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot
                                          writer:writer
                                    elementStore:elementStore
                                           query:xpathQuery
@@ -214,7 +234,22 @@ + (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
     return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
   }
 
-  xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc];
+  xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc
+                                                          elementStore:elementStore.copy
+                                                           forSnapshot:contextRootSnapshot];
+  xmlNodePtr contextNode = NULL;
+  if (NULL != contextNodeQueryResult) {
+    xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval;
+    if (!xmlXPathNodeSetIsEmpty(nodeSet)) {
+      contextNode = nodeSet->nodeTab[0];
+    }
+  }
+  xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
+                                        document:doc
+                                     contextNode:contextNode];
+  if (NULL != contextNodeQueryResult) {
+    xmlXPathFreeObject(contextNodeQueryResult);
+  }
   if (NULL == queryResult) {
     xmlFreeTextWriter(writer);
     xmlFreeDoc(doc);
@@ -256,6 +291,36 @@ + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
   return matchingSnapshots.copy;
 }
 
++ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc
+                                     elementStore:(NSDictionary<NSString *, id<FBXCElementSnapshot>> *)elementStore
+                                      forSnapshot:(nullable id<FBXCElementSnapshot>)snapshot
+{
+  if (nil == snapshot) {
+    return NULL;
+  }
+
+  NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]];
+  if (nil == contextRootUid) {
+    return NULL;
+  }
+
+  for (NSString *key in elementStore) {
+    id<FBXCElementSnapshot> value = [elementStore objectForKey:key];
+    NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]];
+    if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) {
+      continue;
+    }
+    NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key];
+    xmlXPathObjectPtr queryResult = [self evaluate:indexQuery
+                                          document:doc
+                                       contextNode:NULL];
+    if (NULL != queryResult) {
+      return queryResult;
+    }
+  }
+  return NULL;
+}
+
 + (NSSet<Class> *)elementAttributesWithXPathQuery:(NSString *)query
 {
   if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) {
@@ -271,7 +336,7 @@ + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
   return result.copy;
 }
 
-+ (int)xmlRepresentationWithRootElement:(id<FBElement>)root
++ (int)xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
                                  writer:(xmlTextWriterPtr)writer
                            elementStore:(nullable NSMutableDictionary *)elementStore
                                   query:(nullable NSString*)query
@@ -312,14 +377,16 @@ + (int)xmlRepresentationWithRootElement:(id<FBElement>)root
   return 0;
 }
 
-+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc
++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
+                     document:(xmlDocPtr)doc
+                  contextNode:(nullable xmlNodePtr)contextNode
 {
   xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
   if (NULL == xpathCtx) {
     [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
     return NULL;
   }
-  xpathCtx->node = doc->children;
+  xpathCtx->node = NULL == contextNode ? doc->children : contextNode;
 
   xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
   if (NULL == xpathObj) {
@@ -360,7 +427,7 @@ + (int)recordElementAttributes:(xmlTextWriterPtr)writer
   return 0;
 }
 
-+ (int)writeXmlWithRootElement:(id<FBElement>)root
++ (int)writeXmlWithRootElement:(id<FBXCElementSnapshot>)root
                      indexPath:(nullable NSString *)indexPath
                   elementStore:(nullable NSMutableDictionary *)elementStore
             includedAttributes:(nullable NSSet<Class> *)includedAttributes
@@ -368,29 +435,13 @@ + (int)writeXmlWithRootElement:(id<FBElement>)root
 {
   NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil);
 
-  __block id<FBXCElementSnapshot> currentSnapshot;
-  NSArray<id<FBXCElementSnapshot>> *children;
-  if ([root isKindOfClass:XCUIElement.class]) {
-    XCUIElement *element = (XCUIElement *)root;
-    if (nil == includedAttributes || [includedAttributes containsObject:FBVisibleAttribute.class]) {
-      // If the app is not idle state while we retrieve the visiblity state
-      // then the snapshot retrieval operation might freeze and time out
-      [element.application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
-    }
-    @autoreleasepool {
-      currentSnapshot = [element fb_takeSnapshot:YES];
-    }
-    children = currentSnapshot.children;
-  } else {
-    currentSnapshot = (id<FBXCElementSnapshot>)root;
-    children = currentSnapshot.children;
-  }
+  NSArray<id<FBXCElementSnapshot>> *children = root.children;
 
   if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
-    [elementStore setObject:currentSnapshot forKey:topNodeIndexPath];
+    [elementStore setObject:root forKey:topNodeIndexPath];
   }
 
-  FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:currentSnapshot];
+  FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root];
   int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]);
   if (rc < 0) {
     [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc];
@@ -398,7 +449,7 @@ + (int)writeXmlWithRootElement:(id<FBElement>)root
   }
 
   rc = [self recordElementAttributes:writer
-                          forElement:currentSnapshot
+                          forElement:root
                            indexPath:indexPath
                   includedAttributes:includedAttributes];
   if (rc < 0) {
@@ -431,6 +482,20 @@ + (int)writeXmlWithRootElement:(id<FBElement>)root
   return 0;
 }
 
++ (id<FBXCElementSnapshot>)snapshotWithRoot:(id<FBElement>)root
+{
+  if (![root isKindOfClass:XCUIElement.class]) {
+    return (id<FBXCElementSnapshot>)root;
+  }
+
+  // If the app is not idle state while we retrieve the visiblity state
+  // then the snapshot retrieval operation might freeze and time out
+  [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
+  @autoreleasepool {
+    return [(XCUIElement *)root fb_takeSnapshot:YES];
+  }
+}
+
 @end
 
 
diff --git a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m
index adbadeb32..77b3c2bcf 100644
--- a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m
+++ b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m
@@ -96,6 +96,29 @@ - (void)testFindMatchesInElement
   }
 }
 
+- (void)testFindMatchesWithoutContextScopeLimit
+{
+  XCUIElement *button = self.testedApplication.buttons.firstMatch;
+  BOOL previousValue = FBConfiguration.limitXpathContextScope;
+  FBConfiguration.limitXpathContextScope = NO;
+  @try {
+    NSArray *parentSnapshots = [FBXPath matchesWithRootElement:button forQuery:@".."];
+    XCTAssertEqual(parentSnapshots.count, 1);
+    XCTAssertEqualObjects(
+                          [FBXCElementSnapshotWrapper ensureWrapped:[parentSnapshots objectAtIndex:0]].wdLabel,
+                          @"MainView"
+                          );
+    NSArray *currentSnapshots = [FBXPath matchesWithRootElement:button forQuery:@"."];
+    XCTAssertEqual(currentSnapshots.count, 1);
+    XCTAssertEqualObjects(
+                          [FBXCElementSnapshotWrapper ensureWrapped:[currentSnapshots objectAtIndex:0]].wdType,
+                          @"XCUIElementTypeButton"
+                          );
+  } @finally {
+    FBConfiguration.limitXpathContextScope = previousValue;
+  }
+}
+
 - (void)testFindMatchesInElementWithDotNotation
 {
   NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@".//XCUIElementTypeButton"];
diff --git a/WebDriverAgentTests/UnitTests/FBXPathTests.m b/WebDriverAgentTests/UnitTests/FBXPathTests.m
index ad0d1988f..3d549e0b2 100644
--- a/WebDriverAgentTests/UnitTests/FBXPathTests.m
+++ b/WebDriverAgentTests/UnitTests/FBXPathTests.m
@@ -21,7 +21,7 @@ @interface FBXPathTests : XCTestCase
 
 @implementation FBXPathTests
 
-- (NSString *)xmlStringWithElement:(id<FBElement>)element
+- (NSString *)xmlStringWithElement:(id<FBXCElementSnapshot>)snapshot
                         xpathQuery:(nullable NSString *)query
                excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
 {
@@ -33,7 +33,7 @@ - (NSString *)xmlStringWithElement:(id<FBElement>)element
   xmlChar *xmlbuff = NULL;
   int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL);
   if (rc >= 0) {
-    rc = [FBXPath xmlRepresentationWithRootElement:element
+    rc = [FBXPath xmlRepresentationWithRootElement:snapshot
                                             writer:writer
                                       elementStore:elementStore
                                              query:query
@@ -60,7 +60,7 @@ - (void)testDefaultXPathPresentation
 {
   XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
   id<FBElement> element = (id<FBElement>)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot];
-  NSString *resultXml = [self xmlStringWithElement:element
+  NSString *resultXml = [self xmlStringWithElement:(id<FBXCElementSnapshot>)element
                                         xpathQuery:nil
                                excludingAttributes:nil];
   NSString *expectedXml = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" placeholderValue=\"%@\" private_indexPath=\"top\"/>\n",
@@ -72,7 +72,7 @@ - (void)testtXPathPresentationWithSomeAttributesExcluded
 {
   XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
   id<FBElement> element = (id<FBElement>)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot];
-  NSString *resultXml = [self xmlStringWithElement:element
+  NSString *resultXml = [self xmlStringWithElement:(id<FBXCElementSnapshot>)element
                                         xpathQuery:nil
                                excludingAttributes:@[@"type", @"visible", @"value", @"index"]];
   NSString *expectedXml = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<%@ name=\"%@\" label=\"%@\" enabled=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" placeholderValue=\"%@\" private_indexPath=\"top\"/>\n",
@@ -86,7 +86,7 @@ - (void)testXPathPresentationBasedOnQueryMatchingAllAttributes
   snapshot.value = @"йоло<>&\"";
   snapshot.label = @"a\nb";
   id<FBElement> element = (id<FBElement>)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot];
-  NSString *resultXml = [self xmlStringWithElement:element
+  NSString *resultXml = [self xmlStringWithElement:(id<FBXCElementSnapshot>)element
                                         xpathQuery:[NSString stringWithFormat:@"//%@[@*]", element.wdType]
                                excludingAttributes:@[@"visible"]];
   NSString *expectedXml = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" hittable=\"%@\" placeholderValue=\"%@\" private_indexPath=\"top\"/>\n",
@@ -98,7 +98,7 @@ - (void)testXPathPresentationBasedOnQueryMatchingSomeAttributes
 {
   XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
   id<FBElement> element = (id<FBElement>)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot];
-  NSString *resultXml = [self xmlStringWithElement:element
+  NSString *resultXml = [self xmlStringWithElement:(id<FBXCElementSnapshot>)element
                                         xpathQuery:[NSString stringWithFormat:@"//%@[@%@ and contains(@%@, 'blabla')]", element.wdType, @"value", @"name"]
                                excludingAttributes:nil];
   NSString *expectedXml = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<%@ value=\"%@\" name=\"%@\" private_indexPath=\"top\"/>\n",
@@ -117,7 +117,7 @@ - (void)testSnapshotXPathResultsMatching
   NSString *query = [NSString stringWithFormat:@"//%@", root.wdType];
   int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL);
   if (rc >= 0) {
-    rc = [FBXPath xmlRepresentationWithRootElement:root
+    rc = [FBXPath xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
                                             writer:writer
                                       elementStore:elementStore
                                              query:query
@@ -132,7 +132,7 @@ - (void)testSnapshotXPathResultsMatching
     XCTFail(@"Unable to create the source XML document");
   }
 
-  xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc];
+  xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL];
   if (NULL == queryResult) {
     xmlFreeTextWriter(writer);
     xmlFreeDoc(doc);

From d5bca9b46f6a27b78980d504fb500fae6eac09a3 Mon Sep 17 00:00:00 2001
From: Mykola Mokhnach <mokhnach@gmail.com>
Date: Wed, 12 Mar 2025 23:55:29 +0100
Subject: [PATCH 2/4] make linter happy

---
 lib/webdriveragent.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js
index a2a9dbb0d..f36afbd1f 100644
--- a/lib/webdriveragent.js
+++ b/lib/webdriveragent.js
@@ -579,8 +579,8 @@ export class WebDriverAgent {
   setupProxies (sessionId) {
     const proxyOpts = {
       log: this.log,
-      server: this.url.hostname,
-      port: this.url.port,
+      server: this.url.hostname ?? undefined,
+      port: parseInt(this.url.port ?? '', 10) ?? undefined,
       base: this.basePath,
       timeout: this.wdaConnectionTimeout,
       keepAlive: true,

From 479a5d4be7a970fd61124bf59e831004165f4423 Mon Sep 17 00:00:00 2001
From: Mykola Mokhnach <mokhnach@gmail.com>
Date: Wed, 12 Mar 2025 23:56:18 +0100
Subject: [PATCH 3/4] moar

---
 lib/webdriveragent.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js
index f36afbd1f..73b056bd5 100644
--- a/lib/webdriveragent.js
+++ b/lib/webdriveragent.js
@@ -580,7 +580,7 @@ export class WebDriverAgent {
     const proxyOpts = {
       log: this.log,
       server: this.url.hostname ?? undefined,
-      port: parseInt(this.url.port ?? '', 10) ?? undefined,
+      port: parseInt(this.url.port ?? '', 10) || undefined,
       base: this.basePath,
       timeout: this.wdaConnectionTimeout,
       keepAlive: true,

From 5463b04f263ec60e9551449b6431fc3c53e15586 Mon Sep 17 00:00:00 2001
From: Mykola Mokhnach <mokhnach@gmail.com>
Date: Thu, 13 Mar 2025 07:57:50 +0100
Subject: [PATCH 4/4] Bump versions

---
 .github/workflows/functional-test.yml | 14 +++++---------
 test/unit/webdriveragent-specs.js     |  8 ++++----
 2 files changed, 9 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml
index b8c9855aa..524f337dc 100644
--- a/.github/workflows/functional-test.yml
+++ b/.github/workflows/functional-test.yml
@@ -13,17 +13,13 @@ jobs:
       matrix:
         test_targets:
           - HOST_OS: 'macos-15'
-            XCODE_VERSION: '16.1.0'
-            IOS_VERSION: '18.1'
+            XCODE_VERSION: '16.2'
+            IOS_VERSION: '18.2'
             IOS_MODEL: iPhone 16 Plus
-          - HOST_OS: 'macos-14'
-            XCODE_VERSION: '15.3'
-            IOS_VERSION: '17.4'
+          - HOST_OS: 'macos-15'
+            XCODE_VERSION: '15.4'
+            IOS_VERSION: '17.5'
             IOS_MODEL: iPhone 15 Plus
-          - HOST_OS: 'macos-13'
-            XCODE_VERSION: 14.3.1
-            IOS_VERSION: '16.4'
-            IOS_MODEL: iPhone 14 Plus
 
     # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md
     runs-on: ${{matrix.test_targets.HOST_OS}}
diff --git a/test/unit/webdriveragent-specs.js b/test/unit/webdriveragent-specs.js
index 1b844e08a..487f71d91 100644
--- a/test/unit/webdriveragent-specs.js
+++ b/test/unit/webdriveragent-specs.js
@@ -71,10 +71,10 @@ describe('launch', function () {
     await agent.launch('sessionId').should.eventually.eql({build: 'data'});
     agent.url.href.should.eql(override);
     agent.jwproxy.server.should.eql('mockurl');
-    agent.jwproxy.port.should.eql('8100');
+    agent.jwproxy.port.should.eql(8100);
     agent.jwproxy.base.should.eql('');
     agent.noSessionProxy.server.should.eql('mockurl');
-    agent.noSessionProxy.port.should.eql('8100');
+    agent.noSessionProxy.port.should.eql(8100);
     agent.noSessionProxy.base.should.eql('');
     wdaStub.reset();
   });
@@ -97,10 +97,10 @@ describe('use wda proxy url', function () {
     agent.url.hostname.should.eql('127.0.0.1');
     agent.url.path.should.eql('/aabbccdd');
     agent.jwproxy.server.should.eql('127.0.0.1');
-    agent.jwproxy.port.should.eql('8100');
+    agent.jwproxy.port.should.eql(8100);
     agent.jwproxy.base.should.eql('/aabbccdd');
     agent.noSessionProxy.server.should.eql('127.0.0.1');
-    agent.noSessionProxy.port.should.eql('8100');
+    agent.noSessionProxy.port.should.eql(8100);
     agent.noSessionProxy.base.should.eql('/aabbccdd');
   });
 });