appium-mac2-driver 4.1.1 → 4.3.0

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/WebDriverAgentMac/IntegrationTests/AMFindElementTests.m +165 -2
  3. package/WebDriverAgentMac/IntegrationTests/AMIntegrationTestCase.h +11 -0
  4. package/WebDriverAgentMac/IntegrationTests/AMIntegrationTestCase.m +33 -0
  5. package/WebDriverAgentMac/IntegrationTests/AMW3CActionsTests.m +7 -9
  6. package/WebDriverAgentMac/WebDriverAgent/Base.lproj/Main.storyboard +16 -1
  7. package/WebDriverAgentMac/WebDriverAgent/WebViewController.h +21 -0
  8. package/WebDriverAgentMac/WebDriverAgent/WebViewController.m +49 -0
  9. package/WebDriverAgentMac/WebDriverAgentLib/Categories/NSPredicate+FBFormat.m +80 -0
  10. package/WebDriverAgentMac/WebDriverAgentLib/Categories/XCUIElement+AMAttributes.h +29 -1
  11. package/WebDriverAgentMac/WebDriverAgentLib/Categories/XCUIElement+AMAttributes.m +66 -15
  12. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMHTTPServer.h +84 -0
  13. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMHTTPServer.m +493 -0
  14. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMTCPSocket.h +109 -0
  15. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMTCPSocket.m +235 -0
  16. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBWebServer.h +1 -1
  17. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBWebServer.m +9 -21
  18. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteRequest.h +36 -0
  19. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteRequest.m +33 -0
  20. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteResponse.h +37 -0
  21. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteResponse.m +62 -0
  22. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +22 -0
  23. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +5 -11
  24. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/FBElementUtils.m +7 -1
  25. package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +46 -180
  26. package/npm-shrinkwrap.json +75 -146
  27. package/package.json +2 -1
  28. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h +0 -1226
  29. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +0 -8513
  30. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h +0 -1036
  31. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m +0 -5632
  32. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h +0 -12
  33. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m +0 -88
  34. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h +0 -56
  35. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m +0 -106
  36. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h +0 -107
  37. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m +0 -2238
  38. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h +0 -122
  39. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h +0 -48
  40. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +0 -114
  41. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h +0 -149
  42. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h +0 -200
  43. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m +0 -720
  44. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h +0 -13
  45. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m +0 -83
  46. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h +0 -9
  47. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +0 -40
  48. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h +0 -13
  49. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m +0 -84
  50. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h +0 -18
  51. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m +0 -11
  52. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h +0 -16
  53. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m +0 -50
  54. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h +0 -20
  55. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m +0 -66
  56. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h +0 -5
  57. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m +0 -142
  58. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h +0 -55
  59. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m +0 -288
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [4.3.0](https://github.com/appium/appium-mac2-driver/compare/v4.2.0...v4.3.0) (2026-08-26)
2
+
3
+ ### Features
4
+
5
+ * port WebDriverAgentMac's HTTP server to Network.framework ([#415](https://github.com/appium/appium-mac2-driver/issues/415)) ([f4ff684](https://github.com/appium/appium-mac2-driver/commit/f4ff68477925a0839c004a280bdda447cf9a4e17))
6
+
7
+ ## [4.2.0](https://github.com/appium/appium-mac2-driver/compare/v4.1.1...v4.2.0) (2026-08-10)
8
+
9
+ ### Features
10
+
11
+ * locate WKWebView web elements via predicate string and class chain ([#412](https://github.com/appium/appium-mac2-driver/issues/412)) ([42a49aa](https://github.com/appium/appium-mac2-driver/commit/42a49aa8c321a2b554facfcf330d61d4feff99ed))
12
+
1
13
  ## [4.1.1](https://github.com/appium/appium-mac2-driver/compare/v4.1.0...v4.1.1) (2026-08-08)
2
14
 
3
15
  ### Bug Fixes
@@ -18,10 +18,15 @@
18
18
 
19
19
  #import "AMIntegrationTestCase.h"
20
20
  #import "FBConfiguration.h"
21
+ #import "XCUIElement+AMAttributes.h"
21
22
  #import "XCUIElement+FBFind.h"
22
23
  #import "XCUIElement+FBClassChain.h"
23
24
 
24
25
 
26
+ static NSString *const AMWebContainerDomId = @"am-web-container";
27
+ static NSString *const AMWebButtonDomId = @"am-dom-button";
28
+ static NSTimeInterval const AMWebContentTimeout = 10.0;
29
+
25
30
  @interface AMFindElementTests : AMIntegrationTestCase
26
31
  @end
27
32
 
@@ -36,6 +41,34 @@
36
41
  });
37
42
  }
