Skip to content

Commit

Permalink
Merge pull request wix#2467 from wix/AccessibilityElementsSupport
Browse files Browse the repository at this point in the history
Add support for accessibility elements
  • Loading branch information
LeoNatan authored Nov 17, 2020
2 parents aba0be9 + 2b14907 commit ee3d092
Show file tree
Hide file tree
Showing 18 changed files with 628 additions and 298 deletions.
44 changes: 28 additions & 16 deletions detox/ios/Detox.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
//
// UIView+Detox.h
// ExampleApp
// NSObject+DetoxActions.h
// Detox
//
// Created by Leo Natan (Wix) on 4/16/20.
// Created by Leo Natan on 11/16/20.
// Copyright © 2020 Wix. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

BOOL DTXClearText(void);
BOOL DTXTypeText(NSString* text);

@interface UIView (Detox)
@interface NSObject (DetoxActions)

- (void)dtx_tapAtAccessibilityActivationPoint;
- (void)dtx_tapAtAccessibilityActivationPointWithNumberOfTaps:(NSUInteger)numberOfTaps;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
//
// UIView+Detox.m
// ExampleApp
// NSObject+DetoxActions.m
// Detox
//
// Created by Leo Natan (Wix) on 4/16/20.
// Created by Leo Natan on 11/16/20.
// Copyright © 2020 Wix. All rights reserved.
//

#import "UIView+DetoxActions.h"
#import "NSObject+DetoxActions.h"
#import "NSObject+DetoxUtils.h"

@import Darwin;
@import AudioToolbox;

#import "DTXAppleInternals.h"
#import "DTXSyntheticEvents.h"
#import "UIView+DetoxUtils.h"

DTX_DIRECT_MEMBERS
@implementation UIView (Detox)
@implementation NSObject (DetoxActions)

- (void)dtx_tapAtAccessibilityActivationPoint
{
Expand All @@ -36,15 +36,22 @@ - (void)dtx_tapAtPoint:(CGPoint)point numberOfTaps:(NSUInteger)numberOfTaps
return;
}

[self dtx_assertHittableAtPoint:point];

NSParameterAssert(numberOfTaps >= 1);
point = [self.window convertPoint:point fromView:self];

UIView* view = self.dtx_view;
UIWindow* window = view.window;
CGPoint viewPoint = [self dtx_convertRelativePointToViewCoordinateSpace:point];

[view dtx_assertHittableAtPoint:viewPoint];

CGPoint windowPoint = [window convertPoint:viewPoint fromView:view];

for (NSUInteger idx = 0; idx < numberOfTaps; idx++) {
[DTXSyntheticEvents touchAlongPath:@[@(point)] relativeToWindow:self.window holdDurationOnLastTouch:0.0];
[DTXSyntheticEvents touchAlongPath:@[@(windowPoint)] relativeToWindow:window holdDurationOnLastTouch:0.0];
}
}


