appium-webdriveragent 16.1.2 → 16.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.3</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.3</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";
@@ -24,7 +24,6 @@
24
24
  #import "FBXCTestDaemonsProxy.h"
25
25
  #import "XCUIApplication+FBQuiescence.h"
26
26
  #import "XCUIElement.h"
27
- #import "XCUIElement+FBClassChain.h"
28
27
 
29
28
  /*!
30
29
  The intial value for the default application property.
@@ -56,14 +55,10 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
56
55
  NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector;
57
56
  if ([autoClickAlertSelector length] > 0) {
58
57
  @try {
59
- NSArray<XCUIElement*> *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector
60
- shouldReturnAfterFirstMatch:YES];
61
- if (matches.count > 0) {
62
- [[matches objectAtIndex:0] tap];
63
- }
58
+ [alert clickElementMatchingClassChain:autoClickAlertSelector];
64
59
  } @catch (NSException *e) {
65
60
  [FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@",
66
- autoClickAlertSelector, e.description];
61
+ autoClickAlertSelector, e.reason];
67
62
  }
68
63
  // This setting has priority over other settings if enabled
69
64
  return;
@@ -73,14 +68,17 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
73
68
  return;
74
69
  }
75
70
 
76
- NSError *error;
77
71
  if ([self.defaultAlertAction isEqualToString:@"accept"]) {
78
- if (![alert acceptWithError:&error]) {
79
- [FBLogger logFmt:@"Cannot accept the alert. Original error: %@", error.description];
72
+ @try {
73
+ [alert accept];
74
+ } @catch (NSException *e) {
75
+ [FBLogger logFmt:@"Cannot accept the alert. Original error: %@", e.reason];
80
76
  }
81
77
  } else if ([self.defaultAlertAction isEqualToString:@"dismiss"]) {
82
- if (![alert dismissWithError:&error]) {
83
- [FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", error.description];
78
+ @try {
79
+ [alert dismiss];
80
+ } @catch (NSException *e) {
81
+ [FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", e.reason];
84
82
  }
85
83
  } else {
86
84
  [FBLogger logFmt:@"'%@' default alert action is unsupported", self.defaultAlertAction];
@@ -213,7 +211,7 @@ static FBSession *_activeSession = nil;
213
211
  XCUIApplicationState testedAppState = self.testedApplication.state;
214
212
  if (testedAppState >= XCUIApplicationStateRunningForeground) {
215
213
  NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}",
216
- @"elementType", @(XCUIElementTypeAlert),
214
+ @"elementType", @(XCUIElementTypeAlert),
217
215
  // To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946
218
216
  @"identifier", @"SBTransientOverlayWindow",
219
217
  // To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting
@@ -52,26 +52,28 @@ static const NSTimeInterval FB_MONTORING_INTERVAL = 2.0;
52
52
  NSArray<XCUIApplication *> *activeApps = XCUIApplication.fb_activeApplications;
53
53
  BOOL didDetectAlert = NO;
54
54
  for (XCUIApplication *activeApp in activeApps) {
55
- XCUIElement *alertElement = nil;
56
55
  @try {
57
- alertElement = activeApp.fb_alertElement;
58
- if (nil != alertElement) {
59
- [delegate didDetectAlert:[FBAlert alertWithElement:alertElement]];
56
+ FBAlert *alert = [FBAlert alertWithApplication:activeApp];
57
+ if (alert.isPresent) {
58
+ [delegate didDetectAlert:alert];
59
+ didDetectAlert = YES;
60
60
  }
61
61
  } @catch (NSException *e) {
62
62
  [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols];
63
63
  }
64
- if (nil != alertElement) {
65
- didDetectAlert = YES;
64
+ if (didDetectAlert) {
66
65
  break;
67
66
  }
68
67
  }
69
68
 
70
69
  if (!didDetectAlert) {
71
70
  @try {
72
- XCUIElement *alertElement = [XCUIApplication fb_limitedAccessPromptAlertElement];
73
- if (nil != alertElement) {
74
- [delegate didDetectAlert:[FBAlert alertWithElement:alertElement]];
71
+ XCUIApplication *promptApp = XCUIApplication.fb_limitedAccessPromptApplication;
72
+ if (nil != promptApp) {
73
+ FBAlert *alert = [FBAlert alertWithApplication:promptApp];
74
+ if (alert.isPresent) {
75
+ [delegate didDetectAlert:alert];
76
+ }
75
77
  }
76
78
  } @catch (NSException *e) {
77
79
  [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols];