appium-webdriveragent 16.13.4 → 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
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## [16.13.4](https://github.com/appium/WebDriverAgent/compare/v16.13.3...v16.13.4) (2026-09-30)
|
|
2
8
|
|
|
3
9
|
### Performance Improvements
|
|
@@ -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
|
|
@@ -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
|