// 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/spawned_test_server/spawned_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/files/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(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 ExecuteScript(ApplicationConnection* connection, const std::string& script) { html_viewer::TestHTMLViewerPtr test_html_viewer; connection->ConnectToService(&test_html_viewer); scoped_ptr result; test_html_viewer->ExecuteScript(script, [&result](const String& json_string) { result = base::JSONReader::Read(json_string.To()); 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 frames_navigated_; // The set of frames waiting for a navigation to occur. std::set frames_waiting_for_navigate_; // Set of frames waiting for a certain number of children and navigation. scoped_ptr 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/files/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 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::SpawnedTestServer( net::SpawnedTestServer::TYPE_HTTP, net::SpawnedTestServer::kLocalhost, base::FilePath(FILE_PATH_LITERAL("components/test/data/html_viewer")))); ASSERT_TRUE(http_server_->Start()); } void TearDown() override { frame_tree_.reset(); http_server_.reset(); WindowServerTestBase::TearDown(); } scoped_ptr http_server_; scoped_ptr frame_tree_; scoped_ptr frame_tree_delegate_; private: DISALLOW_COPY_AND_ASSIGN(HTMLFrameTest); }; TEST_F(HTMLFrameTest, PageWithSingleFrame) { mus::Window* embed_window = window_manager()->NewWindow(); FrameConnection* root_connection = InitFrameTree( embed_window, "http://127.0.0.1:%u/files/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(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. TEST_F(HTMLFrameTest, ChangeLocationOfChildFrame) { mus::Window* embed_window = window_manager()->NewWindow(); ASSERT_TRUE(InitFrameTree( embed_window, "http://127.0.0.1:%u/files/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(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/files/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(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 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 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 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 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 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