- (void)dtx_longPressAtAccessibilityActivationPoint
{
[self dtx_longPressAtAccessibilityActivationPointForDuration:1.0];
Expand All @@ -57,16 +64,20 @@ - (void)dtx_longPressAtAccessibilityActivationPointForDuration:(NSTimeInterval)d

- (void)dtx_longPressAtPoint:(CGPoint)point duration:(NSTimeInterval)duration
{
[self dtx_assertHittableAtPoint:point];
UIView* view = self.dtx_view;
UIWindow* window = view.window;
CGPoint viewPoint = [self dtx_convertRelativePointToViewCoordinateSpace:point];

point = [self.window convertPoint:point fromView:self];
[DTXSyntheticEvents touchAlongPath:@[@(point)] relativeToWindow:self.window holdDurationOnLastTouch:duration];
[view dtx_assertHittableAtPoint:viewPoint];

CGPoint windowPoint = [window convertPoint:viewPoint fromView:view];
[DTXSyntheticEvents touchAlongPath:@[@(windowPoint)] relativeToWindow:window holdDurationOnLastTouch:duration];
}

static void _DTXApplySwipe(UIWindow* window, CGPoint startPoint, CGPoint endPoint, CGFloat velocity)
{
NSCAssert(CGPointEqualToPoint(startPoint, endPoint) == NO, @"Start and end points for swipe cannot be equal");

NSMutableArray<NSValue*>* points = [NSMutableArray new];

for (CGFloat p = 0.0; p <= 1.0; p += 1.0 / (20.0 * velocity))
Expand All @@ -76,7 +87,7 @@ static void _DTXApplySwipe(UIWindow* window, CGPoint startPoint, CGPoint endPoin

[points addObject:@(CGPointMake(x, y))];
}

[DTXSyntheticEvents touchAlongPath:points relativeToWindow:window holdDurationOnLastTouch:0.0];
}

Expand All @@ -99,9 +110,12 @@ - (void)dtx_swipeWithNormalizedOffset:(CGPoint)normalizedOffset velocity:(CGFloa
CGPoint startPoint;
CGPoint endPoint;

UIWindow* window = self.dtx_view.window;
UIView* view = self.dtx_view;

CGRect safeBounds = self.dtx_safeAreaBounds;
CGRect safeBoundsInScreenSpace = [self.window.screen.coordinateSpace convertRect:safeBounds fromCoordinateSpace:self.coordinateSpace];
CGRect screenBounds = self.window.screen.bounds;
CGRect safeBoundsInScreenSpace = [window.screen.coordinateSpace convertRect:safeBounds fromCoordinateSpace:view.coordinateSpace];
CGRect screenBounds = window.screen.bounds;

if(normalizedOffset.x != 0)
{
Expand All @@ -113,12 +127,12 @@ - (void)dtx_swipeWithNormalizedOffset:(CGPoint)normalizedOffset velocity:(CGFloa
}


[self dtx_assertHittableAtPoint:[self.coordinateSpace convertPoint:startPoint fromCoordinateSpace:self.window.screen.coordinateSpace]];
[view dtx_assertHittableAtPoint:[view.coordinateSpace convertPoint:startPoint fromCoordinateSpace:window.screen.coordinateSpace]];

startPoint = [self.window.coordinateSpace convertPoint:startPoint fromCoordinateSpace:self.window.screen.coordinateSpace];
endPoint = [self.window.coordinateSpace convertPoint:endPoint fromCoordinateSpace:self.window.screen.coordinateSpace];
startPoint = [window.coordinateSpace convertPoint:startPoint fromCoordinateSpace:window.screen.coordinateSpace];
endPoint = [window.coordinateSpace convertPoint:endPoint fromCoordinateSpace:window.screen.coordinateSpace];

_DTXApplySwipe(self.window, startPoint, endPoint, 1.0 / velocity);
_DTXApplySwipe(window, startPoint, endPoint, 1.0 / velocity);
}

static void _DTXApplyPinch(UIWindow* window, CGPoint startPoint1, CGPoint endPoint1, CGPoint startPoint2, CGPoint endPoint2, CGFloat velocity)
Expand All @@ -138,7 +152,7 @@ static void _DTXApplyPinch(UIWindow* window, CGPoint startPoint1, CGPoint endPoi

[points2 addObject:@(CGPointMake(x, y))];
}

[DTXSyntheticEvents touchAlongMultiplePaths:@[points1, points2] relativeToWindow:window holdDurationOnLastTouch:0.0];
}

Expand Down Expand Up @@ -190,6 +204,8 @@ - (void)dtx_pinchWithScale:(CGFloat)scale velocity:(CGFloat)velocity angle:(CGFl
return;
}

UIView* view = self.dtx_view;
UIWindow* window = view.window;
CGRect safeBounds = self.dtx_safeAreaBounds;

CGPoint startPoint1;
Expand Down Expand Up @@ -218,12 +234,12 @@ - (void)dtx_pinchWithScale:(CGFloat)scale velocity:(CGFloat)velocity angle:(CGFl
[self dtx_assertHittableAtPoint:startPoint1];
[self dtx_assertHittableAtPoint:startPoint2];

startPoint1 = [self.window convertPoint:startPoint1 fromView:self];
endPoint1 = [self.window convertPoint:endPoint1 fromView:self];
startPoint2 = [self.window convertPoint:startPoint2 fromView:self];
endPoint2 = [self.window convertPoint:endPoint2 fromView:self];
startPoint1 = [window convertPoint:startPoint1 fromView:view];
endPoint1 = [window convertPoint:endPoint1 fromView:view];
startPoint2 = [window convertPoint:startPoint2 fromView:view];
endPoint2 = [window convertPoint:endPoint2 fromView:view];

_DTXApplyPinch(self.window, startPoint1, endPoint1, startPoint2, endPoint2, 1.0 / velocity);
_DTXApplyPinch(window, startPoint1, endPoint1, startPoint2, endPoint2, 1.0 / velocity);
}

static UIView* _isViewOrDescendantFirstResponder(UIView* view)
Expand Down Expand Up @@ -267,7 +283,7 @@ - (void)dtx_pinchWithScale:(CGFloat)scale velocity:(CGFloat)velocity angle:(CGFl

if(firstResponder == nil)
{
DTXCViewAssert(firstResponder == nil, firstResponder.dtx_viewDebugAttributes, @"Failed to make view “%@” first responder", view.dtx_shortDescription);
DTXCViewAssert(firstResponder == nil, firstResponder.dtx_elementDebugAttributes, @"Failed to make view “%@” first responder", view.dtx_shortDescription);
}

return firstResponder;
Expand All @@ -280,7 +296,7 @@ static BOOL _assertFirstResponderSupportsTextInput(UIView* firstResponder)
return YES;
}

DTXCViewAssert(NO, firstResponder.dtx_viewDebugAttributes, @"First responder “%@” does not conform to “UITextInput” protocol", firstResponder);
DTXCViewAssert(NO, firstResponder.dtx_elementDebugAttributes, @"First responder “%@” does not conform to “UITextInput” protocol", firstResponder);

return NO;
}
Expand Down Expand Up @@ -326,7 +342,7 @@ static void _DTXFixupKeyboard(void)
{
[controller setValue:@YES forPreferenceKey:@"DidShowContinuousPathIntroduction"];
}

