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.
6 #include "base/command_line.h"
7 #include "base/json/json_reader.h"
8 #include "base/run_loop.h"
9 #include "base/strings/stringprintf.h"
10 #include "base/test/test_timeouts.h"
11 #include "base/values.h"
12 #include "components/html_viewer/public/interfaces/test_html_viewer.mojom.h"
13 #include "components/view_manager/public/cpp/tests/view_manager_test_base.h"
14 #include "components/view_manager/public/cpp/view.h"
15 #include "components/view_manager/public/cpp/view_manager.h"
16 #include "mandoline/tab/frame.h"
17 #include "mandoline/tab/frame_connection.h"
18 #include "mandoline/tab/frame_tree.h"
19 #include "mandoline/tab/public/interfaces/frame_tree.mojom.h"
20 #include "mojo/application/public/cpp/application_impl.h"
21 #include "net/test/spawned_test_server/spawned_test_server.h"
22 #include "third_party/mojo_services/src/accessibility/public/interfaces/accessibility.mojom.h"
24 using mandoline::Frame
;
25 using mandoline::FrameConnection
;
26 using mandoline::FrameTree
;
27 using mandoline::FrameTreeClient
;
33 // Switch to enable out of process iframes.
34 const char kOOPIF
[] = "oopifs";
36 const char kAddFrameWithEmptyPageScript
[] =
37 "var iframe = document.createElement(\"iframe\");"
38 "iframe.src = \"http://127.0.0.1:%u/files/empty_page.html\";"
39 "document.body.appendChild(iframe);";
42 return base::CommandLine::ForCurrentProcess()->HasSwitch(kOOPIF
);
45 mojo::ApplicationConnection
* ApplicationConnectionForFrame(Frame
* frame
) {
46 return static_cast<FrameConnection
*>(frame
->user_data())
47 ->application_connection();
50 std::string
GetFrameText(ApplicationConnection
* connection
) {
51 html_viewer::TestHTMLViewerPtr test_html_viewer
;
52 connection
->ConnectToService(&test_html_viewer
);
54 test_html_viewer
->GetContentAsText([&result
](const String
& mojo_string
) {
56 ASSERT_TRUE(ViewManagerTestBase::QuitRunLoop());
58 if (!ViewManagerTestBase::DoRunLoopWithTimeout())
59 ADD_FAILURE() << "Timed out waiting for execute to complete";
60 // test_html_viewer.WaitForIncomingResponse();
64 scoped_ptr
<base::Value
> ExecuteScript(ApplicationConnection
* connection
,
65 const std::string
& script
) {
66 html_viewer::TestHTMLViewerPtr test_html_viewer
;
67 connection
->ConnectToService(&test_html_viewer
);
68 scoped_ptr
<base::Value
> result
;
69 test_html_viewer
->ExecuteScript(script
, [&result
](const String
& json_string
) {
70 result
= base::JSONReader::Read(json_string
.To
<std::string
>());
71 ASSERT_TRUE(ViewManagerTestBase::QuitRunLoop());
73 if (!ViewManagerTestBase::DoRunLoopWithTimeout())
74 ADD_FAILURE() << "Timed out waiting for execute to complete";
80 class HTMLFrameTest
: public ViewManagerTestBase
{
83 ~HTMLFrameTest() override
{}
86 // Creates the frame tree showing an empty page at the root and adds (via
87 // script) a frame showing the same empty page.
88 Frame
* LoadEmptyPageAndCreateFrame() {
89 View
* embed_view
= window_manager()->CreateView();
90 FrameConnection
* root_connection
=
91 InitFrameTree(embed_view
, "http://127.0.0.1:%u/files/empty_page.html");
92 const std::string frame_text
=
93 GetFrameText(root_connection
->application_connection());
94 if (frame_text
!= "child") {
95 ADD_FAILURE() << "unexpected text " << frame_text
;
99 return CreateEmptyChildFrame(frame_tree_
->root());
102 Frame
* CreateEmptyChildFrame(Frame
* parent
) {
103 const size_t initial_frame_count
= parent
->children().size();
104 // Dynamically add a new frame.
105 ExecuteScript(ApplicationConnectionForFrame(parent
),
106 AddPortToString(kAddFrameWithEmptyPageScript
));
108 // Wait for the frame to appear.
109 if (parent
->children().size() != initial_frame_count
+ 1u &&
110 !WaitForEmbedForDescendant()) {
111 ADD_FAILURE() << "timed out waiting for child";
115 if (parent
->view()->children().size() != initial_frame_count
+ 1u) {
116 ADD_FAILURE() << "unexpected number of children "
117 << parent
->view()->children().size();
121 return parent
->FindFrame(parent
->view()->children().back()->id());
124 std::string
AddPortToString(const std::string
& string
) {
125 const uint16_t assigned_port
= http_server_
->host_port_pair().port();
126 return base::StringPrintf(string
.c_str(), assigned_port
);
129 mojo::URLRequestPtr
BuildRequestForURL(const std::string
& url_string
) {
130 mojo::URLRequestPtr
request(mojo::URLRequest::New());
131 request
->url
= mojo::String::From(AddPortToString(url_string
));
132 return request
.Pass();
135 FrameConnection
* InitFrameTree(View
* view
, const std::string
& url_string
) {
136 scoped_ptr
<FrameConnection
> frame_connection(new FrameConnection
);
137 FrameConnection
* result
= frame_connection
.get();
138 ViewManagerClientPtr view_manager_client
;
139 frame_connection
->Init(application_impl(), BuildRequestForURL(url_string
),
140 &view_manager_client
);
141 FrameTreeClient
* frame_tree_client
= frame_connection
->frame_tree_client();
142 frame_tree_
.reset(new FrameTree(view
, nullptr, frame_tree_client
,
143 frame_connection
.Pass()));
144 view
->Embed(view_manager_client
.Pass());
148 bool WaitForEmbedForDescendant() {
151 embed_run_loop_
.reset(new base::RunLoop
);
152 embed_run_loop_
->Run();
153 embed_run_loop_
.reset();
158 void SetUp() override
{
159 ViewManagerTestBase::SetUp();
161 // Make it so we get OnEmbedForDescendant().
162 window_manager()->SetEmbedRoot();
164 // Start a test server.
165 http_server_
.reset(new net::SpawnedTestServer(
166 net::SpawnedTestServer::TYPE_HTTP
, net::SpawnedTestServer::kLocalhost
,
167 base::FilePath(FILE_PATH_LITERAL("components/test/data/html_viewer"))));
168 ASSERT_TRUE(http_server_
->Start());
170 void TearDown() override
{
172 http_server_
.reset();
173 ViewManagerTestBase::TearDown();
175 void OnEmbedForDescendant(View
* view
,
176 URLRequestPtr request
,
177 ViewManagerClientPtr
* client
) override
{
178 Frame
* frame
= Frame::FindFirstFrameAncestor(view
);
179 scoped_ptr
<FrameConnection
> frame_connection(new FrameConnection
);
180 frame_connection
->Init(application_impl(), request
.Pass(), client
);
181 FrameTreeClient
* frame_tree_client
= frame_connection
->frame_tree_client();
182 frame_tree_
->CreateOrReplaceFrame(frame
, view
, frame_tree_client
,
183 frame_connection
.Pass());
185 embed_run_loop_
->Quit();
188 scoped_ptr
<net::SpawnedTestServer
> http_server_
;
189 scoped_ptr
<FrameTree
> frame_tree_
;
192 // A runloop specifically for OnEmbedForDescendant(). We use a separate
193 // runloop here as it's possible at the time OnEmbedForDescendant() is invoked
194 // a separate RunLoop is already running that we shouldn't quit.
195 scoped_ptr
<base::RunLoop
> embed_run_loop_
;
197 DISALLOW_COPY_AND_ASSIGN(HTMLFrameTest
);
200 TEST_F(HTMLFrameTest
, PageWithSingleFrame
) {
204 View
* embed_view
= window_manager()->CreateView();
206 FrameConnection
* root_connection
= InitFrameTree(
207 embed_view
, "http://127.0.0.1:%u/files/page_with_single_frame.html");
209 ASSERT_EQ("Page with single frame",
210 GetFrameText(root_connection
->application_connection()));
212 // page_with_single_frame contains a child frame. The child frame should
213 // create a new View and Frame.
214 if (frame_tree_
->root()->children().empty())
215 ASSERT_TRUE(WaitForEmbedForDescendant());
217 ASSERT_EQ(1u, embed_view
->children().size());
219 frame_tree_
->root()->FindFrame(embed_view
->children()[0]->id());
220 ASSERT_TRUE(child_frame
);
223 GetFrameText(static_cast<FrameConnection
*>(child_frame
->user_data())
224 ->application_connection()));
227 namespace {} // namespace
229 TEST_F(HTMLFrameTest
, DynamicallyAddFrameAndVerifyParent
) {
233 Frame
* child_frame
= LoadEmptyPageAndCreateFrame();
234 ASSERT_TRUE(child_frame
);
236 mojo::ApplicationConnection
* child_frame_connection
=
237 ApplicationConnectionForFrame(child_frame
);
239 ASSERT_EQ("child", GetFrameText(child_frame_connection
));
240 // The child's parent should not be itself:
241 const char kGetWindowParentNameScript
[] =
242 "window.parent == window ? 'parent is self' : 'parent not self';";
243 scoped_ptr
<base::Value
> parent_value(
244 ExecuteScript(child_frame_connection
, kGetWindowParentNameScript
));
245 ASSERT_TRUE(parent_value
->IsType(base::Value::TYPE_LIST
));
246 base::ListValue
* parent_list
;
247 ASSERT_TRUE(parent_value
->GetAsList(&parent_list
));
248 ASSERT_EQ(1u, parent_list
->GetSize());
249 std::string parent_name
;
250 ASSERT_TRUE(parent_list
->GetString(0u, &parent_name
));
251 EXPECT_EQ("parent not self", parent_name
);
254 TEST_F(HTMLFrameTest
, DynamicallyAddFrameAndSeeNameChange
) {
258 Frame
* child_frame
= LoadEmptyPageAndCreateFrame();
259 ASSERT_TRUE(child_frame
);
261 mojo::ApplicationConnection
* child_frame_connection
=
262 ApplicationConnectionForFrame(child_frame
);
264 // Change the name of the child's window.
265 ExecuteScript(child_frame_connection
, "window.name = 'new_child';");
267 // Eventually the parent should see the change. There is no convenient way
268 // to observe this change, so we repeatedly ask for it and timeout if we
269 // never get the right value.
270 const base::TimeTicks
start_time(base::TimeTicks::Now());
271 std::string find_window_result
;
273 find_window_result
.clear();
274 scoped_ptr
<base::Value
> script_value(
275 ExecuteScript(ApplicationConnectionForFrame(frame_tree_
->root()),
276 "window.frames['new_child'] != null ? 'found frame' : "
277 "'unable to find frame';"));
278 if (script_value
->IsType(base::Value::TYPE_LIST
)) {
279 base::ListValue
* script_value_as_list
;
280 if (script_value
->GetAsList(&script_value_as_list
) &&
281 script_value_as_list
->GetSize() == 1) {
282 script_value_as_list
->GetString(0u, &find_window_result
);
285 } while (find_window_result
!= "found frame" &&
286 base::TimeTicks::Now() - start_time
<
287 TestTimeouts::action_timeout());
288 EXPECT_EQ("found frame", find_window_result
);
291 // Triggers dynamic addition and removal of a frame.
292 TEST_F(HTMLFrameTest
, FrameTreeOfThreeLevels
) {
296 // Create a child frame, and in that child frame create another child frame.
297 Frame
* child_frame
= LoadEmptyPageAndCreateFrame();
298 ASSERT_TRUE(child_frame
);
300 ASSERT_TRUE(CreateEmptyChildFrame(child_frame
));
302 // Make sure the parent can see the child and child's child. There is no
303 // convenient way to observe this change, so we repeatedly ask for it and
304 // timeout if we never get the right value.
305 const char kGetChildChildFrameCount
[] =
306 "if (window.frames.length > 0)"
307 " window.frames[0].frames.length.toString();"
310 const base::TimeTicks
start_time(base::TimeTicks::Now());
311 std::string child_child_frame_count
;
313 child_child_frame_count
.clear();
314 scoped_ptr
<base::Value
> script_value(
315 ExecuteScript(ApplicationConnectionForFrame(frame_tree_
->root()),
316 kGetChildChildFrameCount
));
317 if (script_value
->IsType(base::Value::TYPE_LIST
)) {
318 base::ListValue
* script_value_as_list
;
319 if (script_value
->GetAsList(&script_value_as_list
) &&
320 script_value_as_list
->GetSize() == 1) {
321 script_value_as_list
->GetString(0u, &child_child_frame_count
);
324 } while (child_child_frame_count
!= "1" &&
325 base::TimeTicks::Now() - start_time
<
326 TestTimeouts::action_timeout());
327 EXPECT_EQ("1", child_child_frame_count
);
329 // Remove the child's child and make sure the root doesn't see it anymore.
330 const char kRemoveLastIFrame
[] =
331 "document.body.removeChild(document.body.lastChild);";
332 ExecuteScript(ApplicationConnectionForFrame(child_frame
), kRemoveLastIFrame
);
334 child_child_frame_count
.clear();
335 scoped_ptr
<base::Value
> script_value(
336 ExecuteScript(ApplicationConnectionForFrame(frame_tree_
->root()),
337 kGetChildChildFrameCount
));
338 if (script_value
->IsType(base::Value::TYPE_LIST
)) {
339 base::ListValue
* script_value_as_list
;
340 if (script_value
->GetAsList(&script_value_as_list
) &&
341 script_value_as_list
->GetSize() == 1) {
342 script_value_as_list
->GetString(0u, &child_child_frame_count
);
345 } while (child_child_frame_count
!= "0" &&
346 base::TimeTicks::Now() - start_time
<
347 TestTimeouts::action_timeout());
348 ASSERT_EQ("0", child_child_frame_count
);