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.
- package/CHANGELOG.md +6 -0
- package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +20 -8
- package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +116 -60
- package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +15 -0
- package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +16 -0
- package/WebDriverAgentLib/Commands/FBAlertViewCommands.m +10 -36
- package/WebDriverAgentLib/FBAlert.h +43 -30
- package/WebDriverAgentLib/FBAlert.m +257 -91
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBExceptionHandler.m +8 -1
- package/WebDriverAgentLib/Routing/FBExceptions.h +9 -0
- package/WebDriverAgentLib/Routing/FBExceptions.m +3 -0
- package/WebDriverAgentLib/Routing/FBSession.m +11 -13
- package/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +11 -9
- package/WebDriverAgentLib/Utilities/FBPasteboard.m +8 -4
- package/WebDriverAgentTests/IntegrationTests/FBAlertTests.m +32 -51
- package/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m +9 -1
- package/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m +1 -1
- package/package.json +1 -1
|
@@ -9,21 +9,26 @@
|
|
|
9
9
|
#import "FBAlert.h"
|
|
10
10
|
|
|
11
11
|
#import "FBConfiguration.h"
|
|
12
|
-
#import "
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
- (
|
|
106
|
+
- (void)fb_raiseActionFailedExceptionWithReason:(NSString *)reason __attribute__((noreturn))
|
|
60
107
|
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
- (
|
|
169
|
+
- (void)typeText:(NSString *)text
|
|
116
170
|
{
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
- (
|
|
222
|
+
- (void)accept
|
|
161
223
|
{
|
|
162
|
-
|
|
163
|
-
|
|
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 = [[
|
|
172
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
- (
|
|
279
|
+
- (void)dismiss
|
|
202
280
|
{
|
|
203
|
-
|
|
204
|
-
|
|
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 = [[
|
|
213
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
- (
|
|
331
|
+
- (void)clickAlertButton:(NSString *)label
|
|
244
332
|
{
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
- (
|
|
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
|
-
|
|
421
|
+
@try {
|
|
264
422
|
XCUIApplication *systemApp = XCUIApplication.fb_systemApplication;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
|
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.
|
|
18
|
+
<string>16.1.3</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
[
|
|
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
|
-
|
|
83
|
-
[
|
|
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
|
-
|
|
58
|
-
if (
|
|
59
|
-
[delegate didDetectAlert:
|
|
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 (
|
|
65
|
-
didDetectAlert = YES;
|
|
64
|
+
if (didDetectAlert) {
|
|
66
65
|
break;
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
if (!didDetectAlert) {
|
|
71
70
|
@try {
|
|
72
|
-
|
|
73
|
-
if (nil !=
|
|
74
|
-
|
|
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];
|