blob: 03843b9bb7dd5a57bbf371066db4467382ff5926 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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/web_state.h"
#import <UIKit/UIKit.h>
#include "base/bind.h"
#include "base/path_service.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#import "ios/web/navigation/wk_based_navigation_manager_impl.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/crw_navigation_item_storage.h"
#import "ios/web/public/crw_session_storage.h"
#include "ios/web/public/features.h"
#import "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.h"
#import "ios/web/public/test/fakes/test_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"
#include "ui/gfx/image/image.h"
#include "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 from the test HTML page in the session storage returned by
// GetTestSessionStorage().
const char kTestSessionStoragePageText[] = "pony";
// Returns a session storage with a single committed entry of a test HTML page.
CRWSessionStorage* GetTestSessionStorage() {
base::FilePath path;
base::PathService::Get(base::DIR_MODULE, &path);
path = path.Append(
FILE_PATH_LITERAL("ios/testing/data/http_server_files/pony.html"));
GURL testFileUrl(base::StringPrintf("file://%s", path.value().c_str()));
CRWSessionStorage* result = [[CRWSessionStorage alloc] init];
result.lastCommittedItemIndex = 0;
CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
[item setVirtualURL:testFileUrl];
[result setItemStorages:@[ item ]];
return result;
}
} // namespace
using wk_navigation_util::IsWKInternalUrl;
// WebStateTest is parameterized on this enum to test both the legacy
// implementation of navigation manager and the experimental implementation.
enum NavigationManagerChoice {
TEST_LEGACY_NAVIGATION_MANAGER,
TEST_WK_BASED_NAVIGATION_MANAGER,
};
// Test fixture for web::WebTest class.
class WebStateTest
: public WebTestWithWebState,
public ::testing::WithParamInterface<NavigationManagerChoice> {
protected:
WebStateTest() {
if (GetParam() == TEST_LEGACY_NAVIGATION_MANAGER) {
feature_list_.InitAndDisableFeature(
web::features::kSlimNavigationManager);
} else {
feature_list_.InitAndEnableFeature(web::features::kSlimNavigationManager);
}
}
base::HistogramTester histogram_tester_;
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests script execution with and without callback.
TEST_P(WebStateTest, ScriptExecution) {
ASSERT_TRUE(LoadHtml("<html></html>"));
// Execute script without callback.
web_state()->ExecuteJavaScript(base::UTF8ToUTF16("window.foo = 'bar'"));
// Execute script with callback.
__block std::unique_ptr<base::Value> execution_result;
__block bool execution_complete = false;
web_state()->ExecuteJavaScript(base::UTF8ToUTF16("window.foo"),
base::BindOnce(^(const base::Value* value) {
execution_result = value->CreateDeepCopy();
execution_complete = true;
}));
WaitForCondition(^{
return execution_complete;
});
ASSERT_TRUE(execution_result);
std::string string_result;
execution_result->GetAsString(&string_result);
EXPECT_EQ("bar", string_result);
}
// Tests that executing user JavaScript registers user interaction.
TEST_P(WebStateTest, UserScriptExecution) {
web::TestWebStateDelegate delegate;
web_state()->SetDelegate(&delegate);
ASSERT_TRUE(delegate.child_windows().empty());
ASSERT_TRUE(LoadHtml("<html></html>"));
web_state()->ExecuteUserJavaScript(@"window.open('', target='_blank');");
web::TestWebStateDelegate* 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_P(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 page which overrides window.webkit object does not break the
// messaging system.
TEST_P(WebStateTest, OverridingWebKitObject) {
// Add a script command handler.
__block bool message_received = false;
const web::WebState::ScriptCommandCallback callback =
base::BindRepeating(^bool(const base::DictionaryValue&, const GURL&,
/*interacted*/ bool, /*is_main_frame*/ bool,
/*sender_frame*/ web::WebFrame*) {
message_received = true;
return true;
});
web_state()->AddScriptCommandCallback(callback, "test");
// Load the page which overrides window.webkit object and wait until the
// test message is received.
ASSERT_TRUE(LoadHtml(
"<script>"
" webkit = undefined;"
" __gCrWeb.message.invokeOnHost({'command': 'test.webkit-overriding'});"
"</script>"));
WaitForCondition(^{
return message_received;
});
web_state()->RemoveScriptCommandCallback("test");
}
// Tests that reload with web::ReloadType::NORMAL is no-op when navigation
// manager is empty.
TEST_P(WebStateTest, ReloadWithNormalTypeWithEmptyNavigationManager) {
NavigationManager* navigation_manager = web_state()->GetNavigationManager();
ASSERT_FALSE(navigation_manager->GetTransientItem());
ASSERT_FALSE(navigation_manager->GetPendingItem());
ASSERT_FALSE(navigation_manager->GetLastCommittedItem());
navigation_manager->Reload(web::ReloadType::NORMAL,
false /* check_for_repost */);
ASSERT_FALSE(navigation_manager->GetTransientItem());
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_P(WebStateTest, ReloadWithOriginalTypeWithEmptyNavigationManager) {
NavigationManager* navigation_manager = web_state()->GetNavigationManager();
ASSERT_FALSE(navigation_manager->GetTransientItem());
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->GetTransientItem());
ASSERT_FALSE(navigation_manager->GetPendingItem());
ASSERT_FALSE(navigation_manager->GetLastCommittedItem());
}
// Tests that the snapshot method returns an image of a rendered html page.
TEST_P(WebStateTest, Snapshot) {
ASSERT_TRUE(
LoadHtml("<html><div style='background-color:#FF0000; width:50%; "
"height:100%;'></div></html>"));
__block bool snapshot_complete = false;
[[[UIApplication sharedApplication] keyWindow]
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::TimeDelta::FromSecondsD(0.2));
web_state()->TakeSnapshot(
rect, base::BindOnce(^(gfx::Image snapshot) {
if (@available(iOS 11, *)) {
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 message sent from main frame triggers the ScriptCommandCallback
// with |is_main_frame| = true.
TEST_P(WebStateTest, MessageFromMainFrame) {
// Add a script command handler.
__block bool message_received = false;
__block bool message_from_main_frame = false;
__block base::Value message_value;
const web::WebState::ScriptCommandCallback callback = base::BindRepeating(
^bool(const base::DictionaryValue& value, const GURL&,
bool user_interacted, bool is_main_frame, WebFrame* sender_frame) {
message_received = true;
message_from_main_frame = is_main_frame;
message_value = value.Clone();
return true;
});
web_state()->AddScriptCommandCallback(callback, "test");
ASSERT_TRUE(LoadHtml(
"<script>"
" __gCrWeb.message.invokeOnHost({'command': 'test.from-main-frame'});"
"</script>"));
WaitForCondition(^{
return message_received;
});
web_state()->RemoveScriptCommandCallback("test");
EXPECT_TRUE(message_from_main_frame);
EXPECT_TRUE(message_value.is_dict());
EXPECT_EQ(message_value.DictSize(), size_t(1));
base::Value* command = message_value.FindKey("command");
EXPECT_NE(command, nullptr);
EXPECT_TRUE(command->is_string());
EXPECT_EQ(command->GetString(), "test.from-main-frame");
}
// Tests that message sent from main frame triggers the ScriptCommandCallback
// with |is_main_frame| = false.
TEST_P(WebStateTest, MessageFromIFrame) {
// Add a script command handler.
__block bool message_received = false;
__block bool message_from_main_frame = false;
__block base::Value message_value;
const web::WebState::ScriptCommandCallback callback = base::BindRepeating(
^bool(const base::DictionaryValue& value, const GURL&,
bool user_interacted, bool is_main_frame, WebFrame* sender_frame) {
message_received = true;
message_from_main_frame = is_main_frame;
message_value = value.Clone();
return true;
});
web_state()->AddScriptCommandCallback(callback, "test");
ASSERT_TRUE(LoadHtml(
"<iframe srcdoc='"
"<script>"
" __gCrWeb.message.invokeOnHost({\"command\": \"test.from-iframe\"});"
"</script>"
"'/>"));
WaitForCondition(^{
return message_received;
});
web_state()->RemoveScriptCommandCallback("test");
EXPECT_FALSE(message_from_main_frame);
EXPECT_TRUE(message_value.is_dict());
EXPECT_EQ(message_value.DictSize(), size_t(1));
base::Value* command = message_value.FindKey("command");
EXPECT_NE(command, nullptr);
EXPECT_TRUE(command->is_string());
EXPECT_EQ(command->GetString(), "test.from-iframe");
}
// Tests that the web state has an opener after calling SetHasOpener().
TEST_P(WebStateTest, SetHasOpener) {
ASSERT_FALSE(web_state()->HasOpener());
web_state()->SetHasOpener(true);
EXPECT_TRUE(web_state()->HasOpener());
}
// Verifies that large session can be restored. SlimNavigationManagder has max
// session size limit of |wk_navigation_util::kMaxSessionSize|.
TEST_P(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.virtualURL = 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.itemStorages = item_storages;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
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.
int kExpectedItemCount = web::GetWebClient()->IsSlimNavigationManagerEnabled()
? wk_navigation_util::kMaxSessionSize
: kItemCount;
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
bool restored = navigation_manager->GetItemCount() == kExpectedItemCount &&
navigation_manager->CanGoForward();
if (!restored) {
EXPECT_FALSE(navigation_manager->CanGoForward());
EXPECT_TRUE(navigation_manager->GetBackwardItems().empty());
EXPECT_TRUE(navigation_manager->GetForwardItems().empty());
DCHECK_EQ(0.0, web_state_ptr->GetLoadingProgress());
// TODO(crbug.com/877671): Ensure that the following API work correctly:
// - WebState::GetLastCommittedURL
// - NavigationManager::GetLastCommittedItem
// - NavigationManager::GetPendingItem
// - NavigationManager::GetLastCommittedItemIndex
// - NavigationManager::GetPendingItemIndex
} else {
EXPECT_EQ("http://www.0.com/", web_state_ptr->GetLastCommittedURL());
NavigationItem* last_committed_item =
navigation_manager->GetLastCommittedItem();
EXPECT_TRUE(last_committed_item);
EXPECT_TRUE(last_committed_item &&
last_committed_item->GetURL() == "http://www.0.com/");
EXPECT_EQ(0, navigation_manager->GetLastCommittedItemIndex());
EXPECT_TRUE(navigation_manager->GetBackwardItems().empty());
EXPECT_EQ(std::max(navigation_manager->GetItemCount() - 1, 0),
static_cast<int>(navigation_manager->GetForwardItems().size()));
}
// TODO(crbug.com/877671): Ensure that the following API work correctly:
// - WebState::GetTitle
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(navigation_manager->GetTransientItem());
EXPECT_FALSE(IsWKInternalUrl(web_state_ptr->GetVisibleURL()));
return restored;
}));
EXPECT_EQ(kExpectedItemCount, navigation_manager->GetItemCount());
EXPECT_TRUE(navigation_manager->CanGoForward());
histogram_tester_.ExpectTotalCount(kRestoreNavigationItemCount, 1);
histogram_tester_.ExpectBucketCount(kRestoreNavigationItemCount, 100, 1);
if (web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
histogram_tester_.ExpectTotalCount(kRestoreNavigationTime, 1);
}
// Now wait until the last committed item is fully loaded.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
EXPECT_FALSE(IsWKInternalUrl(web_state_ptr->GetVisibleURL()));
return !navigation_manager->GetPendingItem() &&
!web_state_ptr->IsLoading() &&
web_state_ptr->GetLoadingProgress() == 1.0;
}));
}
// 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_P(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.itemStorages = item_storages;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
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();
}));
}
// 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_P(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.itemStorages = item_storages;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
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();
}));
}
// Tests that if a saved session is provided when creating a new WebState, it is
// restored after the first NavigationManager::LoadIfNecessary() call.
TEST_P(WebStateTest, RestoredFromHistory) {
auto web_state = WebState::CreateWithStorageSession(
WebState::CreateParams(GetBrowserState()), GetTestSessionStorage());
ASSERT_FALSE(test::IsWebViewContainingText(web_state.get(),
kTestSessionStoragePageText));
web_state->GetNavigationManager()->LoadIfNecessary();
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state.get(),
kTestSessionStoragePageText));
}
// Verifies that each page title is restored.
TEST_P(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.virtualURL = 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.itemStorages = item_storages;
auto web_state = WebState::CreateWithStorageSession(params, session_storage);
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 NavigationManager::LoadIfNecessary() restores the page after
// disabling and re-enabling web usage.
TEST_P(WebStateTest, DisableAndReenableWebUsage) {
auto web_state = WebState::CreateWithStorageSession(
WebState::CreateParams(GetBrowserState()), GetTestSessionStorage());
web_state->GetNavigationManager()->LoadIfNecessary();
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state.get(),
kTestSessionStoragePageText));
web_state->SetWebUsageEnabled(false);
web_state->SetWebUsageEnabled(true);
// NavigationManager::LoadIfNecessary() should restore the page.
ASSERT_FALSE(test::IsWebViewContainingText(web_state.get(),
kTestSessionStoragePageText));
web_state->GetNavigationManager()->LoadIfNecessary();
EXPECT_TRUE(test::WaitForWebViewContainingText(web_state.get(),
kTestSessionStoragePageText));
}
INSTANTIATE_TEST_CASE_P(
ProgrammaticWebStateTest,
WebStateTest,
::testing::Values(
NavigationManagerChoice::TEST_LEGACY_NAVIGATION_MANAGER,
NavigationManagerChoice::TEST_WK_BASED_NAVIGATION_MANAGER));
} // namespace web