chromium/ios/chrome/browser/ui/popup_menu/request_desktop_mobile_site_egtest.mm

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

#import "base/containers/contains.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "components/strings/grit/components_strings.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/popup_menu/popup_menu_constants.h"
#import "ios/chrome/browser/ui/settings/settings_table_view_controller_constants.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/web_http_server_chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/common/features.h"
#import "ios/web/common/user_agent.h"
#import "ios/web/public/test/http_server/data_response_provider.h"
#import "ios/web/public/test/http_server/http_server.h"
#import "ios/web/public/test/http_server/http_server_util.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

const char kUserAgentTestURL[] =
    "http://ios/testing/data/http_server_files/user_agent_test_page.html";

const char kMobileSiteLabel[] = "Mobile";

const char kDesktopSiteLabel[] = "Desktop";
const char kDesktopPlatformLabel[] = "MacIntel";

// URL to be used when the page needs to be reloaded on back/forward
// navigations.
const char kPurgeURL[] = "url-purge.com";
// JavaScript used to reload the page on back/forward navigations.
const char kJavaScriptReload[] =
    "<script>window.onpageshow = function(event) {"
    "    if (event.persisted) {"
    "       window.location.href = window.location.href + \"?reloaded\""
    "    }"
    "};</script>";

// Custom timeout used when waiting for a web state after requesting desktop
// or mobile mode.
constexpr base::TimeDelta kWaitForUserAgentChangeTimeout = base::Seconds(15);

// Returns the correct matcher for the collection view containing the Request
// Desktop/Mobile button given the current overflow menu.
id<GREYMatcher> CollectionViewMatcher() {
  return [ChromeEarlGrey isNewOverflowMenuEnabled]
             ? grey_accessibilityID(kPopupMenuToolsMenuActionListId)
             : grey_accessibilityID(kPopupMenuToolsMenuTableViewId);
}

// Select the button to request desktop site by scrolling the collection.
// 200 is a reasonable scroll displacement that works for all UI elements, while
// not being too slow.
GREYElementInteraction* RequestMobileButton() {
  return [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                              kToolsMenuRequestMobileId),
                                          grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 200)
      onElementWithMatcher:CollectionViewMatcher()];
}

// Select the button to request mobile site by scrolling the collection.
// 200 is a reasonable scroll displacement that works for all UI elements, while
// not being too slow.
GREYElementInteraction* RequestDesktopButton() {
  return [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                              kToolsMenuRequestDesktopId),
                                          grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 200)
      onElementWithMatcher:CollectionViewMatcher()];
}

// A ResponseProvider that provides user agent for httpServer request.
class UserAgentResponseProvider : public web::DataResponseProvider {
 public:
  bool CanHandleRequest(const Request& request) override { return true; }

  void GetResponseHeadersAndBody(
      const Request& request,
      scoped_refptr<net::HttpResponseHeaders>* headers,
      std::string* response_body) override {
    // Do not return anything if static plist file has been requested,
    // as plain text is not a valid property list content.
    if ([[base::SysUTF8ToNSString(request.url.spec()) pathExtension]
            isEqualToString:@"plist"]) {
      *headers =
          web::ResponseProvider::GetResponseHeaders("", net::HTTP_NO_CONTENT);
      return;
    }

    std::string purge_additions = "";
    if (base::Contains(request.url.path(), kPurgeURL)) {
      purge_additions = kJavaScriptReload;
    }

    *headers = web::ResponseProvider::GetDefaultResponseHeaders();
    std::optional<std::string> userAgent =
        request.headers.GetHeader("User-Agent");
    std::string desktop_product =
        "CriOS/" + version_info::GetMajorVersionNumber();
    std::string desktop_user_agent =
        web::BuildDesktopUserAgent(desktop_product);
    if (userAgent == desktop_user_agent) {
      response_body->assign(std::string(kDesktopSiteLabel) + "\n" +
                            purge_additions);
    } else {
      response_body->assign(std::string(kMobileSiteLabel) + "\n" +
                            purge_additions);
    }
  }
};
}  // namespace

