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,418 @@
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 "FBXPathExtensions.h"
10
+
11
+ #import "FBLogger.h"
12
+
13
+ #import <libxml/xpathInternals.h>
14
+
15
+ static void FBRegisterXPathExtensions(xmlXPathContextPtr xpathCtx);
16
+
17
+ static NSString *const FBXPathTokenSequenceSeparator = @"\x1E";
18
+ static const NSRegularExpressionOptions FBXPathNoRegexOptions = (NSRegularExpressionOptions)0;
19
+ static const NSMatchingOptions FBXPathNoMatchingOptions = (NSMatchingOptions)0;
20
+
21
+ @interface FBXPathExtensions ()
22
+ @property (nonatomic, nullable, readwrite, copy) NSString *lastEvaluationError;
23
+ @end
24
+
25
+ static FBXPathExtensions *FBXPathExtensionsFromParserContext(xmlXPathParserContextPtr ctxt)
26
+ {
27
+ if (NULL == ctxt || NULL == ctxt->context || NULL == ctxt->context->userData) {
28
+ return nil;
29
+ }
30
+ return (__bridge FBXPathExtensions *)ctxt->context->userData;
31
+ }
32
+
33
+ static void FBXPathSetEvaluationError(xmlXPathParserContextPtr ctxt, int xpathErrorCode, NSString *message)
34
+ {
35
+ FBXPathExtensions *extensions = FBXPathExtensionsFromParserContext(ctxt);
36
+ extensions.lastEvaluationError = message;
37
+ [FBLogger logFmt:@"XPath extension evaluation error: %@", message];
38
+ if (NULL == ctxt) {
39
+ return;
40
+ }
41
+ xmlXPatherror(ctxt, __FILE__, __LINE__, xpathErrorCode);
42
+ ctxt->error = xpathErrorCode;
43
+ }
44
+
45
+ static void FBXPathSetInvalidArityError(xmlXPathParserContextPtr ctxt)
46
+ {
47
+ if (NULL == ctxt) {
48
+ return;
49
+ }
50
+ xmlXPatherror(ctxt, __FILE__, __LINE__, XPATH_INVALID_ARITY);
51
+ ctxt->error = XPATH_INVALID_ARITY;
52
+ }
53
+
54
+ static BOOL FBXPathFlagsAreValid(NSString *flags, BOOL allowsQFlag)
55
+ {
56
+ if (nil == flags || 0 == flags.length) {
57
+ return YES;
58
+ }
59
+
60
+ NSString *validFlags = allowsQFlag ? @"imsxq" : @"imsx";
61
+ for (NSUInteger index = 0; index < flags.length; index++) {
62
+ unichar flag = [flags characterAtIndex:index];
63
+ if ([validFlags rangeOfString:[NSString stringWithCharacters:&flag length:1]].location == NSNotFound) {
64
+ return NO;
65
+ }
66
+ }
67
+ return YES;
68
+ }
69
+
70
+ static NSString *FBXPathStringFromUTF8Bytes(const xmlChar *bytes)
71
+ {
72
+ if (NULL == bytes) {
73
+ return nil;
74
+ }
75
+ return [NSString stringWithUTF8String:(const char *)bytes];
76
+ }
77
+
78
+ @implementation FBXPathExtensions
79
+
80
+ - (void)registerFunctionsWithContext:(xmlXPathContextPtr)xpathCtx
81
+ {
82
+ xpathCtx->userData = (__bridge void *)self;
83
+ FBRegisterXPathExtensions(xpathCtx);
84
+ }
85
+
86
+ @end
87
+
88
+ static NSString *FBXPathPopNSString(xmlXPathParserContextPtr ctxt)
89
+ {
90
+ xmlChar *value = xmlXPathPopString(ctxt);
91
+ if (NULL == value || xmlXPathCheckError(ctxt)) {
92
+ return nil;
93
+ }
94
+ NSString *result = [NSString stringWithUTF8String:(const char *)value];
95
+ xmlFree(value);
96
+ return result;
97
+ }
98
+
99
+ static NSRegularExpressionOptions FBXPathRegexOptionsFromFlags(NSString *flags)
100
+ {
101
+ NSRegularExpressionOptions options = FBXPathNoRegexOptions;
102
+ if (nil != flags && [flags rangeOfString:@"i"].location != NSNotFound) {
103
+ options |= NSRegularExpressionCaseInsensitive;
104
+ }
105
+ return options;
106
+ }
107
+
108
+ static NSRegularExpression *FBXPathRegexWithPattern(NSString *pattern,
109
+ NSString *flags,
110
+ BOOL allowsQFlag,
111
+ xmlXPathParserContextPtr ctxt)
112
+ {
113
+ if (!FBXPathFlagsAreValid(flags, allowsQFlag)) {
114
+ FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, @"Invalid regular expression flags");
115
+ return nil;
116
+ }
117
+
118
+ NSError *error = nil;
119
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
120
+ options:FBXPathRegexOptionsFromFlags(flags)
121
+ error:&error];
122
+ if (nil == regex) {
123
+ NSString *message = error.localizedDescription ?: @"Invalid regular expression";
124
+ FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, message);
125
+ return nil;
126
+ }
127
+ return regex;
128
+ }
129
+
130
+ static BOOL FBXPathTokenizeString(NSString *input,
131
+ NSString *pattern,
132
+ xmlXPathParserContextPtr ctxt,
133
+ NSArray<NSString *> **outTokens)
134
+ {
135
+ if (0 == input.length) {
136
+ *outTokens = @[];
137
+ return YES;
138
+ }
139
+
140
+ if (nil == pattern) {
141
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\S+"
142
+ options:FBXPathNoRegexOptions
143
+ error:nil];
144
+ if (nil == regex) {
145
+ FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, @"Invalid regular expression");
146
+ return NO;
147
+ }
148
+ NSMutableArray<NSString *> *tokens = [NSMutableArray array];
149
+ [regex enumerateMatchesInString:input
150
+ options:FBXPathNoMatchingOptions
151
+ range:NSMakeRange(0, input.length)
152
+ usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
153
+ if (nil != result) {
154
+ [tokens addObject:[input substringWithRange:result.range]];
155
+ }
156
+ }];
157
+ *outTokens = tokens.copy;
158
+ return YES;
159
+ }
160
+
161
+ if (0 == pattern.length) {
162
+ NSMutableArray<NSString *> *tokens = [NSMutableArray array];
163
+ [input enumerateSubstringsInRange:NSMakeRange(0, input.length)
164
+ options:NSStringEnumerationByComposedCharacterSequences
165
+ usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
166
+ if (substring.length > 0) {
167
+ [tokens addObject:substring];
168
+ }
169
+ }];
170
+ *outTokens = tokens.copy;
171
+ return YES;
172
+ }
173
+
174
+ NSError *error = nil;
175
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
176
+ options:FBXPathNoRegexOptions
177
+ error:&error];
178
+ if (nil == regex) {
179
+ NSString *message = error.localizedDescription ?: @"Invalid regular expression";
180
+ FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, message);
181
+ return NO;
182
+ }
183
+
184
+ NSMutableArray<NSString *> *tokens = [NSMutableArray array];
185
+ __block NSUInteger lastIndex = 0;
186
+ [regex enumerateMatchesInString:input
187
+ options:FBXPathNoMatchingOptions
188
+ range:NSMakeRange(0, input.length)
189
+ usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
190
+ if (nil == result) {
191
+ return;
192
+ }
193
+ if (result.range.location > lastIndex) {
194
+ NSString *token = [input substringWithRange:NSMakeRange(lastIndex, result.range.location - lastIndex)];
195
+ if (token.length > 0) {
196
+ [tokens addObject:token];
197
+ }
198
+ }
199
+ lastIndex = NSMaxRange(result.range);
200
+ }];
201
+ if (lastIndex < input.length) {
202
+ NSString *token = [input substringFromIndex:lastIndex];
203
+ if (token.length > 0) {
204
+ [tokens addObject:token];
205
+ }
206
+ }
207
+ *outTokens = tokens.copy;
208
+ return YES;
209
+ }
210
+
211
+ static void FBXPathReturnNSString(xmlXPathParserContextPtr ctxt, NSString *value)
212
+ {
213
+ if (nil == value) {
214
+ xmlXPathReturnEmptyString(ctxt);
215
+ return;
216
+ }
217
+ xmlChar *copiedValue = xmlStrdup((const xmlChar *)[value UTF8String]);
218
+ if (NULL == copiedValue) {
219
+ xmlXPathReturnEmptyString(ctxt);
220
+ return;
221
+ }
222
+ // xmlXPathWrapString takes ownership of the buffer passed to xmlXPathReturnString.
223
+ xmlXPathReturnString(ctxt, copiedValue);
224
+ }
225
+
226
+ static NSArray<NSString *> *FBXPathPartsFromXPathObject(xmlXPathObjectPtr sequence)
227
+ {
228
+ if (sequence->type == XPATH_NODESET && NULL != sequence->nodesetval) {
229
+ NSMutableArray<NSString *> *parts = [NSMutableArray array];
230
+ for (int index = 0; index < sequence->nodesetval->nodeNr; index++) {
231
+ xmlChar *content = xmlNodeGetContent(sequence->nodesetval->nodeTab[index]);
232
+ if (NULL != content) {
233
+ NSString *part = FBXPathStringFromUTF8Bytes(content);
234
+ xmlFree(content);
235
+ if (nil != part) {
236
+ [parts addObject:part];
237
+ }
238
+ }
239
+ }
240
+ return parts.copy;
241
+ }
242
+
243
+ xmlChar *asString = xmlXPathCastToString(sequence);
244
+ if (NULL == asString) {
245
+ return @[];
246
+ }
247
+ NSString *value = FBXPathStringFromUTF8Bytes(asString);
248
+ xmlFree(asString);
249
+ if (nil == value || 0 == value.length) {
250
+ return @[];
251
+ }
252
+ if ([value rangeOfString:FBXPathTokenSequenceSeparator].location != NSNotFound) {
253
+ return [value componentsSeparatedByString:FBXPathTokenSequenceSeparator];
254
+ }
255
+ return @[value];
256
+ }
257
+
258
+ static void FBXPathMatchesFunction(xmlXPathParserContextPtr ctxt, int nargs)
259
+ {
260
+ if (nargs < 2 || nargs > 3) {
261
+ FBXPathSetInvalidArityError(ctxt);
262
+ return;
263
+ }
264
+
265
+ NSString *flags = nargs == 3 ? FBXPathPopNSString(ctxt) : nil;
266
+ NSString *pattern = FBXPathPopNSString(ctxt);
267
+ NSString *input = FBXPathPopNSString(ctxt);
268
+ if (nil == pattern || nil == input || xmlXPathCheckError(ctxt)) {
269
+ return;
270
+ }
271
+
272
+ NSRegularExpression *regex = FBXPathRegexWithPattern(pattern, flags, NO, ctxt);
273
+ if (nil == regex) {
274
+ return;
275
+ }
276
+
277
+ NSRange range = NSMakeRange(0, input.length);
278
+ NSTextCheckingResult *match = [regex firstMatchInString:input options:FBXPathNoMatchingOptions range:range];
279
+ xmlXPathReturnBoolean(ctxt, nil != match);
280
+ }
281
+
282
+ static void FBXPathEndsWithFunction(xmlXPathParserContextPtr ctxt, int nargs)
283
+ {
284
+ if (nargs != 2) {
285
+ FBXPathSetInvalidArityError(ctxt);
286
+ return;
287
+ }
288
+
289
+ NSString *suffix = FBXPathPopNSString(ctxt);
290
+ NSString *input = FBXPathPopNSString(ctxt);
291
+ if (nil == suffix || nil == input || xmlXPathCheckError(ctxt)) {
292
+ return;
293
+ }
294
+
295
+ xmlXPathReturnBoolean(ctxt, [input hasSuffix:suffix]);
296
+ }
297
+
298
+ static void FBXPathLowerCaseFunction(xmlXPathParserContextPtr ctxt, int nargs)
299
+ {
300
+ if (nargs != 1) {
301
+ FBXPathSetInvalidArityError(ctxt);
302
+ return;
303
+ }
304
+
305
+ NSString *input = FBXPathPopNSString(ctxt);
306
+ if (nil == input || xmlXPathCheckError(ctxt)) {
307
+ return;
308
+ }
309
+
310
+ FBXPathReturnNSString(ctxt, input.lowercaseString);
311
+ }
312
+
313
+ static void FBXPathUpperCaseFunction(xmlXPathParserContextPtr ctxt, int nargs)
314
+ {
315
+ if (nargs != 1) {
316
+ FBXPathSetInvalidArityError(ctxt);
317
+ return;
318
+ }
319
+
320
+ NSString *input = FBXPathPopNSString(ctxt);
321
+ if (nil == input || xmlXPathCheckError(ctxt)) {
322
+ return;
323
+ }
324
+
325
+ FBXPathReturnNSString(ctxt, input.uppercaseString);
326
+ }
327
+
328
+ static void FBXPathReplaceFunction(xmlXPathParserContextPtr ctxt, int nargs)
329
+ {
330
+ if (nargs < 3 || nargs > 4) {
331
+ FBXPathSetInvalidArityError(ctxt);
332
+ return;
333
+ }
334
+
335
+ NSString *flags = nargs == 4 ? FBXPathPopNSString(ctxt) : nil;
336
+ NSString *replacement = FBXPathPopNSString(ctxt);
337
+ NSString *pattern = FBXPathPopNSString(ctxt);
338
+ NSString *input = FBXPathPopNSString(ctxt);
339
+ if (nil == replacement || nil == pattern || nil == input || xmlXPathCheckError(ctxt)) {
340
+ return;
341
+ }
342
+
343
+ NSRegularExpression *regex = FBXPathRegexWithPattern(pattern, flags, YES, ctxt);
344
+ if (nil == regex) {
345
+ return;
346
+ }
347
+
348
+ NSRange range = NSMakeRange(0, input.length);
349
+ NSString *result = [regex stringByReplacingMatchesInString:input
350
+ options:FBXPathNoMatchingOptions
351
+ range:range
352
+ withTemplate:replacement];
353
+ FBXPathReturnNSString(ctxt, result);
354
+ }
355
+
356
+ static void FBXPathTokenizeFunction(xmlXPathParserContextPtr ctxt, int nargs)
357
+ {
358
+ if (nargs < 1 || nargs > 2) {
359
+ FBXPathSetInvalidArityError(ctxt);
360
+ return;
361
+ }
362
+
363
+ NSString *pattern = nargs == 2 ? FBXPathPopNSString(ctxt) : nil;
364
+ NSString *input = FBXPathPopNSString(ctxt);
365
+ if (nil == input || xmlXPathCheckError(ctxt)) {
366
+ return;
367
+ }
368
+
369
+ NSArray<NSString *> *tokens = nil;
370
+ if (!FBXPathTokenizeString(input, pattern, ctxt, &tokens)) {
371
+ return;
372
+ }
373
+
374
+ FBXPathReturnNSString(ctxt, [tokens componentsJoinedByString:FBXPathTokenSequenceSeparator]);
375
+ }
376
+
377
+ static void FBXPathStringJoinFunction(xmlXPathParserContextPtr ctxt, int nargs)
378
+ {
379
+ if (nargs != 2) {
380
+ FBXPathSetInvalidArityError(ctxt);
381
+ return;
382
+ }
383
+
384
+ xmlChar *separatorChars = xmlXPathPopString(ctxt);
385
+ xmlXPathObjectPtr sequence = valuePop(ctxt);
386
+ if (xmlXPathCheckError(ctxt) || NULL == sequence || NULL == separatorChars) {
387
+ if (NULL != separatorChars) {
388
+ xmlFree(separatorChars);
389
+ }
390
+ if (NULL != sequence) {
391
+ xmlXPathFreeObject(sequence);
392
+ }
393
+ return;
394
+ }
395
+
396
+ NSString *separator = FBXPathStringFromUTF8Bytes(separatorChars);
397
+ xmlFree(separatorChars);
398
+ if (nil == separator) {
399
+ xmlXPathFreeObject(sequence);
400
+ return;
401
+ }
402
+
403
+ NSArray<NSString *> *parts = FBXPathPartsFromXPathObject(sequence);
404
+ xmlXPathFreeObject(sequence);
405
+
406
+ FBXPathReturnNSString(ctxt, [parts componentsJoinedByString:separator]);
407
+ }
408
+
409
+ static void FBRegisterXPathExtensions(xmlXPathContextPtr xpathCtx)
410
+ {
411
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "matches", FBXPathMatchesFunction);
412
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "ends-with", FBXPathEndsWithFunction);
413
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "lower-case", FBXPathLowerCaseFunction);
414
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "upper-case", FBXPathUpperCaseFunction);
415
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "replace", FBXPathReplaceFunction);
416
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "tokenize", FBXPathTokenizeFunction);
417
+ xmlXPathRegisterFunc(xpathCtx, BAD_CAST "string-join", FBXPathStringJoinFunction);
418
+ }
@@ -35,11 +35,16 @@ NSArray<NSNumber *> *(*XCAXAccessibilityAttributesForStringAttributes)(id);
35
35
 
