|
8 | 8 | // which Square, Inc. licenses this file to you. |
9 | 9 |
|
10 | 10 | #import "NSError-KIFAdditions.h" |
| 11 | +#import "NSPredicate+KIFAdditions.h" |
11 | 12 | #import "UIAccessibilityElement-KIFAdditions.h" |
12 | 13 | #import "UIApplication-KIFAdditions.h" |
13 | 14 | #import "UIScrollView-KIFAdditions.h" |
@@ -62,7 +63,7 @@ + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(o |
62 | 63 |
|
63 | 64 | if (!element) { |
64 | 65 | if (error) { |
65 | | - *error = [NSError KIFErrorWithFormat:@"Could not find view matching: %@", predicate]; |
| 66 | + *error = [self errorForFailingPredicate:predicate]; |
66 | 67 | } |
67 | 68 | return NO; |
68 | 69 | } |
@@ -174,4 +175,118 @@ + (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element |
174 | 175 | return view; |
175 | 176 | } |
176 | 177 |
|
| 178 | ++ (NSError *)errorForFailingPredicate:(NSPredicate*)failingPredicate; |
| 179 | +{ |
| 180 | + NSPredicate *closestMatchingPredicate = [self findClosestMatchingPredicate:failingPredicate]; |
| 181 | + if (closestMatchingPredicate) { |
| 182 | + return [NSError KIFErrorWithFormat:@"Found element with %@ but not %@", \ |
| 183 | + closestMatchingPredicate.kifPredicateDescription, \ |
| 184 | + [failingPredicate minusSubpredicatesFrom:closestMatchingPredicate].kifPredicateDescription]; |
| 185 | + } |
| 186 | + return [NSError KIFErrorWithFormat:@"Could not find element with %@", failingPredicate.kifPredicateDescription]; |
| 187 | +} |
| 188 | + |
| 189 | ++ (NSPredicate *)findClosestMatchingPredicate:(NSPredicate *)aPredicate; |
| 190 | +{ |
| 191 | + if (!aPredicate) { |
| 192 | + return nil; |
| 193 | + } |
| 194 | + |
| 195 | + UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) { |
| 196 | + return [aPredicate evaluateWithObject:element]; |
| 197 | + }]; |
| 198 | + if (match) { |
| 199 | + return aPredicate; |
| 200 | + } |
| 201 | + |
| 202 | + // Breadth-First algorithm to match as many subpredicates as possible |
| 203 | + NSMutableArray *queue = [NSMutableArray arrayWithObject:aPredicate]; |
| 204 | + while (queue.count > 0) { |
| 205 | + // Dequeuing |
| 206 | + NSPredicate *predicate = [queue firstObject]; |
| 207 | + [queue removeObject:predicate]; |
| 208 | + |
| 209 | + // Remove one subpredicate at a time an then check if an element would match this resulting predicate |
| 210 | + for (NSPredicate *subpredicate in [predicate flatten]) { |
| 211 | + NSPredicate *predicateMinusOneCondition = [predicate minusSubpredicatesFrom:subpredicate]; |
| 212 | + if (predicateMinusOneCondition) { |
| 213 | + UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) { |
| 214 | + return [predicateMinusOneCondition evaluateWithObject:element]; |
| 215 | + }]; |
| 216 | + if (match) { |
| 217 | + return predicateMinusOneCondition; |
| 218 | + } |
| 219 | + [queue addObject:predicateMinusOneCondition]; |
| 220 | + } |
| 221 | + } |
| 222 | + } |
| 223 | + return nil; |
| 224 | +} |
| 225 | + |
| 226 | ++ (NSString *)stringFromAccessibilityTraits:(UIAccessibilityTraits)traits; |
| 227 | +{ |
| 228 | + if (traits == UIAccessibilityTraitNone) { |
| 229 | + return @"UIAccessibilityTraitNone"; |
| 230 | + } |
| 231 | + |
| 232 | + NSString *string = @""; |
| 233 | + |
| 234 | + NSArray *allTraits = @[ |
| 235 | + @(UIAccessibilityTraitButton), |
| 236 | + @(UIAccessibilityTraitLink), |
| 237 | + @(UIAccessibilityTraitHeader), |
| 238 | + @(UIAccessibilityTraitSearchField), |
| 239 | + @(UIAccessibilityTraitImage), |
| 240 | + @(UIAccessibilityTraitSelected), |
| 241 | + @(UIAccessibilityTraitPlaysSound), |
| 242 | + @(UIAccessibilityTraitKeyboardKey), |
| 243 | + @(UIAccessibilityTraitStaticText), |
| 244 | + @(UIAccessibilityTraitSummaryElement), |
| 245 | + @(UIAccessibilityTraitNotEnabled), |
| 246 | + @(UIAccessibilityTraitUpdatesFrequently), |
| 247 | + @(UIAccessibilityTraitStartsMediaSession), |
| 248 | + @(UIAccessibilityTraitAdjustable), |
| 249 | + @(UIAccessibilityTraitAllowsDirectInteraction), |
| 250 | + @(UIAccessibilityTraitCausesPageTurn) |
| 251 | + ]; |
| 252 | + |
| 253 | + NSArray *traitNames = @[ |
| 254 | + @"UIAccessibilityTraitButton", |
| 255 | + @"UIAccessibilityTraitLink", |
| 256 | + @"UIAccessibilityTraitHeader", |
| 257 | + @"UIAccessibilityTraitSearchField", |
| 258 | + @"UIAccessibilityTraitImage", |
| 259 | + @"UIAccessibilityTraitSelected", |
| 260 | + @"UIAccessibilityTraitPlaysSound", |
| 261 | + @"UIAccessibilityTraitKeyboardKey", |
| 262 | + @"UIAccessibilityTraitStaticText", |
| 263 | + @"UIAccessibilityTraitSummaryElement", |
| 264 | + @"UIAccessibilityTraitNotEnabled", |
| 265 | + @"UIAccessibilityTraitUpdatesFrequently", |
| 266 | + @"UIAccessibilityTraitStartsMediaSession", |
| 267 | + @"UIAccessibilityTraitAdjustable", |
| 268 | + @"UIAccessibilityTraitAllowsDirectInteraction", |
| 269 | + @"UIAccessibilityTraitCausesPageTurn" |
| 270 | + ]; |
| 271 | + |
| 272 | + |
| 273 | + for (NSNumber *trait in allTraits) { |
| 274 | + if ((traits & trait.longLongValue) == trait.longLongValue) { |
| 275 | + NSString *name = [traitNames objectAtIndex:[allTraits indexOfObject:trait]]; |
| 276 | + if (string.length > 0) { |
| 277 | + string = [string stringByAppendingString:@", "]; |
| 278 | + } |
| 279 | + string = [string stringByAppendingString:name]; |
| 280 | + traits &= ~trait.longLongValue; |
| 281 | + } |
| 282 | + } |
| 283 | + if (traits != UIAccessibilityTraitNone) { |
| 284 | + if (string.length > 0) { |
| 285 | + string = [string stringByAppendingString:@", "]; |
| 286 | + } |
| 287 | + string = [string stringByAppendingFormat:@"UNKNOWN ACCESSIBILITY TRAIT: %llu", traits]; |
| 288 | + } |
| 289 | + return string; |
| 290 | +} |
| 291 | + |
177 | 292 | @end |
0 commit comments