// Tests for the tools popup menu.
@interface RequestDesktopMobileSiteTestCase : WebHttpServerChromeTestCase
@end

@implementation RequestDesktopMobileSiteTestCase

#pragma mark - Helper

// Sets the default mode to the passed `defaultMode`.
- (void)selectDefaultMode:(NSString*)defaultMode {
  [ChromeEarlGreyUI openSettingsMenu];
  [[[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                              kSettingsContentSettingsCellId),
                                          grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 150)
      onElementWithMatcher:chrome_test_util::SettingsCollectionView()]
      performAction:grey_tap()];
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kSettingsDefaultSiteModeCellId)]
      performAction:grey_tap()];

  [[EarlGrey selectElementWithMatcher:
                 chrome_test_util::StaticTextWithAccessibilityLabel(
                     defaultMode)] performAction:grey_tap()];
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::NavigationBarDoneButton()]
      performAction:grey_tap()];
}

#pragma mark - Tests

// Tests that requesting desktop site of a page works and the user agent
// propagates to the next navigations in the same tab.
- (void)testRequestDesktopSitePropagatesToNextNavigations {
  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  // Request and verify reception of the desktop site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];

  // Verify that desktop user agent propagates.
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://2.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];
}

// Tests that requesting desktop site of a page works and the requested user
// agent is kept when restoring the session.
- (void)testRequestDesktopSiteKeptSessionRestoration {
  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  // Request and verify reception of the desktop site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];

  // Restart the app to trigger a reload.
  [self triggerRestoreByRestartingApplication];

  // Verify that desktop user agent propagates.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestMobileButton() assertWithMatcher:grey_notNil()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];
}

// Tests that requesting desktop site of a page works and desktop user agent
// does not propagate to next the new tab.
- (void)testRequestDesktopSiteDoesNotPropagateToNewTab {
  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  // Request and verify reception of the desktop site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];

  // Verify that desktop user agent does not propagate to new tab.
  [ChromeEarlGreyUI openNewTab];
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://2.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];
}

// Tests that when requesting desktop on another page and coming back to a page
// that has been purged from memory, we still display the mobile page.
- (void)testRequestDesktopSiteGoBackToMobilePurged {

  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl(
                              "http://" + std::string(kPurgeURL))];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://2.com")];

  // Request and verify reception of the desktop site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];

  // Verify that going back returns to the mobile site.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
      performAction:grey_tap()];
  GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
                 base::test::ios::kWaitForPageLoadTimeout,
                 ^bool {
                   return [ChromeEarlGrey webStateVisibleURL].query() ==
                          "reloaded";
                 }),
             @"Page did not reload");
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];
}

// Tests that navigating forward to a page not using the default mode from a
// restored session is using the mode used in the past session.
- (void)testNavigateForwardToDesktopMode {
  // TODO(crbug.com/329210328): Re-enable the test on iPad device.
#if !TARGET_IPHONE_SIMULATOR
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Test skipped on iPad device.");
  }
#endif

  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  // Load the page in the non-default mode.
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];

  [ChromeEarlGrey goBack];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::FakeOmnibox()]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Restart the app to trigger a reload.
  [self triggerRestoreByRestartingApplication];

  // Make sure that the NTP is displayed.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::FakeOmnibox()]
      assertWithMatcher:grey_sufficientlyVisible()];

  // The session is restored, navigate forward and check the mode.
  [ChromeEarlGrey goForward];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];
  [ChromeEarlGreyUI openToolsMenu];
  [RequestMobileButton() assertWithMatcher:grey_notNil()];
}

// Tests that requesting mobile site of a page works and the user agent
// propagates to the next navigations in the same tab.
- (void)testRequestMobileSitePropagatesToNextNavigations {
  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  // Request and verify reception of the desktop site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];

  // Request and verify reception of the mobile site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestMobileButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];

  // Verify that mobile user agent propagates.
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://2.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];
}

