appium-webdriveragent 16.13.3 → 16.13.5
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 +12 -0
- package/WebDriverAgentLib/Categories/XCUIElement+FBFind.m +20 -7
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Utilities/FBXPath.m +145 -124
- package/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m +33 -0
- package/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m +113 -0
- package/WebDriverAgentTests/UnitTests/FBXPathTests.m +65 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [16.13.5](https://github.com/appium/WebDriverAgent/compare/v16.13.4...v16.13.5) (2026-09-30)
|
|
2
|
+
|
|
3
|
+
### Performance Improvements
|
|
4
|
+
|
|
5
|
+
* stream page-source XML with exception-safe resource cleanup ([#1276](https://github.com/appium/WebDriverAgent/issues/1276)) ([4e8639b](https://github.com/appium/WebDriverAgent/commit/4e8639bf73f962591e3e7b3be211f289a19ebf84))
|
|
6
|
+
|
|
7
|
+
## [16.13.4](https://github.com/appium/WebDriverAgent/compare/v16.13.3...v16.13.4) (2026-09-30)
|
|
8
|
+
|
|
9
|
+
### Performance Improvements
|
|
10
|
+
|
|
11
|
+
* use native predicates for accessibility id lookups ([#1286](https://github.com/appium/WebDriverAgent/issues/1286)) ([2ceb940](https://github.com/appium/WebDriverAgent/commit/2ceb940939c2033cb37b846962a966d42bb0ddab))
|
|
12
|
+
|
|
1
13
|
## [16.13.3](https://github.com/appium/WebDriverAgent/compare/v16.13.2...v16.13.3) (2026-09-30)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -120,14 +120,27 @@
|
|
|
120
120
|
- (NSArray<XCUIElement *> *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId
|
|
121
121
|
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
|
122
122
|
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
// Standard snapshot attributes let XCTest evaluate a structured predicate
|
|
124
|
+
// without WDA's per-snapshot block callback, while preserving wdName semantics.
|
|
125
|
+
// Local benchmarks showed lower end-to-end lookup latency; see measurements
|
|
126
|
+
// and regression checks in https://github.com/appium/WebDriverAgent/pull/1286.
|
|
127
|
+
NSPredicate *predicate = [NSPredicate predicateWithFormat:
|
|
128
|
+
@"(identifier != nil AND identifier != '' AND identifier == %@) OR "
|
|
129
|
+
"((identifier == nil OR identifier == '') AND label != nil AND label != '' AND label == %@)",
|
|
130
|
+
accessibilityId, accessibilityId];
|
|
131
|
+
XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
|
|
132
|
+
matchingPredicate:predicate];
|
|
133
|
+
NSMutableArray<XCUIElement *> *result = [NSMutableArray arrayWithArray:
|
|
134
|
+
[self.class fb_extractMatchingElementsFromQuery:query
|
|
135
|
+
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
|
|
136
|
+
id<FBXCElementSnapshot> cachedSnapshot = [self fb_cachedSnapshotWithQuery:query];
|
|
137
|
+
if ([predicate evaluateWithObject:cachedSnapshot]) {
|
|
138
|
+
if (shouldReturnAfterFirstMatch || result.count == 0) {
|
|
139
|
+
return @[self];
|
|
127
140
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
[result insertObject:self atIndex:0];
|
|
142
|
+
}
|
|
143
|
+
return result.copy;
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
@end
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.13.
|
|
18
|
+
<string>16.13.5</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.13.
|
|
22
|
+
<string>16.13.5</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -175,154 +175,175 @@ static NSString *const topNodeIndexPath = @"top";
|
|
|
175
175
|
+ (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
|
|
176
176
|
options:(nullable FBXMLGenerationOptions *)options
|
|
177
177
|
{
|
|
178
|
-
|
|
179
|
-
xmlTextWriterPtr writer =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (hasScope) {
|
|
186
|
-
rc = xmlTextWriterStartElement(writer,
|
|
187
|
-
(xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
|
|
188
|
-
if (rc < 0) {
|
|
189
|
-
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
|
|
190
|
-
}
|
|
178
|
+
xmlBufferPtr buffer = NULL;
|
|
179
|
+
xmlTextWriterPtr writer = NULL;
|
|
180
|
+
@try {
|
|
181
|
+
buffer = xmlBufferCreate();
|
|
182
|
+
if (NULL == buffer) {
|
|
183
|
+
[FBLogger log:@"Failed to invoke libxml2>xmlBufferCreate"];
|
|
184
|
+
return nil;
|
|
191
185
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
[
|
|
195
|
-
|
|
196
|
-
// to calculate a more accurate value for the 'hittable' attribute.
|
|
197
|
-
rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root
|
|
198
|
-
useNative:FBConfiguration.sharedInstance.includeHittableInPageSource]
|
|
199
|
-
writer:writer
|
|
200
|
-
elementStore:nil
|
|
201
|
-
query:nil
|
|
202
|
-
excludingAttributes:options.excludedAttributes];
|
|
186
|
+
writer = xmlNewTextWriterMemory(buffer, 0);
|
|
187
|
+
if (NULL == writer) {
|
|
188
|
+
[FBLogger log:@"Failed to invoke libxml2>xmlNewTextWriterMemory"];
|
|
189
|
+
return nil;
|
|
203
190
|
}
|
|
191
|
+
xmlTextWriterSetIndent(writer, 1);
|
|
192
|
+
xmlTextWriterSetIndentString(writer, BAD_CAST " ");
|
|
193
|
+
int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
|
|
194
|
+
if (rc < 0) {
|
|
195
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
|
|
196
|
+
} else {
|
|
197
|
+
BOOL hasScope = nil != options.scope && [options.scope length] > 0;
|
|
198
|
+
if (hasScope) {
|
|
199
|
+
rc = xmlTextWriterStartElement(writer,
|
|
200
|
+
(xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
|
|
201
|
+
if (rc < 0) {
|
|
202
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
if (rc >= 0) {
|
|
207
|
+
[self waitUntilStableWithElement:root];
|
|
208
|
+
// If 'includeHittableInPageSource' setting is enabled, then use native snapshots
|
|
209
|
+
// to calculate a more accurate value for the 'hittable' attribute.
|
|
210
|
+
rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root
|
|
211
|
+
useNative:FBConfiguration.sharedInstance.includeHittableInPageSource]
|
|
212
|
+
writer:writer
|
|
213
|
+
elementStore:nil
|
|
214
|
+
query:nil
|
|
215
|
+
excludingAttributes:options.excludedAttributes];
|
|
209
216
|
}
|
|
210
|
-
}
|
|
211
217
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
if (rc >= 0 && hasScope) {
|
|
219
|
+
rc = xmlTextWriterEndElement(writer);
|
|
220
|
+
if (rc < 0) {
|
|
221
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (rc >= 0) {
|
|
226
|
+
rc = xmlTextWriterEndDocument(writer);
|
|
227
|
+
if (rc < 0) {
|
|
228
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
|
|
229
|
+
}
|
|
216
230
|
}
|
|
217
231
|
}
|
|
232
|
+
if (rc < 0) {
|
|
233
|
+
return nil;
|
|
234
|
+
}
|
|
235
|
+
return [[NSString alloc] initWithBytes:xmlBufferContent(buffer)
|
|
236
|
+
length:(NSUInteger)xmlBufferLength(buffer)
|
|
237
|
+
encoding:NSUTF8StringEncoding];
|
|
238
|
+
} @finally {
|
|
239
|
+
if (NULL != writer) {
|
|
240
|
+
xmlFreeTextWriter(writer);
|
|
241
|
+
}
|
|
242
|
+
if (NULL != buffer) {
|
|
243
|
+
xmlBufferFree(buffer);
|
|
244
|
+
}
|
|
218
245
|
}
|
|
219
|
-
if (rc < 0) {
|
|
220
|
-
xmlFreeTextWriter(writer);
|
|
221
|
-
xmlFreeDoc(doc);
|
|
222
|
-
return nil;
|
|
223
|
-
}
|
|
224
|
-
int buffersize;
|
|
225
|
-
xmlChar *xmlbuff;
|
|
226
|
-
xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
|
|
227
|
-
xmlFreeTextWriter(writer);
|
|
228
|
-
xmlFreeDoc(doc);
|
|
229
|
-
NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding];
|
|
230
|
-
xmlFree(xmlbuff);
|
|
231
|
-
return result;
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
+ (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
|
|
235
249
|
forQuery:(NSString *)xpathQuery
|
|
236
250
|
{
|
|
237
|
-
xmlDocPtr doc;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot];
|
|
251
|
+
xmlDocPtr doc = NULL;
|
|
252
|
+
xmlTextWriterPtr writer = NULL;
|
|
253
|
+
xmlXPathObjectPtr contextNodeQueryResult = NULL;
|
|
254
|
+
xmlXPathObjectPtr queryResult = NULL;
|
|
255
|
+
@try {
|
|
256
|
+
writer = xmlNewTextWriterDoc(&doc, 0);
|
|
257
|
+
if (NULL == writer) {
|
|
258
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery];
|
|
259
|
+
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
|
260
|
+
}
|
|
261
|
+
NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
|
|
262
|
+
int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
|
|
263
|
+
id<FBXCElementSnapshot> lookupScopeSnapshot = nil;
|
|
264
|
+
id<FBXCElementSnapshot> contextRootSnapshot = nil;
|
|
265
|
+
BOOL useNativeSnapshot = nil == xpathQuery
|
|
266
|
+
? NO
|
|
267
|
+
: [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class];
|
|
268
|
+
if (rc < 0) {
|
|
269
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
|
|
257
270
|
} else {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// root.lastSnapshot may be stale leftover from an unrelated earlier command.
|
|
262
|
-
contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
|
|
263
|
-
? nil
|
|
264
|
-
: ([(XCUIElement *)root fb_cachedSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root
|
|
265
|
-
useNative:useNativeSnapshot]);
|
|
271
|
+
[self waitUntilStableWithElement:root];
|
|
272
|
+
if (FBConfiguration.sharedInstance.limitXpathContextScope) {
|
|
273
|
+
lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot];
|
|
266
274
|
} else {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
if ([root isKindOfClass:XCUIElement.class]) {
|
|
276
|
+
lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application]
|
|
277
|
+
useNative:useNativeSnapshot];
|
|
278
|
+
// root.lastSnapshot may be stale leftover from an unrelated earlier command.
|
|
279
|
+
contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
|
|
280
|
+
? nil
|
|
281
|
+
: ([(XCUIElement *)root fb_cachedSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root
|
|
282
|
+
useNative:useNativeSnapshot]);
|
|
283
|
+
} else {
|
|
284
|
+
lookupScopeSnapshot = (id<FBXCElementSnapshot>)root;
|
|
285
|
+
contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id<FBXCElementSnapshot>)root;
|
|
286
|
+
while (nil != lookupScopeSnapshot.parent) {
|
|
287
|
+
lookupScopeSnapshot = lookupScopeSnapshot.parent;
|
|
288
|
+
}
|
|
271
289
|
}
|
|
272
290
|
}
|
|
273
|
-
}
|
|
274
291
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
292
|
+
rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot
|
|
293
|
+
writer:writer
|
|
294
|
+
elementStore:elementStore
|
|
295
|
+
query:xpathQuery
|
|
296
|
+
excludingAttributes:nil];
|
|
297
|
+
if (rc >= 0) {
|
|
298
|
+
rc = xmlTextWriterEndDocument(writer);
|
|
299
|
+
if (rc < 0) {
|
|
300
|
+
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
|
|
301
|
+
}
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
xmlFreeDoc(doc);
|
|
290
|
-
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
|
291
|
-
}
|
|
304
|
+
if (rc < 0) {
|
|
305
|
+
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
|
306
|
+
}
|
|
292
307
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
308
|
+
contextNodeQueryResult = [self matchNodeInDocument:doc
|
|
309
|
+
elementStore:elementStore.copy
|
|
310
|
+
forSnapshot:contextRootSnapshot];
|
|
311
|
+
xmlNodePtr contextNode = NULL;
|
|
312
|
+
if (NULL != contextNodeQueryResult) {
|
|
313
|
+
xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval;
|
|
314
|
+
if (!xmlXPathNodeSetIsEmpty(nodeSet)) {
|
|
315
|
+
contextNode = nodeSet->nodeTab[0];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
NSString *evaluationError = nil;
|
|
319
|
+
queryResult = [self evaluate:xpathQuery
|
|
320
|
+
document:doc
|
|
321
|
+
contextNode:contextNode
|
|
322
|
+
errorMessage:&evaluationError];
|
|
323
|
+
if (NULL == queryResult) {
|
|
324
|
+
return [self throwException:FBInvalidXPathException forQuery:xpathQuery detail:evaluationError];
|
|
301
325
|
}
|
|
302
|
-
}
|
|
303
|
-
NSString *evaluationError = nil;
|
|
304
|
-
xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
|
|
305
|
-
document:doc
|
|
306
|
-
contextNode:contextNode
|
|
307
|
-
errorMessage:&evaluationError];
|
|
308
|
-
if (NULL != contextNodeQueryResult) {
|
|
309
|
-
xmlXPathFreeObject(contextNodeQueryResult);
|
|
310
|
-
}
|
|
311
|
-
if (NULL == queryResult) {
|
|
312
|
-
xmlFreeTextWriter(writer);
|
|
313
|
-
xmlFreeDoc(doc);
|
|
314
|
-
return [self throwException:FBInvalidXPathException forQuery:xpathQuery detail:evaluationError];
|
|
315
|
-
}
|
|
316
326
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
327
|
+
NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
|
|
328
|
+
elementStore:elementStore];
|
|
329
|
+
if (nil == matchingSnapshots) {
|
|
330
|
+
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
|
331
|
+
}
|
|
332
|
+
return matchingSnapshots;
|
|
333
|
+
} @finally {
|
|
334
|
+
if (NULL != queryResult) {
|
|
335
|
+
xmlXPathFreeObject(queryResult);
|
|
336
|
+
}
|
|
337
|
+
if (NULL != contextNodeQueryResult) {
|
|
338
|
+
xmlXPathFreeObject(contextNodeQueryResult);
|
|
339
|
+
}
|
|
340
|
+
if (NULL != writer) {
|
|
341
|
+
xmlFreeTextWriter(writer);
|
|
342
|
+
}
|
|
343
|
+
if (NULL != doc) {
|
|
344
|
+
xmlFreeDoc(doc);
|
|
345
|
+
}
|
|
324
346
|
}
|
|
325
|
-
return matchingSnapshots;
|
|
326
347
|
}
|
|
327
348
|
|
|
328
349
|
+ (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
|
|
@@ -10,7 +10,40 @@
|
|
|
10
10
|
|
|
11
11
|
@implementation SceneDelegate
|
|
12
12
|
|
|
13
|
+
- (void)showIdentifierLookupFixture
|
|
14
|
+
{
|
|
15
|
+
UIViewController *controller = [UIViewController new];
|
|
16
|
+
controller.view.backgroundColor = UIColor.whiteColor;
|
|
17
|
+
controller.view.accessibilityIdentifier = @"IdentifierLookupFixture";
|
|
18
|
+
NSArray *identifiers = @[@"something", @"different", @"", NSNull.null, @"something", @"", NSNull.null, @"quote'\"é", NSNull.null];
|
|
19
|
+
NSArray<NSString *> *labels = @[@"other label", @"something", @"something", @"something", @"something", @"", @"", @"quoted", @"日本語"];
|
|
20
|
+
NSMutableArray<UILabel *> *labelViews = [NSMutableArray array];
|
|
21
|
+
for (NSUInteger i = 0; i < identifiers.count; i++) {
|
|
22
|
+
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 90 + i * 40, 300, 35)];
|
|
23
|
+
label.text = [NSString stringWithFormat:@"Fixture %lu", (unsigned long)i];
|
|
24
|
+
label.accessibilityIdentifier = identifiers[i] == NSNull.null ? nil : identifiers[i];
|
|
25
|
+
label.accessibilityLabel = labels[i];
|
|
26
|
+
label.isAccessibilityElement = YES;
|
|
27
|
+
[labelViews addObject:label];
|
|
28
|
+
[controller.view addSubview:label];
|
|
29
|
+
}
|
|
30
|
+
UIButton *mutate = [UIButton buttonWithType:UIButtonTypeSystem];
|
|
31
|
+
mutate.frame = CGRectMake(20, 470, 220, 40);
|
|
32
|
+
[mutate setTitle:@"Change identifier" forState:UIControlStateNormal];
|
|
33
|
+
mutate.accessibilityIdentifier = @"mutateIdentifier";
|
|
34
|
+
[mutate addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
|
|
35
|
+
labelViews[0].accessibilityIdentifier = @"changed";
|
|
36
|
+
}] forControlEvents:UIControlEventTouchUpInside];
|
|
37
|
+
[controller.view addSubview:mutate];
|
|
38
|
+
self.window.rootViewController = controller;
|
|
39
|
+
[self.window makeKeyAndVisible];
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
|
|
43
|
+
if ([NSProcessInfo.processInfo.arguments containsObject:@"--identifier-lookup-test"]) {
|
|
44
|
+
[self showIdentifierLookupFixture];
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
14
47
|
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
|
15
48
|
// If using a storyboard, the `window` property will automatically be set and attached to the scene.
|
|
16
49
|
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession:` instead).
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#import <XCTest/XCTest.h>
|
|
10
10
|
|
|
11
11
|
#import "FBIntegrationTestCase.h"
|
|
12
|
+
#import "FBConfiguration.h"
|
|
12
13
|
#import "FBElementUtils.h"
|
|
13
14
|
#import "FBExceptions.h"
|
|
14
15
|
#import "FBTestMacros.h"
|
|
@@ -558,3 +559,115 @@
|
|
|
558
559
|
}
|
|
559
560
|
|
|
560
561
|
@end
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@interface XCUIElementFBFindTests_IdentifierSemantics : FBIntegrationTestCase
|
|
565
|
+
@end
|
|
566
|
+
|
|
567
|
+
@implementation XCUIElementFBFindTests_IdentifierSemantics
|
|
568
|
+
|
|
569
|
+
- (void)setUp
|
|
570
|
+
{
|
|
571
|
+
[super setUp];
|
|
572
|
+
self.testedApplication.launchArguments = @[@"--identifier-lookup-test"];
|
|
573
|
+
[self.testedApplication launch];
|
|
574
|
+
XCTAssertTrue([self.testedApplication.otherElements[@"IdentifierLookupFixture"] waitForExistenceWithTimeout:5]);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
- (void)tearDown
|
|
578
|
+
{
|
|
579
|
+
[self.testedApplication terminate];
|
|
580
|
+
[super tearDown];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
- (void)testIdentifierTakesPrecedenceOverLabel
|
|
584
|
+
{
|
|
585
|
+
NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingIdentifier:@"something"
|
|
586
|
+
shouldReturnAfterFirstMatch:NO];
|
|
587
|
+
XCTAssertEqual(matches.count, 4);
|
|
588
|
+
XCTAssertEqualObjects([matches valueForKey:@"label"], (@[@"other label", @"something", @"something", @"something"]));
|
|
589
|
+
XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"different" shouldReturnAfterFirstMatch:NO].count, 1);
|
|
590
|
+
// A matching label must not override a different, nonempty identifier.
|
|
591
|
+
XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"other label" shouldReturnAfterFirstMatch:NO].count, 0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
- (void)testEmptyIdentifierDoesNotMatchEmptyAccessibilityId
|
|
595
|
+
{
|
|
596
|
+
// The fixture has an empty identifier with a nonempty label at index 2,
|
|
597
|
+
// and an empty identifier with an empty label at index 5.
|
|
598
|
+
XCUIElement *labelFallback = [self.testedApplication.staticTexts elementBoundByIndex:2];
|
|
599
|
+
XCUIElement *unnamed = [self.testedApplication.staticTexts elementBoundByIndex:5];
|
|
600
|
+
XCTAssertEqualObjects(labelFallback.identifier, @"");
|
|
601
|
+
XCTAssertEqualObjects(labelFallback.label, @"something");
|
|
602
|
+
XCTAssertEqualObjects(unnamed.identifier, @"");
|
|
603
|
+
XCTAssertEqualObjects(unnamed.label, @"");
|
|
604
|
+
|
|
605
|
+
// Regression guard for "identifier != nil AND identifier != ''": the old
|
|
606
|
+
// wdName lookup used the nonempty label, or nil when both fields were empty.
|
|
607
|
+
// Thus an empty query matched neither element. Checking only identifier != nil
|
|
608
|
+
// would instead let identifier == @"" match both, changing existing behavior.
|
|
609
|
+
BOOL previousFirstMatch = FBConfiguration.sharedInstance.useFirstMatch;
|
|
610
|
+
@try {
|
|
611
|
+
for (NSNumber *useFirstMatch in @[@NO, @YES]) {
|
|
612
|
+
FBConfiguration.sharedInstance.useFirstMatch = useFirstMatch.boolValue;
|
|
613
|
+
for (NSNumber *firstMatch in @[@NO, @YES]) {
|
|
614
|
+
XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@""
|
|
615
|
+
shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 0,
|
|
616
|
+
@"An empty accessibility ID must not match empty identifiers, even when they are non-nil");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} @finally {
|
|
620
|
+
FBConfiguration.sharedInstance.useFirstMatch = previousFirstMatch;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
- (void)testMissingQuotedAndUnicodeIdentifiers
|
|
625
|
+
{
|
|
626
|
+
for (NSNumber *firstMatch in @[@NO, @YES]) {
|
|
627
|
+
XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"missing-target" shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 0);
|
|
628
|
+
XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"quote'\"é" shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 1);
|
|
629
|
+
XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"日本語" shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
- (void)testFirstMatchAndParentScopeWithBothBindings
|
|
634
|
+
{
|
|
635
|
+
BOOL previousBinding = FBConfiguration.sharedInstance.boundElementsByIndex;
|
|
636
|
+
@try {
|
|
637
|
+
for (NSNumber *indexed in @[@NO, @YES]) {
|
|
638
|
+
FBConfiguration.sharedInstance.boundElementsByIndex = indexed.boolValue;
|
|
639
|
+
// Use the element returned by WDA lookup, as the element-scoped endpoint does.
|
|
640
|
+
XCUIElement *parent = [self.testedApplication fb_descendantsMatchingIdentifier:@"IdentifierLookupFixture"
|
|
641
|
+
shouldReturnAfterFirstMatch:YES].firstObject;
|
|
642
|
+
XCTAssertNotNil(parent);
|
|
643
|
+
// The HTTP endpoint resolves the parent when registering its element ID.
|
|
644
|
+
XCTAssertNotNil(parent.fb_uid);
|
|
645
|
+
NSArray<XCUIElement *> *matches = [parent fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:NO];
|
|
646
|
+
NSArray<XCUIElement *> *first = [parent fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:YES];
|
|
647
|
+
XCTAssertEqual(matches.count, 4);
|
|
648
|
+
XCTAssertEqual(first.count, 1);
|
|
649
|
+
XCTAssertEqualObjects(first.firstObject.fb_uid, matches.firstObject.fb_uid);
|
|
650
|
+
NSArray<XCUIElement *> *selfMatches = [parent fb_descendantsMatchingIdentifier:@"IdentifierLookupFixture" shouldReturnAfterFirstMatch:NO];
|
|
651
|
+
XCTAssertEqual(selfMatches.count, 1);
|
|
652
|
+
XCTAssertEqualObjects(selfMatches.firstObject.fb_uid, parent.fb_uid);
|
|
653
|
+
}
|
|
654
|
+
} @finally {
|
|
655
|
+
FBConfiguration.sharedInstance.boundElementsByIndex = previousBinding;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
- (void)testIdentifierChangesAreObserved
|
|
660
|
+
{
|
|
661
|
+
NSArray<XCUIElement *> *before = [self.testedApplication fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:NO];
|
|
662
|
+
XCTAssertEqual(before.count, 4);
|
|
663
|
+
NSArray<NSString *> *beforeIds = [before valueForKey:@"fb_uid"];
|
|
664
|
+
[self.testedApplication.buttons[@"mutateIdentifier"] tap];
|
|
665
|
+
NSArray<XCUIElement *> *after = [self.testedApplication fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:NO];
|
|
666
|
+
NSArray<XCUIElement *> *changed = [self.testedApplication fb_descendantsMatchingIdentifier:@"changed" shouldReturnAfterFirstMatch:NO];
|
|
667
|
+
XCTAssertEqual(after.count, 3);
|
|
668
|
+
XCTAssertEqual(changed.count, 1);
|
|
669
|
+
XCTAssertEqualObjects([after valueForKey:@"fb_uid"], [beforeIds subarrayWithRange:NSMakeRange(1, 3)]);
|
|
670
|
+
XCTAssertEqualObjects(changed.firstObject.fb_uid, beforeIds.firstObject);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@end
|
|
@@ -16,6 +16,33 @@
|
|
|
16
16
|
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
|
17
17
|
#import "XCTestPrivateSymbols.h"
|
|
18
18
|
|
|
19
|
+
// libxml's node callbacks are thread-local; restore any existing callbacks after each probe.
|
|
20
|
+
static __thread NSUInteger FBReviewCreatedDocs;
|
|
21
|
+
static __thread NSUInteger FBReviewFreedDocs;
|
|
22
|
+
static __thread xmlRegisterNodeFunc FBReviewPreviousRegister;
|
|
23
|
+
static __thread xmlDeregisterNodeFunc FBReviewPreviousDeregister;
|
|
24
|
+
|
|
25
|
+
static void FBReviewRegisterNode(xmlNodePtr node)
|
|
26
|
+
{
|
|
27
|
+
if (node->type == XML_DOCUMENT_NODE) { FBReviewCreatedDocs++; }
|
|
28
|
+
if (NULL != FBReviewPreviousRegister) { FBReviewPreviousRegister(node); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static void FBReviewDeregisterNode(xmlNodePtr node)
|
|
32
|
+
{
|
|
33
|
+
if (node->type == XML_DOCUMENT_NODE) { FBReviewFreedDocs++; }
|
|
34
|
+
if (NULL != FBReviewPreviousDeregister) { FBReviewPreviousDeregister(node); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@interface FBThrowingLabelSnapshot : XCElementSnapshotDouble
|
|
38
|
+
@end
|
|
39
|
+
@implementation FBThrowingLabelSnapshot
|
|
40
|
+
- (NSString *)label
|
|
41
|
+
{
|
|
42
|
+
@throw [NSException exceptionWithName:@"SnapshotAttributeFailure" reason:@"test" userInfo:nil];
|
|
43
|
+
}
|
|
44
|
+
@end
|
|
45
|
+
|
|
19
46
|
@interface FBTraversalSnapshot : XCElementSnapshotDouble
|
|
20
47
|
@property (nonatomic, copy) NSArray *childSnapshots;
|
|
21
48
|
@property (nonatomic, weak) FBTraversalSnapshot *parentSnapshot;
|
|
@@ -57,6 +84,44 @@
|
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
86
|
|
|
87
|
+
- (void)testDirectXmlWriterPreservesEscapingAndFormatting
|
|
88
|
+
{
|
|
89
|
+
XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
|
|
90
|
+
snapshot.label = @"line one\nline two\t& < > \" йоло";
|
|
91
|
+
snapshot.value = @"value";
|
|
92
|
+
NSString *expected = [[self xmlStringWithElement:(id)snapshot xpathQuery:nil excludingAttributes:nil]
|
|
93
|
+
stringByReplacingOccurrencesOfString:@" private_indexPath=\"top\"" withString:@""];
|
|
94
|
+
NSString *actual = [FBXPath xmlStringWithRootElement:(id)snapshot options:nil];
|
|
95
|
+
XCTAssertEqualObjects(actual, expected);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
- (void)testXmlDocumentsAreFreedWhenSnapshotAttributesThrow
|
|
99
|
+
{
|
|
100
|
+
for (NSNumber *isLookup in @[@NO, @YES]) {
|
|
101
|
+
FBReviewCreatedDocs = 0;
|
|
102
|
+
FBReviewFreedDocs = 0;
|
|
103
|
+
FBReviewPreviousRegister = xmlRegisterNodeDefault(FBReviewRegisterNode);
|
|
104
|
+
FBReviewPreviousDeregister = xmlDeregisterNodeDefault(FBReviewDeregisterNode);
|
|
105
|
+
@try {
|
|
106
|
+
id<FBElement> snapshot = (id)[FBThrowingLabelSnapshot new];
|
|
107
|
+
if (isLookup.boolValue) {
|
|
108
|
+
XCTAssertThrowsSpecificNamed([FBXPath matchesWithRootElement:snapshot forQuery:@"//*[@label]"],
|
|
109
|
+
NSException, @"SnapshotAttributeFailure");
|
|
110
|
+
} else {
|
|
111
|
+
XCTAssertThrowsSpecificNamed([FBXPath xmlStringWithRootElement:snapshot options:nil],
|
|
112
|
+
NSException, @"SnapshotAttributeFailure");
|
|
113
|
+
}
|
|
114
|
+
if (isLookup.boolValue) {
|
|
115
|
+
XCTAssertGreaterThan(FBReviewCreatedDocs, 0u);
|
|
116
|
+
}
|
|
117
|
+
XCTAssertEqual(FBReviewCreatedDocs, FBReviewFreedDocs);
|
|
118
|
+
} @finally {
|
|
119
|
+
xmlRegisterNodeDefault(FBReviewPreviousRegister);
|
|
120
|
+
xmlDeregisterNodeDefault(FBReviewPreviousDeregister);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
60
125
|
- (NSString *)xmlStringWithElement:(id<FBXCElementSnapshot>)snapshot
|
|
61
126
|
xpathQuery:(nullable NSString *)query
|
|
62
127
|
excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
|