// Copyright 2018 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 #include "base/macros.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/http_request.h" #include "net/url_request/url_request_context.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/url_constants.h" #include "webrunner/browser/frame_impl.h" #include "webrunner/browser/test_common.h" #include "webrunner/browser/webrunner_browser_test.h" #include "webrunner/service/common.h" namespace webrunner { using testing::_; using testing::AllOf; using testing::Field; using testing::InvokeWithoutArgs; using testing::Mock; // Use a shorter name for NavigationEvent, because it is // referenced frequently in this file. using NavigationDetails = chromium::web::NavigationEvent; const char kPage1Path[] = "/title1.html"; const char kPage2Path[] = "/title2.html"; const char kPage1Title[] = "title 1"; const char kPage2Title[] = "title 2"; const char kDataUrl[] = "data:text/html;base64,PGI+SGVsbG8sIHdvcmxkLi4uPC9iPg=="; MATCHER(IsSet, "Checks if an optional field is set.") { return !arg.is_null(); } // Defines a suite of tests that exercise Frame-level functionality, such as // navigation commands and page events. class FrameImplTest : public WebRunnerBrowserTest { public: FrameImplTest() = default; ~FrameImplTest() = default; MOCK_METHOD1(OnServeHttpRequest, void(const net::test_server::HttpRequest& request)); protected: // Creates a Frame with |navigation_observer_| attached. chromium::web::FramePtr CreateFrame() { return WebRunnerBrowserTest::CreateFrame(&navigation_observer_); } // Navigates a |controller| to |url|, blocking until navigation is complete. void CheckLoadUrl(const std::string& url, const std::string& expected_title, chromium::web::NavigationController* controller) { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, expected_title), Field(&NavigationDetails::url, url)))) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(url, nullptr); run_loop.Run(); Mock::VerifyAndClearExpectations(this); navigation_observer_.Acknowledge(); } testing::StrictMock navigation_observer_; private: DISALLOW_COPY_AND_ASSIGN(FrameImplTest); }; class WebContentsDeletionObserver : public content::WebContentsObserver { public: explicit WebContentsDeletionObserver(content::WebContents* web_contents) : content::WebContentsObserver(web_contents) {} MOCK_METHOD1(RenderViewDeleted, void(content::RenderViewHost* render_view_host)); }; // Verifies that the browser will navigate and generate a navigation observer // event when LoadUrl() is called. IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigateFrame) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); CheckLoadUrl(url::kAboutBlankURL, url::kAboutBlankURL, controller.get()); frame.Unbind(); } IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigateDataFrame) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); CheckLoadUrl(kDataUrl, kDataUrl, controller.get()); frame.Unbind(); } IN_PROC_BROWSER_TEST_F(FrameImplTest, FrameDeletedBeforeContext) { chromium::web::FramePtr frame = CreateFrame(); // Process the frame creation message. base::RunLoop().RunUntilIdle(); FrameImpl* frame_impl = context_impl()->GetFrameImplForTest(&frame); WebContentsDeletionObserver deletion_observer( frame_impl->web_contents_for_test()); base::RunLoop run_loop; EXPECT_CALL(deletion_observer, RenderViewDeleted(_)) .WillOnce(InvokeWithoutArgs([&run_loop] { run_loop.Quit(); })); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); controller->LoadUrl(url::kAboutBlankURL, nullptr); frame.Unbind(); run_loop.Run(); // Check that |context| remains bound after the frame is closed. EXPECT_TRUE(context()); } IN_PROC_BROWSER_TEST_F(FrameImplTest, ContextDeletedBeforeFrame) { chromium::web::FramePtr frame = CreateFrame(); EXPECT_TRUE(frame); base::RunLoop run_loop; frame.set_error_handler([&run_loop]() { run_loop.Quit(); }); context().Unbind(); run_loop.Run(); EXPECT_FALSE(frame); } IN_PROC_BROWSER_TEST_F(FrameImplTest, GoBackAndForward) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); ASSERT_TRUE(embedded_test_server()->Start()); GURL title1(embedded_test_server()->GetURL(kPage1Path)); GURL title2(embedded_test_server()->GetURL(kPage2Path)); CheckLoadUrl(title1.spec(), kPage1Title, controller.get()); CheckLoadUrl(title2.spec(), kPage2Title, controller.get()); { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage1Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(InvokeWithoutArgs([&run_loop] { run_loop.Quit(); })); controller->GoBack(); run_loop.Run(); navigation_observer_.Acknowledge(); } // At the top of the navigation entry list; this should be a no-op. controller->GoBack(); // Process the navigation request message. base::RunLoop().RunUntilIdle(); { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage2Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(InvokeWithoutArgs([&run_loop] { run_loop.Quit(); })); controller->GoForward(); run_loop.Run(); navigation_observer_.Acknowledge(); } // At the end of the navigation entry list; this should be a no-op. controller->GoForward(); // Process the navigation request message. base::RunLoop().RunUntilIdle(); } IN_PROC_BROWSER_TEST_F(FrameImplTest, ReloadFrame) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr navigation_controller; frame->GetNavigationController(navigation_controller.NewRequest()); embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &FrameImplTest::OnServeHttpRequest, base::Unretained(this))); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL(kPage1Path)); EXPECT_CALL(*this, OnServeHttpRequest(_)); CheckLoadUrl(url.spec(), kPage1Title, navigation_controller.get()); navigation_observer_.Observe( context_impl()->GetFrameImplForTest(&frame)->web_contents_.get()); // Reload with NO_CACHE. { base::RunLoop run_loop; EXPECT_CALL(*this, OnServeHttpRequest(_)); EXPECT_CALL(navigation_observer_, DidFinishLoad(_, url)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); navigation_controller->Reload(chromium::web::ReloadType::NO_CACHE); run_loop.Run(); Mock::VerifyAndClearExpectations(this); navigation_observer_.Acknowledge(); } // Reload with PARTIAL_CACHE. { base::RunLoop run_loop; EXPECT_CALL(*this, OnServeHttpRequest(_)); EXPECT_CALL(navigation_observer_, DidFinishLoad(_, url)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); navigation_controller->Reload(chromium::web::ReloadType::PARTIAL_CACHE); run_loop.Run(); } } IN_PROC_BROWSER_TEST_F(FrameImplTest, GetVisibleEntry) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); // Verify that a Frame returns a null NavigationEntry prior to receiving any // LoadUrl() calls. { base::RunLoop run_loop; controller->GetVisibleEntry( [&run_loop](std::unique_ptr details) { EXPECT_EQ(nullptr, details.get()); run_loop.Quit(); }); run_loop.Run(); } ASSERT_TRUE(embedded_test_server()->Start()); GURL title1(embedded_test_server()->GetURL(kPage1Path)); GURL title2(embedded_test_server()->GetURL(kPage2Path)); // Navigate to a page. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage1Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(testing::InvokeWithoutArgs([&run_loop] { run_loop.Quit(); })); controller->LoadUrl(title1.spec(), nullptr); run_loop.Run(); navigation_observer_.Acknowledge(); } // Verify that GetVisibleEntry() reflects the new Frame navigation state. { base::RunLoop run_loop; controller->GetVisibleEntry( [&run_loop, &title1](std::unique_ptr details) { EXPECT_TRUE(details); EXPECT_EQ(details->url, title1.spec()); EXPECT_EQ(details->title, kPage1Title); run_loop.Quit(); }); run_loop.Run(); } // Navigate to another page. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage2Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(testing::InvokeWithoutArgs([&run_loop] { run_loop.Quit(); })); controller->LoadUrl(title2.spec(), nullptr); run_loop.Run(); navigation_observer_.Acknowledge(); } // Verify the navigation with GetVisibleEntry(). { base::RunLoop run_loop; controller->GetVisibleEntry( [&run_loop, &title2](std::unique_ptr details) { EXPECT_TRUE(details); EXPECT_EQ(details->url, title2.spec()); EXPECT_EQ(details->title, kPage2Title); run_loop.Quit(); }); run_loop.Run(); } // Navigate back to the first page. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage1Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(testing::InvokeWithoutArgs([&run_loop] { run_loop.Quit(); })); controller->GoBack(); run_loop.Run(); navigation_observer_.Acknowledge(); } // Verify the navigation with GetVisibleEntry(). { base::RunLoop run_loop; controller->GetVisibleEntry( [&run_loop, &title1](std::unique_ptr details) { EXPECT_TRUE(details); EXPECT_EQ(details->url, title1.spec()); EXPECT_EQ(details->title, kPage1Title); run_loop.Quit(); }); run_loop.Run(); } } IN_PROC_BROWSER_TEST_F(FrameImplTest, NoNavigationObserverAttached) { chromium::web::FramePtr frame; context()->CreateFrame(frame.NewRequest()); base::RunLoop().RunUntilIdle(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); ASSERT_TRUE(embedded_test_server()->Start()); GURL title1(embedded_test_server()->GetURL(kPage1Path)); GURL title2(embedded_test_server()->GetURL(kPage2Path)); navigation_observer_.Observe( context_impl()->GetFrameImplForTest(&frame)->web_contents_.get()); { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, DidFinishLoad(_, title1)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title1.spec(), nullptr); run_loop.Run(); } { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, DidFinishLoad(_, title2)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title2.spec(), nullptr); run_loop.Run(); } } // Verifies that a Frame will handle navigation observer disconnection events // gracefully. IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigationObserverDisconnected) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); ASSERT_TRUE(embedded_test_server()->Start()); GURL title1(embedded_test_server()->GetURL(kPage1Path)); GURL title2(embedded_test_server()->GetURL(kPage2Path)); navigation_observer_.Observe( context_impl()->GetFrameImplForTest(&frame)->web_contents_.get()); { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, DidFinishLoad(_, title1)); EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage1Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title1.spec(), nullptr); run_loop.Run(); } // Disconnect the observer & spin the runloop to propagate the disconnection // event over IPC. navigation_observer_bindings().CloseAll(); base::RunLoop().RunUntilIdle(); { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, DidFinishLoad(_, title2)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title2.spec(), nullptr); run_loop.Run(); } } IN_PROC_BROWSER_TEST_F(FrameImplTest, DISABLED_DelayedNavigationEventAck) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); ASSERT_TRUE(embedded_test_server()->Start()); GURL title1(embedded_test_server()->GetURL(kPage1Path)); GURL title2(embedded_test_server()->GetURL(kPage2Path)); // Expect an navigation event here, but deliberately postpone acknowledgement // until the end of the test. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage1Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title1.spec(), nullptr); run_loop.Run(); Mock::VerifyAndClearExpectations(this); } // Since we have blocked NavigationEventObserver's flow, we must observe the // WebContents events directly via a test-only seam. navigation_observer_.Observe( context_impl()->GetFrameImplForTest(&frame)->web_contents_.get()); // Navigate to a second page. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, DidFinishLoad(_, title2)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title2.spec(), nullptr); run_loop.Run(); Mock::VerifyAndClearExpectations(this); } // Navigate to the first page. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, DidFinishLoad(_, title1)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(title1.spec(), nullptr); run_loop.Run(); Mock::VerifyAndClearExpectations(this); } // Since there was no observable change in navigation state since the last // ack, there should be no more NavigationEvents generated. { base::RunLoop run_loop; EXPECT_CALL(navigation_observer_, MockableOnNavigationStateChanged(testing::AllOf( Field(&NavigationDetails::title, kPage1Title), Field(&NavigationDetails::url, IsSet())))) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); navigation_observer_.Acknowledge(); run_loop.Run(); } } // Observes events specific to the Stop() test case. struct WebContentsObserverForStop : public content::WebContentsObserver { using content::WebContentsObserver::Observe; MOCK_METHOD1(DidStartNavigation, void(content::NavigationHandle*)); MOCK_METHOD0(NavigationStopped, void()); }; IN_PROC_BROWSER_TEST_F(FrameImplTest, Stop) { chromium::web::FramePtr frame = CreateFrame(); chromium::web::NavigationControllerPtr controller; frame->GetNavigationController(controller.NewRequest()); ASSERT_TRUE(embedded_test_server()->Start()); // Use a request handler that will accept the connection and stall // indefinitely. GURL hung_url(embedded_test_server()->GetURL("/hung")); WebContentsObserverForStop observer; observer.Observe( context_impl()->GetFrameImplForTest(&frame)->web_contents_.get()); { base::RunLoop run_loop; EXPECT_CALL(observer, DidStartNavigation(_)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->LoadUrl(hung_url.spec(), nullptr); run_loop.Run(); Mock::VerifyAndClearExpectations(this); } EXPECT_TRUE( context_impl()->GetFrameImplForTest(&frame)->web_contents_->IsLoading()); { base::RunLoop run_loop; EXPECT_CALL(observer, NavigationStopped()) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); controller->Stop(); run_loop.Run(); Mock::VerifyAndClearExpectations(this); } EXPECT_FALSE( context_impl()->GetFrameImplForTest(&frame)->web_contents_->IsLoading()); } } // namespace webrunner