chromium/ios/chrome/browser/ui/search_with/search_with_mediator_egtest.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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

#import "base/ios/ios_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/chrome/browser/search_engines/model/search_engines_app_interface.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/browser_container/edit_menu_app_interface.h"
#import "ios/chrome/test/earl_grey/chrome_actions.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/public/test/element_selector.h"
#import "net/base/url_util.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"

namespace {

const char kElementToLongPress[] = "selectid";

// An HTML template that puts some text in a simple span element.
const char kBasicSelectionUrl[] = "/basic";
const char kBasicSelectionHtmlTemplate[] =
    "<html>"
    "  <head>"
    "    <meta name='viewport' content='width=device-width, "
    "      initial-scale=1.0, maximum-scale=1.0, user-scalable=no' "
    "    />"
    "  </head>"
    "  <body>"
    "    Page Loaded <br/><br/>"
    "    This text contains a <span id='selectid'>text</span>.<br/><br/><br/>"
    "  </body>"
    "</html>";

// A fake search result page that displays the search query.
const char kSearchResultUrl[] = "/search";
const char kSearchResultHtmlTemplate[] =
    "<html>"
    "  <head>"
    "    <meta name='viewport' content='width=device-width, "
    "      initial-scale=1.0, maximum-scale=1.0, user-scalable=no' "
    "    />"
    "  </head>"
    "  <body>"
    "    Search Result <br/><br/>"
    "    SEARCH_QUERY"
    "  </body>"
    "</html>";

// Provides responses for initial page and destination URLs.
std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
    const net::test_server::HttpRequest& request) {
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_OK);
  GURL request_url = request.GetURL();

  if (request_url.path_piece() == kBasicSelectionUrl) {
    http_response->set_content(kBasicSelectionHtmlTemplate);
  } else if (request_url.path_piece() == kSearchResultUrl) {
    std::string html = kSearchResultHtmlTemplate;
    std::string query;
    bool has_query = net::GetValueForKeyInQuery(request_url, "q", &query);
    if (has_query) {
      base::ReplaceFirstSubstringAfterOffset(&html, 0, "SEARCH_QUERY", query);
    }
    http_response->set_content(html);
  } else {
    return nullptr;
  }

  return std::move(http_response);
}

// Go through the pages and find the element with accessibility
// `accessibility_label`. Returns whether the action can be found.
bool FindEditMenuAction(NSString* accessibility_label) {
  // The menu should be visible.
  [[EarlGrey selectElementWithMatcher:[EditMenuAppInterface editMenuMatcher]]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Start on first screen (previous not visible or disabled).
  NSError* error = nil;
  [[EarlGrey selectElementWithMatcher:[EditMenuAppInterface
                                          editMenuPreviousButtonMatcher]]
      assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
                                   nil)
                  error:&error];
  GREYAssert(error, @"FindEditMenuAction not called on the first page.");
  error = nil;
  [[[EarlGrey
      selectElementWithMatcher:
          grey_allOf(
              [EditMenuAppInterface
                  editMenuActionWithAccessibilityLabel:accessibility_label],
              grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_tap()
      onElementWithMatcher:[EditMenuAppInterface editMenuNextButtonMatcher]]
      assertWithMatcher:grey_sufficientlyVisible()
                  error:&error];
  return !error;
}

// Long presses on `element_id`.
void LongPressElement(const char* element_id) {
  // Use triggers_context_menu = true as this is really "triggers_browser_menu".
  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::LongPressElementForContextMenu(
                        [ElementSelector selectorWithElementID:element_id],
                        true)];
}

// Convenient function to trigger the Edit Menu on `kElementToLongPress`.
// TODO(crbug.com/40264849): extract this function and have all edit menu
// tests use it.
void TriggerEditMenu() {
  [[EarlGrey selectElementWithMatcher:[EditMenuAppInterface editMenuMatcher]]
      assertWithMatcher:grey_notVisible()];
  LongPressElement(kElementToLongPress);

  NSError* error = nil;
  [[EarlGrey selectElementWithMatcher:[EditMenuAppInterface editMenuMatcher]]
      assertWithMatcher:grey_sufficientlyVisible()
                  error:&error];
  if (error) {
    // If edit is not visible, try to tap the element again.
    // This is possible on inputs when the first long press just selects the
    // input.
    LongPressElement(kElementToLongPress);
    [[EarlGrey selectElementWithMatcher:[EditMenuAppInterface editMenuMatcher]]
        assertWithMatcher:grey_sufficientlyVisible()];
  }
}

}  // namespace

// Tests for the Search With Edit menu entry.
@interface SearchWithMediatorTestCase : ChromeTestCase
@property(nonatomic, strong) NSString* defaultSearchEngine;
@end

@implementation SearchWithMediatorTestCase
- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.features_enabled.push_back(kIOSEditMenuSearchWith);
  return config;
}

- (void)setUp {
  [super setUp];
  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&StandardResponse));
  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
  self.defaultSearchEngine = [SearchEnginesAppInterface defaultSearchEngine];
  GURL url = self.testServer->GetURL(kSearchResultUrl);
  [SearchEnginesAppInterface
      addSearchEngineWithName:@"test"
                          URL:base::SysUTF8ToNSString(url.spec() +
                                                      "?q={searchTerms}")
                   setDefault:YES];
}

- (void)tearDown {
  [SearchEnginesAppInterface setSearchEngineTo:self.defaultSearchEngine];
  [SearchEnginesAppInterface removeSearchEngineWithName:@"test"];
  [super tearDown];
}

// Conveniently load a page that has "text" in a selectable field.
- (void)loadPage {
  GURL url = self.testServer->GetURL(kBasicSelectionUrl);
  [ChromeEarlGrey loadURL:url];
  [ChromeEarlGrey waitForWebStateContainingText:"Page Loaded"];
  [ChromeEarlGrey waitForWebStateZoomScale:1.0];
}

- (void)testSearchWith {
  if (!base::ios::IsRunningOnIOS16OrLater()) {
    // Feature disabled on iOS15-.
    EARL_GREY_TEST_SKIPPED(@"Test disabled on iOS15-");
  }
  [self loadPage];
  TriggerEditMenu();
  bool found = FindEditMenuAction(@"Search with test");
  GREYAssertTrue(found, @"Search Web button not found");
  [[EarlGrey selectElementWithMatcher:
                 [EditMenuAppInterface
                     editMenuActionWithAccessibilityLabel:@"Search with test"]]
      performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:"Search Result"];
  [ChromeEarlGrey waitForWebStateContainingText:"text"];
  GREYAssertEqual(2, [ChromeEarlGrey mainTabCount],
                  @"Search Should be in new tab");
}

- (void)testSearchWithIncognito {
  if (!base::ios::IsRunningOnIOS16OrLater()) {
    // Feature disabled on iOS15-.
    EARL_GREY_TEST_SKIPPED(@"Test disabled on iOS15-");
  }
  [ChromeEarlGrey openNewIncognitoTab];
  [self loadPage];
  TriggerEditMenu();
  bool found = FindEditMenuAction(@"Search with test");
  GREYAssertTrue(found, @"Search Web button not found");
  [[EarlGrey selectElementWithMatcher:
                 [EditMenuAppInterface
                     editMenuActionWithAccessibilityLabel:@"Search with test"]]
      performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:"Search Result"];
  [ChromeEarlGrey waitForWebStateContainingText:"text"];
  GREYAssertTrue([ChromeEarlGrey isIncognitoMode],
                 @"Incognito search should stay in incognito");
  GREYAssertEqual(2, [ChromeEarlGrey incognitoTabCount],
                  @"Search Should be in new tab");
}

@end