chromium/ios/chrome/browser/web/model/push_and_replace_state_navigation_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/strings/sys_string_conversions.h"
#import "components/strings/grit/components_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/net/url_test_util.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/public/test/http_server/http_server.h"
#import "ios/web/public/test/http_server/http_server_util.h"

using chrome_test_util::BackButton;
using chrome_test_util::ForwardButton;

namespace {

const char* kHistoryTestUrl =
    "http://ios/testing/data/http_server_files/history.html";
const char* kNonPushedUrl =
    "http://ios/testing/data/http_server_files/pony.html";
const char* kReplaceStateHashWithObjectURL =
    "http://ios/testing/data/http_server_files/history.html#replaceWithObject";
const char* kPushStateHashStringURL =
    "http://ios/testing/data/http_server_files/history.html#string";
const char* kReplaceStateHashStringURL =
    "http://ios/testing/data/http_server_files/history.html#replace";
const char* kPushStatePathURL =
    "http://ios/testing/data/http_server_files/path";
const char* kReplaceStateRootPathSpaceURL = "http://ios/rep lace";

}  // namespace

// Tests for pushState/replaceState navigations.
@interface PushAndReplaceStateNavigationTestCase : WebHttpServerChromeTestCase
@end

@implementation PushAndReplaceStateNavigationTestCase

// Tests calling history.pushState() multiple times and navigating back/forward.
- (void)testHtml5HistoryPushStateThenGoBackAndForward {
  const GURL pushStateHashWithObjectURL = web::test::HttpServer::MakeUrl(
      "http://ios/testing/data/http_server_files/history.html#pushWithObject");
  const std::string pushStateHashWithObjectOmniboxText =
      net::GetContentAndFragmentForUrl(pushStateHashWithObjectURL);
  const GURL pushStateRootPathURL =
      web::test::HttpServer::MakeUrl("http://ios/rootpath");
  const std::string pushStateRootPathOmniboxText =
      net::GetContentAndFragmentForUrl(pushStateRootPathURL);
  const GURL pushStatePathSpaceURL =
      web::test::HttpServer::MakeUrl("http://ios/pa%20th");
  const std::string pushStatePathSpaceOmniboxText =
      net::GetContentAndFragmentForUrl(pushStatePathSpaceURL);
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl(kHistoryTestUrl)];

  // Push 3 URLs. Verify that the URL changed and the status was updated.
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateHashWithObject"];
  [self assertStatusText:@"pushStateHashWithObject"
         withOmniboxText:pushStateHashWithObjectOmniboxText];

  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateRootPath"];
  [self assertStatusText:@"pushStateRootPath"
         withOmniboxText:pushStateRootPathOmniboxText];

  [ChromeEarlGrey tapWebStateElementWithID:@"pushStatePathSpace"];
  [self assertStatusText:@"pushStatePathSpace"
         withOmniboxText:pushStatePathSpaceOmniboxText];

  // Go back and check that the page doesn't load and the status text is updated
  // by the popstate event.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:@"pushStateRootPath"
         withOmniboxText:pushStateRootPathOmniboxText];

  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:@"pushStateHashWithObject"
         withOmniboxText:pushStateHashWithObjectOmniboxText];

  [ChromeEarlGrey tapWebStateElementWithID:@"goBack"];
  const GURL historyTestURL = web::test::HttpServer::MakeUrl(kHistoryTestUrl);
  [self assertStatusText:nil
         withOmniboxText:net::GetContentAndFragmentForUrl(historyTestURL)];

  // Go forward 2 pages and check that the page doesn't load and the status text
  // is updated by the popstate event.
  [ChromeEarlGrey tapWebStateElementWithID:@"goForward2"];
  [self assertStatusText:@"pushStateRootPath"
         withOmniboxText:pushStateRootPathOmniboxText];
}

