1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ios/web/test/web_test.h"
7 #include "base/base64.h"
8 #include "base/strings/stringprintf.h"
9 #import "base/test/ios/wait_util.h"
10 #import "ios/testing/ocmock_complex_type_helper.h"
11 #import "ios/web/navigation/crw_session_controller.h"
12 #import "ios/web/net/crw_url_verifying_protocol_handler.h"
13 #include "ios/web/public/active_state_manager.h"
14 #include "ios/web/public/referrer.h"
15 #import "ios/web/public/web_state/crw_web_delegate.h"
16 #import "ios/web/web_state/js/crw_js_invoke_parameter_queue.h"
17 #import "ios/web/web_state/ui/crw_wk_web_view_web_controller.h"
18 #import "ios/web/web_state/web_state_impl.h"
19 #include "third_party/ocmock/OCMock/OCMock.h"
21 // Helper Mock to stub out API with C++ objects in arguments.
22 @interface WebDelegateMock : OCMockComplexTypeHelper
25 @implementation WebDelegateMock
26 // Stub implementation always returns YES.
27 - (BOOL)webController:(CRWWebController*)webController
28 shouldOpenURL:(const GURL&)url
29 mainDocumentURL:(const GURL&)mainDocumentURL
30 linkClicked:(BOOL)linkClicked {
40 WebTest::~WebTest() {}
42 void WebTest::SetUp() {
43 PlatformTest::SetUp();
44 web::SetWebClient(&client_);
45 BrowserState::GetActiveStateManager(&browser_state_)->SetActive(true);
48 void WebTest::TearDown() {
49 BrowserState::GetActiveStateManager(&browser_state_)->SetActive(false);
50 web::SetWebClient(nullptr);
51 PlatformTest::TearDown();
56 WebTestBase::WebTestBase() {}
58 WebTestBase::~WebTestBase() {}
60 static int s_html_load_count;
62 void WebTestBase::SetUp() {
65 [NSURLProtocol registerClass:[CRWURLVerifyingProtocolHandler class]];
67 webController_.reset(this->CreateWebController());
69 [webController_ setWebUsageEnabled:YES];
70 // Force generation of child views; necessary for some tests.
71 [webController_ triggerPendingLoad];
72 s_html_load_count = 0;
75 void WebTestBase::TearDown() {
76 [webController_ close];
77 [NSURLProtocol unregisterClass:[CRWURLVerifyingProtocolHandler class]];
81 void WebTestBase::LoadHtml(NSString* html) {
82 LoadHtml([html UTF8String]);
85 void WebTestBase::LoadHtml(const std::string& html) {
86 NSString* load_check = [NSString stringWithFormat:
87 @"<p style=\"display: none;\">%d</p>", s_html_load_count++];
89 std::string marked_html = html + [load_check UTF8String];
90 std::string encoded_html;
91 base::Base64Encode(marked_html, &encoded_html);
92 GURL url("data:text/html;base64," + encoded_html);
95 // Data URLs sometimes lock up navigation, so if the loaded page is not the
96 // one expected, reset the web view. In some cases, document or document.body
97 // does not exist either; also reset in those cases.
98 NSString* inner_html = RunJavaScript(
99 @"(document && document.body && document.body.innerHTML) || 'undefined'");
100 if ([inner_html rangeOfString:load_check].location == NSNotFound) {
101 [webController_ setWebUsageEnabled:NO];
102 [webController_ setWebUsageEnabled:YES];
103 [webController_ triggerPendingLoad];
108 void WebTestBase::LoadURL(const GURL& url) {
109 // First step is to ensure that the web controller has finished any previous
110 // page loads so the new load is not confused.
111 while ([webController_ loadPhase] != PAGE_LOADED)
112 WaitForBackgroundTasks();
113 id originalMockDelegate = [OCMockObject
114 niceMockForProtocol:@protocol(CRWWebDelegate)];
115 id mockDelegate = [[WebDelegateMock alloc]
116 initWithRepresentedObject:originalMockDelegate];
117 id existingDelegate = webController_.get().delegate;
118 webController_.get().delegate = mockDelegate;
120 web::NavigationManagerImpl& navManager =
121 [webController_ webStateImpl]->GetNavigationManagerImpl();
122 navManager.InitializeSession(@"name", nil, NO, 0);
123 [navManager.GetSessionController()
125 referrer:web::Referrer()
126 transition:ui::PAGE_TRANSITION_TYPED
127 rendererInitiated:NO];
129 [webController_ loadCurrentURL];
130 while ([webController_ loadPhase] != PAGE_LOADED)
131 WaitForBackgroundTasks();
132 webController_.get().delegate = existingDelegate;
133 [[webController_ view] layoutIfNeeded];
136 void WebTestBase::WaitForBackgroundTasks() {
137 // Because tasks can add new tasks to either queue, the loop continues until
138 // the first pass where no activity is seen from either queue.
139 bool activitySeen = false;
140 base::MessageLoop* messageLoop = base::MessageLoop::current();
141 messageLoop->AddTaskObserver(this);
143 activitySeen = false;
145 // Yield to the iOS message queue, e.g. [NSObject performSelector:] events.
146 if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) ==
147 kCFRunLoopRunHandledSource)
150 // Yield to the Chromium message queue, e.g. WebThread::PostTask()
152 processed_a_task_ = false;
153 messageLoop->RunUntilIdle();
154 if (processed_a_task_) // Set in TaskObserver method.
157 } while (activitySeen || !MessageQueueIsEmpty());
158 messageLoop->RemoveTaskObserver(this);
161 void WebTestBase::WaitForCondition(ConditionBlock condition) {
162 base::MessageLoop* messageLoop = base::MessageLoop::current();
164 base::test::ios::WaitUntilCondition(condition, messageLoop,
165 base::TimeDelta::FromSeconds(10));
168 bool WebTestBase::MessageQueueIsEmpty() const {
169 // Using this check rather than polymorphism because polymorphising
170 // Chrome*WebViewWebTest would be overengineering. Chrome*WebViewWebTest
171 // inherits from WebTestBase.
172 return [webController_ webViewType] == web::WK_WEB_VIEW_TYPE ||
173 [static_cast<CRWUIWebViewWebController*>(webController_)
174 jsInvokeParameterQueue].isEmpty;
177 NSString* WebTestBase::EvaluateJavaScriptAsString(NSString* script) const {
178 __block base::scoped_nsobject<NSString> evaluationResult;
179 [webController_ evaluateJavaScript:script
180 stringResultHandler:^(NSString* result, NSError* error) {
181 DCHECK([result isKindOfClass:[NSString class]]);
182 evaluationResult.reset([result copy]);
184 base::test::ios::WaitUntilCondition(^bool() {
185 return evaluationResult;
187 return [[evaluationResult retain] autorelease];
190 NSString* WebTestBase::RunJavaScript(NSString* script) {
191 // The platform JSON serializer is used to safely escape the |script| and
192 // decode the result while preserving unicode encoding that can be lost when
193 // converting to Chromium string types.
194 NSError* error = nil;
195 NSData* data = [NSJSONSerialization dataWithJSONObject:@[ script ]
198 DCHECK(data && !error);
199 base::scoped_nsobject<NSString> jsonString(
200 [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
201 // 'eval' is used because it is the only way to stay 100% compatible with
202 // stringByEvaluatingJavaScriptFromString in the event that the script is a
204 NSString* wrappedScript = [NSString stringWithFormat:
206 @" JSON.stringify({" // Expression for the success case.
207 @" result: '' + eval(%@[0])," // '' + converts result to string.
208 @" toJSON: null" // Use default JSON stringifier.
211 @" JSON.stringify({" // Expression for the exception case.
212 @" exception: e.toString(),"
213 @" toJSON: null" // Use default JSON stringifier.
215 @"}", jsonString.get()];
217 // Run asyncronious JavaScript evaluation and wait for its completion.
218 __block base::scoped_nsobject<NSData> evaluationData;
219 [webController_ evaluateJavaScript:wrappedScript
220 stringResultHandler:^(NSString* result, NSError* error) {
221 DCHECK([result length]);
222 evaluationData.reset([[result dataUsingEncoding:
223 NSUTF8StringEncoding] retain]);
225 base::test::ios::WaitUntilCondition(^bool() {
226 return evaluationData;
229 // The output is wrapped in a JSON dictionary to distinguish between an
230 // exception string and a result string.
231 NSDictionary* dictionary = [NSJSONSerialization
232 JSONObjectWithData:evaluationData
235 DCHECK(dictionary && !error);
236 NSString* exception = [dictionary objectForKey:@"exception"];
237 CHECK(!exception) << "Script error: " << [exception UTF8String];
238 return [dictionary objectForKey:@"result"];
241 void WebTestBase::WillProcessTask(const base::PendingTask& pending_task) {
245 void WebTestBase::DidProcessTask(const base::PendingTask& pending_task) {
246 processed_a_task_ = true;
251 CRWWebController* UIWebViewWebTest::CreateWebController() {
252 scoped_ptr<WebStateImpl> web_state_impl(new WebStateImpl(GetBrowserState()));
253 return [[TestWebController alloc] initWithWebState:web_state_impl.Pass()];
256 void UIWebViewWebTest::LoadCommands(NSString* commands,
257 const GURL& origin_url,
258 BOOL user_is_interacting) {
259 [static_cast<CRWUIWebViewWebController*>(webController_)
260 respondToMessageQueue:commands
261 userIsInteracting:user_is_interacting
262 originURL:origin_url];
267 CRWWebController* WKWebViewWebTest::CreateWebController() {
268 scoped_ptr<WebStateImpl> web_state_impl(new WebStateImpl(GetBrowserState()));
269 return [[CRWWKWebViewWebController alloc] initWithWebState:
270 web_state_impl.Pass()];
277 // Declare CRWUIWebViewWebController's (private) implementation of
278 // UIWebViewDelegate.
279 @interface CRWUIWebViewWebController(TestProtocolDeclaration)<UIWebViewDelegate>
282 @implementation TestWebController {
283 BOOL _interceptRequest;
284 BOOL _requestIntercepted;
285 BOOL _invokeShouldStartLoadWithRequestNavigationTypeDone;
288 @synthesize interceptRequest = _interceptRequest;
289 @synthesize requestIntercepted = _requestIntercepted;
290 @synthesize invokeShouldStartLoadWithRequestNavigationTypeDone =
291 _invokeShouldStartLoadWithRequestNavigationTypeDone;
293 - (BOOL)webView:(UIWebView*)webView
294 shouldStartLoadWithRequest:(NSURLRequest*)request
295 navigationType:(UIWebViewNavigationType)navigationType {
296 _invokeShouldStartLoadWithRequestNavigationTypeDone = false;
297 // Conditionally block the request to open a webpage.
298 if (_interceptRequest) {
299 _requestIntercepted = true;
302 BOOL result = [super webView:webView
303 shouldStartLoadWithRequest:request
304 navigationType:navigationType];
305 _invokeShouldStartLoadWithRequestNavigationTypeDone = true;