38
43
 
44
+ - (void)tearDown
45
+ {
46
+ // Other test classes assume the app is on the Buttons tab by default and do not
47
+ // switch to it themselves - restore it so tests that dwell on the WebView tab here
48
+ // do not leak that state into whichever test class happens to run next.
49
+ [self switchToButtonsTab];
50
+ [super tearDown];
51
+ }
52
+
53
+ /**
54
+ WebKit builds a web view's accessibility subtree lazily, so the very first lookup
55
+ after the page appears may need a retry before the web content shows up. Polls the
56
+ given lookup until it finds something or the timeout elapses.
57
+ */
58
+ - (NSArray<XCUIElement *> *)am_waitForWebContentUsingBlock:(NSArray<XCUIElement *> *(^)(void))block
59
+ {
60
+ NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:AMWebContentTimeout];
61
+ NSArray<XCUIElement *> *matches = @[];
62
+ do {
63
+ matches = block();
64
+ if (matches.count > 0) {
65
+ return matches;
66
+ }
67
+ [NSThread sleepForTimeInterval:0.5];
68
+ } while ([deadline timeIntervalSinceNow] > 0);
69
+ return matches;
70
+ }
71
+
39
72
  - (void)testSingleDescendantWithIdentifier
40
73
  {
41
74
  NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingIdentifier:@"_XCUI:CloseWindow"
@@ -46,10 +79,11 @@
46
79
 
47
80
  - (void)testSingleDescendantWithIdentifierWhileDomIdFallbackIsEnabled
48
81
  {
82
+ [self skipUnlessAccessibilityTrusted];
83
+ [self switchToWebViewTab];
49
84
  FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = YES;
50
85
  @try {
51
- // The application under test hosts no web content, so the fallback must find
52
- // nothing extra and native identifiers must keep resolving as usual
86
+ // Native identifiers must keep resolving as usual
53
87
  NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingIdentifier:@"_XCUI:CloseWindow"
54
88
  shouldReturnAfterFirstMatch:NO];
55
89
  XCTAssertEqual(matches.count, 1);
@@ -58,11 +92,28 @@
58
92
  NSArray<XCUIElement *> *noMatches = [self.testedApplication fb_descendantsMatchingIdentifier:@"there_is_no_such_dom_id"
59
93
  shouldReturnAfterFirstMatch:NO];
60
94
  XCTAssertEqual(noMatches.count, 0);
95
+
96
+ // The web button has no native accessibility identifier, only a DOM id - the
97
+ // fallback must resolve it
98
+ NSArray<XCUIElement *> *webMatches = [self am_waitForWebContentUsingBlock:^NSArray<XCUIElement *> * {
99
+ return [self.testedApplication fb_descendantsMatchingIdentifier:AMWebButtonDomId
100
+ shouldReturnAfterFirstMatch:NO];
101
+ }];
102
+ XCTAssertEqual(webMatches.count, 1);
103
+ XCTAssertEqual(webMatches.firstObject.elementType, XCUIElementTypeButton);
61
104
  } @finally {
62
105
  FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = NO;
63
106
  }
64
107
  }
65
108
 
109
+ - (void)testSingleDescendantWithIdentifierFindsNothingForDomIdWhenFallbackIsDisabled
110
+ {
111
+ [self switchToWebViewTab];
112
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingIdentifier:AMWebButtonDomId
113
+ shouldReturnAfterFirstMatch:NO];
114
+ XCTAssertEqual(matches.count, 0);
115
+ }
116
+
66
117
  - (void)testSingleDescendantWithClassName
