| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <objc/runtime.h> |
| |
| #import "base/bind.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "base/test/ios/wait_util.h" |
| #import "ios/chrome/browser/ui/start_surface/start_surface_features.h" |
| #import "ios/chrome/browser/ui/ui_feature_flags.h" |
| #import "ios/chrome/browser/web/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/chrome_test_case.h" |
| #import "ios/net/url_test_util.h" |
| #import "ios/testing/earl_grey/app_launch_manager.h" |
| #import "ios/testing/earl_grey/earl_grey_test.h" |
| #import "ios/web/common/features.h" |
| #import "net/test/embedded_test_server/default_handlers.h" |
| #import "net/test/embedded_test_server/http_request.h" |
| #import "net/test/embedded_test_server/http_response.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using chrome_test_util::OmniboxText; |
| using chrome_test_util::NTPCollectionView; |
| using chrome_test_util::BackButton; |
| using chrome_test_util::ForwardButton; |
| |
| namespace { |
| |
| // Path to two test pages, page1 and page2 with associated contents and titles. |
| const char kPageOnePath[] = "/page1.html"; |
| const char kPageOneContent[] = "This is the first page."; |
| const char kPageOneTitle[] = "The first page title."; |
| const char kPageTwoPath[] = "/page2.html"; |
| const char kPageTwoContent[] = "This is the second page."; |
| const char kPageTwoTitle[] = "The second page title."; |
| |
| // Path to a test page used to count each page load. |
| const char kCountURL[] = "/countme.html"; |
| |
| // Response handler for page1 and page2 that supports 'airplane mode' by |
| // returning an empty RawHttpResponse when `responds_with_content` us false. |
| std::unique_ptr<net::test_server::HttpResponse> RestoreResponse( |
| const bool& responds_with_content, |
| const net::test_server::HttpRequest& request) { |
| if (!responds_with_content) { |
| return std::make_unique<net::test_server::RawHttpResponse>( |
| /*headers=*/"", /*contents=*/""); |
| } |
| std::unique_ptr<net::test_server::BasicHttpResponse> http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| std::string title; |
| std::string body; |
| if (request.relative_url == kPageOnePath) { |
| title = kPageOneTitle; |
| body = kPageOneContent; |
| } else if (request.relative_url == kPageTwoPath) { |
| title = kPageTwoTitle; |
| body = kPageTwoContent; |
| } else { |
| return nullptr; |
| } |
| http_response->set_content("<html><head><title>" + title + |
| "</title></head>" |
| "<body>" + |
| body + "</body></html>"); |
| return std::move(http_response); |
| } |
| |
| // Response handler for `kCountURL` that counts the number of page loads. |
| std::unique_ptr<net::test_server::HttpResponse> CountResponse( |
| int* counter, |
| const net::test_server::HttpRequest& request) { |
| if (request.relative_url != kCountURL) { |
| return nullptr; |
| } |
| std::unique_ptr<net::test_server::BasicHttpResponse> http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| http_response->set_content("<html><head><title>Hello World</title></head>" |
| "<body>Hello World!</body></html>"); |
| (*counter)++; |
| return std::move(http_response); |
| } |
| |
| // Returns true when omnibox contains `text`, otherwise returns false after |
| // after a timeout. |
| [[nodiscard]] bool WaitForOmniboxContaining(std::string text) { |
| return base::test::ios::WaitUntilConditionOrTimeout( |
| base::test::ios::kWaitForUIElementTimeout, ^bool { |
| NSError* error = nil; |
| [[EarlGrey selectElementWithMatcher:OmniboxText(text)] |
| assertWithMatcher:grey_notNil() |
| error:&error]; |
| return error == nil; |
| }); |
| } |
| } |
| |
| // Integration tests for restoring session history. |
| @interface RestoreWithCacheTestCase : ChromeTestCase { |
| // Use a second test server to ensure different origin navigations. |
| std::unique_ptr<net::EmbeddedTestServer> _secondTestServer; |
| } |
| |
| // The secondary EmbeddedTestServer instance. |
| @property(nonatomic, readonly) |
| net::test_server::EmbeddedTestServer* secondTestServer; |
| |
| @property(atomic) bool serverRespondsWithContent; |
| |
| // Start the primary and secondary test server. Separate servers are used to |
| // force cross domain tests (via different ports). |
| - (void)setUpRestoreServers; |
| |
| // Trigger a session history restore. In EG1 this is possible via the TabGrid |
| // CloseAll-Undo-Done method. In EG2, this is possible via |
| // Background-Terminate-Activate |
| - (void)triggerRestore; |
| |
| // Navigate to a set of sites include cross-domains, chrome URLs, error pages |
| // and the NTP. |
| - (void)loadTestPages; |
| |
| // Verify that each page visited in -loadTestPages is properly restored by |
| // navigating to each page and triggering a restore, confirming that pages are |
| // reloaded and back-forward history is preserved. If `checkServerData` is YES, |
| // also check that the proper content is restored. |
| - (void)verifyRestoredTestPages:(BOOL)checkServerData; |
| |
| @end |
| |
| @implementation RestoreWithCacheTestCase |
| |
| - (AppLaunchConfiguration)appConfigurationForTestCase { |
| AppLaunchConfiguration config = [super appConfigurationForTestCase]; |
| return config; |
| } |
| |
| - (net::EmbeddedTestServer*)secondTestServer { |
| if (!_secondTestServer) { |
| _secondTestServer = std::make_unique<net::EmbeddedTestServer>(); |
| NSString* bundlePath = [NSBundle bundleForClass:[self class]].resourcePath; |
| _secondTestServer->ServeFilesFromDirectory( |
| base::FilePath(base::SysNSStringToUTF8(bundlePath)) |
| .AppendASCII("ios/testing/data/http_server_files/")); |
| net::test_server::RegisterDefaultHandlers(_secondTestServer.get()); |
| } |
| return _secondTestServer.get(); |
| } |
| |
| // Navigates to a set of cross-domains, chrome URLs and error pages, and then |
| // tests that they are properly restored. |
| - (void)testRestoreHistory { |
| [self setUpRestoreServers]; |
| [self loadTestPages]; |
| [self verifyRestoredTestPages:YES]; |
| } |
| |
| // Navigates to a set of cross-domains, chrome URLs and error pages, and then |
| // tests that they are properly restored in airplane mode. |
| - (void)testRestoreNoNetwork { |
| [self setUpRestoreServers]; |
| [self loadTestPages]; |
| self.serverRespondsWithContent = false; |
| [self verifyRestoredTestPages:NO]; |
| } |
| |
| // Tests that only the selected web state is loaded on a session restore. |
| - (void)testRestoreOneWebstateOnly { |
| // Visit the background page. |
| int visitCounter = 0; |
| self.testServer->RegisterRequestHandler( |
| base::BindRepeating(&CountResponse, &visitCounter)); |
| GREYAssertTrue(self.testServer->Start(), @"Test server failed to start."); |
| const GURL countPage = self.testServer->GetURL(kCountURL); |
| [ChromeEarlGrey loadURL:countPage]; |
| GREYAssertEqual(1, visitCounter, @"The page should have been loaded once"); |
| |
| // Visit the forground page. |
| [ChromeEarlGrey openNewTab]; |
| const GURL echoPage = self.testServer->GetURL("/echo"); |
| [ChromeEarlGrey loadURL:echoPage]; |
| |
| // Trigger a restore and confirm the background page is not reloaded. |
| [self triggerRestore]; |
| [[EarlGrey selectElementWithMatcher:OmniboxText(echoPage.GetContent())] |
| assertWithMatcher:grey_notNil()]; |
| [ChromeEarlGrey waitForWebStateContainingText:"Echo"]; |
| GREYAssertEqual(1, visitCounter, @"The page should not reload"); |
| } |
| |
| // Tests that only the selected web state is loaded Restore-after-Crash. This |
| // is only possible in EG2. |
| - (void)testRestoreOneWebstateOnlyAfterCrash { |
| // Visit the background page. |
| int visitCounter = 0; |
| self.testServer->RegisterRequestHandler( |
| base::BindRepeating(&CountResponse, &visitCounter)); |
| GREYAssertTrue(self.testServer->Start(), @"Test server failed to start."); |
| const GURL countPage = self.testServer->GetURL(kCountURL); |
| [ChromeEarlGrey loadURL:countPage]; |
| GREYAssertEqual(1, visitCounter, @"The page should have been loaded once"); |
| |
| // Visit the foreground page. |
| [ChromeEarlGrey openNewTab]; |
| const GURL echoPage = self.testServer->GetURL("/echo"); |
| [ChromeEarlGrey loadURL:echoPage]; |
| [ChromeEarlGrey waitForWebStateContainingText:"Echo"]; |
| |
| // Clear cache, save the session and trigger a crash/activate. |
| // Test with the Crash Infobar. |
| [ChromeEarlGrey removeBrowsingCache]; |
| [[AppLaunchManager sharedManager] |
| ensureAppLaunchedWithFeaturesEnabled:{} |
| disabled:{kRemoveCrashInfobar} |
| relaunchPolicy:ForceRelaunchByKilling]; |
| // Restore after crash and confirm the background page is not reloaded. |
| [[EarlGrey selectElementWithMatcher:grey_text(@"Restore")] |
| performAction:grey_tap()]; |
| [[EarlGrey selectElementWithMatcher:OmniboxText(echoPage.GetContent())] |
| assertWithMatcher:grey_notNil()]; |
| [ChromeEarlGrey waitForWebStateContainingText:"Echo"]; |
| GREYAssertEqual(1, visitCounter, @"The page should not reload"); |
| |
| // Clear cache, save the session and trigger a crash/activate. |
| // Test without the Crash Infobar. |
| [ChromeEarlGrey removeBrowsingCache]; |
| [[AppLaunchManager sharedManager] |
| ensureAppLaunchedWithFeaturesEnabled:{kRemoveCrashInfobar} |
| disabled:{} |
| relaunchPolicy:ForceRelaunchByKilling]; |
| // Restore after crash and confirm the background page is not reloaded. |
| [[EarlGrey selectElementWithMatcher:OmniboxText(echoPage.GetContent())] |
| assertWithMatcher:grey_notNil()]; |
| [ChromeEarlGrey waitForWebStateContainingText:"Echo"]; |
| GREYAssertEqual(1, visitCounter, @"The page should not reload"); |
| } |
| |
| #pragma mark Utility methods |
| |
| - (void)setUpRestoreServers { |
| self.testServer->RegisterRequestHandler(base::BindRepeating( |
| &RestoreResponse, std::cref(_serverRespondsWithContent))); |
| self.secondTestServer->RegisterRequestHandler(base::BindRepeating( |
| &RestoreResponse, std::cref(_serverRespondsWithContent))); |
| self.serverRespondsWithContent = true; |
| GREYAssertTrue(self.testServer->Start(), @"Test server failed to start."); |
| GREYAssertTrue(self.secondTestServer->Start(), |
| @"Second test server failed to start."); |
| } |
| |
| - (void)triggerRestore { |
| [[AppLaunchManager sharedManager] |
| ensureAppLaunchedWithFeaturesEnabled:{} |
| disabled:{kStartSurface} |
| relaunchPolicy:ForceRelaunchByCleanShutdown]; |
| } |
| |
| - (void)loadTestPages { |
| // Load page1. |
| const GURL pageOne = self.testServer->GetURL(kPageOnePath); |
| [ChromeEarlGrey loadURL:pageOne]; |
| [ChromeEarlGrey waitForWebStateContainingText:kPageOneContent]; |
| |
| // Load chrome url |
| const GURL chromePage = GURL("chrome://chrome-urls"); |
| [ChromeEarlGrey loadURL:chromePage]; |
| |
| // Load error page. |
| const GURL errorPage = GURL("http://invalid."); |
| [ChromeEarlGrey loadURL:errorPage]; |
| [ChromeEarlGrey waitForWebStateContainingText:"ERR_"]; |
| [ChromeEarlGreyUI waitForAppToIdle]; |
| |
| // Load page2. |
| const GURL pageTwo = self.secondTestServer->GetURL(kPageTwoPath); |
| [ChromeEarlGrey loadURL:pageTwo]; |
| [ChromeEarlGrey waitForWebStateContainingText:kPageTwoContent]; |
| } |
| |
| - (void)verifyRestoredTestPages:(BOOL)checkServerData { |
| const GURL pageOne = self.testServer->GetURL(kPageOnePath); |
| const GURL pageTwo = self.secondTestServer->GetURL(kPageTwoPath); |
| |
| // Restore page2 |
| [self triggerRestore]; |
| [[EarlGrey selectElementWithMatcher:OmniboxText(pageTwo.GetContent())] |
| assertWithMatcher:grey_notNil()]; |
| if (checkServerData) { |
| [ChromeEarlGrey waitForWebStateContainingText:kPageTwoContent]; |
| } |
| |
| // Confirm page1 is still in the history. |
| [[EarlGrey selectElementWithMatcher:BackButton()] |
| performAction:grey_longPress()]; |
| [[EarlGrey |
| selectElementWithMatcher:grey_allOf(grey_text(base::SysUTF8ToNSString( |
| kPageOneTitle)), |
| grey_sufficientlyVisible(), nil)] |
| assertWithMatcher:grey_notNil()]; |
| [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()]; |
| |
| // Go back to error page. |
| [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()]; |
| GREYAssert( |
| WaitForOmniboxContaining("invalid."), |
| @"Timeout while waiting for omnibox text to become \"invalid.\"."); |
| [ChromeEarlGrey waitForWebStateContainingText:"ERR_"]; |
| [ChromeEarlGreyUI waitForAppToIdle]; |
| [self triggerRestore]; |
| GREYAssert( |
| WaitForOmniboxContaining("invalid."), |
| @"Timeout while waiting for omnibox text to become \"invalid.\"."); |
| [ChromeEarlGrey waitForWebStateContainingText:"ERR_"]; |
| [ChromeEarlGreyUI waitForAppToIdle]; |
| |
| // Go back to chrome url. |
| [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()]; |
| GREYAssert(WaitForOmniboxContaining("chrome://chrome-urls"), |
| @"Timeout while waiting for omnibox text to become " |
| @"\"chrome://chrome-urls\"."); |
| [ChromeEarlGrey waitForWebStateContainingText:"List of Chrome"]; |
| [self triggerRestore]; |
| GREYAssert(WaitForOmniboxContaining("chrome://chrome-urls"), |
| @"Timeout while waiting for omnibox text to become " |
| @"\"chrome://chrome-urls\"."); |
| [ChromeEarlGrey waitForWebStateContainingText:"List of Chrome"]; |
| |
| // Go back to page1 and confirm page2 is still in the forward history. |
| [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()]; |
| [[EarlGrey selectElementWithMatcher:OmniboxText(pageOne.GetContent())] |
| assertWithMatcher:grey_notNil()]; |
| if (checkServerData) { |
| [ChromeEarlGrey waitForWebStateContainingText:kPageOneContent]; |
| [[EarlGrey selectElementWithMatcher:ForwardButton()] |
| performAction:grey_longPress()]; |
| [[EarlGrey |
| selectElementWithMatcher:grey_allOf(grey_text(base::SysUTF8ToNSString( |
| kPageTwoTitle)), |
| grey_sufficientlyVisible(), nil)] |
| assertWithMatcher:grey_notNil()]; |
| [[EarlGrey selectElementWithMatcher:ForwardButton()] |
| performAction:grey_tap()]; |
| } |
| [self triggerRestore]; |
| [[EarlGrey selectElementWithMatcher:OmniboxText(pageOne.GetContent())] |
| assertWithMatcher:grey_notNil()]; |
| if (checkServerData) { |
| [ChromeEarlGrey waitForWebStateContainingText:kPageOneContent]; |
| [[EarlGrey selectElementWithMatcher:ForwardButton()] |
| performAction:grey_longPress()]; |
| [[EarlGrey |
| selectElementWithMatcher:grey_allOf(grey_text(base::SysUTF8ToNSString( |
| kPageTwoTitle)), |
| grey_sufficientlyVisible(), nil)] |
| assertWithMatcher:grey_notNil()]; |
| [[EarlGrey selectElementWithMatcher:ForwardButton()] |
| performAction:grey_tap()]; |
| } |
| [[EarlGrey selectElementWithMatcher:BackButton()] performAction:grey_tap()]; |
| [ChromeEarlGrey waitForPageToFinishLoading]; |
| |
| // Confirm the NTP is still at the start. |
| [[EarlGrey selectElementWithMatcher:NTPCollectionView()] |
| assertWithMatcher:grey_notNil()]; |
| [self triggerRestore]; |
| [[EarlGrey selectElementWithMatcher:NTPCollectionView()] |
| assertWithMatcher:grey_notNil()]; |
| } |
| |
| @end |
| |
| // Test using synthesize restore. |
| @interface RestoreWithSynthesizedTestCase : RestoreWithCacheTestCase |
| @end |
| |
| @implementation RestoreWithSynthesizedTestCase |
| |
| - (AppLaunchConfiguration)appConfigurationForTestCase { |
| AppLaunchConfiguration config = [super appConfigurationForTestCase]; |
| config.features_disabled.push_back(web::kRestoreSessionFromCache); |
| return config; |
| } |
| |
| // This is currently needed to prevent this test case from being ignored. |
| - (void)testEmpty { |
| } |
| |
| @end |
| |
| // Test using synthesize restore. |
| @interface RestoreWithLegacyTestCase : RestoreWithCacheTestCase |
| @end |
| |
| @implementation RestoreWithLegacyTestCase |
| |
| - (AppLaunchConfiguration)appConfigurationForTestCase { |
| AppLaunchConfiguration config = [super appConfigurationForTestCase]; |
| config.features_disabled.push_back(web::features::kSynthesizedRestoreSession); |
| config.features_disabled.push_back(web::kRestoreSessionFromCache); |
| return config; |
| } |
| |
| // This is currently needed to prevent this test case from being ignored. |
| - (void)testEmpty { |
| } |
| |
| @end |