// Tests that calling replaceState() changes the current history entry.
- (void)testHtml5HistoryReplaceStateThenGoBackAndForward {
  const GURL initialURL = web::test::HttpServer::MakeUrl(kNonPushedUrl);
  const std::string initialOmniboxText =
      net::GetContentAndFragmentForUrl(initialURL);
  [ChromeEarlGrey loadURL:initialURL];
  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl(kHistoryTestUrl)];

  // Replace the URL and go back then forward.
  const GURL replaceStateHashWithObjectURL =
      web::test::HttpServer::MakeUrl(kReplaceStateHashWithObjectURL);
  const std::string replaceStateHashWithObjectOmniboxText =
      net::GetContentAndFragmentForUrl(replaceStateHashWithObjectURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"replaceStateHashWithObject"];
  [self assertStatusText:@"replaceStateHashWithObject"
         withOmniboxText:replaceStateHashWithObjectOmniboxText];

  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
                                          initialOmniboxText)]
      assertWithMatcher:grey_notNil()];

  [[EarlGrey selectElementWithMatcher:ForwardButton()]
      performAction:grey_tap()];
  // TODO(crbug.com/41350773): WKWebView doesn't fire load event for
  // back/forward navigation.
  [self assertStatusText:@"replaceStateHashWithObject"
         withOmniboxText:replaceStateHashWithObjectOmniboxText];

  // Push URL then replace it. Do this twice.
  const GURL pushStateHashStringURL =
      web::test::HttpServer::MakeUrl(kPushStateHashStringURL);
  const std::string pushStateHashStringOmniboxText =
      net::GetContentAndFragmentForUrl(pushStateHashStringURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateHashString"];
  [self assertStatusText:@"pushStateHashString"
         withOmniboxText:pushStateHashStringOmniboxText];

  const GURL replaceStateHashStringURL =
      web::test::HttpServer::MakeUrl(kReplaceStateHashStringURL);
  const std::string replaceStateHashStringOmniboxText =
      net::GetContentAndFragmentForUrl(replaceStateHashStringURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"replaceStateHashString"];
  [self assertStatusText:@"replaceStateHashString"
         withOmniboxText:replaceStateHashStringOmniboxText];

  const GURL pushStatePathURL =
      web::test::HttpServer::MakeUrl(kPushStatePathURL);
  const std::string pushStatePathOmniboxText =
      net::GetContentAndFragmentForUrl(pushStatePathURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStatePath"];
  [self assertStatusText:@"pushStatePath"
         withOmniboxText:pushStatePathOmniboxText];

  const GURL replaceStateRootPathSpaceURL =
      web::test::HttpServer::MakeUrl(kReplaceStateRootPathSpaceURL);
  const std::string replaceStateRootPathSpaceOmniboxText =
      net::GetContentAndFragmentForUrl(replaceStateRootPathSpaceURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"replaceStateRootPathSpace"];
  [self assertStatusText:@"replaceStateRootPathSpace"
         withOmniboxText:replaceStateRootPathSpaceOmniboxText];

  // Go back and check URLs.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:@"replaceStateHashString"
         withOmniboxText:replaceStateHashStringOmniboxText];
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:@"replaceStateHashWithObject"
         withOmniboxText:replaceStateHashWithObjectOmniboxText];

  // Go forward and check URL.
  [ChromeEarlGrey tapWebStateElementWithID:@"goForward2"];
  [self assertStatusText:@"replaceStateRootPathSpace"
         withOmniboxText:replaceStateRootPathSpaceOmniboxText];
}

// Tests calling history.replaceState(), then history.pushState() and then
// navigating back/forward.
- (void)testHtml5HistoryReplaceStatePushStateThenGoBackAndForward {
  const GURL firstReplaceStateURL = web::test::HttpServer::MakeUrl(
      "http://ios/testing/data/http_server_files/history.html"
      "#firstReplaceState");
  const std::string firstReplaceStateOmniboxText =
      net::GetContentAndFragmentForUrl(firstReplaceStateURL);
  const GURL replaceStateThenPushStateURL = web::test::HttpServer::MakeUrl(
      "http://ios/testing/data/http_server_files/history.html"
      "#replaceStateThenPushState");
  const std::string replaceStateThenPushStateOmniboxText =
      net::GetContentAndFragmentForUrl(replaceStateThenPushStateURL);

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl(kHistoryTestUrl)];

  // Replace state and then push state. Verify that at the end, the URL changed
  // to the pushed URL and the status was updated.
  [ChromeEarlGrey tapWebStateElementWithID:@"replaceStateThenPushState"];
  [self assertStatusText:@"replaceStateThenPushState"
         withOmniboxText:replaceStateThenPushStateOmniboxText];

  // Go back and check URL.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:@"firstReplaceState"
         withOmniboxText:firstReplaceStateOmniboxText];

  // Go forward and check URL.
  [[EarlGrey selectElementWithMatcher:ForwardButton()]
      performAction:grey_tap()];
  [self assertStatusText:@"replaceStateThenPushState"
         withOmniboxText:replaceStateThenPushStateOmniboxText];
}