36
36
  @implementation FBXCTestSymbolsLoader
37
37
 
38
+ #pragma clang diagnostic push
39
+ #pragma clang diagnostic ignored "-Wobjc-load-method"
40
+
38
41
  + (void)load
39
42
  {
40
43
  FBLoadXCTestSymbols();
41
44
  }
42
45
 
46
+ #pragma clang diagnostic pop
47
+
43
48
  @end
44
49
 
45
50
  void FBLoadXCTestSymbols(void)
@@ -22,9 +22,7 @@
22
22
  return 0;
23
23
  }
24
24
 
25
- - (void)setOffset:(UInt64)offset {
26
- ;
27
- }
25
+ - (void)setOffset:(UInt64)offset {}
28
26
 
29
27
  - (NSData*) readDataOfLength:(NSUInteger)length {
30
28
  return nil;
@@ -9,6 +9,7 @@
9
9
  #import <XCTest/XCTest.h>
10
10
 
11
11
  #import "FBIntegrationTestCase.h"
12
+ #import "FBExceptions.h"
12
13
  #import "FBMacros.h"
13
14
  #import "FBTestMacros.h"
14
15
  #import "FBXPath.h"
@@ -51,6 +52,31 @@
51
52
  return snapshot;
52
53
  }
53
54
 