67
118
  {
68
119
  NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingClassName:@"XCUIElementTypeButton"
@@ -144,4 +195,116 @@
144
195
  XCTAssertEqualObjects([matches objectAtIndex:2].identifier, @"_XCUI:MinimizeWindow");
145
196
  }
146
197
 
198
+ - (void)testSingleDescendantWithPredicateMatchingAmIdentifierWhileDomIdFallbackIsEnabled
199
+ {
200
+ [self skipUnlessAccessibilityTrusted];
201
+ [self switchToWebViewTab];
202
+ FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = YES;
203
+ @try {
204
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"amIdentifier == %@ AND elementType == %lu",
205
+ AMWebButtonDomId, XCUIElementTypeButton];
206
+ NSArray<XCUIElement *> *matches = [self am_waitForWebContentUsingBlock:^NSArray<XCUIElement *> * {
207
+ return [self.testedApplication fb_descendantsMatchingPredicate:predicate
208
+ shouldReturnAfterFirstMatch:NO];
209
+ }];
210
+ XCTAssertEqual(matches.count, 1);
211
+ XCTAssertEqual(matches.firstObject.elementType, XCUIElementTypeButton);
212
+ } @finally {
213
+ FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = NO;
214
+ }
215
+ }
216
+
217
+ - (void)testSingleDescendantWithPredicateAmIdentifierAliasesNativeIdentifierWhenFallbackIsDisabled
218
+ {
219
+ // amIdentifier mirrors am_identifier: with the setting off it is just the native
220
+ // identifier, same as the standard identifier attribute
221
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"amIdentifier == %@", @"_XCUI:CloseWindow"];
222
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingPredicate:predicate
223
+ shouldReturnAfterFirstMatch:NO];
224
+ XCTAssertEqual(matches.count, 1);
225
+ }
226
+
227
+ - (void)testSingleDescendantWithPredicateAmIdentifierFindsNothingForDomIdWhenFallbackIsDisabled
228
+ {
229
+ [self switchToWebViewTab];
230
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"amIdentifier == %@", AMWebButtonDomId];
231
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingPredicate:predicate
232
+ shouldReturnAfterFirstMatch:NO];
233
+ XCTAssertEqual(matches.count, 0);
234
+ }
235
+
236
+ - (void)testSingleDescendantWithPredicateMatchingNestedAmRectKeyPath
237
+ {
238
+ // A nested key path into an am_* attribute's resolved value (amRect is a
239
+ // {x, y, width, height} dictionary) must resolve through the same block-predicate
240
+ // rewrite as a bare am_* reference, not fall through to native key path resolution.
241
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingIdentifier:@"_XCUI:CloseWindow"
242
+ shouldReturnAfterFirstMatch:YES];
243
+ XCTAssertEqual(matches.count, 1);
244
+ NSNumber *x = matches.firstObject.am_rect[@"x"];
245
+ XCTAssertNotNil(x);
246
+
247
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identifier == %@ AND amRect.x == %@",
248
+ @"_XCUI:CloseWindow", x];
249
+ NSArray<XCUIElement *> *nestedMatches = [self.testedApplication fb_descendantsMatchingPredicate:predicate
250
+ shouldReturnAfterFirstMatch:NO];
251
+ XCTAssertEqual(nestedMatches.count, 1);
252
+ XCTAssertEqualObjects(nestedMatches.firstObject.identifier, @"_XCUI:CloseWindow");
253
+
254
+ NSPredicate *nonMatchingPredicate = [NSPredicate predicateWithFormat:@"identifier == %@ AND amRect.x == %@",
255
+ @"_XCUI:CloseWindow", @(x.doubleValue + 1000)];
256
+ NSArray<XCUIElement *> *noMatches = [self.testedApplication fb_descendantsMatchingPredicate:nonMatchingPredicate
257
+ shouldReturnAfterFirstMatch:NO];
258
+ XCTAssertEqual(noMatches.count, 0);
259
+ }
260
+
261
+ - (void)testSingleDescendantWithClassChainMatchingAmIdentifierOnLastSegmentWhileDomIdFallbackIsEnabled
262
+ {
263
+ [self skipUnlessAccessibilityTrusted];
264
+ [self switchToWebViewTab];
265
+ FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = YES;
266
+ @try {
267
+ NSString *query = [NSString stringWithFormat:@"**/XCUIElementTypeButton[`amIdentifier == '%@'`]", AMWebButtonDomId];
268
+ NSArray<XCUIElement *> *matches = [self am_waitForWebContentUsingBlock:^NSArray<XCUIElement *> * {
269
+ return [self.testedApplication fb_descendantsMatchingClassChain:query
270
+ shouldReturnAfterFirstMatch:NO];
271
+ }];
272
+ XCTAssertEqual(matches.count, 1);
273
+ XCTAssertEqual(matches.firstObject.elementType, XCUIElementTypeButton);
274
+ } @finally {
275
+ FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = NO;
276
+ }
277
+ }
278
+
279
+ - (void)testSingleDescendantWithClassChainMatchingAmIdentifierOnAncestorSegmentWhileDomIdFallbackIsEnabled
280
+ {
281
+ [self skipUnlessAccessibilityTrusted];
282
+ [self switchToWebViewTab];
283
+ FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = YES;
284
+ @try {
285
+ // The amIdentifier predicate sits on a non-terminal (ancestor) segment here, not
286
+ // the last one - this works because every class chain segment's predicate is
287
+ // formatted through the same NSPredicate+FBFormat rewrite, regardless of position
288
+ NSString *query = [NSString stringWithFormat:@"**/XCUIElementTypeGroup[`amIdentifier == '%@'`]/XCUIElementTypeButton",
289
+ AMWebContainerDomId];
290
+ NSArray<XCUIElement *> *matches = [self am_waitForWebContentUsingBlock:^NSArray<XCUIElement *> * {
291
+ return [self.testedApplication fb_descendantsMatchingClassChain:query
292
+ shouldReturnAfterFirstMatch:NO];
293
+ }];
294
+ XCTAssertEqual(matches.count, 1);
295
+ XCTAssertEqual(matches.firstObject.elementType, XCUIElementTypeButton);
296
+ } @finally {
297
+ FBConfiguration.sharedConfiguration.useDomIdAsAccessibilityId = NO;
298
+ }
299
+ }
300
+
301
+ - (void)testSingleDescendantWithClassChainAmIdentifierFindsNothingForDomIdWhenFallbackIsDisabled
302
+ {
303
+ [self switchToWebViewTab];
304
+ NSString *query = [NSString stringWithFormat:@"**/XCUIElementTypeButton[`amIdentifier == '%@'`]", AMWebButtonDomId];
305
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingClassChain:query
306
+ shouldReturnAfterFirstMatch:NO];
307
+ XCTAssertEqual(matches.count, 0);
308
+ }
309
+
147
310
  @end
