appium-webdriveragent 16.1.2 → 16.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/WebDriverAgent.xcodeproj/project.pbxproj +297 -0
  3. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp_tvOS.xcscheme +78 -0
  4. package/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_tvOS.xcscheme +86 -0
  5. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +20 -8
  6. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +116 -60
  7. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +15 -0
  8. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +16 -0
  9. package/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m +4 -4
  10. package/WebDriverAgentLib/Commands/FBAlertViewCommands.m +10 -36
  11. package/WebDriverAgentLib/Commands/FBElementCommands.m +3 -3
  12. package/WebDriverAgentLib/FBAlert.h +43 -30
  13. package/WebDriverAgentLib/FBAlert.m +257 -91
  14. package/WebDriverAgentLib/Info.plist +2 -2
  15. package/WebDriverAgentLib/Routing/FBExceptionHandler.m +8 -1
  16. package/WebDriverAgentLib/Routing/FBExceptions.h +9 -0
  17. package/WebDriverAgentLib/Routing/FBExceptions.m +3 -0
  18. package/WebDriverAgentLib/Routing/FBSession.m +11 -13
  19. package/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +11 -9
  20. package/WebDriverAgentLib/Utilities/FBPasteboard.m +8 -4
  21. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h +10 -0
  22. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h +29 -4
  23. package/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m +62 -27
  24. package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.h +13 -0
  25. package/WebDriverAgentTests/IntegrationApp_tvOS/AppDelegate.m +26 -0
  26. package/WebDriverAgentTests/IntegrationApp_tvOS/Info.plist +41 -0
  27. package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.h +15 -0
  28. package/WebDriverAgentTests/IntegrationApp_tvOS/SceneDelegate.m +22 -0
  29. package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.h +13 -0
  30. package/WebDriverAgentTests/IntegrationApp_tvOS/ViewController.m +79 -0
  31. package/WebDriverAgentTests/IntegrationApp_tvOS/main.m +17 -0
  32. package/WebDriverAgentTests/IntegrationTests/FBAlertTests.m +32 -51
  33. package/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m +9 -1
  34. package/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m +1 -1
  35. package/WebDriverAgentTests/IntegrationTests_tvOS/FBTVFocusIntegrationTests.m +84 -0
  36. package/WebDriverAgentTests/IntegrationTests_tvOS/Info.plist +24 -0
  37. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.h +27 -0
  38. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCAccessibilityElementDouble.m +23 -0
  39. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.h +43 -0
  40. package/WebDriverAgentTests/UnitTests_tvOS/Doubles/FBXCElementSnapshotDouble.m +24 -0
  41. package/WebDriverAgentTests/UnitTests_tvOS/FBTVNavigationTrackerTests.m +48 -0
  42. package/package.json +1 -1
@@ -9,7 +9,6 @@
9
9
  #import <Foundation/Foundation.h>
10
10
 
11
11
  @class XCUIApplication;
12
- @class XCUIElement;
13
12
 
14
13
  NS_ASSUME_NONNULL_BEGIN
15
14
 
@@ -26,65 +25,79 @@ NS_ASSUME_NONNULL_BEGIN
26
25
  + (instancetype)alertWithApplication:(XCUIApplication *)application;
27
26
 
28
27
  /**
29
- Creates alert helper for given application
30
-
31
- @param element The element which represents the alert
32
- */
33
- + (instancetype)alertWithElement:(XCUIElement *)element;
34
-
35
- /**
36
- Determines whether alert is present
28
+ Determines whether alert is present.
29
+
30
+ An FBAlert instance resolves the alert (and, if found, its snapshot) at
31
+ most once, lazily, on the first call to isPresent, text, buttonLabels,
32
+ accept, dismiss, clickAlertButton:, clickElementMatchingClassChain:, or
33
+ typeText: - every subsequent call on the same instance reuses that result
34
+ rather than re-querying the live UI. This makes an isPresent check
35
+ immediately followed by an action (e.g. the auto-accept flow) act on the
36
+ exact same alert it just observed. Create a fresh FBAlert instance (via
37
+ alertWithApplication:) to observe the current UI state again.
37
38
  */
38
39
  - (BOOL)isPresent;
39
40
 
40
41
  /**
41
- Gets the labels of the buttons visible in the alert
42
+ Gets the labels of the buttons visible in the alert.
43
+ See isPresent for how presence is resolved.
42
44
  */