// Tests that requesting desktop site button is not enabled on new tab pages.
- (void)testRequestDesktopSiteNotEnabledOnNewTabPage {
  // Verify tapping on request desktop button is no-op.
  [ChromeEarlGreyUI openToolsMenu];
  [[RequestDesktopButton() assertWithMatcher:grey_notNil()]
      performAction:grey_tap()];
}

// Tests that requesting desktop site button is not enabled on WebUI pages.
- (void)testRequestDesktopSiteNotEnabledOnWebUIPage {
  [ChromeEarlGrey loadURL:GURL("chrome://version")];

  // Verify tapping on request desktop button is no-op.
  [ChromeEarlGreyUI openToolsMenu];
  [[RequestDesktopButton() assertWithMatcher:grey_notNil()]
      performAction:grey_tap()];
}

// Tests that navigator.appVersion JavaScript API returns correct string for
// mobile User Agent and the platform.
- (void)testAppVersionJSAPIWithMobileUserAgent {
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl(kUserAgentTestURL)];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  std::string defaultPlatform;
  std::string nonDefaultPlatform;
  if ([ChromeEarlGrey isMobileModeByDefault]) {
    defaultPlatform = base::SysNSStringToUTF8([[UIDevice currentDevice] model]);
    nonDefaultPlatform = kDesktopPlatformLabel;
  } else {
    defaultPlatform = kDesktopPlatformLabel;
    nonDefaultPlatform =
        base::SysNSStringToUTF8([[UIDevice currentDevice] model]);
  }
  [ChromeEarlGrey waitForWebStateContainingText:defaultPlatform];

  // Request and verify reception of the desktop site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestDesktopButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];
  [ChromeEarlGrey waitForWebStateContainingText:nonDefaultPlatform];

  // Request and verify reception of the mobile site.
  [ChromeEarlGreyUI openToolsMenu];
  [RequestMobileButton() performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel
                                        timeout:kWaitForUserAgentChangeTimeout];
  [ChromeEarlGrey waitForWebStateContainingText:defaultPlatform];
}

// Tests that changing the default mode of the page load works as expected.
- (void)testRequestDesktopByDefault {
  GREYAssertTrue([ChromeEarlGrey isMobileModeByDefault],
                 @"The default mode should be mobile.");

  [self selectDefaultMode:@"Desktop"];

  GREYAssertFalse([ChromeEarlGrey isMobileModeByDefault],
                  @"The default mode should be desktop.");

  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];

  // Change back to Mobile.
  [self selectDefaultMode:@"Mobile"];

  // Make sure that the page isn't reloaded.
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];

  // Verify that mobile user agent propagates.
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://2.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];
}

// Tests that reloading a page after changing its default mode updates to the
// new mode.
- (void)testReloading {
  GREYAssertTrue([ChromeEarlGrey isMobileModeByDefault],
                 @"The default mode should be mobile.");

  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  [self selectDefaultMode:@"Desktop"];

  GREYAssertFalse([ChromeEarlGrey isMobileModeByDefault],
                  @"The default mode should be desktop.");

  // Reloading should change to desktop.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];
}

// Tests that navigating back to a page after changing default mode doesn't
// change the page mode.
- (void)testGoBackInDifferentDefaultMode {
  GREYAssertTrue([ChromeEarlGrey isMobileModeByDefault],
                 @"The default mode should be mobile.");

  std::unique_ptr<web::DataResponseProvider> provider(
      new UserAgentResponseProvider());
  web::test::SetUpHttpServer(std::move(provider));

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://1.com")];
  // Verify initial reception of the mobile site.
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];

  [self selectDefaultMode:@"Desktop"];

  GREYAssertFalse([ChromeEarlGrey isMobileModeByDefault],
                  @"The default mode should be desktop.");

  // Move to another page, loaded in desktop mode.
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl("http://2.com")];
  [ChromeEarlGrey waitForWebStateContainingText:kDesktopSiteLabel];

  // Go back to the Mobile page.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
      performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:kMobileSiteLabel];
}

@end