OLD | NEW |
| (Empty) |
1 // Copyright 2015 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. | |
4 | |
5 #include <stddef.h> | |
6 #include <stdint.h> | |
7 #include <utility> | |
8 | |
9 #include "base/auto_reset.h" | |
10 #include "base/bind.h" | |
11 #include "base/callback.h" | |
12 #include "base/command_line.h" | |
13 #include "base/json/json_reader.h" | |
14 #include "base/macros.h" | |
15 #include "base/run_loop.h" | |
16 #include "base/strings/stringprintf.h" | |
17 #include "base/test/test_timeouts.h" | |
18 #include "base/time/time.h" | |
19 #include "base/values.h" | |
20 #include "build/build_config.h" | |
21 #include "components/html_viewer/public/interfaces/test_html_viewer.mojom.h" | |
22 #include "components/mus/public/cpp/tests/window_server_test_base.h" | |
23 #include "components/mus/public/cpp/window.h" | |
24 #include "components/mus/public/cpp/window_tree_connection.h" | |
25 #include "components/web_view/frame.h" | |
26 #include "components/web_view/frame_connection.h" | |
27 #include "components/web_view/frame_tree.h" | |
28 #include "components/web_view/public/interfaces/frame.mojom.h" | |
29 #include "components/web_view/test_frame_tree_delegate.h" | |
30 #include "mojo/services/accessibility/public/interfaces/accessibility.mojom.h" | |
31 #include "net/test/embedded_test_server/embedded_test_server.h" | |
32 | |
33 using mus::mojom::WindowTreeClientPtr; | |
34 using mus::WindowServerTestBase; | |
35 using web_view::Frame; | |
36 using web_view::FrameConnection; | |
37 using web_view::FrameTree; | |
38 using web_view::FrameTreeDelegate; | |
39 using web_view::mojom::FrameClient; | |
40 | |
41 namespace mojo { | |
42 | |
43 namespace { | |
44 | |
45 const char kAddFrameWithEmptyPageScript[] = | |
46 "var iframe = document.createElement(\"iframe\");" | |
47 "iframe.src = \"http://127.0.0.1:%u/empty_page.html\";" | |
48 "document.body.appendChild(iframe);"; | |
49 | |
50 void OnGotContentHandlerForRoot(bool* got_callback) { | |
51 *got_callback = true; | |
52 ignore_result(WindowServerTestBase::QuitRunLoop()); | |
53 } | |
54 | |
55 mojo::Connection* ConnectionForFrame(Frame* frame) { | |
56 return static_cast<FrameConnection*>(frame->user_data())->connection(); | |
57 } | |
58 | |
59 std::string GetFrameText(Connection* connection) { | |
60 html_viewer::TestHTMLViewerPtr test_html_viewer; | |
61 connection->ConnectToService(&test_html_viewer); | |
62 std::string result; | |
63 test_html_viewer->GetContentAsText([&result](const String& mojo_string) { | |
64 result = mojo_string; | |
65 ASSERT_TRUE(WindowServerTestBase::QuitRunLoop()); | |
66 }); | |
67 if (!WindowServerTestBase::DoRunLoopWithTimeout()) | |
68 ADD_FAILURE() << "Timed out waiting for execute to complete"; | |
69 // test_html_viewer.WaitForIncomingResponse(); | |
70 return result; | |
71 } | |
72 | |
73 scoped_ptr<base::Value> ExecuteScript(Connection* connection, | |
74 const std::string& script) { | |
75 html_viewer::TestHTMLViewerPtr test_html_viewer; | |
76 connection->ConnectToService(&test_html_viewer); | |
77 scoped_ptr<base::Value> result; | |
78 test_html_viewer->ExecuteScript(script, [&result](const String& json_string) { | |
79 result = base::JSONReader::Read(json_string.To<std::string>()); | |
80 ASSERT_TRUE(WindowServerTestBase::QuitRunLoop()); | |
81 }); | |
82 if (!WindowServerTestBase::DoRunLoopWithTimeout()) | |
83 ADD_FAILURE() << "Timed out waiting for execute to complete"; | |
84 return result; | |
85 } | |
86 | |
87 // FrameTreeDelegate that can block waiting for navigation to start. | |
88 class TestFrameTreeDelegateImpl : public web_view::TestFrameTreeDelegate { | |
89 public: | |
90 explicit TestFrameTreeDelegateImpl(mojo::Shell* shell) | |
91 : TestFrameTreeDelegate(shell), frame_tree_(nullptr) {} | |
92 ~TestFrameTreeDelegateImpl() override {} | |
93 | |
94 void set_frame_tree(FrameTree* frame_tree) { frame_tree_ = frame_tree; } | |
95 | |
96 // Resets the navigation state for |frame|. | |
97 void ClearGotNavigate(Frame* frame) { frames_navigated_.erase(frame); } | |
98 | |
99 // Waits until |frame| has |count| children and the last child has navigated. | |
100 bool WaitForChildFrameCount(Frame* frame, size_t count) { | |
101 if (DidChildNavigate(frame, count)) | |
102 return true; | |
103 | |
104 waiting_for_frame_child_count_.reset(new FrameAndChildCount); | |
105 waiting_for_frame_child_count_->frame = frame; | |
106 waiting_for_frame_child_count_->count = count; | |
107 | |
108 return WindowServerTestBase::DoRunLoopWithTimeout(); | |
109 } | |
110 | |
111 // Returns true if |frame| has navigated. If |frame| hasn't navigated runs | |
112 // a nested message loop until |frame| navigates. | |
113 bool WaitForFrameNavigation(Frame* frame) { | |
114 if (frames_navigated_.count(frame)) | |
115 return true; | |
116 | |
117 frames_waiting_for_navigate_.insert(frame); | |
118 return WindowServerTestBase::DoRunLoopWithTimeout(); | |
119 } | |
120 | |
121 // TestFrameTreeDelegate: | |
122 void DidStartNavigation(Frame* frame) override { | |
123 frames_navigated_.insert(frame); | |
124 | |
125 if (waiting_for_frame_child_count_ && | |
126 DidChildNavigate(waiting_for_frame_child_count_->frame, | |
127 waiting_for_frame_child_count_->count)) { | |
128 waiting_for_frame_child_count_.reset(); | |
129 ASSERT_TRUE(WindowServerTestBase::QuitRunLoop()); | |
130 } | |
131 | |
132 if (frames_waiting_for_navigate_.count(frame)) { | |
133 frames_waiting_for_navigate_.erase(frame); | |
134 ignore_result(WindowServerTestBase::QuitRunLoop()); | |
135 } | |
136 } | |
137 | |
138 private: | |
139 struct FrameAndChildCount { | |
140 Frame* frame; | |
141 size_t count; | |
142 }; | |
143 | |
144 // Returns true if |frame| has |count| children and the last child frame | |
145 // has navigated. | |
146 bool DidChildNavigate(Frame* frame, size_t count) { | |
147 return ((frame->children().size() == count) && | |
148 (frames_navigated_.count(frame->children()[count - 1]))); | |
149 } | |
150 | |
151 FrameTree* frame_tree_; | |
152 // Any time DidStartNavigation() is invoked the frame is added here. Frames | |
153 // are inserted as void* as this does not track destruction of the frames. | |
154 std::set<void*> frames_navigated_; | |
155 | |
156 // The set of frames waiting for a navigation to occur. | |
157 std::set<Frame*> frames_waiting_for_navigate_; | |
158 | |
159 // Set of frames waiting for a certain number of children and navigation. | |
160 scoped_ptr<FrameAndChildCount> waiting_for_frame_child_count_; | |
161 | |
162 DISALLOW_COPY_AND_ASSIGN(TestFrameTreeDelegateImpl); | |
163 }; | |
164 | |
165 } // namespace | |
166 | |
167 class HTMLFrameTest : public WindowServerTestBase { | |
168 public: | |
169 HTMLFrameTest() {} | |
170 ~HTMLFrameTest() override {} | |
171 | |
172 protected: | |
173 // Creates the frame tree showing an empty page at the root and adds (via | |
174 // script) a frame showing the same empty page. | |
175 Frame* LoadEmptyPageAndCreateFrame() { | |
176 mus::Window* embed_window = window_manager()->NewWindow(); | |
177 frame_tree_delegate_.reset(new TestFrameTreeDelegateImpl(shell())); | |
178 FrameConnection* root_connection = InitFrameTree( | |
179 embed_window, "http://127.0.0.1:%u/empty_page2.html"); | |
180 if (!root_connection) { | |
181 ADD_FAILURE() << "unable to establish root connection"; | |
182 return nullptr; | |
183 } | |
184 const std::string frame_text = GetFrameText(root_connection->connection()); | |
185 if (frame_text != "child2") { | |
186 ADD_FAILURE() << "unexpected text " << frame_text; | |
187 return nullptr; | |
188 } | |
189 | |
190 return CreateEmptyChildFrame(frame_tree_->root()); | |
191 } | |
192 | |
193 Frame* CreateEmptyChildFrame(Frame* parent) { | |
194 const size_t initial_frame_count = parent->children().size(); | |
195 // Dynamically add a new frame. | |
196 ExecuteScript(ConnectionForFrame(parent), | |
197 AddPortToString(kAddFrameWithEmptyPageScript)); | |
198 | |
199 frame_tree_delegate_->WaitForChildFrameCount(parent, | |
200 initial_frame_count + 1); | |
201 if (HasFatalFailure()) | |
202 return nullptr; | |
203 | |
204 return parent->FindFrame(parent->window()->children().back()->id()); | |
205 } | |
206 | |
207 std::string AddPortToString(const std::string& string) { | |
208 const uint16_t assigned_port = http_server_->host_port_pair().port(); | |
209 return base::StringPrintf(string.c_str(), assigned_port); | |
210 } | |
211 | |
212 mojo::URLRequestPtr BuildRequestForURL(const std::string& url_string) { | |
213 mojo::URLRequestPtr request(mojo::URLRequest::New()); | |
214 request->url = mojo::String::From(AddPortToString(url_string)); | |
215 return request; | |
216 } | |
217 | |
218 FrameConnection* InitFrameTree(mus::Window* view, | |
219 const std::string& url_string) { | |
220 frame_tree_delegate_.reset(new TestFrameTreeDelegateImpl(shell())); | |
221 scoped_ptr<FrameConnection> frame_connection(new FrameConnection); | |
222 bool got_callback = false; | |
223 frame_connection->Init( | |
224 shell(), BuildRequestForURL(url_string), | |
225 base::Bind(&OnGotContentHandlerForRoot, &got_callback)); | |
226 ignore_result(WindowServerTestBase::DoRunLoopWithTimeout()); | |
227 if (!got_callback) | |
228 return nullptr; | |
229 FrameConnection* result = frame_connection.get(); | |
230 FrameClient* frame_client = frame_connection->frame_client(); | |
231 WindowTreeClientPtr tree_client = frame_connection->GetWindowTreeClient(); | |
232 frame_tree_.reset(new FrameTree( | |
233 result->GetContentHandlerID(), view, std::move(tree_client), | |
234 frame_tree_delegate_.get(), frame_client, std::move(frame_connection), | |
235 Frame::ClientPropertyMap(), base::TimeTicks::Now())); | |
236 frame_tree_delegate_->set_frame_tree(frame_tree_.get()); | |
237 return result; | |
238 } | |
239 | |
240 // ViewManagerTest: | |
241 void SetUp() override { | |
242 WindowServerTestBase::SetUp(); | |
243 | |
244 // Start a test server. | |
245 http_server_.reset(new net::EmbeddedTestServer); | |
246 http_server_->ServeFilesFromSourceDirectory( | |
247 "components/test/data/html_viewer"); | |
248 ASSERT_TRUE(http_server_->Start()); | |
249 } | |
250 void TearDown() override { | |
251 frame_tree_.reset(); | |
252 http_server_.reset(); | |
253 WindowServerTestBase::TearDown(); | |
254 } | |
255 | |
256 scoped_ptr<net::EmbeddedTestServer> http_server_; | |
257 scoped_ptr<FrameTree> frame_tree_; | |
258 | |
259 scoped_ptr<TestFrameTreeDelegateImpl> frame_tree_delegate_; | |
260 | |
261 private: | |
262 DISALLOW_COPY_AND_ASSIGN(HTMLFrameTest); | |
263 }; | |
264 | |
265 // Crashes on linux_chromium_rel_ng only. http://crbug.com/567337 | |
266 #if defined(OS_LINUX) | |
267 #define MAYBE_PageWithSingleFrame DISABLED_PageWithSingleFrame | |
268 #else | |
269 #define MAYBE_PageWithSingleFrame PageWithSingleFrame | |
270 #endif | |
271 TEST_F(HTMLFrameTest, MAYBE_PageWithSingleFrame) { | |
272 mus::Window* embed_window = window_manager()->NewWindow(); | |
273 | |
274 FrameConnection* root_connection = InitFrameTree( | |
275 embed_window, "http://127.0.0.1:%u/page_with_single_frame.html"); | |
276 ASSERT_TRUE(root_connection); | |
277 | |
278 ASSERT_EQ("Page with single frame", | |
279 GetFrameText(root_connection->connection())); | |
280 | |
281 ASSERT_NO_FATAL_FAILURE( | |
282 frame_tree_delegate_->WaitForChildFrameCount(frame_tree_->root(), 1u)); | |
283 | |
284 ASSERT_EQ(1u, embed_window->children().size()); | |
285 Frame* child_frame = | |
286 frame_tree_->root()->FindFrame(embed_window->children()[0]->id()); | |
287 ASSERT_TRUE(child_frame); | |
288 | |
289 ASSERT_EQ("child", | |
290 GetFrameText(static_cast<FrameConnection*>(child_frame->user_data()) | |
291 ->connection())); | |
292 } | |
293 | |
294 // Creates two frames. The parent navigates the child frame by way of changing | |
295 // the location of the child frame. | |
296 // Crashes on linux_chromium_rel_ng only. http://crbug.com/567337 | |
297 #if defined(OS_LINUX) | |
298 #define MAYBE_ChangeLocationOfChildFrame DISABLED_ChangeLocationOfChildFrame | |
299 #else | |
300 #define MAYBE_ChangeLocationOfChildFrame ChangeLocationOfChildFrame | |
301 #endif | |
302 TEST_F(HTMLFrameTest, MAYBE_ChangeLocationOfChildFrame) { | |
303 mus::Window* embed_window = window_manager()->NewWindow(); | |
304 | |
305 ASSERT_TRUE(InitFrameTree( | |
306 embed_window, "http://127.0.0.1:%u/page_with_single_frame.html")); | |
307 | |
308 // page_with_single_frame contains a child frame. The child frame should | |
309 // create a new View and Frame. | |
310 ASSERT_NO_FATAL_FAILURE( | |
311 frame_tree_delegate_->WaitForChildFrameCount(frame_tree_->root(), 1u)); | |
312 | |
313 Frame* child_frame = frame_tree_->root()->children().back(); | |
314 | |
315 ASSERT_EQ("child", | |
316 GetFrameText(static_cast<FrameConnection*>(child_frame->user_data()) | |
317 ->connection())); | |
318 | |
319 // Change the location and wait for the navigation to occur. | |
320 const char kNavigateFrame[] = | |
321 "window.frames[0].location = " | |
322 "'http://127.0.0.1:%u/empty_page2.html'"; | |
323 frame_tree_delegate_->ClearGotNavigate(child_frame); | |
324 ExecuteScript(ConnectionForFrame(frame_tree_->root()), | |
325 AddPortToString(kNavigateFrame)); | |
326 ASSERT_TRUE(frame_tree_delegate_->WaitForFrameNavigation(child_frame)); | |
327 | |
328 // There should still only be one frame. | |
329 ASSERT_EQ(1u, frame_tree_->root()->children().size()); | |
330 | |
331 // The navigation should have changed the text of the frame. | |
332 ASSERT_TRUE(child_frame->user_data()); | |
333 ASSERT_EQ("child2", | |
334 GetFrameText(static_cast<FrameConnection*>(child_frame->user_data()) | |
335 ->connection())); | |
336 } | |
337 | |
338 TEST_F(HTMLFrameTest, DynamicallyAddFrameAndVerifyParent) { | |
339 Frame* child_frame = LoadEmptyPageAndCreateFrame(); | |
340 ASSERT_TRUE(child_frame); | |
341 | |
342 mojo::Connection* child_frame_connection = ConnectionForFrame(child_frame); | |
343 | |
344 ASSERT_EQ("child", GetFrameText(child_frame_connection)); | |
345 // The child's parent should not be itself: | |
346 const char kGetWindowParentNameScript[] = | |
347 "window.parent == window ? 'parent is self' : 'parent not self';"; | |
348 scoped_ptr<base::Value> parent_value( | |
349 ExecuteScript(child_frame_connection, kGetWindowParentNameScript)); | |
350 ASSERT_TRUE(parent_value->IsType(base::Value::TYPE_LIST)); | |
351 base::ListValue* parent_list; | |
352 ASSERT_TRUE(parent_value->GetAsList(&parent_list)); | |
353 ASSERT_EQ(1u, parent_list->GetSize()); | |
354 std::string parent_name; | |
355 ASSERT_TRUE(parent_list->GetString(0u, &parent_name)); | |
356 EXPECT_EQ("parent not self", parent_name); | |
357 } | |
358 | |
359 TEST_F(HTMLFrameTest, DynamicallyAddFrameAndSeeNameChange) { | |
360 Frame* child_frame = LoadEmptyPageAndCreateFrame(); | |
361 ASSERT_TRUE(child_frame); | |
362 | |
363 mojo::Connection* child_frame_connection = ConnectionForFrame(child_frame); | |
364 | |
365 // Change the name of the child's window. | |
366 ExecuteScript(child_frame_connection, "window.name = 'new_child';"); | |
367 | |
368 // Eventually the parent should see the change. There is no convenient way | |
369 // to observe this change, so we repeatedly ask for it and timeout if we | |
370 // never get the right value. | |
371 const base::TimeTicks start_time(base::TimeTicks::Now()); | |
372 std::string find_window_result; | |
373 do { | |
374 find_window_result.clear(); | |
375 scoped_ptr<base::Value> script_value( | |
376 ExecuteScript(ConnectionForFrame(frame_tree_->root()), | |
377 "window.frames['new_child'] != null ? 'found frame' : " | |
378 "'unable to find frame';")); | |
379 if (script_value->IsType(base::Value::TYPE_LIST)) { | |
380 base::ListValue* script_value_as_list; | |
381 if (script_value->GetAsList(&script_value_as_list) && | |
382 script_value_as_list->GetSize() == 1) { | |
383 script_value_as_list->GetString(0u, &find_window_result); | |
384 } | |
385 } | |
386 } while (find_window_result != "found frame" && | |
387 base::TimeTicks::Now() - start_time < | |
388 TestTimeouts::action_timeout()); | |
389 EXPECT_EQ("found frame", find_window_result); | |
390 } | |
391 | |
392 // Triggers dynamic addition and removal of a frame. | |
393 TEST_F(HTMLFrameTest, FrameTreeOfThreeLevels) { | |
394 // Create a child frame, and in that child frame create another child frame. | |
395 Frame* child_frame = LoadEmptyPageAndCreateFrame(); | |
396 ASSERT_TRUE(child_frame); | |
397 | |
398 ASSERT_TRUE(CreateEmptyChildFrame(child_frame)); | |
399 | |
400 // Make sure the parent can see the child and child's child. There is no | |
401 // convenient way to observe this change, so we repeatedly ask for it and | |
402 // timeout if we never get the right value. | |
403 const char kGetChildChildFrameCount[] = | |
404 "if (window.frames.length > 0)" | |
405 " window.frames[0].frames.length.toString();" | |
406 "else" | |
407 " '0';"; | |
408 const base::TimeTicks start_time(base::TimeTicks::Now()); | |
409 std::string child_child_frame_count; | |
410 do { | |
411 child_child_frame_count.clear(); | |
412 scoped_ptr<base::Value> script_value( | |
413 ExecuteScript(ConnectionForFrame(frame_tree_->root()), | |
414 kGetChildChildFrameCount)); | |
415 if (script_value->IsType(base::Value::TYPE_LIST)) { | |
416 base::ListValue* script_value_as_list; | |
417 if (script_value->GetAsList(&script_value_as_list) && | |
418 script_value_as_list->GetSize() == 1) { | |
419 script_value_as_list->GetString(0u, &child_child_frame_count); | |
420 } | |
421 } | |
422 } while (child_child_frame_count != "1" && | |
423 base::TimeTicks::Now() - start_time < | |
424 TestTimeouts::action_timeout()); | |
425 EXPECT_EQ("1", child_child_frame_count); | |
426 | |
427 // Remove the child's child and make sure the root doesn't see it anymore. | |
428 const char kRemoveLastIFrame[] = | |
429 "document.body.removeChild(document.body.lastChild);"; | |
430 ExecuteScript(ConnectionForFrame(child_frame), kRemoveLastIFrame); | |
431 do { | |
432 child_child_frame_count.clear(); | |
433 scoped_ptr<base::Value> script_value( | |
434 ExecuteScript(ConnectionForFrame(frame_tree_->root()), | |
435 kGetChildChildFrameCount)); | |
436 if (script_value->IsType(base::Value::TYPE_LIST)) { | |
437 base::ListValue* script_value_as_list; | |
438 if (script_value->GetAsList(&script_value_as_list) && | |
439 script_value_as_list->GetSize() == 1) { | |
440 script_value_as_list->GetString(0u, &child_child_frame_count); | |
441 } | |
442 } | |
443 } while (child_child_frame_count != "0" && | |
444 base::TimeTicks::Now() - start_time < | |
445 TestTimeouts::action_timeout()); | |
446 ASSERT_EQ("0", child_child_frame_count); | |
447 } | |
448 | |
449 // Verifies PostMessage() works across frames. | |
450 TEST_F(HTMLFrameTest, PostMessage) { | |
451 Frame* child_frame = LoadEmptyPageAndCreateFrame(); | |
452 ASSERT_TRUE(child_frame); | |
453 | |
454 mojo::Connection* child_frame_connection = ConnectionForFrame(child_frame); | |
455 ASSERT_EQ("child", GetFrameText(child_frame_connection)); | |
456 | |
457 // Register an event handler in the child frame. | |
458 const char kRegisterPostMessageHandler[] = | |
459 "window.messageData = null;" | |
460 "function messageFunction(event) {" | |
461 " window.messageData = event.data;" | |
462 "}" | |
463 "window.addEventListener('message', messageFunction, false);"; | |
464 ExecuteScript(child_frame_connection, kRegisterPostMessageHandler); | |
465 | |
466 // Post a message from the parent to the child. | |
467 const char kPostMessageFromParent[] = | |
468 "window.frames[0].postMessage('hello from parent', '*');"; | |
469 ExecuteScript(ConnectionForFrame(frame_tree_->root()), | |
470 kPostMessageFromParent); | |
471 | |
472 // Wait for the child frame to see the message. | |
473 const base::TimeTicks start_time(base::TimeTicks::Now()); | |
474 std::string message_in_child; | |
475 do { | |
476 const char kGetMessageData[] = "window.messageData;"; | |
477 scoped_ptr<base::Value> script_value( | |
478 ExecuteScript(child_frame_connection, kGetMessageData)); | |
479 if (script_value->IsType(base::Value::TYPE_LIST)) { | |
480 base::ListValue* script_value_as_list; | |
481 if (script_value->GetAsList(&script_value_as_list) && | |
482 script_value_as_list->GetSize() == 1) { | |
483 script_value_as_list->GetString(0u, &message_in_child); | |
484 } | |
485 } | |
486 } while (message_in_child != "hello from parent" && | |
487 base::TimeTicks::Now() - start_time < | |
488 TestTimeouts::action_timeout()); | |
489 EXPECT_EQ("hello from parent", message_in_child); | |
490 } | |
491 | |
492 } // namespace mojo | |
OLD | NEW |