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

// Copyright 2020 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_xcui_actions.h"

#import "base/apple/foundation_util.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/testing/earl_grey/earl_grey_test.h"

namespace {

// The splitter is 10 points wide.
const CGFloat kSplitterWidth = 10.0;

// Returns a normalized vector for the given edge.
CGVector GetNormalizedEdgeVector(GREYContentEdge edge) {
  switch (edge) {
    case kGREYContentEdgeLeft:
      return CGVectorMake(0.02, 0.5);
    case kGREYContentEdgeRight:
      return CGVectorMake(.98, 0.5);
    case kGREYContentEdgeTop:
      return CGVectorMake(0.5, 0.02);
    case kGREYContentEdgeBottom:
      return CGVectorMake(0.5, 0.98);
    default:
      return CGVectorMake(0.5, 0.5);
  }
}

NSString* GetWindowAccessibilityIdentifier(int window_number) {
  return [NSString stringWithFormat:@"%d", window_number];
}

// Finds the element with the given `identifier` of given `type` in given
// `window_number`.  If `identitifer` is nil, this will return a element for the
// window with `window_number` itself.
XCUIElement* GetElementMatchingIdentifierInWindow(XCUIApplication* app,
                                                  NSString* identifier,
                                                  int window_number,
                                                  XCUIElementType type) {
  NSString* window_id = GetWindowAccessibilityIdentifier(window_number);
  XCUIElementQuery* query = nil;
  if (identifier) {
    // Check for matching descendants.
    query = [[[app.windows matchingIdentifier:window_id]
        descendantsMatchingType:type] matchingIdentifier:identifier];
  } else {
    // Check for window itself.
    query = [app.windows matchingIdentifier:window_id];
  }

  if (query.count == 0)
    return nil;

  return [query elementBoundByIndex:0];
}

// Long press at `start_point` and drag to `end_point`, with fixed press and
// hold druations and drag velocity.
void LongPressAndDragBetweenCoordinates(XCUICoordinate* start_point,
                                        XCUICoordinate* end_point) {
  [start_point pressForDuration:1.5
           thenDragToCoordinate:end_point
                   withVelocity:XCUIGestureVelocityDefault
            thenHoldForDuration:1.0];
}

// Long press on `src_element`'s center then drag to the point in `dst_element`
// defined by `dst_normalized_offset`. Returns NO if either element is nil, YES
// otherwise.
BOOL LongPressAndDragBetweenElements(XCUIElement* src_element,
                                     XCUIElement* dst_element,
                                     CGVector dst_normalized_offset) {
  if (!src_element || !dst_element)
    return NO;

  XCUICoordinate* start_point =
      [src_element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
  XCUICoordinate* end_point =
      [dst_element coordinateWithNormalizedOffset:dst_normalized_offset];

  LongPressAndDragBetweenCoordinates(start_point, end_point);
  return YES;
}

}  // namespace

namespace chrome_test_util {

BOOL LongPressCellAndDragToEdge(NSString* accessibility_identifier,
                                GREYContentEdge edge,
                                int window_number) {
  XCUIApplication* app = [[XCUIApplication alloc] init];
  XCUIElement* drag_element = GetElementMatchingIdentifierInWindow(
      app, accessibility_identifier, window_number, XCUIElementTypeCell);

  // `app` is still an element, so it can just be passed in directly here.
  return LongPressAndDragBetweenElements(drag_element, app,
                                         GetNormalizedEdgeVector(edge));
}

BOOL LongPressCellAndDragToOffsetOf(NSString* src_accessibility_identifier,
                                    int src_window_number,
                                    NSString* dst_accessibility_identifier,
                                    int dst_window_number,
                                    CGVector dst_normalized_offset) {
  XCUIApplication* app = [[XCUIApplication alloc] init];
  XCUIElement* src_element = GetElementMatchingIdentifierInWindow(
      app, src_accessibility_identifier, src_window_number,
      XCUIElementTypeCell);

  XCUIElement* dst_element = GetElementMatchingIdentifierInWindow(
      app, dst_accessibility_identifier, dst_window_number,
      XCUIElementTypeCell);

  return LongPressAndDragBetweenElements(src_element, dst_element,
                                         dst_normalized_offset);
}

BOOL LongPressLinkAndDragToView(NSString* src_accessibility_identifier,
                                int src_window_number,
                                NSString* dst_accessibility_identifier,
                                int dst_window_number,
                                CGVector dst_normalized_offset) {
  XCUIApplication* app = [[XCUIApplication alloc] init];
  XCUIElement* src_element = GetElementMatchingIdentifierInWindow(
      app, src_accessibility_identifier, src_window_number,
      XCUIElementTypeLink);

  XCUIElement* dst_element = GetElementMatchingIdentifierInWindow(
      app, dst_accessibility_identifier, dst_window_number, XCUIElementTypeAny);

  return LongPressAndDragBetweenElements(src_element, dst_element,
                                         dst_normalized_offset);
}

BOOL LongPressLinkAndDragToView(NSString* src_accessibility_identifier,
                                NSString* dst_accessibility_identifier) {
  return LongPressLinkAndDragToView(src_accessibility_identifier, 0,
                                    dst_accessibility_identifier, 0,
                                    CGVectorMake(0.5, 0.5));
}

BOOL DragWindowSplitterToSize(int first_window_number,
                              int second_window_number,
                              CGFloat first_window_normalized_screen_size) {
  CGRect first_rect =
      [ChromeEarlGrey screenPositionOfScreenWithNumber:first_window_number];
  if (CGRectIsEmpty(first_rect))
    return NO;

  CGRect second_rect =
      [ChromeEarlGrey screenPositionOfScreenWithNumber:second_window_number];
  if (CGRectIsEmpty(second_rect))
    return NO;

  // Fail early if a floating window was passed in in one of the windows.
  if (first_rect.origin.x != second_rect.origin.x &&
      first_rect.origin.y != second_rect.origin.y)
    return NO;

  // Use device orientation to see if it is one of the modes where we need to
  // invert ltr, because the two rects returned above are always related to
  // UIDeviceOrientationPortrait, and always defined with x,y being the top
  // left corner and width and height always positive.
  UIDeviceOrientation orientation =
      [[GREY_REMOTE_CLASS_IN_APP(UIDevice) currentDevice] orientation];
  BOOL inverted_display = orientation == UIDeviceOrientationLandscapeRight ||
                          orientation == UIDeviceOrientationPortraitUpsideDown;

  // Select leftmost window as reference.
  BOOL ltr = first_rect.origin.x < second_rect.origin.x;
  BOOL landscape = NO;
  if (first_rect.origin.x == second_rect.origin.x) {
    ltr = first_rect.origin.y < second_rect.origin.y;
    landscape = YES;
  }
  if (inverted_display)
    ltr = !ltr;
  int left_window = ltr ? first_window_number : second_window_number;

  XCUIApplication* app = [[XCUIApplication alloc] init];
  XCUIElementQuery* query = [app.windows
      matchingIdentifier:GetWindowAccessibilityIdentifier(left_window)];

  if (query.count == 0)
    return NO;
  XCUIElement* element = [query elementBoundByIndex:0];

  // Start on center of splitter, right of leftmost window.
  XCUICoordinate* start_point =
      [[element coordinateWithNormalizedOffset:CGVectorMake(1.0, 0.5)]
          coordinateWithOffset:CGVectorMake(kSplitterWidth / 2.0, 0.0)];

  // Transform screen relative normalized size into offset relative to
  // 'left_window'.
  CGFloat offset;
  if (landscape) {
    CGFloat height =
        first_rect.size.height + kSplitterWidth + second_rect.size.height;
    if (ltr) {
      offset =
          first_window_normalized_screen_size * height / first_rect.size.height;
    } else {
      offset = (1.0 - first_window_normalized_screen_size) * height /
               second_rect.size.height;
    }
  } else {
    CGFloat width =
        first_rect.size.width + kSplitterWidth + second_rect.size.width;
    if (ltr) {
      offset =
          first_window_normalized_screen_size * width / first_rect.size.width;
    } else {
      offset = (1.0 - first_window_normalized_screen_size) * width /
               second_rect.size.width;
    }
  }

  XCUICoordinate* end_point =
      [element coordinateWithNormalizedOffset:CGVectorMake(offset, 0.5)];

  [start_point pressForDuration:0.1
           thenDragToCoordinate:end_point
                   withVelocity:XCUIGestureVelocityFast
            thenHoldForDuration:0.2];

  return YES;
}

BOOL TapAtOffsetOf(NSString* accessibility_identifier,
                   int window_number,
                   CGVector normalized_offset) {
  XCUIApplication* app = [[XCUIApplication alloc] init];
  XCUIElement* element = GetElementMatchingIdentifierInWindow(
      app, accessibility_identifier, window_number, XCUIElementTypeAny);

  if (!element)
    return NO;

  XCUICoordinate* tap_point =
      [element coordinateWithNormalizedOffset:normalized_offset];
  [tap_point tap];

  return YES;
}

BOOL TypeText(NSString* accessibility_identifier,
              int window_number,
              NSString* text) {
  XCUIApplication* app = [[XCUIApplication alloc] init];

  XCUIElement* element = GetElementMatchingIdentifierInWindow(
      app, accessibility_identifier, window_number, XCUIElementTypeTextField);

  if (!element)
    return NO;

  [element typeText:text];

  return YES;
}

}  // namespace chrome_test_util