chromium/ios/chrome/test/earl_grey/chrome_actions_app_interface.mm

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/test/earl_grey/chrome_actions_app_interface.h"

#import "base/apple/foundation_util.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_item.h"
#import "ios/chrome/test/app/tab_test_util.h"
#import "ios/testing/earl_grey/earl_grey_app.h"
#import "ios/web/public/test/earl_grey/web_view_actions.h"
#import "ios/web/public/test/element_selector.h"

namespace {

// Action to swipe left on 150pt.
NSArray<NSValue*>* SwipeLeft(CGPoint startPoint) {
  const CGFloat total_length = 150;
  const int number_of_frames = 30;
  const CGFloat deltaX = total_length / number_of_frames;
  // Initial displacement to trigger a swipe.
  const int initial_displacement = 10;
  const int beginning = ceil(initial_displacement / deltaX);

  NSMutableArray* touchPath = [[NSMutableArray alloc] init];
  [touchPath addObject:[NSValue valueWithCGPoint:startPoint]];

  for (int i = beginning; i < number_of_frames; i++) {
    CGPoint point = CGPointMake(startPoint.x - i * deltaX, startPoint.y);
    [touchPath addObject:[NSValue valueWithCGPoint:point]];
  }

  [touchPath addObject:[NSValue valueWithCGPoint:CGPointMake(startPoint.x -
                                                                 total_length,
                                                             startPoint.y)]];

  return touchPath;
}

NSString* kChromeActionsErrorDomain = @"ChromeActionsError";

}  // namespace

@implementation ChromeActionsAppInterface : NSObject

+ (id<GREYAction>)longPressElement:(ElementSelector*)selector
                triggerContextMenu:(BOOL)triggerContextMenu {
  return WebViewLongPressElementForContextMenu(
      chrome_test_util::GetCurrentWebState(), selector, triggerContextMenu);
}

+ (id<GREYAction>)scrollElementToVisible:(ElementSelector*)selector {
  return WebViewScrollElementToVisible(chrome_test_util::GetCurrentWebState(),
                                       selector);
}

+ (id<GREYAction>)turnTableViewSwitchOn:(BOOL)on {
  id<GREYMatcher> constraints = grey_not(grey_systemAlertViewShown());
  NSString* actionName =
      [NSString stringWithFormat:@"Turn table view switch to %@ state",
                                 on ? @"ON" : @"OFF"];
  return [GREYActionBlock
      actionWithName:actionName
         constraints:constraints
        performBlock:^BOOL(id collectionViewCell,
                           __strong NSError** errorOrNil) {
          // EG2 executes actions on a background thread by default. Since this
          // action interacts with UI, kick it over to the main thread.
          __block BOOL success = NO;
          grey_dispatch_sync_on_main_thread(^{
            TableViewSwitchCell* switchCell =
                base::apple::ObjCCast<TableViewSwitchCell>(collectionViewCell);
            if (!switchCell) {
              NSString* description = @"The element isn't of the expected type "
                                      @"(TableViewSwitchCell).";
              *errorOrNil = [NSError
                  errorWithDomain:kChromeActionsErrorDomain
                             code:0
                         userInfo:@{NSLocalizedDescriptionKey : description}];
              success = NO;
              return;
            }
            UISwitch* switchView = switchCell.switchView;
            if (switchView.on != on) {
              id<GREYAction> action = [GREYActions actionForTurnSwitchOn:on];
              success = [action perform:switchView error:errorOrNil];
              return;
            }
            success = YES;
          });
          return success;
        }];
}

+ (id<GREYAction>)tapWebElement:(ElementSelector*)selector {
  return web::WebViewTapElement(chrome_test_util::GetCurrentWebState(),
                                selector, /*verified*/ true);
}

+ (id<GREYAction>)tapWebElementUnverified:(ElementSelector*)selector {
  return web::WebViewTapElement(chrome_test_util::GetCurrentWebState(),
                                selector, /*verified*/ false);
}