55
+ - (NSSet<NSString *> *)labelsForMatchingSnapshots:(NSArray<id<FBXCElementSnapshot>> *)matchingSnapshots
56
+ {
57
+ NSMutableSet<NSString *> *labels = [NSMutableSet set];
58
+ for (id<FBXCElementSnapshot> snapshot in matchingSnapshots) {
59
+ NSString *label = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdLabel;
60
+ if (nil != label) {
61
+ [labels addObject:label];
62
+ }
63
+ }
64
+ return labels.copy;
65
+ }
66
+
67
+ - (void)assertXPathQuery:(NSString *)query findsButtonLabels:(NSArray<NSString *> *)expectedLabels
68
+ {
69
+ NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication
70
+ forQuery:query];
71
+ NSSet<NSString *> *foundLabels = [self labelsForMatchingSnapshots:matchingSnapshots];
72
+ NSSet<NSString *> *expectedLabelSet = [NSSet setWithArray:expectedLabels];
73
+ XCTAssertEqual(foundLabels.count, expectedLabelSet.count);
74
+ XCTAssertEqualObjects(foundLabels, expectedLabelSet);
75
+ for (id<FBXCElementSnapshot> snapshot in matchingSnapshots) {
76
+ XCTAssertEqualObjects([FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdType, @"XCUIElementTypeButton");
77
+ }
78
+ }
79
+
54
80
  - (void)testApplicationNodeXMLRepresentation