43
45
  - (nullable NSArray *)buttonLabels;
44
46
 
45
47
  /**
46
- Returns alert's title and description separated by new lines
48
+ Returns alert's title and description separated by new lines.
49
+ See isPresent for how presence is resolved.
47
50
  */
48
51
  - (nullable NSString *)text;
49
52
 
50
53
  /**
51
- Accepts alert, if present
54
+ Accepts alert, if present.
55
+ See isPresent for how presence is resolved.
52
56
 
53
- @param error If there is an error, upon return contains an NSError object that describes the problem.
54
- @return YES if the operation succeeds, otherwise NO.
57
+ @throws FBAlertNotPresentException if no alert is present.
58
+ @throws FBAlertActionFailedException if the accept button could not be found.
55
59
  */
56
- - (BOOL)acceptWithError:(NSError **)error;
60
+ - (void)accept;
57
61
 
58
62
  /**
59
- Dismisses alert, if present
63
+ Dismisses alert, if present.
64
+ See isPresent for how presence is resolved.
60
65
 
61
- @param error If there is an error, upon return contains an NSError object that describes the problem.
62
- @return YES if the operation succeeds, otherwise NO.
66
+ @throws FBAlertNotPresentException if no alert is present.
67
+ @throws FBAlertActionFailedException if the dismiss button could not be found.
63
68
  */
64
- - (BOOL)dismissWithError:(NSError **)error;
69
+ - (void)dismiss;
65
70
 
66
71
  /**
67
- Clicks on an alert button, if present
68
-
72
+ Clicks on an alert button, if present.
73
+ See isPresent for how presence is resolved.
74
+
69
75
  @param label The label of the button on which to click.
70
- @param error If there is an error, upon return contains an NSError object that describes the problem.
71
- @return YES if the operation suceeds, otherwise NO.
76
+ @throws FBAlertNotPresentException if no alert is present.
77
+ @throws FBAlertActionFailedException if no button with the given label could be found.
72
78
  */
73
- - (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error;
79
+ - (void)clickAlertButton:(NSString *)label;
74
80
 
75
81
  /**
76
- XCUElement that represents alert
82
+ Taps the first descendant of the alert matching the given class chain
83
+ selector, if present.
84
+ See isPresent for how presence is resolved.
85
+
86
+ @param classChain The class chain selector to match against the alert's descendants.
87
+ @throws FBAlertNotPresentException if no alert is present.
88
+ @throws FBAlertActionFailedException if no matching element could be found.
77
89
  */
78
- - (nullable XCUIElement *)alertElement;
90
+ - (void)clickElementMatchingClassChain:(NSString *)classChain;
79
91
 
80
92
  /**
81
- Types a text into an input inside the alert container, if it is present
93
+ Types a text into an input inside the alert container, if it is present.
94
+ See isPresent for how presence is resolved.
82
95
 
83
96
  @param text the text to type
84
- @param error If there is an error, upon return contains an NSError object that describes the problem.
85
- @return YES if the operation succeeds, otherwise NO.
97
+ @throws FBAlertNotPresentException if no alert is present.
98
+ @throws FBAlertSetTextFailedException if there is no single input field to type into, or typing itself fails.
86
99
  */
87
- - (BOOL)typeText:(NSString *)text error:(NSError **)error;
100
+ - (void)typeText:(NSString *)text;
88
101
 
89
102
  @end
90
103
 
@@ -9,21 +9,26 @@
9
9
  #import "FBAlert.h"
10
10
 
11
11
  #import "FBConfiguration.h"
12
- #import "FBErrorBuilder.h"
12
+ #import "FBExceptions.h"
13
13
  #import "FBLogger.h"
14
14
  #import "FBXCElementSnapshotWrapper+Helpers.h"
15
15
  #import "FBXCodeCompatibility.h"
16
16
  #import "XCUIApplication.h"
17
17
  #import "XCUIApplication+FBAlert.h"
18
+ #import "XCUIApplication+FBHelpers.h"
18
19
  #import "XCUIElement+FBClassChain.h"
19
20
  #import "XCUIElement+FBTyping.h"
21
+ #import "XCUIElement+FBUID.h"
20
22
  #import "XCUIElement+FBUtilities.h"
21
23
  #import "XCUIElement+FBWebDriverAttributes.h"
22
24
 
23
25
 
24
26
  @interface FBAlert ()
25
27
  @property (nonatomic, strong) XCUIApplication *application;
26
- @property (nonatomic, strong, nullable) XCUIElement *element;
28
+ @property (nonatomic, nullable) XCUIElement *cachedAlertElement;
29
+ @property (nonatomic) BOOL hasCachedAlertElement;
30
+ @property (nonatomic, nullable) id<FBXCElementSnapshot> cachedAlertSnapshot;
31
+ @property (nonatomic) BOOL hasCachedAlertSnapshot;
27
32
  @end
28
33
 
29
34
  @implementation FBAlert
@@ -35,32 +40,81 @@
35
40
  return alert;
36
41
  }
