| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "components/viz/host/host_frame_sink_manager.h" |
| #include "content/browser/compositor/surface_utils.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "mojo/public/cpp/bindings/sync_call_restrictions.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/default_handlers.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace content { |
| |
| class ViewTransitionBrowserTest : public ContentBrowserTest { |
| public: |
| class TestCondition : public CommitDeferringCondition { |
| public: |
| TestCondition(NavigationRequest& request, base::RunLoop* run_loop) |
| : CommitDeferringCondition(request), run_loop_(run_loop) {} |
| ~TestCondition() override = default; |
| |
| Result WillCommitNavigation(base::OnceClosure resume) override { |
| GetUIThreadTaskRunner()->PostTask(FROM_HERE, run_loop_->QuitClosure()); |
| return Result::kDefer; |
| } |
| |
| private: |
| raw_ptr<base::RunLoop> run_loop_; |
| }; |
| |
| ViewTransitionBrowserTest() { |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/ |
| {blink::features::kViewTransitionOnNavigation}, |
| /*disabled_features=*/{}); |
| } |
| |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_test_server()->ServeFilesFromSourceDirectory( |
| GetTestDataFilePath()); |
| net::test_server::RegisterDefaultHandlers(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| void WaitForConditionsDone(NavigationRequest* request) { |
| // Inject a condition to know when the VT response has been received but |
| // before the NavigationRequest is notified. |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| request->RegisterCommitDeferringConditionForTesting( |
| std::make_unique<TestCondition>(*request, run_loop_.get())); |
| run_loop_->Run(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| std::unique_ptr<base::RunLoop> run_loop_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest, |
| NavigationCancelledAfterScreenshot) { |
| // Start with a page which has an opt-in for VT. |
| GURL test_url( |
| embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url)); |
| |
| TestNavigationManager navigation_manager(shell()->web_contents(), test_url); |
| ASSERT_TRUE( |
| ExecJs(shell()->web_contents(), "location.href = location.href;")); |
| |
| // Wait for response and resume. The navigation should be blocked by the view |
| // transition condition. |
| ASSERT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| |
| auto* navigation_request = |
| NavigationRequest::From(navigation_manager.GetNavigationHandle()); |
| ASSERT_TRUE(navigation_request); |
| ASSERT_TRUE( |
| navigation_request->IsCommitDeferringConditionDeferredForTesting()); |
| ASSERT_FALSE(navigation_request->commit_params().view_transition_state); |
| |
| WaitForConditionsDone(navigation_request); |
| ASSERT_TRUE(navigation_request->commit_params().view_transition_state); |
| |
| mojo::ScopedAllowSyncCallForTesting allow_sync; |
| |
| ASSERT_TRUE( |
| GetHostFrameSinkManager()->HasUnclaimedViewTransitionResourcesForTest()); |
| |
| shell()->web_contents()->Stop(); |
| ASSERT_FALSE(navigation_manager.was_committed()); |
| ASSERT_FALSE( |
| GetHostFrameSinkManager()->HasUnclaimedViewTransitionResourcesForTest()); |
| |
| // Ensure the old renderer discards the outgoing transition. |
| EXPECT_TRUE(ExecJs( |
| shell()->web_contents()->GetPrimaryMainFrame(), |
| "(async () => { await document.startViewTransition().ready; })()")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest, |
| NavigationCancelledBeforeScreenshot) { |
| // Start with a page which has an opt-in for VT. |
| GURL test_url( |
| embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url)); |
| |
| TestNavigationManager navigation_manager(shell()->web_contents(), test_url); |
| ASSERT_TRUE( |
| ExecJs(shell()->web_contents(), "location.href = location.href;")); |
| |
| // Wait for response and resume. The navigation should be blocked by the view |
| // transition condition. |
| ASSERT_TRUE(navigation_manager.WaitForResponse()); |
| navigation_manager.ResumeNavigation(); |
| |
| auto* navigation_request = |
| NavigationRequest::From(navigation_manager.GetNavigationHandle()); |
| ASSERT_TRUE(navigation_request); |
| ASSERT_TRUE( |
| navigation_request->IsCommitDeferringConditionDeferredForTesting()); |
| ASSERT_FALSE(navigation_request->commit_params().view_transition_state); |
| |
| // Stop the navigation while the screenshot request is in flight. |
| shell()->web_contents()->Stop(); |
| ASSERT_FALSE(navigation_manager.was_committed()); |
| |
| // Ensure the old renderer discards the outgoing transition. |
| EXPECT_TRUE(ExecJs( |
| shell()->web_contents()->GetPrimaryMainFrame(), |
| "(async () => { await document.startViewTransition().ready; })()")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest, |
| OwnershipTransferredToNewRenderer) { |
| // Start with a page which has an opt-in for VT. |
| GURL test_url( |
| embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url)); |
| |
| TestNavigationManager navigation_manager(shell()->web_contents(), test_url); |
| ASSERT_TRUE( |
| ExecJs(shell()->web_contents(), "location.href = location.href;")); |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>( |
| shell()->web_contents()->GetRenderWidgetHostView()) |
| ->HasViewTransitionResourcesForTesting()); |
| } |
| |
| // Ensure a browser-initiated navigation (i.e. typing URL into omnibox) does |
| // not trigger a view transitions. |
| IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest, |
| NoOpOnBrowserInitiatedNavigations) { |
| // Start with a page which has an opt-in for VT. |
| GURL test_url( |
| embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url)); |
| |
| GURL test_url_next(embedded_test_server()->GetURL( |
| "/view_transitions/basic-vt-opt-in.html?next")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url_next)); |
| WaitForCopyableViewInWebContents(shell()->web_contents()); |
| |
| EXPECT_EQ(false, EvalJs(shell()->web_contents(), "had_incoming_transition")); |
| } |
| |
| class ViewTransitionBrowserTestTraverse |
| : public ViewTransitionBrowserTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| bool BFCacheEnabled() const { return GetParam(); } |
| |
| bool NavigateBack(GURL back_url, WebContents* contents = nullptr) { |
| if (!contents) { |
| contents = shell()->web_contents(); |
| } |
| // We need to trigger the navigation *after* executing the script below so |
| // the event handlers the script relies on are set before they're dispatched |
| // by the navigation. |
| // |
| // We pass this as a callback to EvalJs so the navigation is initiated |
| // before we wait for the script result since it relies on events dispatched |
| // during the navigation. |
| auto trigger_navigation = base::BindOnce( |
| &ViewTransitionBrowserTestTraverse::TriggerBackNavigation, |
| base::Unretained(this), back_url, contents); |
| |
| auto result = |
| EvalJs(contents, |
| JsReplace( |
| R"( |
| (async () => { |
| let navigateFired = false; |
| navigation.onnavigate = (event) => { |
| navigateFired = (event.navigationType === "traverse"); |
| }; |
| let pageswapfired = new Promise((resolve) => { |
| onpageswap = (e) => { |
| if (!navigateFired || e.viewTransition == null) { |
| resolve(null); |
| return; |
| } |
| activation = e.activation; |
| resolve(activation); |
| }; |
| }); |
| let result = await pageswapfired; |
| return result != null; |
| })(); |
| )"), |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, ISOLATED_WORLD_ID_GLOBAL, |
| std::move(trigger_navigation)); |
| return result.ExtractBool(); |
| } |
| |
| void TriggerBackNavigation(GURL back_url, WebContents* web_contents) { |
| if (BFCacheEnabled()) { |
| TestActivationManager manager(web_contents, back_url); |
| web_contents->GetController().GoBack(); |
| manager.WaitForNavigationFinished(); |
| } else { |
| TestNavigationManager manager(web_contents, back_url); |
| web_contents->GetController().GoBack(); |
| ASSERT_TRUE(manager.WaitForNavigationFinished()); |
| } |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse, |
| NavigateEventFiresBeforeCapture) { |
| if (!BFCacheEnabled()) { |
| DisableBackForwardCacheForTesting( |
| shell()->web_contents(), |
| BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING); |
| } else if (!base::FeatureList::IsEnabled(features::kBackForwardCache)) { |
| GTEST_SKIP(); |
| } |
| |
| GURL test_url( |
| embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url)); |
| |
| GURL second_url(embedded_test_server()->GetURL( |
| "/view_transitions/basic-vt-opt-in.html?new")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), second_url)); |
| WaitForCopyableViewInWebContents(shell()->web_contents()); |
| |
| auto& nav_controller = static_cast<NavigationControllerImpl&>( |
| shell()->web_contents()->GetController()); |
| ASSERT_TRUE(nav_controller.CanGoBack()); |
| ASSERT_TRUE(NavigateBack(test_url)); |
| } |
| |
| // A session restore (e.g. "Duplicate Tab", "Undo Close Tab") uses RESTORE |
| // navigation types when traversing the session history. Ensure these |
| // navigations trigger a view transition. |
| IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse, |
| TransitionOnSessionRestoreTraversal) { |
| // A restored session will never have its session history in BFCache so |
| // there's no need to run a BFCache version of the test. |
| if (BFCacheEnabled()) { |
| GTEST_SKIP(); |
| } |
| |
| // Start with a page which has an opt-in for VT. |
| GURL url_a( |
| embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_a)); |
| |
| // Navigate to another page with an opt-in. (There's no transition due to |
| // being browser-initiated) |
| GURL url_b(embedded_test_server()->GetURL( |
| "/view_transitions/basic-vt-opt-in.html?next")); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_b)); |
| |
| // Clone the tab and load the page. Note: the cloned web contents must be put |
| // into a window to generate BeginFrames which are required since a view |
| // transition will not trigger unless a frame has been generated and the page |
| // revealed. |
| std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone(); |
| WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get()); |
| shell()->AddNewContents(nullptr, std::move(new_tab), url_b, |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| blink::mojom::WindowFeatures(), false, nullptr); |
| NavigationController& new_controller = new_tab_impl->GetController(); |
| |
| { |
| TestNavigationObserver clone_observer(new_tab_impl); |
| new_controller.LoadIfNecessary(); |
| clone_observer.Wait(); |
| } |
| |
| // Ensure the page has been revealed before navigating back so that a |
| // transition will be triggered. |
| WaitForCopyableViewInWebContents(new_tab_impl); |
| |
| // TODO(crbug.com/331226127) Intentionally ignore the return value as the |
| // navigation API (erroneously?) doesn't fire events for restored traversals. |
| NavigateBack(url_a, new_tab_impl); |
| |
| // Ensure a frame has been generated so that the reveal event would have been |
| // fired. |
| WaitForCopyableViewInWebContents(new_tab_impl); |
| |
| EXPECT_EQ(true, EvalJs(new_tab_impl, "had_incoming_transition")); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(P, |
| ViewTransitionBrowserTestTraverse, |
| ::testing::Bool()); |
| |
| } // namespace content |