chromium/ios/chrome/browser/ui/browser_container/edit_menu_app_interface.mm

// Copyright 2023 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/browser/ui/browser_container/edit_menu_app_interface.h"

#import "base/apple/foundation_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/app/tab_test_util.h"
#import "ios/testing/earl_grey/earl_grey_app.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// Recursively iterate through the `view` subviews trees and returns the
// accessibility labels of the visible edit menu actions.
// `menu_offset` and `menu_width` are used in the recursion to exclude invisible
// items.
// Items are returned as a dictionary <x_position>,<items_label> so they can
// be sorted.
// As visible items are presented horizontally, there should not be any
// collision on the X coordinate.
NSDictionary* ExtractMenuElements(UIView* view,
                                  CGFloat menu_offset,
                                  CGFloat menu_width) {
  // The type of the edit menu cell depends on the OS version.
  bool is_edit_menu_cell = false;
  if (@available(iOS 16, *)) {
    is_edit_menu_cell =
        [view isKindOfClass:NSClassFromString(@"_UIEditMenuListViewCell")];
  } else {
    // Back and forward buttons have the same type as the actions, so exclude
    // them using the accessibility labels.
    is_edit_menu_cell =
        [view isKindOfClass:NSClassFromString(@"UICalloutBarButton")] &&
        ![view.accessibilityIdentifier
            isEqualToString:@"show.previous.items.menu.button"] &&
        ![view.accessibilityIdentifier
            isEqualToString:@"show.next.items.menu.button"];
  }

  if (is_edit_menu_cell) {
    // Exclude views that are hidden or outside of the scrollview visible part.
    if (view.hidden) {
      return @{};
    }
    // Consider the center to avoid rounding issues.
    CGFloat center = CGRectGetMidX(view.frame);
    if (center < menu_offset || center >= menu_offset + menu_width) {
      return @{};
    }
    return @{@(center) : view.accessibilityLabel};
  }

  NSMutableDictionary* sub_views_elements = [NSMutableDictionary dictionary];
  CGFloat new_width = std::min(menu_width, view.frame.size.width);
  if ([view isKindOfClass:[UIScrollView class]]) {
    UIScrollView* scroll_view = (UIScrollView*)view;
    menu_offset = scroll_view.contentOffset.x;
  }
  for (UIView* subView in view.subviews) {
    [sub_views_elements
        addEntriesFromDictionary:ExtractMenuElements(subView, menu_offset,
                                                     new_width)];
  }
  return sub_views_elements;
}

}  // namespace

@implementation EditMenuAppInterface

+ (id<GREYMatcher>)editMenuMatcher {
  if (@available(iOS 16, *)) {
    return grey_kindOfClassName(@"_UIEditMenuListView");
  } else {
    return grey_kindOfClassName(@"UICalloutBar");
  }
}

+ (id<GREYMatcher>)editMenuButtonMatcher {
  if (@available(iOS 16.0, *)) {
    return grey_kindOfClass(NSClassFromString(@"_UIEditMenuListViewCell"));
  } else {
    return grey_kindOfClass(NSClassFromString(@"UICalloutBarButton"));
  }
}

+ (id<GREYMatcher>)editMenuNextButtonMatcher {
  id<GREYMatcher> editMenu = [EditMenuAppInterface editMenuMatcher];
  if (@available(iOS 16, *)) {
    id<GREYMatcher> nextButton = grey_allOf(
        grey_ancestor(editMenu), grey_kindOfClassName(@"_UIEditMenuPageButton"),
        grey_accessibilityLabel(@"Forward"), nil);
    return nextButton;
  } else {
    id<GREYMatcher> nextButton = grey_allOf(
        grey_ancestor(editMenu), grey_kindOfClassName(@"UICalloutBarButton"),
        grey_accessibilityID(@"show.next.items.menu.button"), nil);
    return nextButton;
  }
}

+ (id<GREYMatcher>)editMenuPreviousButtonMatcher {
  id<GREYMatcher> editMenu = [EditMenuAppInterface editMenuMatcher];
  if (@available(iOS 16, *)) {
    id<GREYMatcher> previousButton = grey_allOf(
        grey_ancestor(editMenu), grey_kindOfClassName(@"_UIEditMenuPageButton"),
        grey_accessibilityLabel(@"Previous"), nil);
    return previousButton;
  } else {
    id<GREYMatcher> previousButton = grey_allOf(
        grey_ancestor(editMenu), grey_kindOfClassName(@"UICalloutBarButton"),
        grey_accessibilityID(@"show.previous.items.menu.button"), nil);
    return previousButton;
  }
}

+ (id<GREYMatcher>)editMenuActionWithAccessibilityLabel:
    (NSString*)accessibilityLabel {
  id<GREYMatcher> editMenu = [EditMenuAppInterface editMenuMatcher];
  id<GREYMatcher> editMenuButton = [EditMenuAppInterface editMenuButtonMatcher];
  id<GREYMatcher> actionButton =
      grey_allOf(grey_ancestor(editMenu), editMenuButton,
                 grey_accessibilityLabel(accessibilityLabel), nil);
  return actionButton;
}

+ (id<GREYMatcher>)editMenuLinkToTextButtonMatcher {
  return [EditMenuAppInterface
      editMenuActionWithAccessibilityLabel:l10n_util::GetNSString(
                                               IDS_IOS_SHARE_LINK_TO_TEXT)];
}

+ (id<GREYMatcher>)editMenuCopyButtonMatcher {
  return [EditMenuAppInterface editMenuActionWithAccessibilityLabel:@"Copy"];
}

+ (id<GREYMatcher>)editMenuCutButtonMatcher {
  return [EditMenuAppInterface editMenuActionWithAccessibilityLabel:@"Cut"];
}

+ (id<GREYMatcher>)editMenuPasteButtonMatcher {
  return [EditMenuAppInterface editMenuActionWithAccessibilityLabel:@"Paste"];
}

+ (NSArray*)editMenuActions {
  NSMutableDictionary* menuElements = [NSMutableDictionary dictionary];
  for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
    UIWindowScene* windowScene =
        base::apple::ObjCCastStrict<UIWindowScene>(scene);
    for (UIWindow* window in windowScene.windows) {
      if ([window isKindOfClass:NSClassFromString(@"ChromeOverlayWindow")]) {
        continue;
      }
      [menuElements
          addEntriesFromDictionary:ExtractMenuElements(
                                       window, 0, window.bounds.size.width)];
    }
  }
  NSArray* sortedKeys =
      [[menuElements allKeys] sortedArrayUsingSelector:@selector(compare:)];
  NSMutableArray* sortedValues = [NSMutableArray array];
  for (NSNumber* key in sortedKeys) {
    [sortedValues addObject:[menuElements objectForKey:key]];
  }
  return sortedValues;
}

@end