37
42
 
38
- + (instancetype)alertWithElement:(XCUIElement *)element
43
+ // Resolved (and, if found, snapshotted) at most once per instance and then
44
+ // reused for every subsequent call - see FBAlert.h. This is what makes an
45
+ // isPresent check followed by an action (the FBAlertsMonitor/FBSession
46
+ // auto-accept flow) act on the exact same alert it just observed, instead of
47
+ // re-resolving against a UI that may have changed in between.
48
+ - (nullable XCUIElement *)alertElement
39
49
  {
40
- FBAlert *alert = [FBAlert new];
41
- alert.element = element;
42
- alert.application = element.application;
43
- return alert;
50
+ if (!self.hasCachedAlertElement) {
51
+ id<FBXCElementSnapshot> snapshot = nil;
52
+ self.cachedAlertElement = [self alertElementFromApplication:&snapshot];
53
+ self.hasCachedAlertElement = YES;
54
+ if (nil != snapshot) {
55
+ self.cachedAlertSnapshot = snapshot;
56
+ self.hasCachedAlertSnapshot = YES;
57
+ }
58
+ }
59
+ return self.cachedAlertElement;
60
+ }
61
+
62
+ // The alert element's own subtree snapshot, taken once. All read-only
63
+ // accessors (text, buttonLabels) and all element lookups (buttons, input
64
+ // fields) work off this single snapshot in memory - only tapping/typing into
65
+ // a specific already-located element pays for one more, narrowly targeted
66
+ // accessibility round trip (see XCUIApplication.fb_elementForSnapshot:underElement:).
67
+ // fb_cachedSnapshot is tried before fb_customSnapshot: it can reconstruct
68
+ // the element's snapshot from the detection query's already-fetched
69
+ // rootElementSnapshot without a further round trip (verified empirically -
70
+ // see XCUIApplication+FBAlert.m).
71
+ - (nullable id<FBXCElementSnapshot>)alertSnapshot
72
+ {
73
+ if (!self.hasCachedAlertSnapshot) {
74
+ XCUIElement *alertElement = self.alertElement;
75
+ self.cachedAlertSnapshot = nil == alertElement
76
+ ? nil
77
+ : (alertElement.lastSnapshot ?: alertElement.fb_cachedSnapshot ?: [alertElement fb_customSnapshot]);
78
+ self.hasCachedAlertSnapshot = YES;
79
+ }
80
+ return self.cachedAlertSnapshot;
44
81
  }
45
82
 
46
83
  - (BOOL)isPresent
47
84
  {
48
- @try {
49
- if (nil == self.alertElement) {
50
- return NO;
85
+ return nil != self.alertElement;
86
+ }
87
+
88
+ + (NSArray<id<FBXCElementSnapshot>> *)fb_buttonSnapshotsInSnapshot:(id<FBXCElementSnapshot>)snapshot
89
+ {
90
+ NSMutableArray<id<FBXCElementSnapshot>> *buttons = [NSMutableArray array];
91
+ [snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
92
+ if (descendant.elementType == XCUIElementTypeButton) {
93
+ [buttons addObject:descendant];
51
94
  }
52
- [self.alertElement fb_customSnapshot];
53
- return YES;
54
- } @catch (NSException *) {
55
- return NO;
56
- }
95
+ }];
96
+ return buttons.copy;
97
+ }
98
+
99
+ - (void)fb_raiseNotPresentException __attribute__((noreturn))
100
+ {
101
+ @throw [NSException exceptionWithName:FBAlertNotPresentException
102
+ reason:@"No alert is open"
103
+ userInfo:nil];
57
104
  }
58
105
 
