blob: 69eb749d95ee0db877a8cbb42e0f3813e78d4cb9 [file] [log] [blame]
// Copyright 2016 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/test/web_test_with_web_state.h"
#include "base/ios/ios_util.h"
#include "base/run_loop.h"
#include "base/scoped_observer.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/current_thread.h"
#import "base/test/ios/wait_util.h"
#include "ios/web/common/features.h"
#import "ios/web/js_messaging/crw_js_injector.h"
#import "ios/web/navigation/crw_wk_navigation_states.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#import "ios/web/navigation/wk_navigation_util.h"
#include "ios/web/public/deprecated/url_verification_constants.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/web_client.h"
#include "ios/web/public/web_state_observer.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#import "ios/web/web_state/web_state_impl.h"
#include "url/url_constants.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::kWaitForActionTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::kWaitForPageLoadTimeout;
namespace {
// Returns CRWWebController for the given |web_state|.
CRWWebController* GetWebController(web::WebState* web_state) {
web::WebStateImpl* web_state_impl =
static_cast<web::WebStateImpl*>(web_state);
return web_state_impl->GetWebController();
}
} // namespace
namespace web {
WebTestWithWebState::WebTestWithWebState(WebTaskEnvironment::Options options)
: WebTest(options) {}
WebTestWithWebState::WebTestWithWebState(
std::unique_ptr<web::WebClient> web_client,
WebTaskEnvironment::Options options)
: WebTest(std::move(web_client), options) {}
WebTestWithWebState::~WebTestWithWebState() {}
void WebTestWithWebState::SetUp() {
WebTest::SetUp();
web::WebState::CreateParams params(GetBrowserState());
web_state_ = web::WebState::Create(params);
// Force generation of child views; necessary for some tests.
web_state_->GetView();
web_state()->SetKeepRenderProcessAlive(true);
}
void WebTestWithWebState::TearDown() {
DestroyWebState();
WebTest::TearDown();
}
void WebTestWithWebState::AddPendingItem(const GURL& url,
ui::PageTransition transition) {
GetWebController(web_state())
.webStateImpl->GetNavigationManagerImpl()
.AddPendingItem(url, Referrer(), transition,
web::NavigationInitiationType::BROWSER_INITIATED);
}
bool WebTestWithWebState::LoadHtmlWithoutSubresources(const std::string& html) {
NSString* block_all = @"[{"
" \"trigger\": {"
" \"url-filter\": \".*\""
" },"
" \"action\": {"
" \"type\": \"block\""
" }"
"}]";
__block WKContentRuleList* content_rule_list = nil;
__block NSError* error = nil;
__block BOOL rule_compilation_completed = NO;
[WKContentRuleListStore.defaultStore
compileContentRuleListForIdentifier:@"block_everything"
encodedContentRuleList:block_all
completionHandler:^(WKContentRuleList* rule_list,
NSError* err) {
error = err;
content_rule_list = rule_list;
rule_compilation_completed = YES;
}];
bool success = WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
return rule_compilation_completed;
});
if (!success) {
DLOG(WARNING) << "ContentRuleList compilation timed out.";
return false;
}
if (error) {
DLOG(WARNING) << "ContentRuleList compilation failed with error: "
<< base::SysNSStringToUTF8(error.description);
return false;
}
DCHECK(content_rule_list);
WKWebViewConfigurationProvider& configuration_provider =
WKWebViewConfigurationProvider::FromBrowserState(GetBrowserState());
WKWebViewConfiguration* configuration =
configuration_provider.GetWebViewConfiguration();
[configuration.userContentController addContentRuleList:content_rule_list];
bool result = LoadHtml(html);
[configuration.userContentController removeContentRuleList:content_rule_list];
return result;
}
void WebTestWithWebState::LoadHtml(NSString* html, const GURL& url) {
// Initiate asynchronous HTML load.
CRWWebController* web_controller = GetWebController(web_state());
ASSERT_EQ(web::WKNavigationState::FINISHED, web_controller.navigationState);
// If the underlying WKWebView is empty, first load a placeholder to create a
// WKBackForwardListItem to store the NavigationItem associated with the
// |-loadHTML|.
// TODO(crbug.com/777884): consider changing |-loadHTML| to match WKWebView's
// |-loadHTMLString:baseURL| that doesn't create a navigation entry.
if (!web_state()->GetNavigationManager()->GetItemCount()) {
GURL placeholder_url(url::kAboutBlankURL);
if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))
placeholder_url = wk_navigation_util::CreatePlaceholderUrlForUrl(url);
NavigationManager::WebLoadParams params(placeholder_url);
web_state()->GetNavigationManager()->LoadURLWithParams(params);
if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage)) {
// Set NoNavigationError so the placeHolder doesn't trigger a
// kNavigatingToFailedNavigationItem.
web::WebStateImpl* web_state_impl =
static_cast<web::WebStateImpl*>(web_state());
web_state_impl->GetNavigationManagerImpl()
.GetCurrentItemImpl()
->error_retry_state_machine()
.SetIgnorePlaceholderNavigation();
}
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return web_controller.navigationState == web::WKNavigationState::FINISHED;
}));
}
[web_controller loadHTML:html forURL:url];
ASSERT_EQ(web::WKNavigationState::REQUESTED, web_controller.navigationState);
// Wait until the page is loaded.
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
base::RunLoop().RunUntilIdle();
return web_controller.navigationState == web::WKNavigationState::FINISHED;
}));
// Wait until the script execution is possible. Script execution will fail if
// WKUserScript was not jet injected by WKWebView.
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^bool {
return [ExecuteJavaScript(@"0;") isEqual:@0];
}));
}
void WebTestWithWebState::LoadHtml(NSString* html) {
GURL url("https://chromium.test/");
LoadHtml(html, url);
}
bool WebTestWithWebState::LoadHtml(const std::string& html) {
LoadHtml(base::SysUTF8ToNSString(html));
// TODO(crbug.com/780062): LoadHtml(NSString*) should return bool.
return true;
}
void WebTestWithWebState::WaitForBackgroundTasks() {
// Because tasks can add new tasks to either queue, the loop continues until
// the first pass where no activity is seen from either queue.
bool activitySeen = false;
base::CurrentThread messageLoop = base::CurrentThread::Get();
messageLoop->AddTaskObserver(this);
do {
activitySeen = false;
// Yield to the iOS message queue, e.g. [NSObject performSelector:] events.
if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) ==
kCFRunLoopRunHandledSource)
activitySeen = true;
// Yield to the Chromium message queue, e.g. WebThread::PostTask()
// events.
processed_a_task_ = false;
base::RunLoop().RunUntilIdle();
if (processed_a_task_) // Set in TaskObserver method.
activitySeen = true;
} while (activitySeen);
messageLoop->RemoveTaskObserver(this);
}
void WebTestWithWebState::WaitForCondition(ConditionBlock condition) {
base::test::ios::WaitUntilCondition(condition, true,
base::TimeDelta::FromSeconds(1000));
}
bool WebTestWithWebState::WaitUntilLoaded() {
return WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
WaitForBackgroundTasks();
return !web_state()->IsLoading();
});
}
id WebTestWithWebState::ExecuteJavaScript(NSString* script) {
__block id execution_result = nil;
__block bool execution_completed = false;
SCOPED_TRACE(base::SysNSStringToUTF8(script));
[GetWebController(web_state()).jsInjector
executeJavaScript:script
completionHandler:^(id result, NSError* error) {
// Most of executed JS does not return the result, and there is no need
// to log WKErrorJavaScriptResultTypeIsUnsupported error code.
if (error && error.code != WKErrorJavaScriptResultTypeIsUnsupported) {
DLOG(WARNING) << "Script execution of:"
<< base::SysNSStringToUTF8(script)
<< "\nfailed with error: "
<< base::SysNSStringToUTF8(error.description);
}
execution_result = [result copy];
execution_completed = true;
}];
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout,
^{
return execution_completed;
}))
<< "Timed out trying to execute: " << script;
return execution_result;
}
#if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
// Synchronously executes |script| in |content_world| and returns result.
id WebTestWithWebState::ExecuteJavaScript(WKContentWorld* content_world,
NSString* script) {
DCHECK(base::ios::IsRunningOnIOS14OrLater());
WKWebView* web_view = [GetWebController(web_state()) ensureWebViewCreated];
if (@available(ios 14, *)) {
return web::test::ExecuteJavaScript(web_view, content_world, script);
}
return nil;
}
#endif // defined(__IPHONE14_0)
void WebTestWithWebState::DestroyWebState() {
web_state_.reset();
}
std::string WebTestWithWebState::BaseUrl() const {
web::URLVerificationTrustLevel unused_level;
return web_state()->GetCurrentURL(&unused_level).spec();
}
web::WebState* WebTestWithWebState::web_state() {
return web_state_.get();
}
const web::WebState* WebTestWithWebState::web_state() const {
return web_state_.get();
}
void WebTestWithWebState::WillProcessTask(const base::PendingTask&, bool) {
// Nothing to do.
}
void WebTestWithWebState::DidProcessTask(const base::PendingTask&) {
processed_a_task_ = true;
}
} // namespace web