55
81
  {
56
82
  id<FBXCElementSnapshot> snapshot = [self.testedApplication fb_customSnapshot];
@@ -154,4 +180,81 @@
154
180
  }
155
181
  }
156
182
 
183
+ - (void)testFindMatchesWithMatchesFunction
184
+ {
185
+ [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '^Alerts$')]"
186
+ findsButtonLabels:@[@"Alerts"]];
187
+ }
188
+
189
+ - (void)testFindMatchesWithMatchesFunctionCaseInsensitive
190
+ {
191
+ [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '^alerts$', 'i')]"
192
+ findsButtonLabels:@[@"Alerts"]];
193
+ }
194
+
195
+ - (void)testFindMatchesWithEndsWithFunction
196
+ {
197
+ [self assertXPathQuery:@"//XCUIElementTypeButton[ends-with(@label, 'ing')]"
198
+ findsButtonLabels:@[@"Scrolling"]];
199
+ }
200
+
201
+ - (void)testFindMatchesWithLowerCaseFunction
202
+ {
203
+ [self assertXPathQuery:@"//XCUIElementTypeButton[lower-case(@label)='alerts']"
204
+ findsButtonLabels:@[@"Alerts"]];
205
+ }
206
+
207
+ - (void)testFindMatchesWithUpperCaseFunction
208
+ {
209
+ [self assertXPathQuery:@"//XCUIElementTypeButton[upper-case(@label)='TOUCH']"
210
+ findsButtonLabels:@[@"Touch"]];
211
+ }
212
+
213
+ - (void)testFindMatchesWithReplaceFunction
214
+ {
215
+ [self assertXPathQuery:@"//XCUIElementTypeButton[replace(@label, ' ', '')='Deadlockapp']"
216
+ findsButtonLabels:@[@"Deadlock app"]];
217
+ }
218
+
219
+ - (void)testFindMatchesWithTokenizeAndStringJoinFunctions
220
+ {
221
+ [self assertXPathQuery:@"//XCUIElementTypeButton[string-join(tokenize(@label, ' '), '-')='Deadlock-app']"
222
+ findsButtonLabels:@[@"Deadlock app"]];
223
+ }
224
+
225
+ - (void)testFindMatchesWithExtensionFunctionsNoMatches
226
+ {
227
+ NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication
228
+ forQuery:@"//XCUIElementTypeButton[matches(@label, '^NoSuchButton$')]"];
229
+ XCTAssertEqual(matchingSnapshots.count, 0);
230
+ }
231
+
232
+ - (void)testFindMultipleMatchesWithMatchesFunction
233
+ {
234
+ [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '.*')]"
235
+ findsButtonLabels:@[@"Alerts", @"Deadlock app", @"Attributes", @"Scrolling", @"Touch"]];
236
+ }
237
+
238
+ - (void)testInvalidXPathExtensionFunctionViaElementLookup
239
+ {
240
+ XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[matches(@label)]"
241
+ shouldReturnAfterFirstMatch:NO],
242
+ NSException,
243
+ FBInvalidXPathException);
244
+ }
245
+
246
+ - (void)testInvalidXPathExtensionRegexpViaElementLookup
247
+ {
248
+ NSException *exception = nil;
249
+ @try {
250
+ [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[matches(@label, '[')]"
251
+ shouldReturnAfterFirstMatch:NO];
252
+ } @catch (NSException *caughtException) {
253
+ exception = caughtException;
254
+ }
255
+ XCTAssertEqualObjects(exception.name, FBInvalidXPathException);
256
+ XCTAssertTrue([exception.reason containsString:@"Cannot evaluate results for XPath expression"]);
257
+ XCTAssertTrue([exception.reason rangeOfString:@"invalid" options:NSCaseInsensitiveSearch].location != NSNotFound);
258
+ }
259
+
157
260
  @end
