appium-webdriveragent 13.1.3 → 13.2.1

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.
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import "FBSettingsHandler.h"
10
+
11
+ #import "FBActiveAppDetectionPoint.h"
12
+ #import "FBClassChainQueryParser.h"
13
+ #import "FBCommandStatus.h"
14
+ #import "FBConfiguration.h"
15
+ #import "FBSession.h"
16
+ #import "FBSettings.h"
17
+
18
+ typedef FBCommandStatus * _Nullable (^FBSettingApplyBlock)(FBSession *session, id value);
19
+ typedef id _Nonnull (^FBSettingGetBlock)(FBSession *session);
20
+
21
+ static id FBNormalizedSettingValue(id value)
22
+ {
23
+ return value == NSNull.null ? nil : value;
24
+ }
25
+
26
+ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
27
+ {
28
+ static NSSet<NSString *> *keys;
29
+ static dispatch_once_t onceToken;
30
+ dispatch_once(&onceToken, ^{
31
+ keys = [NSSet setWithObjects:
32
+ FB_SETTING_DEFAULT_ALERT_ACTION,
33
+ FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR,
34
+ FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR,
35
+ FB_SETTING_AUTO_CLICK_ALERT_SELECTOR,
36
+ nil];
37
+ });
38
+ return keys;
39
+ }
40
+
41
+ @implementation FBSettingsHandler
42
+
43
+ + (NSDictionary<NSString *, FBSettingApplyBlock> *)settersMap
44
+ {
45
+ static NSDictionary<NSString *, FBSettingApplyBlock> *settersMap;
46
+ static dispatch_once_t onceToken;
47
+ dispatch_once(&onceToken, ^{
48
+ NSMutableDictionary<NSString *, FBSettingApplyBlock> *map = [NSMutableDictionary dictionary];
49
+ map[FB_SETTING_USE_COMPACT_RESPONSES] = ^FBCommandStatus *(FBSession *session, id value) {
50
+ [FBConfiguration setShouldUseCompactResponses:[value boolValue]];
51
+ return nil;
52
+ };
53
+ map[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES] = ^FBCommandStatus *(FBSession *session, id value) {
54
+ [FBConfiguration setElementResponseAttributes:(NSString *)value];
55
+ return nil;
56
+ };
57
+ map[FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] = ^FBCommandStatus *(FBSession *session, id value) {
58
+ [FBConfiguration setMjpegServerScreenshotQuality:[value unsignedIntegerValue]];
59
+ return nil;
60
+ };
61
+ map[FB_SETTING_MJPEG_SERVER_FRAMERATE] = ^FBCommandStatus *(FBSession *session, id value) {
62
+ [FBConfiguration setMjpegServerFramerate:[value unsignedIntegerValue]];
63
+ return nil;
64
+ };
65
+ map[FB_SETTING_SCREENSHOT_QUALITY] = ^FBCommandStatus *(FBSession *session, id value) {
66
+ [FBConfiguration setScreenshotQuality:[value unsignedIntegerValue]];
67
+ return nil;
68
+ };
69
+ map[FB_SETTING_MJPEG_SCALING_FACTOR] = ^FBCommandStatus *(FBSession *session, id value) {
70
+ [FBConfiguration setMjpegScalingFactor:[value floatValue]];
71
+ return nil;
72
+ };
73
+ map[FB_SETTING_MJPEG_FIX_ORIENTATION] = ^FBCommandStatus *(FBSession *session, id value) {
74
+ [FBConfiguration setMjpegShouldFixOrientation:[value boolValue]];
75
+ return nil;
76
+ };
77
+ map[FB_SETTING_KEYBOARD_AUTOCORRECTION] = ^FBCommandStatus *(FBSession *session, id value) {
78
+ [FBConfiguration setKeyboardAutocorrection:[value boolValue]];
79
+ return nil;
80
+ };
81
+ map[FB_SETTING_KEYBOARD_PREDICTION] = ^FBCommandStatus *(FBSession *session, id value) {
82
+ [FBConfiguration setKeyboardPrediction:[value boolValue]];
83
+ return nil;
84
+ };
85
+ map[FB_SETTING_RESPECT_SYSTEM_ALERTS] = ^FBCommandStatus *(FBSession *session, id value) {
86
+ [FBConfiguration setShouldRespectSystemAlerts:[value boolValue]];
87
+ return nil;
88
+ };
89
+ map[FB_SETTING_SNAPSHOT_MAX_DEPTH] = ^FBCommandStatus *(FBSession *session, id value) {
90
+ [FBConfiguration setSnapshotMaxDepth:[value intValue]];
91
+ return nil;
92
+ };
93
+ map[FB_SETTING_SNAPSHOT_MAX_CHILDREN] = ^FBCommandStatus *(FBSession *session, id value) {
94
+ [FBConfiguration setSnapshotMaxChildren:[value intValue]];
95
+ return nil;
96
+ };
97
+ map[FB_SETTING_USE_FIRST_MATCH] = ^FBCommandStatus *(FBSession *session, id value) {
98
+ [FBConfiguration setUseFirstMatch:[value boolValue]];
99
+ return nil;
100
+ };
101
+ map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^FBCommandStatus *(FBSession *session, id value) {
102
+ [FBConfiguration setBoundElementsByIndex:[value boolValue]];
103
+ return nil;
104
+ };
105
+ map[FB_SETTING_REDUCE_MOTION] = ^FBCommandStatus *(FBSession *session, id value) {
106
+ [FBConfiguration setReduceMotionEnabled:[value boolValue]];
107
+ return nil;
108
+ };
109
+ map[FB_SETTING_DEFAULT_ACTIVE_APPLICATION] = ^FBCommandStatus *(FBSession *session, id value) {
110
+ session.defaultActiveApplication = (NSString *)value;
111
+ return nil;
112
+ };
113
+ map[FB_SETTING_ACTIVE_APP_DETECTION_POINT] = ^FBCommandStatus *(FBSession *session, id value) {
114
+ NSError *error;
115
+ if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)value
116
+ error:&error]) {
117
+ return [FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
118
+ traceback:nil];
119
+ }
120
+ return nil;
121
+ };
122
+ map[FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR] = ^FBCommandStatus *(FBSession *session, id value) {
123
+ [FBConfiguration setAcceptAlertButtonSelector:(NSString *)value];
124
+ return nil;
125
+ };
126
+ map[FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR] = ^FBCommandStatus *(FBSession *session, id value) {
127
+ [FBConfiguration setDismissAlertButtonSelector:(NSString *)value];
128
+ return nil;
129
+ };
130
+ map[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] = ^FBCommandStatus *(FBSession *session, id value) {
131
+ return [self configureAutoClickAlertWithSelector:(NSString *)value forSession:session];
132
+ };
133
+ map[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] = ^FBCommandStatus *(FBSession *session, id value) {
134
+ [FBConfiguration setWaitForIdleTimeout:[value doubleValue]];
135
+ return nil;
136
+ };
137
+ map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^FBCommandStatus *(FBSession *session, id value) {
138
+ [FBConfiguration setAnimationCoolOffTimeout:[value doubleValue]];
139
+ return nil;
140
+ };
141
+ map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^FBCommandStatus *(FBSession *session, id value) {
142
+ if (nil == value) {
143
+ session.defaultAlertAction = nil;
144
+ } else if ([value isKindOfClass:NSString.class]) {
145
+ session.defaultAlertAction = [(NSString *)value lowercaseString];
146
+ }
147
+ return nil;
148
+ };
149
+ map[FB_SETTING_MAX_TYPING_FREQUENCY] = ^FBCommandStatus *(FBSession *session, id value) {
150
+ [FBConfiguration setMaxTypingFrequency:[value unsignedIntegerValue]];
151
+ return nil;
152
+ };
153
+ map[FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] = ^FBCommandStatus *(FBSession *session, id value) {
154
+ [FBConfiguration setUseClearTextShortcut:[value boolValue]];
155
+ return nil;
156
+ };
157
+ map[FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) {
158
+ [FBConfiguration setIncludeHittableInPageSource:[value boolValue]];
159
+ return nil;
160
+ };
161
+ map[FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) {
162
+ [FBConfiguration setIncludeNativeFrameInPageSource:[value boolValue]];
163
+ return nil;
164
+ };
165
+ map[FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) {
166
+ [FBConfiguration setIncludeMinMaxValueInPageSource:[value boolValue]];
167
+ return nil;
168
+ };
169
+ map[FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] = ^FBCommandStatus *(FBSession *session, id value) {
170
+ [FBConfiguration setIncludeCustomActionsInPageSource:[value boolValue]];
171
+ return nil;
172
+ };
173
+ map[FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] = ^FBCommandStatus *(FBSession *session, id value) {
174
+ [FBConfiguration setEnforceCustomSnapshots:[value boolValue]];
175
+ return nil;
176
+ };
177
+ map[FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] = ^FBCommandStatus *(FBSession *session, id value) {
178
+ [FBConfiguration setLimitXpathContextScope:[value boolValue]];
179
+ return nil;
180
+ };
181
+ #if !TARGET_OS_TV
182
+ map[FB_SETTING_SCREENSHOT_ORIENTATION] = ^FBCommandStatus *(FBSession *session, id value) {
183
+ NSError *error;
184
+ if (![FBConfiguration setScreenshotOrientation:(NSString *)value error:&error]) {
185
+ return [FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
186
+ traceback:nil];
187
+ }
188
+ return nil;
189
+ };
190
+ #endif
191
+ settersMap = map.copy;
192
+ });
193
+ return settersMap;
194
+ }
195
+
196
+ + (NSDictionary<NSString *, FBSettingGetBlock> *)gettersMap
197
+ {
198
+ static NSDictionary<NSString *, FBSettingGetBlock> *gettersMap;
199
+ static dispatch_once_t onceToken;
200
+ dispatch_once(&onceToken, ^{
201
+ NSMutableDictionary<NSString *, FBSettingGetBlock> *map = [NSMutableDictionary dictionary];
202
+ map[FB_SETTING_USE_COMPACT_RESPONSES] = ^id(FBSession *session) {
203
+ return @([FBConfiguration shouldUseCompactResponses]);
204
+ };
205
+ map[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES] = ^id(FBSession *session) {
206
+ return [FBConfiguration elementResponseAttributes];
207
+ };
208
+ map[FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] = ^id(FBSession *session) {
209
+ return @([FBConfiguration mjpegServerScreenshotQuality]);
210
+ };
211
+ map[FB_SETTING_MJPEG_SERVER_FRAMERATE] = ^id(FBSession *session) {
212
+ return @([FBConfiguration mjpegServerFramerate]);
213
+ };
214
+ map[FB_SETTING_MJPEG_SCALING_FACTOR] = ^id(FBSession *session) {
215
+ return @([FBConfiguration mjpegScalingFactor]);
216
+ };
217
+ map[FB_SETTING_MJPEG_FIX_ORIENTATION] = ^id(FBSession *session) {
218
+ return @([FBConfiguration mjpegShouldFixOrientation]);
219
+ };
220
+ map[FB_SETTING_SCREENSHOT_QUALITY] = ^id(FBSession *session) {
221
+ return @([FBConfiguration screenshotQuality]);
222
+ };
223
+ map[FB_SETTING_KEYBOARD_AUTOCORRECTION] = ^id(FBSession *session) {
224
+ return @([FBConfiguration keyboardAutocorrection]);
225
+ };
226
+ map[FB_SETTING_KEYBOARD_PREDICTION] = ^id(FBSession *session) {
227
+ return @([FBConfiguration keyboardPrediction]);
228
+ };
229
+ map[FB_SETTING_SNAPSHOT_MAX_DEPTH] = ^id(FBSession *session) {
230
+ return @([FBConfiguration snapshotMaxDepth]);
231
+ };
232
+ map[FB_SETTING_SNAPSHOT_MAX_CHILDREN] = ^id(FBSession *session) {
233
+ return @([FBConfiguration snapshotMaxChildren]);
234
+ };
235
+ map[FB_SETTING_USE_FIRST_MATCH] = ^id(FBSession *session) {
236
+ return @([FBConfiguration useFirstMatch]);
237
+ };
238
+ map[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] = ^id(FBSession *session) {
239
+ return @([FBConfiguration waitForIdleTimeout]);
240
+ };
241
+ map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^id(FBSession *session) {
242
+ return @([FBConfiguration animationCoolOffTimeout]);
243
+ };
244
+ map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^id(FBSession *session) {
245
+ return @([FBConfiguration boundElementsByIndex]);
246
+ };
247
+ map[FB_SETTING_REDUCE_MOTION] = ^id(FBSession *session) {
248
+ return @([FBConfiguration reduceMotionEnabled]);
249
+ };
250
+ map[FB_SETTING_DEFAULT_ACTIVE_APPLICATION] = ^id(FBSession *session) {
251
+ return session.defaultActiveApplication;
252
+ };
253
+ map[FB_SETTING_ACTIVE_APP_DETECTION_POINT] = ^id(FBSession *session) {
254
+ return FBActiveAppDetectionPoint.sharedInstance.stringCoordinates;
255
+ };
256
+ map[FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR] = ^id(FBSession *session) {
257
+ return FBConfiguration.acceptAlertButtonSelector;
258
+ };
259
+ map[FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR] = ^id(FBSession *session) {
260
+ return FBConfiguration.dismissAlertButtonSelector;
261
+ };
262
+ map[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] = ^id(FBSession *session) {
263
+ return FBConfiguration.autoClickAlertSelector;
264
+ };
265
+ map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^id(FBSession *session) {
266
+ return session.defaultAlertAction ?: @"";
267
+ };
268
+ map[FB_SETTING_MAX_TYPING_FREQUENCY] = ^id(FBSession *session) {
269
+ return @([FBConfiguration maxTypingFrequency]);
270
+ };
271
+ map[FB_SETTING_RESPECT_SYSTEM_ALERTS] = ^id(FBSession *session) {
272
+ return @([FBConfiguration shouldRespectSystemAlerts]);
273
+ };
274
+ map[FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] = ^id(FBSession *session) {
275
+ return @([FBConfiguration useClearTextShortcut]);
276
+ };
277
+ map[FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] = ^id(FBSession *session) {
278
+ return @([FBConfiguration includeHittableInPageSource]);
279
+ };
280
+ map[FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] = ^id(FBSession *session) {
281
+ return @([FBConfiguration includeNativeFrameInPageSource]);
282
+ };
283
+ map[FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] = ^id(FBSession *session) {
284
+ return @([FBConfiguration includeMinMaxValueInPageSource]);
285
+ };
286
+ map[FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] = ^id(FBSession *session) {
287
+ return @([FBConfiguration includeCustomActionsInPageSource]);
288
+ };
289
+ map[FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] = ^id(FBSession *session) {
290
+ return @([FBConfiguration enforceCustomSnapshots]);
291
+ };
292
+ map[FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] = ^id(FBSession *session) {
293
+ return @([FBConfiguration limitXpathContextScope]);
294
+ };
295
+ #if !TARGET_OS_TV
296
+ map[FB_SETTING_SCREENSHOT_ORIENTATION] = ^id(FBSession *session) {
297
+ return [FBConfiguration humanReadableScreenshotOrientation];
298
+ };
299
+ #endif
300
+ gettersMap = map.copy;
301
+ });
302
+ return gettersMap;
303
+ }
304
+
305
+ + (NSDictionary *)currentSettingsForSession:(FBSession *)session
306
+ {
307
+ NSDictionary<NSString *, FBSettingGetBlock> *gettersMap = [self gettersMap];
308
+ NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:gettersMap.count];
309
+ for (NSString *key in gettersMap) {
310
+ settings[key] = gettersMap[key](session);
311
+ }
312
+ return settings.copy;
313
+ }
314
+
315
+ + (nullable FBCommandStatus *)applySettings:(NSDictionary *)settings toSession:(FBSession *)session
316
+ {
317
+ NSDictionary<NSString *, FBSettingApplyBlock> *settersMap = [self settersMap];
318
+ NSSet<NSString *> *nilClearableKeys = FBNilClearableSettingKeys();
319
+ for (NSString *key in settings) {
320
+ FBSettingApplyBlock handler = settersMap[key];
321
+ if (nil == handler) {
322
+ continue;
323
+ }
324
+ id value = FBNormalizedSettingValue(settings[key]);
325
+ if (nil == value && ![nilClearableKeys containsObject:key]) {
326
+ continue;
327
+ }
328
+ FBCommandStatus *status = handler(session, value);
329
+ if (status.hasError) {
330
+ return status;
331
+ }
332
+ }
333
+ return nil;
334
+ }
335
+
336
+ + (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector
337
+ forSession:(FBSession *)session
338
+ {
339
+ if (0 == [selector length]) {
340
+ [FBConfiguration setAutoClickAlertSelector:selector];
341
+ [session disableAlertsMonitor];
342
+ return [FBCommandStatus ok];
343
+ }
344
+
345
+ NSError *error;
346
+ FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error];
347
+ if (nil == parsedChain) {
348
+ return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription
349
+ traceback:nil];
350
+ }
351
+ [FBConfiguration setAutoClickAlertSelector:selector];
352
+ [session enableAlertsMonitor];
353
+ return [FBCommandStatus ok];
354
+ }
355
+
356
+ @end
@@ -51,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN
51
51
  document:(xmlDocPtr)doc
