/
UIAccessibilityElement-KIFAdditions.m
349 lines (298 loc) · 15.3 KB
/
UIAccessibilityElement-KIFAdditions.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
//
// UIAccessibilityElement-KIFAdditions.m
// KIF
//
// Created by Eric Firestone on 5/23/11.
// Licensed to Square, Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Square, Inc. licenses this file to you.
#import "NSError-KIFAdditions.h"
#import "NSPredicate+KIFAdditions.h"
#import "UIAccessibilityElement-KIFAdditions.h"
#import "UIApplication-KIFAdditions.h"
#import "UIScrollView-KIFAdditions.h"
#import "UIView-KIFAdditions.h"
#import "LoadableCategory.h"
#import "KIFTestActor.h"
MAKE_CATEGORIES_LOADABLE(UIAccessibilityElement_KIFAdditions)
@interface UIAccessibilityElement (KIFAdditions_Private)
- (id)tableViewCell; // UITableViewCellAccessibilityElement
@end
@implementation UIAccessibilityElement (KIFAdditions)
+ (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element;
{
while (element && ![element isKindOfClass:[UIView class]]) {
// Sometimes accessibilityContainer will return a view that's too far up the view hierarchy
// UIAccessibilityElement instances will sometimes respond to view, so try to use that and then fall back to accessibilityContainer
id view = [element respondsToSelector:@selector(view)] ? [(id)element view]
: [element respondsToSelector:@selector(tableViewCell)] ? [(id)element tableViewCell]
: nil;
if (view) {
element = view;
} else {
element = [element accessibilityContainer];
}
}
return (UIView *)element;
}
+ (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withLabel:(NSString *)label value:(NSString *)value traits:(UIAccessibilityTraits)traits tappable:(BOOL)mustBeTappable error:(out NSError **)error
{
return [self accessibilityElement:foundElement view:foundView withLabel:label value:value traits:traits fromRootView:NULL tappable:mustBeTappable error:error];
}
+ (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withLabel:(NSString *)label value:(NSString *)value traits:(UIAccessibilityTraits)traits fromRootView:(UIView *)fromView tappable:(BOOL)mustBeTappable error:(out NSError **)error
{
UIAccessibilityElement *element = [self accessibilityElementWithLabel:label value:value traits:traits fromRootView:fromView error:error];
if (!element) {
return NO;
}
UIView *view = [self viewContainingAccessibilityElement:element tappable:mustBeTappable error:error];
if (!view) {
return NO;
}
// viewContainingAccessibilityElement:.. can cause scrolling, which can cause cell reuse.
// If this happens, the element we kept a reference to might have been reconfigured, and a
// different element might be the one that matches.
if (![UIView accessibilityElement:element hasLabel:label accessibilityValue:value traits:traits]) {
return NO;
}
if (foundElement) { *foundElement = element; }
if (foundView) { *foundView = view; }
return YES;
}
+ (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error;
{
UIAccessibilityElement *element = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL(UIAccessibilityElement *element) {
return [predicate evaluateWithObject:element];
}];
if (!element) {
if (error) {
*error = [self errorForFailingPredicate:predicate];
}
return NO;
}
UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element tappable:mustBeTappable error:error];
if (!view) {
return NO;
}
if (foundElement) { *foundElement = element; }
if (foundView) { *foundView = view; }
return YES;
}
+ (BOOL)accessibilityElement:(out UIAccessibilityElement *__autoreleasing *)foundElement view:(out UIView *__autoreleasing *)foundView withElementMatchingPredicate:(NSPredicate *)predicate fromRootView:(UIView *)fromView tappable:(BOOL)mustBeTappable error:(out NSError *__autoreleasing *)error
{
UIAccessibilityElement *element = [fromView accessibilityElementMatchingBlock:^BOOL(UIAccessibilityElement *element) {
return [predicate evaluateWithObject:element];
}];
if (!element) {
if (error) {
*error = [NSError KIFErrorWithFormat:@"Could not find view matching: %@", predicate];
}
return NO;
}
UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element tappable:mustBeTappable error:error];
if (!view) {
return NO;
}
if (foundElement) { *foundElement = element; }
if (foundView) { *foundView = view; }
return YES;
}
+ (UIAccessibilityElement *)accessibilityElementWithLabel:(NSString *)label value:(NSString *)value traits:(UIAccessibilityTraits)traits error:(out NSError **)error
{
return [self accessibilityElementWithLabel:label value:value traits:traits fromRootView:NULL error:error];
}
+ (UIAccessibilityElement *)accessibilityElementWithLabel:(NSString *)label value:(NSString *)value traits:(UIAccessibilityTraits)traits fromRootView:(UIView *)fromView error:(out NSError **)error;
{
UIAccessibilityElement *element = NULL;
if (fromView == NULL) {
element = [[UIApplication sharedApplication] accessibilityElementWithLabel:label accessibilityValue:value traits:traits];
} else {
element = [fromView accessibilityElementWithLabel:label accessibilityValue:value traits:traits];
}
if (element || !error) {
return element;
}
element = [[UIApplication sharedApplication] accessibilityElementWithLabel:label accessibilityValue:nil traits:traits];
// For purposes of a better error message, see if we can find the view, just not a view with the specified value.
if (value && element) {
*error = [NSError KIFErrorWithFormat:@"Found an accessibility element with the label \"%@\", but with the value \"%@\", not \"%@\"", label, element.accessibilityValue, value];
return nil;
}
// Check the traits, too.
element = [[UIApplication sharedApplication] accessibilityElementWithLabel:label accessibilityValue:nil traits:UIAccessibilityTraitNone];
if (traits != UIAccessibilityTraitNone && element) {
*error = [NSError KIFErrorWithFormat:@"Found an accessibility element with the label \"%@\", but not with the traits \"%llu\"", label, traits];
return nil;
}
*error = [NSError KIFErrorWithFormat:@"Failed to find accessibility element with the label \"%@\"", label];
return nil;
}
+ (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element tappable:(BOOL)mustBeTappable error:(NSError **)error;
{
// Small safety mechanism. If someone calls this method after a failing call to accessibilityElementWithLabel:..., we don't want to wipe out the error message.
if (!element && error && *error) {
return nil;
}
// Make sure the element is visible
UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element];
if (!view) {
if (error) {
*error = [NSError KIFErrorWithFormat:@"Cannot find view containing accessibility element with the label \"%@\"", element.accessibilityLabel];
}
return nil;
}
// Scroll the view (and superviews) to be visible if necessary
UIView *superview = (UIScrollView *)view;
while (superview) {
// Fix for iOS7 table view cells containing scroll views
if ([superview.superview isKindOfClass:[UITableViewCell class]]) {
break;
}
if ([superview isKindOfClass:[UIScrollView class]]) {
UIScrollView *scrollView = (UIScrollView *)superview;
if (((UIAccessibilityElement *)view == element) && ![view isKindOfClass:[UITableViewCell class]]) {
[scrollView scrollViewToVisible:view animated:YES];
} else if ([view isKindOfClass:[UITableViewCell class]] && [scrollView.superview isKindOfClass:[UITableView class]]) {
UITableViewCell *cell = (UITableViewCell *)view;
UITableView *tableView = (UITableView *)scrollView.superview;
NSIndexPath *indexPath = [tableView indexPathForCell:cell];
[tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
} else {
CGRect elementFrame = [view.window convertRect:element.accessibilityFrame toView:scrollView];
CGRect visibleRect = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, CGRectGetWidth(scrollView.bounds), CGRectGetHeight(scrollView.bounds));
// Only call scrollRectToVisible if the element isn't already visible
// iOS 8 will sometimes incorrectly scroll table views so the element scrolls out of view
if (!CGRectContainsRect(visibleRect, elementFrame)) {
[scrollView scrollRectToVisible:elementFrame animated:YES];
}
}
// Give the scroll view a small amount of time to perform the scroll.
KIFRunLoopRunInModeRelativeToAnimationSpeed(kCFRunLoopDefaultMode, 0.3, false);
}
superview = superview.superview;
}
if ([[UIApplication sharedApplication] isIgnoringInteractionEvents]) {
if (error) {
*error = [NSError KIFErrorWithFormat:@"Application is ignoring interaction events"];
}
return nil;
}
// If we don't require tappability, at least make sure it's not hidden
if ([view isHidden]) {
if (error) {
*error = [NSError KIFErrorWithFormat:@"Accessibility element with label \"%@\" is hidden.", element.accessibilityLabel];
}
return nil;
}
if (mustBeTappable && !view.isProbablyTappable) {
if (error) {
*error = [NSError KIFErrorWithFormat:@"Accessibility element %@ for view %@ with label \"%@\" is not tappable. It may be blocked by other views.", element, view, element.accessibilityLabel];
}
return nil;
}
return view;
}
+ (NSError *)errorForFailingPredicate:(NSPredicate*)failingPredicate;
{
NSPredicate *closestMatchingPredicate = [self findClosestMatchingPredicate:failingPredicate];
if (closestMatchingPredicate) {
return [NSError KIFErrorWithFormat:@"Found element with %@ but not %@", \
closestMatchingPredicate.kifPredicateDescription, \
[failingPredicate minusSubpredicatesFrom:closestMatchingPredicate].kifPredicateDescription];
}
return [NSError KIFErrorWithFormat:@"Could not find element with %@", failingPredicate.kifPredicateDescription];
}
+ (NSPredicate *)findClosestMatchingPredicate:(NSPredicate *)aPredicate;
{
if (!aPredicate) {
return nil;
}
UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) {
return [aPredicate evaluateWithObject:element];
}];
if (match) {
return aPredicate;
}
// Breadth-First algorithm to match as many subpredicates as possible
NSMutableArray *queue = [NSMutableArray arrayWithObject:aPredicate];
while (queue.count > 0) {
// Dequeuing
NSPredicate *predicate = [queue firstObject];
[queue removeObject:predicate];
// Remove one subpredicate at a time an then check if an element would match this resulting predicate
for (NSPredicate *subpredicate in [predicate flatten]) {
NSPredicate *predicateMinusOneCondition = [predicate minusSubpredicatesFrom:subpredicate];
if (predicateMinusOneCondition) {
UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) {
return [predicateMinusOneCondition evaluateWithObject:element];
}];
if (match) {
return predicateMinusOneCondition;
}
[queue addObject:predicateMinusOneCondition];
}
}
}
return nil;
}
+ (NSString *)stringFromAccessibilityTraits:(UIAccessibilityTraits)traits;
{
if (traits == UIAccessibilityTraitNone) {
return @"UIAccessibilityTraitNone";
}
NSString *string = @"";
NSArray *allTraits = @[
@(UIAccessibilityTraitButton),
@(UIAccessibilityTraitLink),
@(UIAccessibilityTraitHeader),
@(UIAccessibilityTraitSearchField),
@(UIAccessibilityTraitImage),
@(UIAccessibilityTraitSelected),
@(UIAccessibilityTraitPlaysSound),
@(UIAccessibilityTraitKeyboardKey),
@(UIAccessibilityTraitStaticText),
@(UIAccessibilityTraitSummaryElement),
@(UIAccessibilityTraitNotEnabled),
@(UIAccessibilityTraitUpdatesFrequently),
@(UIAccessibilityTraitStartsMediaSession),
@(UIAccessibilityTraitAdjustable),
@(UIAccessibilityTraitAllowsDirectInteraction),
@(UIAccessibilityTraitCausesPageTurn)
];
NSArray *traitNames = @[
@"UIAccessibilityTraitButton",
@"UIAccessibilityTraitLink",
@"UIAccessibilityTraitHeader",
@"UIAccessibilityTraitSearchField",
@"UIAccessibilityTraitImage",
@"UIAccessibilityTraitSelected",
@"UIAccessibilityTraitPlaysSound",
@"UIAccessibilityTraitKeyboardKey",
@"UIAccessibilityTraitStaticText",
@"UIAccessibilityTraitSummaryElement",
@"UIAccessibilityTraitNotEnabled",
@"UIAccessibilityTraitUpdatesFrequently",
@"UIAccessibilityTraitStartsMediaSession",
@"UIAccessibilityTraitAdjustable",
@"UIAccessibilityTraitAllowsDirectInteraction",
@"UIAccessibilityTraitCausesPageTurn"
];
for (NSNumber *trait in allTraits) {
if ((traits & trait.longLongValue) == trait.longLongValue) {
NSString *name = [traitNames objectAtIndex:[allTraits indexOfObject:trait]];
if (string.length > 0) {
string = [string stringByAppendingString:@", "];
}
string = [string stringByAppendingString:name];
traits &= ~trait.longLongValue;
}
}
if (traits != UIAccessibilityTraitNone) {
if (string.length > 0) {
string = [string stringByAppendingString:@", "];
}
string = [string stringByAppendingFormat:@"UNKNOWN ACCESSIBILITY TRAIT: %llu", traits];
}
return string;
}
@end