@@ -32,6 +32,17 @@ NS_ASSUME_NONNULL_BEGIN
32
32
 
33
33
  - (void)switchToEditsTab;
34
34
 
35
+ - (void)switchToWebViewTab;
36
+
37
+ /**
38
+ Skips the current test unless this process is trusted for the Accessibility API.
39
+ Tests that rely on the useDomIdAsAccessibilityId fallback need this - without it,
40
+ WebKit DOM identifier resolution silently returns nothing, which otherwise surfaces
41
+ as a confusing "found 0 elements" test failure rather than a clear "grant
42
+ Accessibility permission and re-run" skip reason.
43
+ */
44
+ - (void)skipUnlessAccessibilityTrusted;
45
+
35
46
  @end
36
47
 
37
48
  NS_ASSUME_NONNULL_END
@@ -16,6 +16,7 @@
16
16
 
17
17
  #import "AMIntegrationTestCase.h"
18
18
 
19
+ #import "AMSnapshotUtils.h"
19
20
  #import "FBConfiguration.h"
20
21
 
21
22
  @interface AMIntegrationTestCase ()
@@ -53,4 +54,36 @@
53
54
  [self.testedApplication.radioButtons[@"Edits"].firstMatch click];
54
55
  }
55
56
 
57
+ - (void)switchToWebViewTab
58
+ {
59
+ [self.testedApplication.radioButtons[@"WebView"].firstMatch click];
60
+ }
61
+
62
+ /**
63
+ Reading AXDOMIdentifier (unlike ordinary XCTest snapshotting) goes through the macOS
64
+ Accessibility API, which requires the process to be trusted under System Settings > Privacy &
65
+ Security > Accessibility. Two things make that grant easy to lose track of while iterating
66
+ locally in Xcode:
67
+
68
+ - Ad-hoc code signing loses the grant on every rebuild. macOS ties an Accessibility grant to
69
+ the binary's signing identity (its CDHash), and the default ad-hoc signature
70
+ (Signature=adhoc, no Team ID) gets a new CDHash on every build. The permission then appears
71
+ to "not stick" - it was actually granted, just to a binary that no longer exists. Assigning
72
+ a stable signing identity (e.g. an "Apple Development" certificate under a real Team ID) in
73
+ the target's Signing & Capabilities keeps the CDHash constant across rebuilds, so the grant
74
+ persists. This has to be done for both the WebDriverAgent app target and the
75
+ IntegrationTests test bundle target - granting only one leaves the other still ad-hoc signed
76
+ and still losing trust every build.
77
+ - xcodebuild build alone does not regenerate the signed XCTRunner wrapper app that actually
78
+ hosts and runs the tests, so a plain build after a signing change can silently run against a
79
+ stale runner. Use build-for-testing (or test, or Xcode's own Test action) instead.
80
+ */
81
+ - (void)skipUnlessAccessibilityTrusted
82
+ {
83
+ XCTSkipUnless(AMSnapshotUtils.isAccessibilityTrusted,
84
+ @"This process is not trusted for the Accessibility API. Grant Accessibility "
85
+ @"permission to it in System Settings > Privacy & Security > Accessibility "
86
+ @"(note: an ad-hoc-signed build's grant does not survive a rebuild), then re-run.");
87
+ }
88
+
56
89
  @end