59
- - (BOOL)notPresentWithError:(NSError **)error
106
+ - (void)fb_raiseActionFailedExceptionWithReason:(NSString *)reason __attribute__((noreturn))
60
107
  {
61
- return [[[FBErrorBuilder builder]
62
- withDescriptionFormat:@"No alert is open"]
63
- buildError:error];
108
+ @throw [NSException exceptionWithName:FBAlertActionFailedException
109
+ reason:reason
110
+ userInfo:nil];
111
+ }
112
+
113
+ - (void)fb_raiseSetTextFailedExceptionWithReason:(NSString *)reason __attribute__((noreturn))
114
+ {
115
+ @throw [NSException exceptionWithName:FBAlertSetTextFailedException
116
+ reason:reason
117
+ userInfo:nil];
64
118
  }
65
119
 
66
120
  + (BOOL)isSafariWebAlertWithSnapshot:(id<FBXCElementSnapshot>)snapshot
@@ -76,19 +130,19 @@
76
130
 
77
131
  - (NSString *)text
78
132
  {
79
- if (!self.isPresent) {
133
+ id<FBXCElementSnapshot> snapshot = self.alertSnapshot;
134
+ if (nil == snapshot) {
80
135
  return nil;
81
136
  }
82
137
 
83
138
  NSMutableArray<NSString *> *resultText = [NSMutableArray array];
84
- id<FBXCElementSnapshot> snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
85
139
  BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot];
86
140
  [snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
87
141
  XCUIElementType elementType = descendant.elementType;
88
142
  if (!(elementType == XCUIElementTypeTextView || elementType == XCUIElementTypeStaticText)) {
89
143
  return;
90
144
  }
91
-
145
+
92
146
  FBXCElementSnapshotWrapper *descendantWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant];
93
147
  if (elementType == XCUIElementTypeStaticText
94
148
  && nil != [descendantWrapper fb_parentMatchingType:XCUIElementTypeButton]) {
@@ -112,39 +166,47 @@
112
166
  return [resultText componentsJoinedByString:@"\n"];
113
167
  }
114
168
 
115
- - (BOOL)typeText:(NSString *)text error:(NSError **)error
169
+ - (void)typeText:(NSString *)text
116
170
  {
117
- if (!self.isPresent) {
118
- return [self notPresentWithError:error];
171
+ XCUIElement *alertElement = self.alertElement;
172
+ id<FBXCElementSnapshot> alertSnapshot = self.alertSnapshot;
173
+ if (nil == alertElement || nil == alertSnapshot) {
174
+ [self fb_raiseNotPresentException];
119
175
  }
120
176
 
121
- NSPredicate *textCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu}",
122
- XCUIElementTypeTextField, XCUIElementTypeSecureTextField];
123
- NSArray<XCUIElement *> *dstFields = [[self.alertElement descendantsMatchingType:XCUIElementTypeAny]
124
- matchingPredicate:textCollectorPredicate].allElementsBoundByIndex;
125
- if (dstFields.count > 1) {
126
- return [[[FBErrorBuilder builder]
127
- withDescriptionFormat:@"The alert contains more than one input field"]
128
- buildError:error];
177
+ NSMutableArray<id<FBXCElementSnapshot>> *dstFieldSnapshots = [NSMutableArray array];
178
+ [alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
179
+ XCUIElementType elementType = descendant.elementType;
180
+ if (elementType == XCUIElementTypeTextField || elementType == XCUIElementTypeSecureTextField) {
181
+ [dstFieldSnapshots addObject:descendant];
182
+ }
183
+ }];
184
+ if (dstFieldSnapshots.count > 1) {
185
+ [self fb_raiseSetTextFailedExceptionWithReason:@"The alert contains more than one input field"];
129
186
  }
130
- if (0 == dstFields.count) {
131
- return [[[FBErrorBuilder builder]
132
- withDescriptionFormat:@"The alert contains no input fields"]
133
- buildError:error];
187
+ id<FBXCElementSnapshot> dstFieldSnapshot = dstFieldSnapshots.firstObject;
188
+ if (nil == dstFieldSnapshot) {
189
+ [self fb_raiseSetTextFailedExceptionWithReason:@"The alert contains no input fields"];
190
+ }
191
+ XCUIElement *dstField = [XCUIApplication fb_elementForSnapshot:dstFieldSnapshot
192
+ underElement:alertElement];
193
+ if (nil == dstField) {
194
+ [self fb_raiseSetTextFailedExceptionWithReason:@"Failed to resolve the input field element"];
195
+ }
196
+ NSError *error;
197
+ if (![dstField fb_typeText:text shouldClear:YES error:&error]) {
198
+ [self fb_raiseSetTextFailedExceptionWithReason:error.description];
134
199
  }
135
- return [dstFields.firstObject fb_typeText:text
136
- shouldClear:YES
137
- error:error];
138
200
  }
139
201
 
140
202
  - (NSArray *)buttonLabels
141
203
  {
142
- if (!self.isPresent) {
204
+ id<FBXCElementSnapshot> alertSnapshot = self.alertSnapshot;
205
+ if (nil == alertSnapshot) {
143
206
  return nil;
144
207
  }
145
208
 
146
209
  NSMutableArray<NSString *> *labels = [NSMutableArray array];
147
- id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
148
210
  [alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
149
211
  if (descendant.elementType != XCUIElementTypeButton) {
150
212
  return;
@@ -157,19 +219,20 @@
157
219
  return labels.copy;
158
220
  }
159
221
 
160
- - (BOOL)acceptWithError:(NSError **)error
222
+ - (void)accept
161
223
  {
162
- if (!self.isPresent) {
163
- return [self notPresentWithError:error];
224
+ XCUIElement *alertElement = self.alertElement;
225
+ id<FBXCElementSnapshot> alertSnapshot = self.alertSnapshot;
226
+ if (nil == alertElement || nil == alertSnapshot) {
227
+ [self fb_raiseNotPresentException];
164
228
  }
165
229
 
166
- id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
167
230
  XCUIElement *acceptButton = nil;
168
231
  if (FBConfiguration.acceptAlertButtonSelector.length) {
169
232
  NSString *errorReason = nil;
170
233
  @try {
171
- acceptButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.acceptAlertButtonSelector
172
- shouldReturnAfterFirstMatch:YES] firstObject];
234
+ acceptButton = [[alertElement fb_descendantsMatchingClassChain:FBConfiguration.acceptAlertButtonSelector
235
+ shouldReturnAfterFirstMatch:YES] firstObject];
173
236
  } @catch (NSException *ex) {
174
237
  errorReason = ex.reason;
175
238
  }
@@ -183,34 +246,50 @@
183
246
  }
184
247
  }
185
248
  if (nil == acceptButton) {
186
- NSArray<XCUIElement *> *buttons = [self.alertElement.fb_query
187
- descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
188
- acceptButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
189
- ? buttons.lastObject
190
- : buttons.firstObject;
249
+ // buttonSnapshotsInSnapshot's tree-order walk doesn't always match a
250
+ // live query's ordering: on the system location-permission alert (iOS
251
+ // 17.5, confirmed via manual testing), that mismatch put a
252
+ // non-dismissing "Precise: On/Off" toggle first, so accept/dismiss
253
+ // silently tapped the wrong control and left the alert on screen. Not
254
+ // reproduced on iOS 18+, so keep the cheaper snapshot walk there and
255
+ // only pay for a live query below iOS 18.
256
+ if (@available(iOS 18.0, *)) {
257
+ NSArray<id<FBXCElementSnapshot>> *buttonSnapshots = [self.class fb_buttonSnapshotsInSnapshot:alertSnapshot];
258
+ id<FBXCElementSnapshot> chosenSnapshot = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
259
+ ? buttonSnapshots.lastObject
260
+ : buttonSnapshots.firstObject;
261
+ if (nil != chosenSnapshot) {
262
+ acceptButton = [XCUIApplication fb_elementForSnapshot:chosenSnapshot underElement:alertElement];
263
+ }
264
+ } else {
265
+ NSArray<XCUIElement *> *buttons = [alertElement.fb_query
266
+ descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
267
+ acceptButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
268
+ ? buttons.lastObject
269
+ : buttons.firstObject;
270
+ }
191
271
  }
192
272
  if (nil == acceptButton) {
193
- return [[[FBErrorBuilder builder]
194
- withDescriptionFormat:@"Failed to find accept button for alert: %@", self.alertElement]
195
- buildError:error];
273
+ [self fb_raiseActionFailedExceptionWithReason:
274
+ [NSString stringWithFormat:@"Failed to find accept button for alert: %@", alertElement]];
196
275
  }
197
276
  [acceptButton tap];
198
- return YES;
199
277
  }
200
278
 
201
- - (BOOL)dismissWithError:(NSError **)error
279
+ - (void)dismiss
202
280
  {
203
- if (!self.isPresent) {
204
- return [self notPresentWithError:error];
281
+ XCUIElement *alertElement = self.alertElement;
282
+ id<FBXCElementSnapshot> alertSnapshot = self.alertSnapshot;
283
+ if (nil == alertElement || nil == alertSnapshot) {
284
+ [self fb_raiseNotPresentException];
205
285
  }
206
286
 
207
- id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
208
287
  XCUIElement *dismissButton = nil;
209
288
  if (FBConfiguration.dismissAlertButtonSelector.length) {
210
289
  NSString *errorReason = nil;
211
290
  @try {
212
- dismissButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.dismissAlertButtonSelector
213
- shouldReturnAfterFirstMatch:YES] firstObject];
291
+ dismissButton = [[alertElement fb_descendantsMatchingClassChain:FBConfiguration.dismissAlertButtonSelector
292
+ shouldReturnAfterFirstMatch:YES] firstObject];
214
293
  } @catch (NSException *ex) {
215
294
  errorReason = ex.reason;
216
295
  }
@@ -224,54 +303,141 @@
224
303
  }
225
304
  }
226
305
  if (nil == dismissButton) {
227
- NSArray<XCUIElement *> *buttons = [self.alertElement.fb_query
228
- descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
229
- dismissButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
230
- ? buttons.firstObject
231
- : buttons.lastObject;
306
+ // See the matching comment in accept.
307
+ if (@available(iOS 18.0, *)) {
308
+ NSArray<id<FBXCElementSnapshot>> *buttonSnapshots = [self.class fb_buttonSnapshotsInSnapshot:alertSnapshot];
309
+ id<FBXCElementSnapshot> chosenSnapshot = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
310
+ ? buttonSnapshots.firstObject
311
+ : buttonSnapshots.lastObject;
312
+ if (nil != chosenSnapshot) {
313
+ dismissButton = [XCUIApplication fb_elementForSnapshot:chosenSnapshot underElement:alertElement];
314
+ }
315
+ } else {
316
+ NSArray<XCUIElement *> *buttons = [alertElement.fb_query
317
+ descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
318
+ dismissButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
319
+ ? buttons.firstObject
320
+ : buttons.lastObject;
321
+ }
232
322
  }
233
323
 
234
324
  if (nil == dismissButton) {
235
- return [[[FBErrorBuilder builder]
236
- withDescriptionFormat:@"Failed to find dismiss button for alert: %@", self.alertElement]
237
- buildError:error];
325
+ [self fb_raiseActionFailedExceptionWithReason:
326
+ [NSString stringWithFormat:@"Failed to find dismiss button for alert: %@", alertElement]];
238
327
  }
239
328
  [dismissButton tap];
240
- return YES;
241
329
  }
242
330
 
243
- - (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error
331
+ - (void)clickAlertButton:(NSString *)label
244
332
  {
245
- if (!self.isPresent) {
246
- return [self notPresentWithError:error];
333
+ XCUIElement *alertElement = self.alertElement;
334
+ id<FBXCElementSnapshot> alertSnapshot = self.alertSnapshot;
335
+ if (nil == alertElement || nil == alertSnapshot) {
336
+ [self fb_raiseNotPresentException];
247
337
  }
248
338
 
249
- NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label == %@", label];
250
- XCUIElement *requestedButton = [[self.alertElement descendantsMatchingType:XCUIElementTypeButton]
251
- matchingPredicate:predicate].allElementsBoundByIndex.firstObject;
339
+ __block id<FBXCElementSnapshot> matchedSnapshot = nil;
340
+ [alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
341
+ if (nil != matchedSnapshot || descendant.elementType != XCUIElementTypeButton) {
342
+ return;
343
+ }
344
+ NSString *btnLabel = [FBXCElementSnapshotWrapper ensureWrapped:descendant].wdLabel;
345
+ if (nil != btnLabel && [btnLabel isEqualToString:label]) {
346
+ matchedSnapshot = descendant;
347
+ }
348
+ }];
349
+ XCUIElement *requestedButton = nil == matchedSnapshot
350
+ ? nil
351
+ : [XCUIApplication fb_elementForSnapshot:matchedSnapshot underElement:alertElement];
252
352
  if (!requestedButton) {
253
- return [[[FBErrorBuilder builder]
254
- withDescriptionFormat:@"Failed to find button with label '%@' for alert: %@", label, self.alertElement]
255
- buildError:error];
353
+ [self fb_raiseActionFailedExceptionWithReason:
354
+ [NSString stringWithFormat:@"Failed to find button with label '%@' for alert: %@", label, alertElement]];
256
355
  }
257
356
  [requestedButton tap];
258
- return YES;
259
357
  }
260
358
 
261
- - (XCUIElement *)alertElement
359
+ - (void)clickElementMatchingClassChain:(NSString *)classChain
360
+ {
361
+ XCUIElement *alertElement = self.alertElement;
362
+ if (nil == alertElement) {
363
+ [self fb_raiseNotPresentException];
364
+ }
365
+
366
+ // For a Safari web alert, alertElement is the containing scrollView, not
367
+ // the alert's own div (see fb_alertElementWithSnapshot:), so a classChain
368
+ // query from it could also match unrelated page content. Constrain
369
+ // matches to descendants of alertSnapshot by uid when available.
370
+ NSSet<NSString *> *alertSnapshotUids = nil;
371
+ id<FBXCElementSnapshot> alertSnapshot = self.alertSnapshot;
372
+ if (nil != alertSnapshot) {
373
+ NSMutableSet<NSString *> *uids = [NSMutableSet set];
374
+ NSString *rootUid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:alertSnapshot];
375
+ if (nil != rootUid) {
376
+ [uids addObject:rootUid];
377
+ }
378
+ [alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
379
+ NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:descendant];
380
+ if (nil != uid) {
381
+ [uids addObject:uid];
382
+ }
383
+ }];
384
+ alertSnapshotUids = uids;
385
+ }
386
+
387
+ NSArray<XCUIElement *> *matches = nil;
388
+ @try {
389
+ matches = [alertElement fb_descendantsMatchingClassChain:classChain
390
+ shouldReturnAfterFirstMatch:NO];
391
+ } @catch (NSException *ex) {
392
+ [self fb_raiseActionFailedExceptionWithReason:
393
+ [NSString stringWithFormat:@"Failed to match class chain selector '%@' for alert: %@. Original error: %@", classChain, alertElement, ex.reason]];
394
+ }
395
+ XCUIElement *matchedElement = nil;
396
+ for (XCUIElement *match in matches) {
397
+ if (nil == alertSnapshotUids || [alertSnapshotUids containsObject:match.fb_uid]) {
398
+ matchedElement = match;
399
+ break;
400
+ }
401
+ }
402
+ if (nil == matchedElement) {
403
+ [self fb_raiseActionFailedExceptionWithReason:
404
+ [NSString stringWithFormat:@"Failed to find any element matching class chain selector '%@' for alert: %@", classChain, alertElement]];
405
+ }
406
+ [matchedElement tap];
407
+ }
408
+
409
+ // Single source of truth for alert detection: checks each candidate
410
+ // application (systemApp, then self.application if different, then the iOS
411
+ // 18+ limited access prompt app) via targeted, predicate-filtered live
412
+ // queries (see XCUIApplication.fb_alertElementWithSnapshot:) instead of
413
+ // snapshotting and walking the whole application tree - the cost stays
414
+ // proportional to the number of alert-shaped elements rather than the
415
+ // size/depth of the app, which matters a lot for deeply nested view
416
+ // hierarchies. Only ever invoked once per instance, by the alertElement
417
+ // getter above, which caches both the element and (if one comes back) the
418
+ // snapshot obtained as a side effect of resolving it.
419
+ - (nullable XCUIElement *)alertElementFromApplication:(id<FBXCElementSnapshot> _Nullable *)snapshotOut
262
420
  {
263
- if (nil == self.element) {
421
+ @try {
264
422
  XCUIApplication *systemApp = XCUIApplication.fb_systemApplication;
265
- if ([systemApp fb_isSameAppAs:self.application]) {
266
- self.element = systemApp.fb_alertElement;
267
- } else {
268
- self.element = systemApp.fb_alertElement ?: self.application.fb_alertElement;
423
+ NSMutableArray<XCUIApplication *> *candidates = [NSMutableArray arrayWithObject:systemApp];
424
+ if (![systemApp fb_isSameAppAs:self.application]) {
425
+ [candidates addObject:self.application];
269
426
  }
270
- if (nil == self.element) {
271
- self.element = [XCUIApplication fb_limitedAccessPromptAlertElement];
427
+ XCUIApplication *promptApp = XCUIApplication.fb_limitedAccessPromptApplication;
428
+ if (nil != promptApp) {
429
+ [candidates addObject:promptApp];
272
430
  }
431
+ for (XCUIApplication *candidate in candidates) {
432
+ XCUIElement *element = [candidate fb_alertElementWithSnapshot:snapshotOut];
433
+ if (nil != element) {
434
+ return element;
435
+ }
436
+ }
437
+ } @catch (NSException *) {
438
+ return nil;
273
439
  }
274
- return self.element;
440
+ return nil;
275
441
  }
276
442
 
277
443
  @end
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.1.2</string>
18
+ <string>16.1.4</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.1.2</string>
22
+ <string>16.1.4</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -28,9 +28,13 @@
28
28
  commandStatus = [FBCommandStatus invalidArgumentErrorWithMessage:exception.reason
29
29
  traceback:traceback];
30
30
  } else if ([exception.name isEqualToString:FBApplicationCrashedException]
31
- || [exception.name isEqualToString:FBApplicationDeadlockDetectedException]) {
31
+ || [exception.name isEqualToString:FBApplicationDeadlockDetectedException]
32
+ || [exception.name isEqualToString:FBAlertActionFailedException]) {
32
33
  commandStatus = [FBCommandStatus invalidElementStateErrorWithMessage:exception.reason
33
34
  traceback:traceback];
35
+ } else if ([exception.name isEqualToString:FBAlertSetTextFailedException]) {
36
+ commandStatus = [FBCommandStatus unsupportedOperationErrorWithMessage:exception.reason
37
+ traceback:traceback];
34
38
  } else if ([exception.name isEqualToString:FBInvalidXPathException]
35
39
  || [exception.name isEqualToString:FBClassChainQueryParseException]) {
36
40
  commandStatus = [FBCommandStatus invalidSelectorErrorWithMessage:exception.reason
@@ -47,6 +51,9 @@
47
51
  } else if ([exception.name isEqualToString:FBSessionCreationException]) {
48
52
  commandStatus = [FBCommandStatus sessionNotCreatedError:exception.reason
49
53
  traceback:traceback];
54
+ } else if ([exception.name isEqualToString:FBAlertNotPresentException]) {
55
+ commandStatus = [FBCommandStatus noAlertOpenErrorWithMessage:exception.reason
56
+ traceback:traceback];
50
57
  } else {
51
58
  commandStatus = [FBCommandStatus unknownErrorWithMessage:exception.reason
52
59
  traceback:traceback];
@@ -57,4 +57,13 @@ extern NSString *const FBApplicationMissingException;
57
57
  /*! Exception used to notify about WDA incompatibility with the current platform version */
58
58
  extern NSString *const FBIncompatibleWdaException;
59
59
 
60
+ /*! Exception used to notify that no alert is currently present */
61
+ extern NSString *const FBAlertNotPresentException;
62
+
63
+ /*! Exception used to notify that an alert action (e.g. finding a button to tap) could not complete, although the alert itself is present */
64
+ extern NSString *const FBAlertActionFailedException;
65
+
66
+ /*! Exception used to notify that typing text into an alert failed (e.g. no or multiple input fields) */
67
+ extern NSString *const FBAlertSetTextFailedException;
68
+
60
69
  NS_ASSUME_NONNULL_END
@@ -22,3 +22,6 @@ NSString *const FBClassChainQueryParseException = @"FBClassChainQueryParseExcept
22
22
  NSString *const FBApplicationCrashedException = @"FBApplicationCrashedException";
23
23
  NSString *const FBApplicationMissingException = @"FBApplicationMissingException";
24
24
  NSString *const FBIncompatibleWdaException = @"FBIncompatibleWdaException";
25
+ NSString *const FBAlertNotPresentException = @"FBAlertNotPresentException";
26
+ NSString *const FBAlertActionFailedException = @"FBAlertActionFailedException";
27
+ NSString *const FBAlertSetTextFailedException = @"FBAlertSetTextFailedException";