[controller synchronizePreferences];
}

Expand All @@ -343,7 +359,7 @@ static void _DTXTypeText(NSString* text)

[UIKeyboardImpl.sharedInstance.taskQueue performTask:^(id ctx) {
[UIKeyboardImpl.sharedInstance handleKeyWithString:grapheme forKeyEvent:nil executionContext:ctx];

NSArray* sounds = @[@1104, @1155, @1156];

AudioServicesPlaySystemSound([sounds[grapheme.hash % 3] unsignedIntValue]);
Expand All @@ -361,12 +377,13 @@ static void _DTXTypeText(NSString* text)

- (void)dtx_clearText
{
UIView<UITextInput>* firstResponder = (id)_ensureFirstResponderIfNeeded(self);
UIView* view = self.dtx_view;
UIView<UITextInput>* firstResponder = (id)_ensureFirstResponderIfNeeded(view);
_assertFirstResponderSupportsTextInput(firstResponder);

UITextPosition* beginningOfDocument = firstResponder.beginningOfDocument;
UITextPosition* endOfDocument = firstResponder.endOfDocument;

UITextRange* range = [firstResponder textRangeFromPosition:beginningOfDocument toPosition:endOfDocument];
if(range.isEmpty == YES)
{
Expand All @@ -387,7 +404,8 @@ - (void)dtx_typeText:(NSString*)text

- (void)dtx_typeText:(NSString*)text atTextRange:(UITextRange*)textRange
{
UIView<UITextInput>* firstResponder = (id)_ensureFirstResponderIfNeeded(self);
UIView* view = self.dtx_view;
UIView<UITextInput>* firstResponder = (id)_ensureFirstResponderIfNeeded(view);
_assertFirstResponderSupportsTextInput(firstResponder);
_ensureSelectionAtRange(firstResponder, textRange);

Expand All @@ -396,7 +414,8 @@ - (void)dtx_typeText:(NSString*)text atTextRange:(UITextRange*)textRange

- (void)dtx_replaceText:(NSString*)text
{
UIView<UITextInput>* firstResponder = (id)_ensureFirstResponderIfNeeded(self);
UIView* view = self.dtx_view;
UIView<UITextInput>* firstResponder = (id)_ensureFirstResponderIfNeeded(view);
_assertFirstResponderSupportsTextInput(firstResponder);

BOOL isControl = [firstResponder isKindOfClass:UIControl.class];
Expand Down Expand Up @@ -424,7 +443,7 @@ - (void)dtx_replaceText:(NSString*)text

UITextPosition* beginningOfDocument = firstResponder.beginningOfDocument;
UITextPosition* endOfDocument = firstResponder.endOfDocument;

UITextRange* range = [firstResponder textRangeFromPosition:beginningOfDocument toPosition:endOfDocument];

[firstResponder replaceRange:range withText:text];
Expand Down Expand Up @@ -455,4 +474,3 @@ - (void)dtx_replaceText:(NSString*)text
}

@end

4 changes: 2 additions & 2 deletions detox/ios/Detox/Actions/UIPickerView+DetoxActions.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ - (void)dtx_setComponent:(NSInteger)component toValue:(id)value

NSInteger componentCount = [self.dataSource numberOfComponentsInPickerView:self];

DTXViewAssert(componentCount > component, self.dtx_viewDebugAttributes, @"Invalid component “%@” for picker view “%@", @(component), self.dtx_shortDescription);
DTXViewAssert(componentCount > component, self.dtx_elementDebugAttributes, @"Invalid component “%@” for picker view “%@", @(component), self.dtx_shortDescription);

NSInteger rowCount = [self.dataSource pickerView:self numberOfRowsInComponent:component];

Expand Down Expand Up @@ -66,7 +66,7 @@ - (void)dtx_setComponent:(NSInteger)component toValue:(id)value
}
}

DTXViewAssert(NO, self.dtx_viewDebugAttributes, @"Picker view “%@” does not contain value “%@” for component “%@", self.dtx_shortDescription, value, @(component));
DTXViewAssert(NO, self.dtx_elementDebugAttributes, @"Picker view “%@” does not contain value “%@” for component “%@", self.dtx_shortDescription, value, @(component));
}

@end
2 changes: 1 addition & 1 deletion detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ - (void)_dtx_scrollWithOffset:(CGPoint)offset normalizedStartingPoint:(CGPoint)n
}
}