@@ -152,4 +152,129 @@
152
152
  XCTAssertEqual(1, [matchingSnapshots count]);
153
153
  }
154
154
 
155
+ - (NSString *)xpathStringResultForQuery:(NSString *)query document:(xmlDocPtr)doc
156
+ {
157
+ xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL];
158
+ if (NULL == queryResult) {
159
+ return nil;
160
+ }
161
+ xmlChar *stringValue = xmlXPathCastToString(queryResult);
162
+ xmlXPathFreeObject(queryResult);
163
+ if (NULL == stringValue) {
164
+ return nil;
165
+ }
166
+ NSString *result = [NSString stringWithUTF8String:(const char *)stringValue];
167
+ xmlFree(stringValue);
168
+ return result;
169
+ }
170
+
171
+ - (BOOL)xpathBooleanResultForQuery:(NSString *)query document:(xmlDocPtr)doc
172
+ {
173
+ xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL];
174
+ if (NULL == queryResult) {
175
+ return NO;
176
+ }
177
+ BOOL result = queryResult->boolval;
178
+ xmlXPathFreeObject(queryResult);
179
+ return result;
180
+ }
181
+
182
+ - (xmlDocPtr)documentForSnapshot:(XCElementSnapshotDouble *)snapshot query:(NSString *)query
183
+ {
184
+ xmlDocPtr doc;
185
+ xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
186
+ NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
187
+ id<FBElement> root = (id<FBElement>)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot];
188
+ int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL);
189
+ if (rc >= 0) {
190
+ rc = [FBXPath xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
191
+ writer:writer
192
+ elementStore:elementStore
193
+ query:query
194
+ excludingAttributes:nil];
195
+ if (rc >= 0) {
196
+ rc = xmlTextWriterEndDocument(writer);
197
+ }
198
+ }
199
+ xmlFreeTextWriter(writer);
200
+ XCTAssertTrue(rc >= 0);
201
+ return doc;
202
+ }
203
+
204
+ - (void)testXPathExtensionFunctions
205
+ {
206
+ XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
207
+ snapshot.label = @"Hello World";
208
+ snapshot.value = @"One-Two-Three";
209
+
210
+ xmlDocPtr doc = [self documentForSnapshot:snapshot query:@"//*[@label and @name and @value]"];
211
+
212
+ @try {
213
+ XCTAssertTrue([self xpathBooleanResultForQuery:@"matches(//XCUIElementTypeOther/@label, 'Hello.*')" document:doc]);
214
+ XCTAssertFalse([self xpathBooleanResultForQuery:@"matches(//XCUIElementTypeOther/@label, 'hello.*')" document:doc]);
215
+ XCTAssertTrue([self xpathBooleanResultForQuery:@"matches(//XCUIElementTypeOther/@label, 'hello.*', 'i')" document:doc]);
216
+ XCTAssertTrue([self xpathBooleanResultForQuery:@"ends-with(//XCUIElementTypeOther/@name, 'Name')" document:doc]);
217
+ XCTAssertFalse([self xpathBooleanResultForQuery:@"ends-with(//XCUIElementTypeOther/@name, 'Foo')" document:doc]);
218
+ XCTAssertEqualObjects([self xpathStringResultForQuery:@"lower-case(//XCUIElementTypeOther/@label)" document:doc], @"hello world");
219
+ XCTAssertEqualObjects([self xpathStringResultForQuery:@"upper-case(//XCUIElementTypeOther/@name)" document:doc], @"TESTNAME");
220
+ XCTAssertEqualObjects([self xpathStringResultForQuery:@"replace(//XCUIElementTypeOther/@value, '-', '_')" document:doc], @"One_Two_Three");
221
+ XCTAssertEqualObjects([self xpathStringResultForQuery:@"string-join(tokenize(//XCUIElementTypeOther/@value, '-'), '|')" document:doc], @"One|Two|Three");
222
+ } @finally {
223
+ xmlFreeDoc(doc);
224
+ }
225
+ }
226
+
227
+ - (void)testInvalidXPathExtensionRegexp
228
+ {
229
+ XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
230
+ snapshot.label = @"Hello World";
231
+ snapshot.value = @"One-Two-Three";
232
+
233
+ xmlDocPtr doc = [self documentForSnapshot:snapshot query:@"//*[@label and @name and @value]"];
234
+
235
+ @try {
236
+ [self assertXPathEvaluationFailsForQuery:@"matches(//XCUIElementTypeOther/@label, '[')" document:doc];
237
+ [self assertXPathEvaluationFailsForQuery:@"replace(//XCUIElementTypeOther/@label, '[', '')" document:doc];
238
+ [self assertXPathEvaluationFailsForQuery:@"tokenize(//XCUIElementTypeOther/@value, '[')" document:doc];
239
+ [self assertXPathEvaluationFailsForQuery:@"matches(//XCUIElementTypeOther/@label, 'a', 'z')" document:doc];
240
+ [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[matches(@label, '[')]" document:doc];
241
+ } @finally {
242
+ xmlFreeDoc(doc);
243
+ }
244
+ }
245
+
246
+ - (void)assertXPathEvaluationFailsForQuery:(NSString *)query document:(xmlDocPtr)doc
247
+ {
248
+ xmlXPathObjectPtr queryResult = [FBXPath evaluate:query document:doc contextNode:NULL];
249
+ @try {
250
+ XCTAssertEqual(NULL, queryResult);
251
+ } @finally {
252
+ if (NULL != queryResult) {
253
+ xmlXPathFreeObject(queryResult);
254
+ }
255
+ }
256
+ }
257
+
258
+ - (void)testInvalidXPathExtensionFunctionArity
259
+ {
260
+ XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new];
261
+ snapshot.label = @"Hello World";
262
+ snapshot.value = @"One-Two-Three";
263
+
264
+ xmlDocPtr doc = [self documentForSnapshot:snapshot query:@"//*[@label and @name and @value]"];
265
+
266
+ @try {
267
+ [self assertXPathEvaluationFailsForQuery:@"matches(//XCUIElementTypeOther/@label)" document:doc];
268
+ [self assertXPathEvaluationFailsForQuery:@"lower-case()" document:doc];
269
+ [self assertXPathEvaluationFailsForQuery:@"string-join(//XCUIElementTypeOther/@label)" document:doc];
270
+ [self assertXPathEvaluationFailsForQuery:@"replace(//XCUIElementTypeOther/@label, '-')" document:doc];
271
+ [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[matches(@label)]" document:doc];
272
+ [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[lower-case()]" document:doc];
273
+ [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[string-join(@label)]" document:doc];
274
+ [self assertXPathEvaluationFailsForQuery:@"//XCUIElementTypeOther[replace(@label, '-')]" document:doc];
275
+ } @finally {
276
+ xmlFreeDoc(doc);
277
+ }
278
+ }
279
+
155
280
  @end