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.
- package/CHANGELOG.md +12 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +23 -3
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +11 -176
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Utilities/FBSettingsHandler.h +35 -0
- package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +356 -0
- package/WebDriverAgentLib/Utilities/FBXPath-Private.h +5 -0
- package/WebDriverAgentLib/Utilities/FBXPath.m +36 -4
- package/WebDriverAgentLib/Utilities/FBXPathExtensions.h +40 -0
- package/WebDriverAgentLib/Utilities/FBXPathExtensions.m +418 -0
- package/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m +5 -0
- package/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +1 -3
- package/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m +103 -0
- package/WebDriverAgentTests/UnitTests/FBXPathTests.m +125 -0
- package/package.json +1 -1
|
@@ -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)
|
|
@@ -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
|