52
52
  contextNode:(nullable xmlNodePtr)contextNode;
53
53
 
54
+ + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
55
+ document:(xmlDocPtr)doc
56
+ contextNode:(nullable xmlNodePtr)contextNode
57
+ errorMessage:(NSString * _Nullable * _Nullable)errorMessage;
58
+
54
59
  @end
55
60
 
56
61
  NS_ASSUME_NONNULL_END
@@ -14,6 +14,7 @@
14
14
  #import "FBLogger.h"
15
15
  #import "FBMacros.h"
16
16
  #import "FBXMLGenerationOptions.h"
17
+ #import "FBXPathExtensions.h"
17
18
  #import "FBXCElementSnapshotWrapper+Helpers.h"
18
19
  #import "NSString+FBXMLSafeString.h"
19
20
  #import "XCUIApplication.h"
@@ -152,7 +153,17 @@ static NSString *const topNodeIndexPath = @"top";
152
153
 
153
154
  + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery
154
155
  {
155
- NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
156
+ return [self throwException:name forQuery:xpathQuery detail:nil];
157
+ }
158
+
159
+ + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery detail:(nullable NSString *)detail
160
+ {
161
+ NSString *reason;
162
+ if (nil != detail) {
163
+ reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\": %@", xpathQuery, detail];
164
+ } else {
165
+ reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
166
+ }
156
167
  @throw [NSException exceptionWithName:name reason:reason userInfo:@{}];