DTXViewAssert(strict == NO || successfullyAppliedScrolls > 0, self.dtx_viewDebugAttributes, @"Unable to scroll %@ in “%@", _DTXScrollDirectionDescriptionWithOffset(offset), self.dtx_shortDescription);
DTXViewAssert(strict == NO || successfullyAppliedScrolls > 0, self.dtx_elementDebugAttributes, @"Unable to scroll %@ in “%@", _DTXScrollDirectionDescriptionWithOffset(offset), self.dtx_shortDescription);

self.dtx_disableDecelerationForScroll = NO;
// self.bounces = oldBounces;
Expand Down
3 changes: 2 additions & 1 deletion detox/ios/Detox/Detox.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ framework module Detox {

explicit module Private {
header "UIView+DetoxMatchers.h"
header "UIView+DetoxActions.h"
header "UIPickerView+DetoxActions.h"
header "UIDatePicker+DetoxActions.h"
header "UIScrollView+DetoxActions.h"
Expand All @@ -21,5 +20,7 @@ framework module Detox {
header "DTXDurationFormatter.h"
header "UIWindow+DetoxUtils.h"
header "DTXAppleInternals.h"
header "NSObject+DetoxUtils.h"
header "NSObject+DetoxActions.h"
}
}
1 change: 0 additions & 1 deletion detox/ios/Detox/Invocation/Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ class LongPressAction : Action {
if let param = params?.first as? Double {
duration = param.toSeconds()
} else {
//TODO: Check default value in Detox
duration = 1.0
}

Expand Down
19 changes: 6 additions & 13 deletions detox/ios/Detox/Invocation/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,21 @@ class Element : NSObject {
}
}

// private var cachedViews : [UIView]?
private var views : [UIView] {
// if let cachedViews = cachedViews {
// return cachedViews
// }

private var views : [NSObject] {
//TODO: Consider searching here in all windows from all scenes.
let array = (UIView.dtx_findViewsInKeySceneWindows(passing: predicate.predicateForQuery()) as! [UIView])
let array = (UIView.dtx_findViewsInKeySceneWindows(passing: predicate.predicateForQuery()) as! [NSObject])

guard array.count > 0 else {
dtx_fatalError("No elements found for “\(self.description)", viewDescription: failDebugAttributes)
}

// cachedViews = array

return array
}

private var view : UIView {
private var view : NSObject {
let array = self.views

let element : UIView
let element : NSObject
if let index = index {
guard index < array.count else {
dtx_fatalError("Index \(index) beyond bounds \(array.count > 0 ? "[0 .. \(array.count - 1)] " : " ")for “\(self.description)", viewDescription: failDebugAttributes)
Expand Down Expand Up @@ -111,7 +104,7 @@ class Element : NSObject {
// }
var rv: [String: Any]! = nil
try dtx_try {
rv = view.dtx_viewDebugAttributes
rv = view.dtx_elementDebugAttributes
}
return rv
} catch {
Expand Down Expand Up @@ -199,7 +192,7 @@ class Element : NSObject {

func isVisible() throws -> Bool {
var error: NSError? = nil
let rv = view.dtx_isVisible(at: view.dtx_accessibilityActivationPointInViewCoordinateSpace, error: &error)
let rv = view.dtx_isVisible(at: view.dtx_bounds, error: &error)
if let error = error {
throw error
}
Expand Down
Loading

0 comments on commit ee3d092

Please sign in to comment.