// Tests calling history.pushState(), then history.replaceState() and then
// navigating back/forward.
- (void)testHtml5HistoryPushStateReplaceStateThenGoBackAndForward {
  const GURL firstPushStateURL = web::test::HttpServer::MakeUrl(
      "http://ios/testing/data/http_server_files/history.html#firstPushState");
  const std::string firstPushStateOmniboxText =
      net::GetContentAndFragmentForUrl(firstPushStateURL);
  const GURL pushStateThenReplaceStateURL = web::test::HttpServer::MakeUrl(
      "http://ios/testing/data/http_server_files/history.html"
      "#pushStateThenReplaceState");
  const std::string pushStateThenReplaceStateOmniboxText =
      net::GetContentAndFragmentForUrl(pushStateThenReplaceStateURL);

  const GURL historyTestURL = web::test::HttpServer::MakeUrl(kHistoryTestUrl);
  [ChromeEarlGrey loadURL:historyTestURL];
  const std::string historyTestOmniboxText =
      net::GetContentAndFragmentForUrl(historyTestURL);

  // Push state and then replace state. Verify that at the end, the URL changed
  // to the replaceState URL and the status was updated.
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateThenReplaceState"];
  [self assertStatusText:@"pushStateThenReplaceState"
         withOmniboxText:pushStateThenReplaceStateOmniboxText];

  // Go back and check URL.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:nil withOmniboxText:historyTestOmniboxText];

  // Go forward and check URL.
  [[EarlGrey selectElementWithMatcher:ForwardButton()]
      performAction:grey_tap()];
  [self assertStatusText:@"pushStateThenReplaceState"
         withOmniboxText:pushStateThenReplaceStateOmniboxText];
}

// Tests that page loads occur when navigating to or past a non-pushed URL.
- (void)testHtml5HistoryNavigatingPastNonPushedURL {
  GURL nonPushedURL = web::test::HttpServer::MakeUrl(kNonPushedUrl);
  const GURL historyTestURL = web::test::HttpServer::MakeUrl(kHistoryTestUrl);
  [ChromeEarlGrey loadURL:historyTestURL];
  const std::string historyTestOmniboxText =
      net::GetContentAndFragmentForUrl(historyTestURL);

  // Push same URL twice. Verify that URL changed and the status was updated.
  const GURL pushStateHashStringURL =
      web::test::HttpServer::MakeUrl(kPushStateHashStringURL);
  const std::string pushStateHashStringOmniboxText =
      net::GetContentAndFragmentForUrl(pushStateHashStringURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateHashString"];
  [self assertStatusText:@"pushStateHashString"
         withOmniboxText:pushStateHashStringOmniboxText];
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateHashString"];
  [self assertStatusText:@"pushStateHashString"
         withOmniboxText:pushStateHashStringOmniboxText];

  // Load a non-pushed URL.
  [ChromeEarlGrey loadURL:nonPushedURL];

  // Load history.html and push another URL.
  [ChromeEarlGrey loadURL:historyTestURL];
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateHashString"];
  [self assertStatusText:@"pushStateHashString"
         withOmniboxText:pushStateHashStringOmniboxText];

  // At this point the history looks like this:
  // [NTP, history.html, #string, #string, nonPushedURL, history.html, #string]

  // Go back (to second history.html) and verify page did not load.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:nil withOmniboxText:historyTestOmniboxText];

  // Go back twice (to second #string) and verify page did load.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  // TODO(crbug.com/41350773): WKWebView doesn't fire load event for
  // back/forward navigation.
  [self assertStatusText:nil withOmniboxText:pushStateHashStringOmniboxText];

  // Go back once (to first #string) and verify page did not load.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:@"pushStateHashString"
         withOmniboxText:pushStateHashStringOmniboxText];

  // Go forward 4 entries at once (to third #string) and verify page did load.
  [ChromeEarlGrey tapWebStateElementWithID:@"goForward4"];

  [self assertStatusText:nil withOmniboxText:pushStateHashStringOmniboxText];

  // Go back 4 entries at once (to first #string) and verify page did load.
  [ChromeEarlGrey tapWebStateElementWithID:@"goBack4"];

  [self assertStatusText:nil withOmniboxText:pushStateHashStringOmniboxText];
}

