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/ui/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 WebTestWithWebController::WebTestWithWebController() {}
58 WebTestWithWebController::~WebTestWithWebController() {}
60 static int s_html_load_count;
62 void WebTestWithWebController::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 WebTestWithWebController::TearDown() {
76 [webController_ close];
77 [NSURLProtocol unregisterClass:[CRWURLVerifyingProtocolHandler class]];
81 void WebTestWithWebController::LoadHtml(NSString* html) {
82 LoadHtml([html UTF8String]);
85 void WebTestWithWebController::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 WebTestWithWebController::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 WebTestWithWebController::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 WebTestWithWebController::WaitForCondition(ConditionBlock condition) {
162 base::MessageLoop* messageLoop = base::MessageLoop::current();
164 base::test::ios::WaitUntilCondition(condition, messageLoop,
165 base::TimeDelta::FromSeconds(10));
168 bool WebTestWithWebController::MessageQueueIsEmpty() const {
169 // Using this check rather than polymorphism because polymorphising
170 // Chrome*WebViewWebTest would be overengineering. Chrome*WebViewWebTest
171 // inherits from WebTestWithWebController.
172 return [webController_ webViewType] == web::WK_WEB_VIEW_TYPE ||
173 [static_cast<CRWUIWebViewWebController*>(webController_)
174 jsInvokeParameterQueue].isEmpty;
177 NSString* WebTestWithWebController::EvaluateJavaScriptAsString(
178 NSString* script) const {
179 __block base::scoped_nsobject<NSString> evaluationResult;
180 [webController_ evaluateJavaScript:script
181 stringResultHandler:^(NSString* result, NSError* error) {
182 DCHECK([result isKindOfClass:[NSString class]]);
183 evaluationResult.reset([result copy]);
185 base::test::ios::WaitUntilCondition(^bool() {
186 return evaluationResult;
188 return [[evaluationResult retain] autorelease];
191 NSString* WebTestWithWebController::RunJavaScript(NSString* script) {
192 // The platform JSON serializer is used to safely escape the |script| and
193 // decode the result while preserving unicode encoding that can be lost when
194 // converting to Chromium string types.
195 NSError* error = nil;
196 NSData* data = [NSJSONSerialization dataWithJSONObject:@[ script ]
199 DCHECK(data && !error);
200 base::scoped_nsobject<NSString> jsonString(
201 [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
202 // 'eval' is used because it is the only way to stay 100% compatible with
203 // stringByEvaluatingJavaScriptFromString in the event that the script is a
205 NSString* wrappedScript = [NSString stringWithFormat:
207 @" JSON.stringify({" // Expression for the success case.
208 @" result: '' + eval(%@[0])," // '' + converts result to string.
209 @" toJSON: null" // Use default JSON stringifier.
212 @" JSON.stringify({" // Expression for the exception case.
213 @" exception: e.toString(),"
214 @" toJSON: null" // Use default JSON stringifier.
216 @"}", jsonString.get()];
218 // Run asyncronious JavaScript evaluation and wait for its completion.
219 __block base::scoped_nsobject<NSData> evaluationData;
220 [webController_ evaluateJavaScript:wrappedScript
221 stringResultHandler:^(NSString* result, NSError* error) {
222 DCHECK([result length]);
223 evaluationData.reset([[result dataUsingEncoding:
224 NSUTF8StringEncoding] retain]);
226 base::test::ios::WaitUntilCondition(^bool() {
227 return evaluationData;
230 // The output is wrapped in a JSON dictionary to distinguish between an
231 // exception string and a result string.
232 NSDictionary* dictionary = [NSJSONSerialization
233 JSONObjectWithData:evaluationData
236 DCHECK(dictionary && !error);
237 NSString* exception = [dictionary objectForKey:@"exception"];
238 CHECK(!exception) << "Script error: " << [exception UTF8String];
239 return [dictionary objectForKey:@"result"];
242 void WebTestWithWebController::WillProcessTask(
243 const base::PendingTask& pending_task) {
247 void WebTestWithWebController::DidProcessTask(
248 const base::PendingTask& pending_task) {
249 processed_a_task_ = true;
254 CRWWebController* WebTestWithUIWebViewWebController::CreateWebController() {
255 scoped_ptr<WebStateImpl> web_state_impl(new WebStateImpl(GetBrowserState()));
256 return [[TestWebController alloc] initWithWebState:web_state_impl.Pass()];
259 void WebTestWithUIWebViewWebController::LoadCommands(NSString* commands,
260 const GURL& origin_url,
261 BOOL user_is_interacting) {
262 [static_cast<CRWUIWebViewWebController*>(webController_)
263 respondToMessageQueue:commands
264 userIsInteracting:user_is_interacting
265 originURL:origin_url];
270 CRWWebController* WebTestWithWKWebViewWebController::CreateWebController() {
271 scoped_ptr<WebStateImpl> web_state_impl(new WebStateImpl(GetBrowserState()));
272 return [[CRWWKWebViewWebController alloc] initWithWebState:
273 web_state_impl.Pass()];
280 // Declare CRWUIWebViewWebController's (private) implementation of
281 // UIWebViewDelegate.
282 @interface CRWUIWebViewWebController(TestProtocolDeclaration)<UIWebViewDelegate>
285 @implementation TestWebController {
286 BOOL _interceptRequest;
287 BOOL _requestIntercepted;
288 BOOL _invokeShouldStartLoadWithRequestNavigationTypeDone;
291 @synthesize interceptRequest = _interceptRequest;
292 @synthesize requestIntercepted = _requestIntercepted;
293 @synthesize invokeShouldStartLoadWithRequestNavigationTypeDone =
294 _invokeShouldStartLoadWithRequestNavigationTypeDone;
296 - (BOOL)webView:(UIWebView*)webView
297 shouldStartLoadWithRequest:(NSURLRequest*)request
298 navigationType:(UIWebViewNavigationType)navigationType {
299 _invokeShouldStartLoadWithRequestNavigationTypeDone = false;
300 // Conditionally block the request to open a webpage.
301 if (_interceptRequest) {
302 _requestIntercepted = true;
305 BOOL result = [super webView:webView
306 shouldStartLoadWithRequest:request
307 navigationType:navigationType];
308 _invokeShouldStartLoadWithRequestNavigationTypeDone = true;