157
168
  return nil;
158
169
  }
@@ -284,16 +295,18 @@ static NSString *const topNodeIndexPath = @"top";
284
295
  contextNode = nodeSet->nodeTab[0];
285
296
  }
286
297
  }
298
+ NSString *evaluationError = nil;
287
299
  xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
288
300
  document:doc
289
- contextNode:contextNode];
301
+ contextNode:contextNode
302
+ errorMessage:&evaluationError];
290
303
  if (NULL != contextNodeQueryResult) {
291
304
  xmlXPathFreeObject(contextNodeQueryResult);
292
305
  }
293
306
  if (NULL == queryResult) {
294
307
  xmlFreeTextWriter(writer);
295
308
  xmlFreeDoc(doc);
296
- return [self throwException:FBInvalidXPathException forQuery:xpathQuery];
309
+ return [self throwException:FBInvalidXPathException forQuery:xpathQuery detail:evaluationError];
297
310
  }
298
311
 
299
312
  NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
@@ -435,6 +448,14 @@ static NSString *const topNodeIndexPath = @"top";
435
448
  + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
436
449
  document:(xmlDocPtr)doc
437
450
  contextNode:(nullable xmlNodePtr)contextNode
451
+ {
452
+ return [self evaluate:xpathQuery document:doc contextNode:contextNode errorMessage:nil];
453
+ }
454
+
455
+ + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
456
+ document:(xmlDocPtr)doc
457
+ contextNode:(nullable xmlNodePtr)contextNode
458
+ errorMessage:(NSString * _Nullable * _Nullable)errorMessage
438
459
  {
439
460
  xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
440
461
  if (NULL == xpathCtx) {
@@ -443,10 +464,21 @@ static NSString *const topNodeIndexPath = @"top";
443
464
  }
444
465
  xpathCtx->node = NULL == contextNode ? doc->children : contextNode;
445
466
 
467
+ FBXPathExtensions *extensions = [FBXPathExtensions new];
468
+ [extensions registerFunctionsWithContext:xpathCtx];
469
+
446
470
  xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
447
471
  if (NULL == xpathObj) {
472
+ NSString *detail = extensions.lastEvaluationError;
473
+ if (NULL != errorMessage) {
474
+ *errorMessage = detail;
475
+ }
476
+ if (nil != detail) {
477
+ [FBLogger logFmt:@"Failed to evaluate XPath query \"%@\": %@", xpathQuery, detail];
478
+ } else {
479
+ [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
480
+ }
448
481
  xmlXPathFreeContext(xpathCtx);
449
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
450
482
  return NULL;
451
483
  }
452
484
  xmlXPathFreeContext(xpathCtx);
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <Foundation/Foundation.h>
10
+
11
+ #ifdef __clang__
12
+ #pragma clang diagnostic push
13
+ #pragma clang diagnostic ignored "-Wpadded"
14
+ #endif
15
+
16
+ #import <libxml/xpath.h>
17
+
18
+ #ifdef __clang__
19
+ #pragma clang diagnostic pop
20
+ #endif
21
+
22
+ NS_ASSUME_NONNULL_BEGIN
23
+
24
+ @interface FBXPathExtensions : NSObject
25
+
26
+ /**
27
+ Registers XPath 2-compatible extension functions on the given libxml2 context.
28
+ */
29
+ - (void)registerFunctionsWithContext:(xmlXPathContextPtr)xpathCtx;
30
+
31
+ /**
32
+ Human-readable message for the most recent XPath extension evaluation failure on this instance,
33
+ for example an invalid regular expression pattern or flags. Nil when no extension error has occurred.
34
+ Scoped to the libxml2 context this instance is registered with; each evaluation should use its own instance.
35
+ */
36
+ @property (nonatomic, nullable, readonly, copy) NSString *lastEvaluationError;
37
+
38
+ @end
39
+
40
+ NS_ASSUME_NONNULL_END