| // Copyright 2015 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. |
| |
| #include "base/auto_reset.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/command_line.h" |
| #include "base/json/json_reader.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/html_viewer/public/interfaces/test_html_viewer.mojom.h" |
| #include "components/mus/public/cpp/tests/window_server_test_base.h" |
| #include "components/mus/public/cpp/window.h" |
| #include "components/mus/public/cpp/window_tree_connection.h" |
| #include "components/web_view/frame.h" |
| #include "components/web_view/frame_connection.h" |
| #include "components/web_view/frame_tree.h" |
| #include "components/web_view/public/interfaces/frame.mojom.h" |
| #include "components/web_view/test_frame_tree_delegate.h" |
| #include "mojo/application/public/cpp/application_impl.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "third_party/mojo_services/src/accessibility/public/interfaces/accessibility.mojom.h" |
| |
| using mus::mojom::WindowTreeClientPtr; |
| using mus::WindowServerTestBase; |
| using web_view::Frame; |
| using web_view::FrameConnection; |
| using web_view::FrameTree; |
| using web_view::FrameTreeDelegate; |
| using web_view::mojom::FrameClient; |
| |
| namespace mojo { |
| |
| namespace { |
| |
| const char kAddFrameWithEmptyPageScript[] = |
| "var iframe = document.createElement(\"iframe\");" |
| "iframe.src = \"http://127.0.0.1:%u/empty_page.html\";" |
| "document.body.appendChild(iframe);"; |
| |
| void OnGotContentHandlerForRoot(bool* got_callback) { |
| *got_callback = true; |
| ignore_result(WindowServerTestBase::QuitRunLoop()); |
| } |
| |
| mojo::ApplicationConnection* ApplicationConnectionForFrame(Frame* frame) { |
| return static_cast<FrameConnection*>(frame->user_data()) |
| ->application_connection(); |
| } |
| |
| std::string GetFrameText(ApplicationConnection* connection) { |
| html_viewer::TestHTMLViewerPtr test_html_viewer; |
| connection->ConnectToService(&test_html_viewer); |
| std::string result; |
| test_html_viewer->GetContentAsText([&result](const String& mojo_string) { |
| result = mojo_string; |
| ASSERT_TRUE(WindowServerTestBase::QuitRunLoop()); |
| }); |
| if (!WindowServerTestBase::DoRunLoopWithTimeout()) |
| ADD_FAILURE() << "Timed out waiting for execute to complete"; |
| // test_html_viewer.WaitForIncomingResponse(); |
| return result; |
| } |
| |
| scoped_ptr<base::Value> ExecuteScript(ApplicationConnection* connection, |
| const std::string& script) { |
| html_viewer::TestHTMLViewerPtr test_html_viewer; |
| connection->ConnectToService(&test_html_viewer); |
| scoped_ptr<base::Value> result; |
| test_html_viewer->ExecuteScript(script, [&result](const String& json_string) { |
| result = base::JSONReader::Read(json_string.To<std::string>()); |
| ASSERT_TRUE(WindowServerTestBase::QuitRunLoop()); |
| }); |
| if (!WindowServerTestBase::DoRunLoopWithTimeout()) |
| ADD_FAILURE() << "Timed out waiting for execute to complete"; |
| return result.Pass(); |
| } |
| |
| // FrameTreeDelegate that can block waiting for navigation to start. |
| class TestFrameTreeDelegateImpl : public web_view::TestFrameTreeDelegate { |
| public: |
| explicit TestFrameTreeDelegateImpl(mojo::ApplicationImpl* app) |
| : TestFrameTreeDelegate(app), frame_tree_(nullptr) {} |
| ~TestFrameTreeDelegateImpl() override {} |
| |
| void set_frame_tree(FrameTree* frame_tree) { frame_tree_ = frame_tree; } |
| |
| // Resets the navigation state for |frame|. |
| void ClearGotNavigate(Frame* frame) { frames_navigated_.erase(frame); } |
| |
| // Waits until |frame| has |count| children and the last child has navigated. |
| bool WaitForChildFrameCount(Frame* frame, size_t count) { |
| if (DidChildNavigate(frame, count)) |
| return true; |
| |
| waiting_for_frame_child_count_.reset(new FrameAndChildCount); |
| waiting_for_frame_child_count_->frame = frame; |
| waiting_for_frame_child_count_->count = count; |
| |
| return WindowServerTestBase::DoRunLoopWithTimeout(); |
| } |
| |
| // Returns true if |frame| has navigated. If |frame| hasn't navigated runs |
| // a nested message loop until |frame| navigates. |
| bool WaitForFrameNavigation(Frame* frame) { |
| if (frames_navigated_.count(frame)) |
| return true; |
| |
| frames_waiting_for_navigate_.insert(frame); |
| return WindowServerTestBase::DoRunLoopWithTimeout(); |
| } |
| |
| // TestFrameTreeDelegate: |
| void DidStartNavigation(Frame* frame) override { |
| frames_navigated_.insert(frame); |
| |
| if (waiting_for_frame_child_count_ && |
| DidChildNavigate(waiting_for_frame_child_count_->frame, |
| waiting_for_frame_child_count_->count)) { |
| waiting_for_frame_child_count_.reset(); |
| ASSERT_TRUE(WindowServerTestBase::QuitRunLoop()); |
| } |
| |
| if (frames_waiting_for_navigate_.count(frame)) { |
| frames_waiting_for_navigate_.erase(frame); |
| ignore_result(WindowServerTestBase::QuitRunLoop()); |
| } |
| } |
| |
| private: |
| struct FrameAndChildCount { |
| Frame* frame; |
| size_t count; |
| }; |
| |
| // Returns true if |frame| has |count| children and the last child frame |
| // has navigated. |
| bool DidChildNavigate(Frame* frame, size_t count) { |
| return ((frame->children().size() == count) && |
| (frames_navigated_.count(frame->children()[count - 1]))); |
| } |
| |
| FrameTree* frame_tree_; |
| // Any time DidStartNavigation() is invoked the frame is added here. Frames |
| // are inserted as void* as this does not track destruction of the frames. |
| std::set<void*> frames_navigated_; |
| |
| // The set of frames waiting for a navigation to occur. |
| std::set<Frame*> frames_waiting_for_navigate_; |
| |
| // Set of frames waiting for a certain number of children and navigation. |
| scoped_ptr<FrameAndChildCount> waiting_for_frame_child_count_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestFrameTreeDelegateImpl); |
| }; |
| |
| } // namespace |
| |
| class HTMLFrameTest : public WindowServerTestBase { |
| public: |
| HTMLFrameTest() {} |
| ~HTMLFrameTest() override {} |
| |
| protected: |
| // Creates the frame tree showing an empty page at the root and adds (via |
| // script) a frame showing the same empty page. |
| Frame* LoadEmptyPageAndCreateFrame() { |
| mus::Window* embed_window = window_manager()->NewWindow(); |
| frame_tree_delegate_.reset( |
| new TestFrameTreeDelegateImpl(application_impl())); |
| FrameConnection* root_connection = InitFrameTree( |
| embed_window, "http://127.0.0.1:%u/empty_page2.html"); |
| if (!root_connection) { |
| ADD_FAILURE() << "unable to establish root connection"; |
| return nullptr; |
| } |
| const std::string frame_text = |
| GetFrameText(root_connection->application_connection()); |
| if (frame_text != "child2") { |
| ADD_FAILURE() << "unexpected text " << frame_text; |
| return nullptr; |
| } |
| |
| return CreateEmptyChildFrame(frame_tree_->root()); |
| } |
| |
| Frame* CreateEmptyChildFrame(Frame* parent) { |
| const size_t initial_frame_count = parent->children().size(); |
| // Dynamically add a new frame. |
| ExecuteScript(ApplicationConnectionForFrame(parent), |
| AddPortToString(kAddFrameWithEmptyPageScript)); |
| |
| frame_tree_delegate_->WaitForChildFrameCount(parent, |
| initial_frame_count + 1); |
| if (HasFatalFailure()) |
| return nullptr; |
| |
| return parent->FindFrame(parent->window()->children().back()->id()); |
| } |
| |
| std::string AddPortToString(const std::string& string) { |
| const uint16_t assigned_port = http_server_->host_port_pair().port(); |
| return base::StringPrintf(string.c_str(), assigned_port); |
| } |
| |
| mojo::URLRequestPtr BuildRequestForURL(const std::string& url_string) { |
| mojo::URLRequestPtr request(mojo::URLRequest::New()); |
| request->url = mojo::String::From(AddPortToString(url_string)); |
| return request.Pass(); |
| } |
| |
| FrameConnection* InitFrameTree(mus::Window* view, |
| const std::string& url_string) { |
| frame_tree_delegate_.reset( |
| new TestFrameTreeDelegateImpl(application_impl())); |
| scoped_ptr<FrameConnection> frame_connection(new FrameConnection); |
| bool got_callback = false; |
| frame_connection->Init( |
| application_impl(), BuildRequestForURL(url_string), |
| base::Bind(&OnGotContentHandlerForRoot, &got_callback)); |
| ignore_result(WindowServerTestBase::DoRunLoopWithTimeout()); |
| if (!got_callback) |
| return nullptr; |
| FrameConnection* result = frame_connection.get(); |
| FrameClient* frame_client = frame_connection->frame_client(); |
| WindowTreeClientPtr tree_client = frame_connection->GetWindowTreeClient(); |
| frame_tree_.reset(new FrameTree( |
| result->GetContentHandlerID(), view, tree_client.Pass(), |
| frame_tree_delegate_.get(), frame_client, frame_connection.Pass(), |
| Frame::ClientPropertyMap(), base::TimeTicks::Now())); |
| frame_tree_delegate_->set_frame_tree(frame_tree_.get()); |
| return result; |
| } |
| |
| // ViewManagerTest: |
| void SetUp() override { |
| WindowServerTestBase::SetUp(); |
| |
| // Start a test server. |
| http_server_.reset(new net::EmbeddedTestServer); |
| http_server_->ServeFilesFromSourceDirectory( |
| "components/test/data/html_viewer"); |
| ASSERT_TRUE(http_server_->Start()); |
| } |
| void TearDown() override { |
| frame_tree_.reset(); |
| http_server_.reset(); |
| WindowServerTestBase::TearDown(); |
| } |
| |
| scoped_ptr<net::EmbeddedTestServer> http_server_; |
| scoped_ptr<FrameTree> frame_tree_; |
| |
| scoped_ptr<TestFrameTreeDelegateImpl> frame_tree_delegate_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(HTMLFrameTest); |
| }; |
| |
| // Crashes on linux_chromium_rel_ng only. http://crbug.com/567337 |
| #if defined(OS_LINUX) |
| #define MAYBE_PageWithSingleFrame DISABLED_PageWithSingleFrame |
| #else |
| #define MAYBE_PageWithSingleFrame PageWithSingleFrame |
| #endif |
| TEST_F(HTMLFrameTest, MAYBE_PageWithSingleFrame) { |
| mus::Window* embed_window = window_manager()->NewWindow(); |
| |
| FrameConnection* root_connection = InitFrameTree( |
| embed_window, "http://127.0.0.1:%u/page_with_single_frame.html"); |
| ASSERT_TRUE(root_connection); |
| |
| ASSERT_EQ("Page with single frame", |
| GetFrameText(root_connection->application_connection())); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| frame_tree_delegate_->WaitForChildFrameCount(frame_tree_->root(), 1u)); |
| |
| ASSERT_EQ(1u, embed_window->children().size()); |
| Frame* child_frame = |
| frame_tree_->root()->FindFrame(embed_window->children()[0]->id()); |
| ASSERT_TRUE(child_frame); |
| |
| ASSERT_EQ("child", |
| GetFrameText(static_cast<FrameConnection*>(child_frame->user_data()) |
| ->application_connection())); |
| } |
| |
| // Creates two frames. The parent navigates the child frame by way of changing |
| // the location of the child frame. |
| // Crashes on linux_chromium_rel_ng only. http://crbug.com/567337 |
| #if defined(OS_LINUX) |
| #define MAYBE_ChangeLocationOfChildFrame DISABLED_ChangeLocationOfChildFrame |
| #else |
| #define MAYBE_ChangeLocationOfChildFrame ChangeLocationOfChildFrame |
| #endif |
| TEST_F(HTMLFrameTest, MAYBE_ChangeLocationOfChildFrame) { |
| mus::Window* embed_window = window_manager()->NewWindow(); |
| |
| ASSERT_TRUE(InitFrameTree( |
| embed_window, "http://127.0.0.1:%u/page_with_single_frame.html")); |
| |
| // page_with_single_frame contains a child frame. The child frame should |
| // create a new View and Frame. |
| ASSERT_NO_FATAL_FAILURE( |
| frame_tree_delegate_->WaitForChildFrameCount(frame_tree_->root(), 1u)); |
| |
| Frame* child_frame = frame_tree_->root()->children().back(); |
| |
| ASSERT_EQ("child", |
| GetFrameText(static_cast<FrameConnection*>(child_frame->user_data()) |
| ->application_connection())); |
| |
| // Change the location and wait for the navigation to occur. |
| const char kNavigateFrame[] = |
| "window.frames[0].location = " |
| "'http://127.0.0.1:%u/empty_page2.html'"; |
| frame_tree_delegate_->ClearGotNavigate(child_frame); |
| ExecuteScript(ApplicationConnectionForFrame(frame_tree_->root()), |
| AddPortToString(kNavigateFrame)); |
| ASSERT_TRUE(frame_tree_delegate_->WaitForFrameNavigation(child_frame)); |
| |
| // There should still only be one frame. |
| ASSERT_EQ(1u, frame_tree_->root()->children().size()); |
| |
| // The navigation should have changed the text of the frame. |
| ASSERT_TRUE(child_frame->user_data()); |
| ASSERT_EQ("child2", |
| GetFrameText(static_cast<FrameConnection*>(child_frame->user_data()) |
| ->application_connection())); |
| } |
| |
| TEST_F(HTMLFrameTest, DynamicallyAddFrameAndVerifyParent) { |
| Frame* child_frame = LoadEmptyPageAndCreateFrame(); |
| ASSERT_TRUE(child_frame); |
| |
| mojo::ApplicationConnection* child_frame_connection = |
| ApplicationConnectionForFrame(child_frame); |
| |
| ASSERT_EQ("child", GetFrameText(child_frame_connection)); |
| // The child's parent should not be itself: |
| const char kGetWindowParentNameScript[] = |
| "window.parent == window ? 'parent is self' : 'parent not self';"; |
| scoped_ptr<base::Value> parent_value( |
| ExecuteScript(child_frame_connection, kGetWindowParentNameScript)); |
| ASSERT_TRUE(parent_value->IsType(base::Value::TYPE_LIST)); |
| base::ListValue* parent_list; |
| ASSERT_TRUE(parent_value->GetAsList(&parent_list)); |
| ASSERT_EQ(1u, parent_list->GetSize()); |
| std::string parent_name; |
| ASSERT_TRUE(parent_list->GetString(0u, &parent_name)); |
| EXPECT_EQ("parent not self", parent_name); |
| } |
| |
| TEST_F(HTMLFrameTest, DynamicallyAddFrameAndSeeNameChange) { |
| Frame* child_frame = LoadEmptyPageAndCreateFrame(); |
| ASSERT_TRUE(child_frame); |
| |
| mojo::ApplicationConnection* child_frame_connection = |
| ApplicationConnectionForFrame(child_frame); |
| |
| // Change the name of the child's window. |
| ExecuteScript(child_frame_connection, "window.name = 'new_child';"); |
| |
| // Eventually the parent should see the change. There is no convenient way |
| // to observe this change, so we repeatedly ask for it and timeout if we |
| // never get the right value. |
| const base::TimeTicks start_time(base::TimeTicks::Now()); |
| std::string find_window_result; |
| do { |
| find_window_result.clear(); |
| scoped_ptr<base::Value> script_value( |
| ExecuteScript(ApplicationConnectionForFrame(frame_tree_->root()), |
| "window.frames['new_child'] != null ? 'found frame' : " |
| "'unable to find frame';")); |
| if (script_value->IsType(base::Value::TYPE_LIST)) { |
| base::ListValue* script_value_as_list; |
| if (script_value->GetAsList(&script_value_as_list) && |
| script_value_as_list->GetSize() == 1) { |
| script_value_as_list->GetString(0u, &find_window_result); |
| } |
| } |
| } while (find_window_result != "found frame" && |
| base::TimeTicks::Now() - start_time < |
| TestTimeouts::action_timeout()); |
| EXPECT_EQ("found frame", find_window_result); |
| } |
| |
| // Triggers dynamic addition and removal of a frame. |
| TEST_F(HTMLFrameTest, FrameTreeOfThreeLevels) { |
| // Create a child frame, and in that child frame create another child frame. |
| Frame* child_frame = LoadEmptyPageAndCreateFrame(); |
| ASSERT_TRUE(child_frame); |
| |
| ASSERT_TRUE(CreateEmptyChildFrame(child_frame)); |
| |
| // Make sure the parent can see the child and child's child. There is no |
| // convenient way to observe this change, so we repeatedly ask for it and |
| // timeout if we never get the right value. |
| const char kGetChildChildFrameCount[] = |
| "if (window.frames.length > 0)" |
| " window.frames[0].frames.length.toString();" |
| "else" |
| " '0';"; |
| const base::TimeTicks start_time(base::TimeTicks::Now()); |
| std::string child_child_frame_count; |
| do { |
| child_child_frame_count.clear(); |
| scoped_ptr<base::Value> script_value( |
| ExecuteScript(ApplicationConnectionForFrame(frame_tree_->root()), |
| kGetChildChildFrameCount)); |
| if (script_value->IsType(base::Value::TYPE_LIST)) { |
| base::ListValue* script_value_as_list; |
| if (script_value->GetAsList(&script_value_as_list) && |
| script_value_as_list->GetSize() == 1) { |
| script_value_as_list->GetString(0u, &child_child_frame_count); |
| } |
| } |
| } while (child_child_frame_count != "1" && |
| base::TimeTicks::Now() - start_time < |
| TestTimeouts::action_timeout()); |
| EXPECT_EQ("1", child_child_frame_count); |
| |
| // Remove the child's child and make sure the root doesn't see it anymore. |
| const char kRemoveLastIFrame[] = |
| "document.body.removeChild(document.body.lastChild);"; |
| ExecuteScript(ApplicationConnectionForFrame(child_frame), kRemoveLastIFrame); |
| do { |
| child_child_frame_count.clear(); |
| scoped_ptr<base::Value> script_value( |
| ExecuteScript(ApplicationConnectionForFrame(frame_tree_->root()), |
| kGetChildChildFrameCount)); |
| if (script_value->IsType(base::Value::TYPE_LIST)) { |
| base::ListValue* script_value_as_list; |
| if (script_value->GetAsList(&script_value_as_list) && |
| script_value_as_list->GetSize() == 1) { |
| script_value_as_list->GetString(0u, &child_child_frame_count); |
| } |
| } |
| } while (child_child_frame_count != "0" && |
| base::TimeTicks::Now() - start_time < |
| TestTimeouts::action_timeout()); |
| ASSERT_EQ("0", child_child_frame_count); |
| } |
| |
| // Verifies PostMessage() works across frames. |
| TEST_F(HTMLFrameTest, PostMessage) { |
| Frame* child_frame = LoadEmptyPageAndCreateFrame(); |
| ASSERT_TRUE(child_frame); |
| |
| mojo::ApplicationConnection* child_frame_connection = |
| ApplicationConnectionForFrame(child_frame); |
| ASSERT_EQ("child", GetFrameText(child_frame_connection)); |
| |
| // Register an event handler in the child frame. |
| const char kRegisterPostMessageHandler[] = |
| "window.messageData = null;" |
| "function messageFunction(event) {" |
| " window.messageData = event.data;" |
| "}" |
| "window.addEventListener('message', messageFunction, false);"; |
| ExecuteScript(child_frame_connection, kRegisterPostMessageHandler); |
| |
| // Post a message from the parent to the child. |
| const char kPostMessageFromParent[] = |
| "window.frames[0].postMessage('hello from parent', '*');"; |
| ExecuteScript(ApplicationConnectionForFrame(frame_tree_->root()), |
| kPostMessageFromParent); |
| |
| // Wait for the child frame to see the message. |
| const base::TimeTicks start_time(base::TimeTicks::Now()); |
| std::string message_in_child; |
| do { |
| const char kGetMessageData[] = "window.messageData;"; |
| scoped_ptr<base::Value> script_value( |
| ExecuteScript(child_frame_connection, kGetMessageData)); |
| if (script_value->IsType(base::Value::TYPE_LIST)) { |
| base::ListValue* script_value_as_list; |
| if (script_value->GetAsList(&script_value_as_list) && |
| script_value_as_list->GetSize() == 1) { |
| script_value_as_list->GetString(0u, &message_in_child); |
| } |
| } |
| } while (message_in_child != "hello from parent" && |
| base::TimeTicks::Now() - start_time < |
| TestTimeouts::action_timeout()); |
| EXPECT_EQ("hello from parent", message_in_child); |
| } |
| |
| } // namespace mojo |