// 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 <XCTest/XCTest.h>
#import <map>
#import <memory>
#import <string>
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/shared/public/features/features.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/scoped_block_popups_pref.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/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 "net/http/http_response_headers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
using chrome_test_util::OmniboxText;
using chrome_test_util::OmniboxContainingText;
namespace {
// URL used for the reload test.
const char kReloadTestUrl[] = "http://mock/reloadTest";
// Returns the number of serviced requests in HTTP body.
class ReloadResponseProvider : public web::DataResponseProvider {
public:
ReloadResponseProvider() : request_number_(0) {}
// URL used for the reload test.
static GURL GetReloadTestUrl() {
return web::test::HttpServer::MakeUrl(kReloadTestUrl);
}
bool CanHandleRequest(const Request& request) override {
return request.url == ReloadResponseProvider::GetReloadTestUrl();
}
void GetResponseHeadersAndBody(
const Request& request,
scoped_refptr<net::HttpResponseHeaders>* headers,
std::string* response_body) override {
DCHECK_EQ(ReloadResponseProvider::GetReloadTestUrl(), request.url);
*headers = GetDefaultResponseHeaders();
*response_body = GetResponseBody(request_number_++);
}
// static
static std::string GetResponseBody(int request_number) {
return base::StringPrintf("Load request %d", request_number);
}
private:
int request_number_; // Count of requests received by the response provider.
};
} // namespace
// Tests web browsing scenarios.
@interface BrowsingTestCase : WebHttpServerChromeTestCase
@end
@implementation BrowsingTestCase
- (AppLaunchConfiguration)appConfigurationForTestCase {
AppLaunchConfiguration config;
config.features_enabled.push_back(kModernTabStrip);
return config;
}
// Matcher for the title of the current tab (on tablet only), which is
// sufficiently visible.
id<GREYMatcher> TabWithTitle(const std::string& tab_title) {
return grey_allOf(
grey_accessibilityLabel(base::SysUTF8ToNSString(tab_title)),
grey_ancestor(grey_kindOfClassName(@"TabStripTabCell")),
grey_not(grey_accessibilityTrait(UIAccessibilityTraitStaticText)),
grey_sufficientlyVisible(), nil);
}
// Tests that page successfully reloads.
- (void)testReload {
// Set up test HTTP server responses.
std::unique_ptr<web::DataResponseProvider> provider(
new ReloadResponseProvider());
web::test::SetUpHttpServer(std::move(provider));
GURL URL = ReloadResponseProvider::GetReloadTestUrl();
[ChromeEarlGrey loadURL:URL];
std::string expectedBodyBeforeReload(
ReloadResponseProvider::GetResponseBody(0 /* request number */));
[ChromeEarlGrey waitForWebStateContainingText:expectedBodyBeforeReload];
[ChromeEarlGreyUI reload];
std::string expectedBodyAfterReload(
ReloadResponseProvider::GetResponseBody(1 /* request_number */));
[ChromeEarlGrey waitForWebStateContainingText:expectedBodyAfterReload];
}
// Tests that a tab's title is based on the URL when no other information is
// available.
- (void)testBrowsingTabTitleSetFromURL {
if (![ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(@"Tab Title not displayed on handset.");
}
GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
const GURL destinationURL = self.testServer->GetURL("/destination.html");
[ChromeEarlGrey loadURL:destinationURL];
// Add 3 for the "://" which is not considered part of the scheme
std::string URLWithoutScheme =
destinationURL.spec().substr(destinationURL.scheme().length() + 3);
[[EarlGrey selectElementWithMatcher:TabWithTitle(URLWithoutScheme)]
assertWithMatcher:grey_notNil()];
}
// Tests that after a PDF is loaded, the title appears in the tab bar on iPad.
- (void)testPDFLoadTitle {
if (![ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(@"Tab Title not displayed on handset.");
}
GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
const GURL destinationURL = self.testServer->GetURL("/testpage.pdf");
[ChromeEarlGrey loadURL:destinationURL];
// Add 3 for the "://" which is not considered part of the scheme
std::string URLWithoutScheme =
destinationURL.spec().substr(destinationURL.scheme().length() + 3);
[[EarlGrey selectElementWithMatcher:TabWithTitle(URLWithoutScheme)]
assertWithMatcher:grey_notNil()];
}
// Tests that tab title is set to the specified title from a JavaScript.
- (void)testBrowsingTabTitleSetFromScript {
if (![ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(@"Tab Title not displayed on handset.");
}
const char* kPageTitle = "Some title";
const GURL URL = GURL(base::StringPrintf(
"data:text/html;charset=utf-8,<script>document.title = "
"\"%s\"</script>",
kPageTitle));
[ChromeEarlGrey loadURL:URL];
[[EarlGrey selectElementWithMatcher:TabWithTitle(kPageTitle)]
assertWithMatcher:grey_notNil()];
}
// Tests that clicking a link with URL changed by onclick uses the href of the
// anchor tag instead of the one specified in JavaScript. Also verifies a new
// tab is opened by target '_blank'.
// TODO(crbug.com/41299306): WKWebView does not open a new window as expected by
// this test.
- (void)DISABLED_testBrowsingPreventDefaultWithLinkOpenedByJavascript {
// Create map of canned responses and set up the test HTML server.
std::map<GURL, std::string> responses;
const GURL URL = web::test::HttpServer::MakeUrl(
"http://preventDefaultWithLinkOpenedByJavascript");
const GURL anchorURL =
web::test::HttpServer::MakeUrl("http://anchorDestination");
const GURL destinationURL =
web::test::HttpServer::MakeUrl("http://javaScriptDestination");
// This is a page with a link where the href and JavaScript are setting the
// destination to two different URLs so the test can verify which one the
// browser uses.
responses[URL] = base::StringPrintf(
"<a id='link' href='%s' target='_blank' "
"onclick='window.location.href=\"%s\"; "
"event.stopPropagation()' id='link'>link</a>",
anchorURL.spec().c_str(), destinationURL.spec().c_str());
responses[anchorURL] = "anchor destination";
web::test::SetUpSimpleHttpServer(responses);
ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_ALLOW);
[ChromeEarlGrey loadURL:URL];
[ChromeEarlGrey waitForMainTabCount:1];
[ChromeEarlGrey tapWebStateElementWithID:@"link"];
[ChromeEarlGrey waitForMainTabCount:2];
// Verify the new tab was opened with the expected URL.
[[EarlGrey selectElementWithMatcher:OmniboxText(anchorURL.GetContent())]
assertWithMatcher:grey_notNil()];
}
// Tests tapping a link that navigates to a page that immediately navigates
// again via document.location.href.
// TODO(crbug.com/40234734): Flaky on iPhone.
- (void)DISABLED_testBrowsingWindowDataLinkScriptRedirect {
// Create map of canned responses and set up the test HTML server.
std::map<GURL, std::string> responses;
const GURL URL =
web::test::HttpServer::MakeUrl("http://windowDataLinkScriptRedirect");
const GURL intermediateURL =
web::test::HttpServer::MakeUrl("http://intermediate");
const GURL destinationURL =
web::test::HttpServer::MakeUrl("http://destination");
// This is a page with a link to the intermediate page.
responses[URL] =
base::StringPrintf("<a id='link' href='%s' target='_blank'>link</a>",
intermediateURL.spec().c_str());
// This intermediate page uses JavaScript to immediately navigate to the
// destination page.
responses[intermediateURL] =
base::StringPrintf("<script>document.location.href=\"%s\"</script>",
destinationURL.spec().c_str());
// This is the page that should be showing at the end of the test.
responses[destinationURL] = "You've arrived!";
web::test::SetUpSimpleHttpServer(responses);
ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_ALLOW);
[ChromeEarlGrey loadURL:URL];
[ChromeEarlGrey waitForMainTabCount:1];
[ChromeEarlGrey tapWebStateElementWithID:@"link"];
[ChromeEarlGrey waitForMainTabCount:2];
// Verify the new tab was opened with the expected URL.
[[EarlGrey selectElementWithMatcher:OmniboxText(destinationURL.GetContent())]
assertWithMatcher:grey_notNil()];
}
// Tests that a link with a JavaScript-based navigation changes the page and
// that the back button works as expected afterwards.
- (void)testBrowsingJavaScriptBasedNavigation {
std::map<GURL, std::string> responses;
const GURL URL = web::test::HttpServer::MakeUrl("http://origin");
const GURL destURL = web::test::HttpServer::MakeUrl("http://destination");
// Page containing a link with onclick attribute that sets window.location
// to the destination URL.
responses[URL] = base::StringPrintf(
"<a href='#' onclick=\"window.location='%s';\" id='link'>Link</a>",
destURL.spec().c_str());
// Page with some text.
responses[destURL] = "You've arrived!";
web::test::SetUpSimpleHttpServer(responses);
[ChromeEarlGrey loadURL:URL];
[ChromeEarlGrey tapWebStateElementWithID:@"link"];
[[EarlGrey selectElementWithMatcher:OmniboxText(destURL.GetContent())]
assertWithMatcher:grey_notNil()];
[ChromeEarlGrey goBack];
[ChromeEarlGrey waitForWebStateContainingText:"Link"];
// Using partial match for Omnibox text because the displayed URL is now
// "http://origin/#" due to the link click. This is consistent with all
// other browsers.
[[EarlGrey selectElementWithMatcher:chrome_test_util::Omnibox()]
assertWithMatcher:chrome_test_util::OmniboxContainingText(
URL.GetContent())];
GREYAssertEqual(web::test::HttpServer::MakeUrl("http://origin/#"),
[ChromeEarlGrey webStateVisibleURL],
@"Unexpected URL after going back");
}
// Tests that a link with WebUI URL does not trigger a load. WebUI pages may
// have increased power and using the same web process (which may potentially
// be controlled by an attacker) is dangerous.
- (void)testTapLinkWithWebUIURL {
// Create map of canned responses and set up the test HTML server.
std::map<GURL, std::string> responses;
const GURL URL(web::test::HttpServer::MakeUrl("http://pageWithWebUILink"));
const char kPageHTML[] =
"<script>"
" function printMsg() {"
" document.body.appendChild(document.createTextNode('Hello world!'));"
" }"
"</script>"
"<a href='chrome://version' id='link' onclick='printMsg()'>Version</a>";
responses[URL] = kPageHTML;
web::test::SetUpSimpleHttpServer(responses);
// Assert that test is starting with one tab.
[ChromeEarlGrey waitForMainTabCount:1];
[ChromeEarlGrey waitForIncognitoTabCount:0];
[ChromeEarlGrey loadURL:URL];
// Tap on chrome://version link.
[ChromeEarlGrey tapWebStateElementWithID:@"link"];
// Verify that page did not change by checking its URL and message printed by
// onclick event.
[[EarlGrey selectElementWithMatcher:OmniboxText("chrome://version")]
assertWithMatcher:grey_nil()];
[ChromeEarlGrey waitForWebStateContainingText:"Hello world!"];
// Verify that no new tabs were open which could load chrome://version.
[ChromeEarlGrey waitForMainTabCount:1];
}
// Tests that evaluating user JavaScript that causes navigation correctly
// modifies history.
// TODO(crbug.com/362621166): Test is flaky.
- (void)DISABLED_testBrowsingUserJavaScriptNavigation {
// TODO(crbug.com/40511873): Keyboard entry inside the omnibox fails only on
// iPad.
if ([ChromeEarlGrey isIPadIdiom])
return;
// Create map of canned responses and set up the test HTML server.
std::map<GURL, std::string> responses;
const GURL startURL = web::test::HttpServer::MakeUrl("http://startpage");
responses[startURL] = "<html><body><p>Ready to begin.</p></body></html>";
const GURL targetURL = web::test::HttpServer::MakeUrl("http://targetpage");
responses[targetURL] = "<html><body><p>You've arrived!</p></body></html>";
web::test::SetUpSimpleHttpServer(responses);
// Load the first page and run JS (using the codepath that user-entered JS in
// the omnibox would take, not page-triggered) that should navigate.
[ChromeEarlGrey loadURL:startURL];
NSString* script =
[NSString stringWithFormat:@"javascript:window.location='%s'",
targetURL.spec().c_str()];
[ChromeEarlGreyUI focusOmniboxAndReplaceText:script];
if (@available(iOS 16, *)) {
// TODO(crbug.com/40227513): Move this logic into EG.
XCUIApplication* app = [[XCUIApplication alloc] init];
[[[app keyboards] buttons][@"go"] tap];
} else {
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"Go")]
performAction:grey_tap()];
}
[ChromeEarlGrey waitForPageToFinishLoading];
[[EarlGrey selectElementWithMatcher:OmniboxText(targetURL.GetContent())]
assertWithMatcher:grey_notNil()];
[ChromeEarlGrey goBack];
[[EarlGrey selectElementWithMatcher:OmniboxText(startURL.GetContent())]
assertWithMatcher:grey_notNil()];
}
// Tests that evaluating non-navigation user JavaScript doesn't affect history.
// TODO(crbug.com/362621166): Test is flaky.
- (void)DISABLED_testBrowsingUserJavaScriptWithoutNavigation {
// TODO(crbug.com/40511873): Keyboard entry inside the omnibox fails only on
// iPad.
if ([ChromeEarlGrey isIPadIdiom])
return;
// Create map of canned responses and set up the test HTML server.
std::map<GURL, std::string> responses;
const GURL firstURL = web::test::HttpServer::MakeUrl("http://firstURL");
const std::string firstResponse = "Test Page 1";
const GURL secondURL = web::test::HttpServer::MakeUrl("http://secondURL");
const std::string secondResponse = "Test Page 2";
responses[firstURL] = firstResponse;
responses[secondURL] = secondResponse;
web::test::SetUpSimpleHttpServer(responses);
[ChromeEarlGrey loadURL:firstURL];
[ChromeEarlGrey loadURL:secondURL];
// Execute some JavaScript in the omnibox.
[ChromeEarlGreyUI
focusOmniboxAndReplaceText:@"javascript:document.write('foo')"];
// TODO(crbug.com/40916974): Use simulatePhysicalKeyboardEvent until
// replaceText can properly handle \n.
[ChromeEarlGrey simulatePhysicalKeyboardEvent:@"\n" flags:0];
[ChromeEarlGrey waitForWebStateContainingText:"foo"];
// Verify that the JavaScript did not affect history by going back and then
// forward again.
[ChromeEarlGrey goBack];
[[EarlGrey selectElementWithMatcher:OmniboxText(firstURL.GetContent())]
assertWithMatcher:grey_notNil()];
[ChromeEarlGrey goForward];
[[EarlGrey selectElementWithMatcher:OmniboxText(secondURL.GetContent())]
assertWithMatcher:grey_notNil()];
}
@end