+ (id<GREYAction>)scrollToTop {
  GREYPerformBlock scrollToTopBlock = ^BOOL(id element,
                                            __strong NSError** error) {
    grey_dispatch_sync_on_main_thread(^{
      UIScrollView* view = base::apple::ObjCCast<UIScrollView>(element);
      if (!view) {
        *error = [NSError
            errorWithDomain:kChromeActionsErrorDomain
                       code:0
                   userInfo:@{
                     NSLocalizedDescriptionKey : @"View is not a UIScrollView"
                   }];
      }
      view.contentOffset = CGPointZero;
    });
    return YES;
  };
  return [GREYActionBlock actionWithName:@"Scroll to top"
                            performBlock:scrollToTopBlock];
}

+ (id<GREYAction>)tapAtPointAtxOriginStartPercentage:(CGFloat)x
                              yOriginStartPercentage:(CGFloat)y {
  DCHECK(0 <= x && x <= 1);
  DCHECK(0 <= y && y <= 1);

  id<GREYMatcher> constraints = grey_notNil();
  NSString* actionName =
      [NSString stringWithFormat:@"Tap at point at percentage"];

  GREYPerformBlock actionBlock = ^BOOL(id view, __strong NSError** errorOrNil) {
    __block BOOL success = NO;
    grey_dispatch_sync_on_main_thread(^{
      CGRect rect = [view accessibilityFrame];
      CGPoint pointToTap =
          CGPointMake(rect.size.width * x, rect.size.height * y);
      success =
          [[GREYActions actionForTapAtPoint:pointToTap] perform:view
                                                          error:errorOrNil];
    });
    return success;
  };
  return [GREYActionBlock actionWithName:actionName
                             constraints:constraints
                            performBlock:actionBlock];
}

+ (id<GREYAction>)swipeToShowDeleteButton {
  return [GREYActionBlock
      actionWithName:@"Swipe to display delete button"
         constraints:nil
        performBlock:^(UIView* element, NSError* __strong* errorOrNil) {
          if ([element window] == nil) {
            NSString* errorDescription = [NSString
                stringWithFormat:
                    @"Cannot swipe on this view as it has no window and "
                    @"isn't a window itself:\n%@",
                    [element grey_description]];
            *errorOrNil = [NSError
                errorWithDomain:@"No window available"
                           code:0
                       userInfo:@{@"Failure Reason" : (errorDescription)}];
            // Indicates that the action failed.
            return NO;
          }
          CGRect accessibilityFrame = element.accessibilityFrame;
          CGPoint startPoint = CGPointMake(
              accessibilityFrame.origin.x + accessibilityFrame.size.width * 0.5,
              accessibilityFrame.origin.y +
                  accessibilityFrame.size.height * 0.5);
          // Invoke a custom selector that animates the window of the element.
          [GREYSyntheticEvents touchAlongPath:SwipeLeft(startPoint)
                             relativeToWindow:[element window]
                                  forDuration:1
                                      timeout:10];
          // Indicates that the action was executed successfully.
          return YES;
        }];
}

+ (id<GREYAction>)accessibilitySwipeRight {
  return [GREYActionBlock
      actionWithName:@"Swipe right with 3-finger"
         constraints:nil
        performBlock:^(UIScrollView* element, NSError* __strong* errorOrNil) {
          if (![element isKindOfClass:UIScrollView.class]) {
            NSString* errorDescription =
                [NSString stringWithFormat:@"Cannot swipe on this view as it "
                                           @"is not a scroll view:\n%@",
                                           [element grey_description]];
            *errorOrNil = [NSError
                errorWithDomain:@"Not a scroll view"
                           code:0
                       userInfo:@{@"Failure Reason" : (errorDescription)}];
            // Indicates that the action failed.
            return NO;
          }
          if ([element window] == nil) {
            NSString* errorDescription = [NSString
                stringWithFormat:
                    @"Cannot swipe on this view as it has no window and "
                    @"isn't a window itself:\n%@",
                    [element grey_description]];
            *errorOrNil = [NSError
                errorWithDomain:@"No window available"
                           code:0
                       userInfo:@{@"Failure Reason" : (errorDescription)}];
            // Indicates that the action failed.
            return NO;
          }
          CGPoint currentOffset = element.contentOffset;
          currentOffset.x = currentOffset.x - element.bounds.size.width;
          [element setContentOffset:currentOffset animated:NO];
          [element.delegate scrollViewDidEndDecelerating:element];
          // Indicates that the action was executed successfully.
          return YES;
        }];
}

@end