blob: 6ec4c5d11a8366fb3557f05b18f70a53a302b440 [file]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/public/web_state.h"
#import <UIKit/UIKit.h>
#import "base/functional/bind.h"
#import "base/path_service.h"
#import "base/run_loop.h"
#import "base/strings/stringprintf.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/values.h"
#import "components/sessions/core/session_id.h"
#import "ios/net/protocol_handler_util.h"
#import "ios/web/common/features.h"
#import "ios/web/common/uikit_ui_util.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/session/crw_navigation_item_storage.h"
#import "ios/web/public/session/crw_session_storage.h"
#import "ios/web/public/test/error_test_util.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/fakes/fake_web_state_delegate.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/test/web_view_content_test_util.h"
#import "ios/web/public/web_client.h"
#import "ios/web/public/web_state_observer.h"
#import "ios/web/test/test_url_constants.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/web_state_impl.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "ui/gfx/geometry/rect_f.h"
#import "ui/gfx/image/image.h"
#import "ui/gfx/image/image_unittest_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::kWaitForPageLoadTimeout;
namespace web {
namespace {
// A text string that is included in `kTestPageHTML`.
const char kTextInTestPageHTML[] = "this_is_a_test_string";
// A test page HTML containing `kTextInTestPageHTML`.
const char kTestPageHTML[] = "<html><body>this_is_a_test_string</body><html>";
// Returns the NSURLErrorUnsupportedURL error expected for tests.
NSError* CreateUnsupportedURLError() {
return web::testing::CreateErrorWithUnderlyingErrorChain(
{{NSURLErrorDomain, NSURLErrorUnsupportedURL},
{net::kNSErrorDomain, net::ERR_INVALID_URL}});
}
} // namespace
using wk_navigation_util::IsWKInternalUrl;
// Test fixture for web::WebTest class.
class WebStateTest : public FakeWebClient, public WebTestWithWebState {
void SetUp() override {
WebTestWithWebState::SetUp();
web::IgnoreOverRealizationCheck();
}
protected:
base::HistogramTester histogram_tester_;
};
// Tests that executing user JavaScript registers user interaction.
TEST_F(WebStateTest, UserScriptExecution) {
web::FakeWebStateDelegate delegate;
web_state()->SetDelegate(&delegate);
ASSERT_TRUE(delegate.child_windows().empty());
ASSERT_TRUE(LoadHtml("<html></html>"));
web_state()->ExecuteUserJavaScript(@"window.open('', target='_blank');");
web::FakeWebStateDelegate* delegate_ptr = &delegate;
bool suceess = WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
// Child window can only be open if the user interaction was registered.
return delegate_ptr->child_windows().size() == 1;
});
ASSERT_TRUE(suceess);
EXPECT_TRUE(delegate.child_windows()[0]);
}
// Tests loading progress.
TEST_F(WebStateTest, LoadingProgress) {
EXPECT_FLOAT_EQ(0.0, web_state()->GetLoadingProgress());
ASSERT_TRUE(LoadHtml("<html></html>"));
WaitForCondition(^bool() {
return web_state()->GetLoadingProgress() == 1.0;
});
}
// Tests that reload with web::ReloadType::NORMAL is no-op when navigation
// manager is empty.
TEST_F(WebStateTest, ReloadWithNormalTypeWithEmptyNavigationManager) {
NavigationManager* navigation_manager = web_state()->GetNavigationManager();
ASSERT_FALSE(navigation_manager->GetPendingItem());
ASSERT_FALSE(navigation_manager->GetLastCommittedItem());
navigation_manager->Reload(web::ReloadType::NORMAL,
false /* check_for_repost */);
ASSERT_FALSE(navigation_manager->GetPendingItem());
ASSERT_FALSE(navigation_manager->GetLastCommittedItem());
}
// Tests that reload with web::ReloadType::ORIGINAL_REQUEST_URL is no-op when
// navigation manager is empty.
TEST_F(WebStateTest, ReloadWithOriginalTypeWithEmptyNavigationManager) {
NavigationManager* navigation_manager = web_state()->GetNavigationManager();
ASSERT_FALSE(navigation_manager->GetPendingItem());
ASSERT_FALSE(navigation_manager->GetLastCommittedItem());
navigation_manager->Reload(web::ReloadType::ORIGINAL_REQUEST_URL,
false /* check_for_repost */);
ASSERT_FALSE(navigation_manager->GetPendingItem());
ASSERT_FALSE(navigation_manager->GetLastCommittedItem());
}
// Tests that the snapshot method returns an image of a rendered html page.
TEST_F(WebStateTest, Snapshot) {
ASSERT_TRUE(
LoadHtml("<html><div style='background-color:#FF0000; width:50%; "
"height:100%;'></div></html>"));
__block bool snapshot_complete = false;
[GetAnyKeyWindow() addSubview:web_state()->GetView()];
// The subview is added but not immediately painted, so a small delay is
// necessary.
CGRect rect = [web_state()->GetView() bounds];
base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.2));
web_state()->TakeSnapshot(
gfx::RectF(rect), base::BindRepeating(^(const gfx::Image& snapshot) {
ASSERT_FALSE(snapshot.IsEmpty());
EXPECT_GT(snapshot.Width(), 0);
EXPECT_GT(snapshot.Height(), 0);
int red_pixel_x = (snapshot.Width() / 2) - 10;
int white_pixel_x = (snapshot.Width() / 2) + 10;
// Test a pixel on the left (red) side.
gfx::test::CheckColors(
gfx::test::GetPlatformImageColor(
gfx::test::ToPlatformType(snapshot), red_pixel_x, 50),
SK_ColorRED);
// Test a pixel on the right (white) side.
gfx::test::CheckColors(
gfx::test::GetPlatformImageColor(
gfx::test::ToPlatformType(snapshot), white_pixel_x, 50),
SK_ColorWHITE);
snapshot_complete = true;
}));
WaitForCondition(^{
return snapshot_complete;
});
}
// Tests that the create PDF method returns a PDF of a rendered html page when
// running a supported iOS version.
TEST_F(WebStateTest, CreateFullPagePdf_ValidURL_iOS14) {
// PDF generation is supported on iOS 14+.
if (@available(iOS 14, *)) {
[GetAnyKeyWindow() addSubview:web_state()->GetView()];
// Load a URL and some HTML in the WebState.
GURL url("https://www.chromium.org");
NavigationManager::WebLoadParams load_params(url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return web_state()->GetLastCommittedURL() == url &&
!web_state()->IsLoading();
}));
NSString* data_html =
@"<html><div style='background-color:#FF0000; width:50%; "
"height:100%;'></div></html>";
web_state()->LoadData([data_html dataUsingEncoding:NSUTF8StringEncoding],
@"text/html", url);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return !web_state()->IsLoading();
}));
// Create a PDF for this page and validate the data.
__block NSData* callback_data = nil;
web_state()->CreateFullPagePdf(base::BindOnce(^(NSData* pdf_document_data) {
callback_data = [pdf_document_data copy];
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return callback_data;
}));
CGPDFDocumentRef pdf = CGPDFDocumentCreateWithProvider(
CGDataProviderCreateWithCFData((CFDataRef)callback_data));
CGSize pdf_size =
CGPDFPageGetBoxRect(CGPDFDocumentGetPage(pdf, 1), kCGPDFMediaBox).size;
CGFloat kSaveAreaTopInset = GetAnyKeyWindow().safeAreaInsets.top;
EXPECT_GE(pdf_size.height,
UIScreen.mainScreen.bounds.size.height - kSaveAreaTopInset);
EXPECT_GE(pdf_size.width, [[UIScreen mainScreen] bounds].size.width);
CGPDFDocumentRelease(pdf);
}
// If not an earlier version, then no PDF should be created.
}
// Tests that the create PDF method returns nil when running an unsupported iOS
// version.
TEST_F(WebStateTest, CreateFullPagePdf_ValidURL_NotSupported) {
if (@available(iOS 14, *)) {
// Return early when running on a support iOS version.
return;
}
[GetAnyKeyWindow() addSubview:web_state()->GetView()];
// Load a URL and some HTML in the WebState.
GURL url("https://www.chromium.org");
NavigationManager::WebLoadParams load_params(url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return web_state()->GetLastCommittedURL() == url &&
!web_state()->IsLoading();
}));
NSString* data_html =
@"<html><div style='background-color:#FF0000; width:50%; "
"height:100%;'></div></html>";
web_state()->LoadData([data_html dataUsingEncoding:NSUTF8StringEncoding],
@"text/html", url);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return !web_state()->IsLoading();
}));
// Attempt to create a PDF for this page and validate that it return nil.
__block BOOL callback_called = NO;
web_state()->CreateFullPagePdf(base::BindOnce(^(NSData* pdf_document_data) {
EXPECT_EQ(nil, pdf_document_data);
callback_called = YES;
}));
EXPECT_TRUE(callback_called);
}
// Tests that CreateFullPagePdf invokes completion callback nil when an invalid
// URL is loaded.
TEST_F(WebStateTest, CreateFullPagePdf_InvalidURLs) {
GURL app_specific_url(
base::StringPrintf("%s://app_specific_url", kTestAppSpecificScheme));
// Empty URL and app-specific URLs (e.g. app_specific_url) should get nil
// data through the completion callback.
std::vector<GURL> invalid_urls = {GURL(), app_specific_url};
NSString* data_html = @(kTestPageHTML);
for (auto& url : invalid_urls) {
web_state()->LoadData([data_html dataUsingEncoding:NSUTF8StringEncoding],
@"text/html", url);
NavigationManager::WebLoadParams load_params(url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForPageLoadTimeout, ^bool {
return web_state()->GetLastCommittedURL() == url;
}));
__block NSData* callback_data = nil;
__block bool callback_called = false;
web_state()->CreateFullPagePdf(base::BindOnce(^(NSData* pdf_document_data) {
callback_data = [pdf_document_data copy];
callback_called = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return callback_called;
}));
ASSERT_FALSE(callback_data);
}
}
// Tests that CreateFullPagePdf invokes completion callback nil when the
// WebState content is not HTML (e.g. a PDF file).
TEST_F(WebStateTest, CreateFullPagePdfWebStatePdfContent) {
CGRect fake_bounds = CGRectMake(0, 0, 100, 100);
UIGraphicsPDFRenderer* pdf_renderer =
[[UIGraphicsPDFRenderer alloc] initWithBounds:fake_bounds];
NSData* pdf_data = [pdf_renderer
PDFDataWithActions:^(UIGraphicsPDFRendererContext* context) {
[context beginPage];
[[UIColor blueColor] setFill];
[context fillRect:fake_bounds];
}];
GURL test_url("https://www.chromium.org/somePDF.pdf");
NavigationManager::WebLoadParams load_params(test_url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForPageLoadTimeout, ^bool {
return web_state()->GetLastCommittedURL() == test_url &&
!web_state()->IsLoading();
}));
std::string mime_type = "application/pdf";
web_state()->LoadData(
pdf_data, [NSString stringWithUTF8String:mime_type.c_str()], test_url);
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForPageLoadTimeout, ^bool {
return !web_state()->IsLoading();
}));
__block NSData* callback_data = nil;
__block bool callback_called = false;
web_state()->CreateFullPagePdf(base::BindOnce(^(NSData* pdf_document_data) {
callback_data = [pdf_document_data copy];
callback_called = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return callback_called;
}));
ASSERT_FALSE(callback_data);
}
// Tests that the web state has an opener after calling SetHasOpener().
TEST_F(WebStateTest, SetHasOpener) {
ASSERT_FALSE(web_state()->HasOpener());
web_state()->SetHasOpener(true);
EXPECT_TRUE(web_state()->HasOpener());
}
// Verifies that large session can be restored with max session size limit
// equals to `wk_navigation_util::kMaxSessionSize`.
TEST_F(WebStateTest, RestoreLargeSession) {
// Create session storage with large number of items.
const int kItemCount = 150;
NSMutableArray<CRWNavigationItemStorage*>* item_storages =
[NSMutableArray arrayWithCapacity:kItemCount];
for (unsigned int i = 0; i < kItemCount; i++) {
CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
item.URL = GURL(base::StringPrintf("http://www.%u.com", i));
item.title = base::ASCIIToUTF16(base::StringPrintf("Test%u", i));
[item_storages addObject:item];
}
// Restore the session.
WebState::CreateParams params(GetBrowserState());
CRWSessionStorage* session_storage = [[CRWSessionStorage alloc] init];
session_storage.stableIdentifier = [[NSUUID UUID] UUIDString];
session_storage.uniqueIdentifier = SessionID::NewUnique();
session_storage.itemStorages = item_storages;
session_storage.userAgentType = UserAgentType::MOBILE;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
web_state->SetKeepRenderProcessAlive(true);
WebState* web_state_ptr = web_state.get();
NavigationManager* navigation_manager = web_state->GetNavigationManager();
// TODO(crbug.com/873729): The session will not be restored until
// LoadIfNecessary call. Fix the bug and remove extra call.
navigation_manager->LoadIfNecessary();
int maxSessionSize = wk_navigation_util::kMaxSessionSize;
ui::PageTransition transition_type = ui::PAGE_TRANSITION_RELOAD;
if (@available(iOS 15, *)) {
// kMaxSessionSize is no longer used on iOS15.
maxSessionSize = kItemCount;
// Synthesized restore defaults to transition first.
transition_type = ui::PAGE_TRANSITION_FIRST;
}
// Verify that session was fully restored.
auto block = ^{
bool restored = navigation_manager->GetItemCount() == maxSessionSize &&
navigation_manager->CanGoForward();
EXPECT_EQ(restored, !navigation_manager->IsRestoreSessionInProgress());
if (!restored) {
EXPECT_FALSE(navigation_manager->GetLastCommittedItem());
EXPECT_EQ(-1, navigation_manager->GetLastCommittedItemIndex());
EXPECT_TRUE(web_state_ptr->GetLastCommittedURL().is_empty());
EXPECT_FALSE(navigation_manager->CanGoForward());
EXPECT_TRUE(navigation_manager->GetBackwardItems().empty());
EXPECT_TRUE(navigation_manager->GetForwardItems().empty());
EXPECT_EQ("Test0", base::UTF16ToASCII(web_state_ptr->GetTitle()));
EXPECT_EQ(0.0, web_state_ptr->GetLoadingProgress());
EXPECT_EQ(-1, navigation_manager->GetPendingItemIndex());
EXPECT_FALSE(navigation_manager->GetPendingItem());
} else {
EXPECT_EQ("Test0", base::UTF16ToASCII(web_state_ptr->GetTitle()));
NavigationItem* last_committed_item =
navigation_manager->GetLastCommittedItem();
// After restoration is complete GetLastCommittedItem() will return null
// until fist post-restore navigation is finished.
if (last_committed_item) {
EXPECT_EQ("http://www.0.com/", last_committed_item->GetURL());
EXPECT_EQ("http://www.0.com/", web_state_ptr->GetLastCommittedURL());
EXPECT_EQ(0, navigation_manager->GetLastCommittedItemIndex());
EXPECT_TRUE(ui::PageTransitionCoreTypeIs(
navigation_manager->GetLastCommittedItem()->GetTransitionType(),
transition_type));
} else {
EXPECT_EQ("", web_state_ptr->GetLastCommittedURL());
EXPECT_EQ(-1, navigation_manager->GetLastCommittedItemIndex());
NavigationItem* pending_item = navigation_manager->GetPendingItem();
EXPECT_TRUE(pending_item);
if (pending_item) {
EXPECT_EQ("http://www.0.com/", pending_item->GetURL());
}
}
EXPECT_TRUE(navigation_manager->GetBackwardItems().empty());
EXPECT_EQ(std::max(navigation_manager->GetItemCount() - 1, 0),
static_cast<int>(navigation_manager->GetForwardItems().size()));
}
EXPECT_FALSE(web_state_ptr->IsCrashed());
EXPECT_FALSE(web_state_ptr->IsEvicted());
EXPECT_EQ("http://www.0.com/", web_state_ptr->GetVisibleURL());
NavigationItem* visible_item = navigation_manager->GetVisibleItem();
EXPECT_TRUE(visible_item);
EXPECT_TRUE(visible_item && visible_item->GetURL() == "http://www.0.com/");
EXPECT_FALSE(navigation_manager->CanGoBack());
EXPECT_FALSE(IsWKInternalUrl(web_state_ptr->GetVisibleURL()));
return restored;
};
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, block));
EXPECT_EQ(maxSessionSize, navigation_manager->GetItemCount());
EXPECT_TRUE(navigation_manager->CanGoForward());
histogram_tester_.ExpectTotalCount(kRestoreNavigationItemCount, 1);
histogram_tester_.ExpectBucketCount(kRestoreNavigationItemCount, 100, 1);
if (@available(iOS 15, *)) {
} else {
// kRestoreNavigationTime only applies to legacy session restore.
histogram_tester_.ExpectTotalCount(kRestoreNavigationTime, 1);
}
// Now wait until the last committed item is fully loaded.
auto block2 = ^{
EXPECT_FALSE(IsWKInternalUrl(web_state_ptr->GetVisibleURL()));
return !navigation_manager->GetPendingItem() &&
!web_state_ptr->IsLoading() &&
web_state_ptr->GetLoadingProgress() == 1.0;
};
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, block2));
EXPECT_TRUE(ui::PageTransitionCoreTypeIs(
navigation_manager->GetLastCommittedItem()->GetTransitionType(),
transition_type));
// The restoration of www.0.com ends with displaying an error page which may
// not be complete at this point.
// Queue some javascript to wait for every handler to complete.
// TODO(crbug.com/1244067): Remove this workaround.
__block BOOL called = false;
CRWWebController* web_controller =
WebStateImpl::FromWebState(web_state.get())->GetWebController();
[web_controller executeJavaScript:@"0;"
completionHandler:^(id, NSError*) {
called = true;
}];
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return called;
}));
}
// Verifies that calling WebState::Stop() does not stop the session restoration.
// Session restoration should be opaque to the user and embedder, so calling
// Stop() is no-op.
TEST_F(WebStateTest, CallStopDuringSessionRestore) {
// Create session storage with large number of items.
const int kItemCount = 10;
NSMutableArray<CRWNavigationItemStorage*>* item_storages =
[NSMutableArray arrayWithCapacity:kItemCount];
for (unsigned int i = 0; i < kItemCount; i++) {
CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
item.virtualURL = GURL(base::StringPrintf("http://www.%u.com", i));
[item_storages addObject:item];
}
// Restore the session.
WebState::CreateParams params(GetBrowserState());
CRWSessionStorage* session_storage = [[CRWSessionStorage alloc] init];
session_storage.stableIdentifier = [[NSUUID UUID] UUIDString];
session_storage.uniqueIdentifier = SessionID::NewUnique();
session_storage.itemStorages = item_storages;
session_storage.userAgentType = UserAgentType::MOBILE;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
web_state->SetKeepRenderProcessAlive(true);
WebState* web_state_ptr = web_state.get();
NavigationManager* navigation_manager = web_state->GetNavigationManager();
// TODO(crbug.com/873729): The session will not be restored until
// LoadIfNecessary call. Fix the bug and remove extra call.
navigation_manager->LoadIfNecessary();
// Verify that session was fully restored.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
bool restored = navigation_manager->GetItemCount() == kItemCount &&
navigation_manager->CanGoForward();
if (!restored) {
web_state_ptr->Stop(); // Attempt to interrupt the session restoration.
}
return restored;
}));
EXPECT_EQ(kItemCount, navigation_manager->GetItemCount());
EXPECT_TRUE(navigation_manager->CanGoForward());
// Now wait until the last committed item is fully loaded.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !navigation_manager->GetPendingItem() && !web_state_ptr->IsLoading();
}));
// Wait for the error to be displayed.
EXPECT_TRUE(web::test::WaitForWebViewContainingText(
web_state_ptr, "error", base::test::ios::kWaitForJSCompletionTimeout));
}
// Verifies that calling NavigationManager::LoadURLWithParams() does not stop
// the session restoration and eventually loads the requested URL.
TEST_F(WebStateTest, CallLoadURLWithParamsDuringSessionRestore) {
// Create session storage with large number of items.
const int kItemCount = 10;
NSMutableArray<CRWNavigationItemStorage*>* item_storages =
[NSMutableArray arrayWithCapacity:kItemCount];
for (unsigned int i = 0; i < kItemCount; i++) {
CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
item.virtualURL = GURL(base::StringPrintf("http://www.%u.test", i));
item.userAgentType = UserAgentType::MOBILE;
[item_storages addObject:item];
}
// Restore the session.
WebState::CreateParams params(GetBrowserState());
CRWSessionStorage* session_storage = [[CRWSessionStorage alloc] init];
session_storage.stableIdentifier = [[NSUUID UUID] UUIDString];
session_storage.uniqueIdentifier = SessionID::NewUnique();
session_storage.itemStorages = item_storages;
session_storage.userAgentType = UserAgentType::MOBILE;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
web_state->SetKeepRenderProcessAlive(true);
WebState* web_state_ptr = web_state.get();
NavigationManager* navigation_manager = web_state->GetNavigationManager();
// TODO(crbug.com/873729): The session will not be restored until
// LoadIfNecessary call. Fix the bug and remove extra call.
navigation_manager->LoadIfNecessary();
// Attempt to interrupt the session restoration.
GURL url("http://foo.test/");
NavigationManager::WebLoadParams load_params(url);
navigation_manager->LoadURLWithParams(load_params);
// Verify that session was fully restored.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
bool restored = navigation_manager->GetItemCount() == kItemCount &&
navigation_manager->CanGoForward();
if (!restored) {
// Attempt to interrupt the session restoration multiple times, which is
// something that the user can do on the slow network.
navigation_manager->LoadURLWithParams(load_params);
}
return restored;
}));
EXPECT_EQ(kItemCount, navigation_manager->GetItemCount());
EXPECT_TRUE(navigation_manager->CanGoForward());
// Now wait until the last committed item is fully loaded.
// TODO(crbug.com/996544) On Xcode 11 beta 6 this became very slow. This
// appears to only affect simulator, and will hopefully be fixed in a future
// Xcode release. Revert this to `kWaitForPageLoadTimeout` alone when fixed.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout * 7, ^{
return web_state_ptr->GetLastCommittedURL() == url;
}));
}
// Verifies that calling NavigationManager::Reload() does not stop the session
// restoration. Session restoration should be opaque to the user and embedder,
// so calling Reload() is no-op.
TEST_F(WebStateTest, CallReloadDuringSessionRestore) {
// Create session storage with large number of items.
const int kItemCount = 10;
NSMutableArray<CRWNavigationItemStorage*>* item_storages =
[NSMutableArray arrayWithCapacity:kItemCount];
for (unsigned int i = 0; i < kItemCount; i++) {
CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
item.virtualURL = GURL(base::StringPrintf("http://www.%u.com", i));
[item_storages addObject:item];
}
// Restore the session.
WebState::CreateParams params(GetBrowserState());
CRWSessionStorage* session_storage = [[CRWSessionStorage alloc] init];
session_storage.stableIdentifier = [[NSUUID UUID] UUIDString];
session_storage.uniqueIdentifier = SessionID::NewUnique();
session_storage.itemStorages = item_storages;
session_storage.userAgentType = UserAgentType::MOBILE;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
web_state->SetKeepRenderProcessAlive(true);
WebState* web_state_ptr = web_state.get();
NavigationManager* navigation_manager = web_state->GetNavigationManager();
// TODO(crbug.com/873729): The session will not be restored until
// LoadIfNecessary call. Fix the bug and remove extra call.
navigation_manager->LoadIfNecessary();
// Verify that session was fully restored.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
bool restored = navigation_manager->GetItemCount() == kItemCount &&
navigation_manager->CanGoForward();
if (!restored) {
// Attempt to interrupt the session restoration.
navigation_manager->Reload(web::ReloadType::NORMAL,
/*check_for_repost=*/false);
}
return restored;
}));
EXPECT_EQ(kItemCount, navigation_manager->GetItemCount());
EXPECT_TRUE(navigation_manager->CanGoForward());
// Now wait until the last committed item is fully loaded.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !navigation_manager->GetPendingItem() && !web_state_ptr->IsLoading();
}));
// Wait for the error to be displayed.
EXPECT_TRUE(web::test::WaitForWebViewContainingText(
web_state_ptr, "error", base::test::ios::kWaitForJSCompletionTimeout));
}
// Verifies that each page title is restored.
TEST_F(WebStateTest, RestorePageTitles) {
// Create session storage.
const int kItemCount = 3;
NSMutableArray<CRWNavigationItemStorage*>* item_storages =
[NSMutableArray arrayWithCapacity:kItemCount];
for (unsigned int i = 0; i < kItemCount; i++) {
CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
item.URL = GURL(base::StringPrintf("http://www.%u.com", i));
item.title = base::ASCIIToUTF16(base::StringPrintf("Test%u", i));
[item_storages addObject:item];
}
// Restore the session.
WebState::CreateParams params(GetBrowserState());
CRWSessionStorage* session_storage = [[CRWSessionStorage alloc] init];
session_storage.stableIdentifier = [[NSUUID UUID] UUIDString];
session_storage.uniqueIdentifier = SessionID::NewUnique();
session_storage.itemStorages = item_storages;
session_storage.userAgentType = UserAgentType::MOBILE;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
web_state->SetKeepRenderProcessAlive(true);
NavigationManager* navigation_manager = web_state->GetNavigationManager();
// TODO(crbug.com/873729): The session will not be restored until
// LoadIfNecessary call. Fix the bug and remove extra call.
navigation_manager->LoadIfNecessary();
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return navigation_manager->GetItemCount() == kItemCount;
}));
for (unsigned int i = 0; i < kItemCount; i++) {
NavigationItem* item = navigation_manager->GetItemAtIndex(i);
EXPECT_EQ(GURL(base::StringPrintf("http://www.%u.com", i)),
item->GetVirtualURL());
EXPECT_EQ(base::ASCIIToUTF16(base::StringPrintf("Test%u", i)),
item->GetTitle());
}
}
// Tests that loading an HTML page after a failed navigation works.
TEST_F(WebStateTest, LoadChromeThenHTML) {
GURL app_specific_url(
base::StringPrintf("%s://app_specific_url", kTestAppSpecificScheme));
web::NavigationManager::WebLoadParams load_params(app_specific_url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !web_state()->IsLoading();
}));
// Wait for the error loading and check that it corresponds with
// kUnsupportedUrlErrorPage.
EXPECT_TRUE(test::WaitForWebViewContainingText(
web_state(), testing::GetErrorText(web_state(), app_specific_url,
CreateUnsupportedURLError(),
/*is_post=*/false, /*is_otr=*/false,
/*cert_status=*/0)));
NSString* data_html = @(kTestPageHTML);
web_state()->LoadData([data_html dataUsingEncoding:NSUTF8StringEncoding],
@"text/html", GURL("https://www.chromium.org"));
EXPECT_TRUE(
test::WaitForWebViewContainingText(web_state(), kTextInTestPageHTML));
}
// Tests that loading an arbitrary file URL is a no-op.
TEST_F(WebStateTest, LoadFileURL) {
GURL file_url("file:///path/to/file.html");
web::NavigationManager::WebLoadParams load_params(file_url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
EXPECT_FALSE(web_state()->IsLoading());
}
// Tests that reloading after loading HTML page will load the online page.
TEST_F(WebStateTest, LoadChromeThenWaitThenHTMLThenReload) {
net::EmbeddedTestServer server;
net::test_server::RegisterDefaultHandlers(&server);
ASSERT_TRUE(server.Start());
GURL echo_url = server.GetURL("/echo");
GURL app_specific_url(
base::StringPrintf("%s://app_specific_url", kTestAppSpecificScheme));
web::NavigationManager::WebLoadParams load_params(app_specific_url);
web_state()->GetNavigationManager()->LoadURLWithParams(load_params);
// Wait for the error loading.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !web_state()->IsLoading();
}));
EXPECT_TRUE(test::WaitForWebViewContainingText(
web_state(), testing::GetErrorText(web_state(), app_specific_url,
CreateUnsupportedURLError(),
/*is_post=*/false, /*is_otr=*/false,
/*cert_status=*/0)));
NSString* data_html = @(kTestPageHTML);
web_state()->LoadData([data_html dataUsingEncoding:NSUTF8StringEncoding],
@"text/html", echo_url);
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !web_state()->IsLoading();
}));
EXPECT_TRUE(
test::WaitForWebViewContainingText(web_state(), kTextInTestPageHTML));
web_state()->GetNavigationManager()->Reload(web::ReloadType::NORMAL, true);
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !web_state()->IsLoading();
}));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "Echo"));
web_state()->GetNavigationManager()->Reload(web::ReloadType::NORMAL, true);
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return !web_state()->IsLoading();
}));
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state(), "Echo"));
}
} // namespace web