// Tests calling pushState with unicode characters.
- (void)testHtml5HistoryPushUnicodeCharacters {
  // The GURL object %-escapes Unicode characters in the URL's fragment,
  // but the omnibox decodes them back to Unicode for display.
  std::string pushStateUnicode =
      net::GetContentAndFragmentForUrl(web::test::HttpServer::MakeUrl(
          "http://ios/testing/data/http_server_files/"
          "history.html#unicode")) +
      "\xe1\x84\x91";
  std::string pushStateUnicode2 =
      net::GetContentAndFragmentForUrl(web::test::HttpServer::MakeUrl(
          "http://ios/testing/data/http_server_files/"
          "history.html#unicode2")) +
      "\xe2\x88\xa2";
  const char pushStateUnicodeLabel[] = "Action: pushStateUnicodeᄑ";
  NSString* pushStateUnicodeStatus = @"pushStateUnicodeᄑ";
  const char pushStateUnicode2Label[] = "Action: pushStateUnicode2∢";
  NSString* pushStateUnicode2Status = @"pushStateUnicode2∢";

  [ChromeEarlGrey loadURL:web::test::HttpServer::MakeUrl(kHistoryTestUrl)];

  // Do 2 push states with unicode characters.
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateUnicode"];
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::OmniboxText(pushStateUnicode)]
      assertWithMatcher:grey_notNil()];
  [ChromeEarlGrey waitForWebStateContainingText:pushStateUnicodeLabel];

  [ChromeEarlGrey tapWebStateElementWithID:@"pushStateUnicode2"];
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::OmniboxText(pushStateUnicode2)]
      assertWithMatcher:grey_notNil()];
  [ChromeEarlGrey waitForWebStateContainingText:pushStateUnicode2Label];

  // Do a push state without a unicode character.
  const GURL pushStatePathURL =
      web::test::HttpServer::MakeUrl(kPushStatePathURL);
  const std::string pushStatePathOmniboxText =
      net::GetContentAndFragmentForUrl(pushStatePathURL);
  [ChromeEarlGrey tapWebStateElementWithID:@"pushStatePath"];

  [self assertStatusText:@"pushStatePath"
         withOmniboxText:pushStatePathOmniboxText];

  // Go back and check the unicode in the URL and status.
  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:pushStateUnicode2Status
         withOmniboxText:pushStateUnicode2];

  [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()];
  [self assertStatusText:pushStateUnicodeStatus
         withOmniboxText:pushStateUnicode];
}

// Tests that pushState/replaceState handling properly handles <base>.
- (void)testHtml5HistoryWithBase {
  std::map<GURL, std::string> responses;
  GURL originURL =
      web::test::HttpServer::MakeUrl("http://foo.com/foo/bar.html");
  GURL pushResultURL =
      originURL.DeprecatedGetOriginAsURL().Resolve("pushed/relative/url");
  const std::string pushResultOmniboxText =
      net::GetContentAndFragmentForUrl(pushResultURL);
  GURL replaceResultURL =
      originURL.DeprecatedGetOriginAsURL().Resolve("replaced/relative/url");
  const std::string replaceResultOmniboxText =
      net::GetContentAndFragmentForUrl(replaceResultURL);

  // A simple HTML page with a base tag that makes all relative URLs
  // domain-relative, a button to trigger a relative pushState, and a button
  // to trigger a relative replaceState.
  NSString* baseTag = @"<base href=\"/\">";
  NSString* pushAndReplaceButtons =
      @"<input type=\"button\" value=\"pushState\" "
       "id=\"pushState\" onclick=\"history.pushState("
       "{}, 'Foo', './pushed/relative/url');\"><br>"
       "<input type=\"button\" value=\"replaceState\" "
       "id=\"replaceState\" onclick=\"history.replaceState("
       "{}, 'Foo', './replaced/relative/url');\"><br>";
  NSString* simplePage =
      @"<!doctype html><html><head>%@</head><body>%@</body></html>";
  responses[originURL] = base::SysNSStringToUTF8(
      [NSString stringWithFormat:simplePage, baseTag, pushAndReplaceButtons]);
  web::test::SetUpSimpleHttpServer(responses);

  [ChromeEarlGrey loadURL:originURL];
  [ChromeEarlGrey tapWebStateElementWithID:@"pushState"];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
                                          pushResultOmniboxText)]
      assertWithMatcher:grey_notNil()];

  [ChromeEarlGrey tapWebStateElementWithID:@"replaceState"];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
                                          replaceResultOmniboxText)]
      assertWithMatcher:grey_notNil()];
}

#pragma mark - Utility methods

// Assert that status text `status`, if non-nil, is displayed in the webview,
// that the omnibox text is as expected, and that "onload" text is not
// displayed.
- (void)assertStatusText:(NSString*)status
         withOmniboxText:(const std::string&)omniboxText {
  [ChromeEarlGrey waitForWebStateNotContainingText:"onload"];

  if (status != nil) {
    NSString* statusLabel = [NSString stringWithFormat:@"Action: %@", status];
    [ChromeEarlGrey
        waitForWebStateContainingText:base::SysNSStringToUTF8(statusLabel)];
  }

  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::OmniboxText(omniboxText)]
      assertWithMatcher:grey_notNil()];
}

@end