@@ -18,6 +18,7 @@
18
18
 
19
19
  #import "AMIntegrationTestCase.h"
20
20
  #import "XCUIApplication+FBW3CActions.h"
21
+ #import "XCUIElement+AMEditable.h"
21
22
 
22
23
 
23
24
  @interface AMW3CActionsTests : AMIntegrationTestCase
@@ -49,15 +50,6 @@
49
50
  },
50
51
  ],
51
52
 
52
- // Chain element with empty 'actions'
53
- @[@{
54
- @"type": @"pointer",
55
- @"id": @"finger1",
56
- @"parameters": @{@"pointerType": @"mouse"},
57
- @"actions": @[],
58
- },
59
- ],
60
-
61
53
  // Chain element without type
62
54
  @[@{
63
55
  @"id": @"finger1",
@@ -343,6 +335,9 @@
343
335
  [self switchToEditsTab];
344
336
  XCUIElement *edit = self.testedApplication.textFields.firstMatch;
345
337
  [edit click];
338
+ NSError *clearError;
339
+ XCTAssertTrue([edit am_clearTextWithError:&clearError]);
340
+ XCTAssertNil(clearError);
346
341
 
347
342
  NSArray<NSDictionary<NSString *, id> *> *gesture =
348
343
  @[@{
@@ -375,6 +370,9 @@
375
370
  [self switchToEditsTab];
376
371
  XCUIElement *edit = self.testedApplication.textFields.firstMatch;
377
372
  [edit click];
373
+ NSError *clearError;
374
+ XCTAssertTrue([edit am_clearTextWithError:&clearError]);
375
+ XCTAssertNil(clearError);
378
376
 
379
377
  NSArray<NSDictionary<NSString *, id> *> *gesture =
380
378
  @[@{
@@ -685,7 +685,7 @@
685
685
  <scene sceneID="R2V-B0-nI4">
686
686
  <objects>
687
687
  <windowController id="B8D-0N-5wS" sceneMemberID="viewController">
688
- <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
688
+ <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" restorable="NO" id="IQv-IB-iLA">
689
689
  <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
690
690
  <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
691
691
  <rect key="contentRect" x="196" y="240" width="480" height="270"/>
@@ -709,6 +709,7 @@
709
709
  <tabViewItems>
710
710
  <tabViewItem identifier="" id="Mia-WI-iEB"/>
711
711
  <tabViewItem identifier="" id="sIZ-jZ-jj9"/>
712
+ <tabViewItem identifier="" id="Nlk-3F-Wcb"/>
712
713
  </tabViewItems>
713
714
  <tabView key="tabView" type="noTabsNoBorder" id="Rch-4v-RJW">
714
715
  <rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
@@ -722,6 +723,7 @@
722
723
  <outlet property="tabView" destination="Rch-4v-RJW" id="1XV-Um-WgP"/>
723
724
  <segue destination="Pkx-be-7b9" kind="relationship" relationship="tabItems" id="IJQ-4v-ZpO"/>
724
725
  <segue destination="bnY-XE-5sK" kind="relationship" relationship="tabItems" id="MtI-PF-Enx"/>
726
+ <segue destination="Qzv-6y-WKw" kind="relationship" relationship="tabItems" id="Pma-8s-CkV"/>
725
727
  </connections>
726
728
  </tabViewController>
727
729
  <customObject id="iEe-Sq-WC0" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
@@ -832,5 +834,18 @@
832
834
  </objects>
833
835
  <point key="canvasLocation" x="545" y="-408"/>
834
836
  </scene>
837
+ <!--WebView-->
838
+ <scene sceneID="tGx-1M-hRz">
839
+ <objects>
840
+ <viewController title="WebView" id="Qzv-6y-WKw" customClass="WebViewController" sceneMemberID="viewController">
841
+ <view key="view" id="Fbv-Y8-Enh">
842
+ <rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
843
+ <autoresizingMask key="autoresizingMask"/>
844
+ </view>
845
+ </viewController>
846
+ <customObject id="Xdm-hz-1Yc" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
847
+ </objects>
848
+ <point key="canvasLocation" x="1075" y="-408"/>
849
+ </scene>
835
850
  </scenes>
836
851
  </document>
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Licensed under the Apache License, Version 2.0 (the "License");
3
+ * you may not use this file except in compliance with the License.
4
+ * See the NOTICE file distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #import <Cocoa/Cocoa.h>
18
+
19
+ @interface WebViewController : NSViewController
20
+
21
+ @end
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Licensed under the Apache License, Version 2.0 (the "License");
3
+ * you may not use this file except in compliance with the License.
4
+ * See the NOTICE file distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #import "WebViewController.h"
18
+
19
+ #import <WebKit/WebKit.h>
20
+
21
+ /*
22
+ None of these elements set a native accessibility identifier, so WebKit leaves their
23
+ standard AXIdentifier empty and only exposes their DOM `id` through AXDOMIdentifier -
24
+ this mirrors real-world web content and is what useDomIdAsAccessibilityId is meant to
25
+ fall back onto.
26
+ */
27
+ static NSString *const AMWebViewHTML =
28
+ @"<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body>"
29
+ @"<div id=\"am-web-container\" role=\"group\">"
30
+ @"<button id=\"am-dom-button\">Web Button</button>"
31
+ @"<span id=\"am-dom-text\">Web Text</span>"
32
+ @"</div>"
33
+ @"</body></html>";
34
+
35
+ @implementation WebViewController
36
+
37
+ - (void)loadView
38
+ {
39
+ self.view = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 450, 300)];
40
+ }
41
+
42
+ - (void)viewDidLoad
43
+ {
44
+ [super viewDidLoad];
45
+
46
+ [(WKWebView *)self.view loadHTMLString:AMWebViewHTML baseURL:nil];
47
+ }
48
+
49
+ @end
@@ -10,6 +10,7 @@
10
10
  #import "NSPredicate+FBFormat.h"
11
11
 
12
12
  #import "NSExpression+FBFormat.h"
13
+ #import "XCUIElement+AMAttributes.h"
13
14
 
14
15
  @implementation NSPredicate (FBFormat)
15
16
 
@@ -33,9 +34,88 @@
33
34
  return original;
34
35
  }
35
36
 
37
+ /**
38
+ Whether the given expression is a key path referencing one of the am_* predicate
39
+ attributes (AM_IDENTIFIER_ATTRIBUTE_NAME and friends, see XCUIElement+AMAttributes),
40
+ either directly (`amRect`) or through a nested key path into its resolved value
41
+ (`amRect.x`).
42
+
43
+ @return The referenced am_* attribute name, or nil if it isn't one
44
+ */
45
+ + (nullable NSString *)fb_amAttributeNameForExpression:(NSExpression *)expression
46
+ {
47
+ if (NSKeyPathExpressionType != expression.expressionType) {
48
+ return nil;
49
+ }
50
+ NSString *keyPath = expression.keyPath;
51
+ NSUInteger dotPos = [keyPath rangeOfString:@"."].location;
52
+ NSString *attributeName = NSNotFound == dotPos ? keyPath : [keyPath substringToIndex:dotPos];
53
+ return [[XCUIElement am_predicateAttributeNames] containsObject:attributeName] ? attributeName : nil;
54
+ }
55
+
56
+ /**
57
+ Rebuilds a comparison predicate that references an am_* attribute on one side into a
58
+ block predicate that resolves that attribute directly off the snapshot node being
59
+ matched and compares it with the same operator/options as the original. Block
60
+ predicates are evaluated per-node by matchingPredicate:/containingPredicate: (this is
61
+ how the accessibility id DOM fallback already resolves elements), unlike plain
62
+ comparison predicates whose key path resolution WebKit content does not reliably
63
+ observe. A nested key path into the attribute's resolved value (e.g. `amRect.x`) is
64
+ resolved with a plain valueForKeyPath: lookup after the attribute value itself is
65
+ resolved.
66
+
67
+ @param cp the original comparison predicate, known to reference an am_* attribute
68
+ @param amAttributeName the am_* attribute name referenced
69
+ @param amAttributeOnLeft whether it is the left or the right expression
70
+ @return an equivalent block-based predicate
71
+ */
72
+ + (NSPredicate *)fb_amAttributePredicateForComparison:(NSComparisonPredicate *)cp
73
+ amAttributeName:(NSString *)amAttributeName
74
+ amAttributeOnLeft:(BOOL)amAttributeOnLeft
75
+ {
76
+ NSExpression *amAttributeExpression = amAttributeOnLeft ? cp.leftExpression : cp.rightExpression;
77
+ NSExpression *otherExpression = amAttributeOnLeft ? cp.rightExpression : cp.leftExpression;
78
+ NSString *amKeyPath = amAttributeExpression.keyPath;
79
+ NSUInteger dotPos = [amKeyPath rangeOfString:@"."].location;
80
+ NSString *amValueKeyPathSuffix = NSNotFound == dotPos ? nil : [amKeyPath substringFromIndex:(dotPos + 1)];
81
+ NSComparisonPredicateModifier modifier = cp.comparisonPredicateModifier;
82
+ NSPredicateOperatorType operatorType = cp.predicateOperatorType;
83
+ NSComparisonPredicateOptions options = cp.options;
84
+ return [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary *bindings) {
85
+ id amValue = [XCUIElement am_valueForPredicateAttributeName:amAttributeName target:snapshot];
86
+ if (amValueKeyPathSuffix.length > 0) {
87
+ amValue = [amValue valueForKeyPath:amValueKeyPathSuffix];
88
+ }
89
+ amValue = amValue ?: @"";
90
+ NSMutableDictionary *context = bindings ? [bindings mutableCopy] : [NSMutableDictionary dictionary];
91
+ id otherValue = [otherExpression expressionValueWithObject:snapshot context:context];
92
+ NSExpression *amExpression = [NSExpression expressionForConstantValue:amValue];
93
+ NSExpression *otherConstantExpression = [NSExpression expressionForConstantValue:otherValue];
94
+ NSComparisonPredicate *rewritten = amAttributeOnLeft
95
+ ? [NSComparisonPredicate predicateWithLeftExpression:amExpression
96
+ rightExpression:otherConstantExpression
97
+ modifier:modifier
98
+ type:operatorType
99
+ options:options]
100
+ : [NSComparisonPredicate predicateWithLeftExpression:otherConstantExpression
101
+ rightExpression:amExpression
102
+ modifier:modifier
103
+ type:operatorType
104
+ options:options];
105
+ return [rewritten evaluateWithObject:nil];
106
+ }];
107
+ }
108
+
36
109
  + (instancetype)fb_formatSearchPredicate:(NSPredicate *)input
37
110
  {
38
111
  return [self.class fb_predicateWithPredicate:input comparisonModifier:^NSPredicate *(NSComparisonPredicate *cp) {
112
+ NSString *leftAmAttribute = [self.class fb_amAttributeNameForExpression:cp.leftExpression];
113
+ NSString *rightAmAttribute = [self.class fb_amAttributeNameForExpression:cp.rightExpression];
114
+ if (nil != leftAmAttribute || nil != rightAmAttribute) {
115
+ return [self.class fb_amAttributePredicateForComparison:cp
116
+ amAttributeName:leftAmAttribute ?: rightAmAttribute
117
+ amAttributeOnLeft:nil != leftAmAttribute];
118
+ }
39
119
  NSExpression *left = [NSExpression fb_wdExpressionWithExpression:[cp leftExpression]];
40
120
  NSExpression *right = [NSExpression fb_wdExpressionWithExpression:[cp rightExpression]];
41
121
  return [NSComparisonPredicate predicateWithLeftExpression:left
@@ -18,6 +18,17 @@
18
18
 
19
19
  NS_ASSUME_NONNULL_BEGIN
20
20
 
21
+ /*!
22
+ Predicate string / class chain keypath names for the am_* attributes below. These are
23
+ non-standard, Mac2-driver-specific attributes, in addition to the standard
24
+ XCUIElementAttributes ones.
25
+ */
26
+ extern NSString *const AM_IDENTIFIER_ATTRIBUTE_NAME;
27
+ extern NSString *const AM_RECT_ATTRIBUTE_NAME;
28
+ extern NSString *const AM_TEXT_ATTRIBUTE_NAME;
29
+ extern NSString *const AM_TYPE_ATTRIBUTE_NAME;
30
+ extern NSString *const AM_HAS_KEYBOARD_INPUT_FOCUS_ATTRIBUTE_NAME;
31
+
21
32
  @interface XCUIElement (AMAttributes)
22
33
 
23
34
  /**
@@ -58,7 +69,24 @@ NS_ASSUME_NONNULL_BEGIN
58
69
 
59
70
  @return The identifier. Could be an empty string
60
71
  */
61
- - (NSString *)am_wdIdentifier;
72
+ - (NSString *)am_identifier;
73
+
74
+ /**
75
+ The full set of am_* predicate attribute names (AM_IDENTIFIER_ATTRIBUTE_NAME and
76
+ friends) usable in predicate string / class chain locators.
77
+ */
78
+ + (NSArray<NSString *> *)am_predicateAttributeNames;
79
+
80
+ /**
81
+ Resolves the value of an am_* predicate attribute name directly against a target,
82
+ which may be a live XCUIElement or a raw id<XCUIElementSnapshot> - the latter keeps
83
+ per-node predicate evaluation cheap by not needing to resolve a live element first.
84
+
85
+ @param name one of AM_IDENTIFIER_ATTRIBUTE_NAME and friends
86
+ @param target the XCUIElement or id<XCUIElementSnapshot> to resolve the value against
87
+ @return The resolved value, or nil if name is not a known am_* attribute name
88
+ */
89
+ + (nullable id)am_valueForPredicateAttributeName:(NSString *)name target:(id)target;
62
90
 
63
91
  @end
64
92