blob: 62fe322be91fd4924e5a86d96e3c0128bef8d474 [file] [log] [blame]
Khushal Sagar91b544222024-03-12 17:36:591// Copyright 2024 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "base/test/scoped_feature_list.h"
6#include "components/viz/host/host_frame_sink_manager.h"
7#include "content/browser/compositor/surface_utils.h"
8#include "content/browser/renderer_host/frame_tree_node.h"
David Bokan5ae9e452024-03-26 16:23:599#include "content/browser/web_contents/web_contents_impl.h"
Khushal Sagar4e5aec432024-03-14 23:12:4610#include "content/public/test/back_forward_cache_util.h"
Khushal Sagar91b544222024-03-12 17:36:5911#include "content/public/test/browser_test.h"
12#include "content/public/test/browser_test_utils.h"
13#include "content/public/test/content_browser_test.h"
14#include "content/shell/browser/shell.h"
Khushal Sagar4e5aec432024-03-14 23:12:4615#include "content/test/content_browser_test_utils_internal.h"
Khushal Sagar91b544222024-03-12 17:36:5916#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
17#include "net/dns/mock_host_resolver.h"
18#include "net/test/embedded_test_server/default_handlers.h"
19#include "third_party/blink/public/common/features.h"
20
21namespace content {
22
23class ViewTransitionBrowserTest : public ContentBrowserTest {
24 public:
25 class TestCondition : public CommitDeferringCondition {
26 public:
27 TestCondition(NavigationRequest& request, base::RunLoop* run_loop)
28 : CommitDeferringCondition(request), run_loop_(run_loop) {}
29 ~TestCondition() override = default;
30
31 Result WillCommitNavigation(base::OnceClosure resume) override {
32 GetUIThreadTaskRunner()->PostTask(FROM_HERE, run_loop_->QuitClosure());
33 return Result::kDefer;
34 }
35
36 private:
37 raw_ptr<base::RunLoop> run_loop_;
38 };
39
40 ViewTransitionBrowserTest() {
41 feature_list_.InitWithFeatures(
42 /*enabled_features=*/{blink::features::kViewTransitionOnNavigation},
43 /*disabled_features=*/{});
44 }
45
46 void SetUpOnMainThread() override {
47 ContentBrowserTest::SetUpOnMainThread();
48 host_resolver()->AddRule("*", "127.0.0.1");
49 embedded_test_server()->ServeFilesFromSourceDirectory(
50 GetTestDataFilePath());
51 net::test_server::RegisterDefaultHandlers(embedded_test_server());
52 ASSERT_TRUE(embedded_test_server()->Start());
53 }
54
55 void WaitForConditionsDone(NavigationRequest* request) {
56 // Inject a condition to know when the VT response has been received but
57 // before the NavigationRequest is notified.
58 run_loop_ = std::make_unique<base::RunLoop>();
59 request->RegisterCommitDeferringConditionForTesting(
60 std::make_unique<TestCondition>(*request, run_loop_.get()));
61 run_loop_->Run();
62 }
63
64 private:
65 base::test::ScopedFeatureList feature_list_;
66 std::unique_ptr<base::RunLoop> run_loop_;
67};
68
69IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
70 NavigationCancelledAfterScreenshot) {
71 // Start with a page which has an opt-in for VT.
72 GURL test_url(
73 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
74 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
75
76 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
77 ASSERT_TRUE(
78 ExecJs(shell()->web_contents(), "location.href = location.href;"));
79
80 // Wait for response and resume. The navigation should be blocked by the view
81 // transition condition.
82 ASSERT_TRUE(navigation_manager.WaitForResponse());
83 navigation_manager.ResumeNavigation();
84
85 auto* navigation_request =
86 NavigationRequest::From(navigation_manager.GetNavigationHandle());
87 ASSERT_TRUE(navigation_request);
88 ASSERT_TRUE(
89 navigation_request->IsCommitDeferringConditionDeferredForTesting());
90 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
91
92 WaitForConditionsDone(navigation_request);
93 ASSERT_TRUE(navigation_request->commit_params().view_transition_state);
94
95 mojo::ScopedAllowSyncCallForTesting allow_sync;
96
97 ASSERT_TRUE(
98 GetHostFrameSinkManager()->HasUnclaimedViewTransitionResourcesForTest());
99
100 shell()->web_contents()->Stop();
101 ASSERT_FALSE(navigation_manager.was_committed());
102 ASSERT_FALSE(
103 GetHostFrameSinkManager()->HasUnclaimedViewTransitionResourcesForTest());
104}
105
106IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
107 OwnershipTransferredToNewRenderer) {
108 // Start with a page which has an opt-in for VT.
109 GURL test_url(
110 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
111 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
112
113 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
114 ASSERT_TRUE(
115 ExecJs(shell()->web_contents(), "location.href = location.href;"));
116 ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
117 ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(
118 shell()->web_contents()->GetRenderWidgetHostView())
119 ->HasViewTransitionResourcesForTesting());
120}
121
David Bokan5ae9e452024-03-26 16:23:59122// Ensure a browser-initiated navigation (i.e. typing URL into omnibox) does
123// not trigger a view transitions.
124IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
125 NoOpOnBrowserInitiatedNavigations) {
126 // Start with a page which has an opt-in for VT.
127 GURL test_url(
128 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
129 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
130
131 GURL test_url_next(embedded_test_server()->GetURL(
132 "/view_transitions/basic-vt-opt-in.html?next"));
133 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url_next));
134 WaitForCopyableViewInWebContents(shell()->web_contents());
135
136 EXPECT_EQ(false, EvalJs(shell()->web_contents(), "had_incoming_transition"));
137}
138
Khushal Sagar4e5aec432024-03-14 23:12:46139class ViewTransitionBrowserTestTraverse
140 : public ViewTransitionBrowserTest,
141 public testing::WithParamInterface<bool> {
142 public:
143 bool BFCacheEnabled() const { return GetParam(); }
144
David Bokan5ae9e452024-03-26 16:23:59145 bool NavigateBack(GURL back_url, WebContents* contents = nullptr) {
146 if (!contents) {
147 contents = shell()->web_contents();
148 }
Khushal Sagar4e5aec432024-03-14 23:12:46149 // We need to trigger the navigation *after* executing the script below so
150 // the event handlers the script relies on are set before they're dispatched
151 // by the navigation.
152 //
153 // We pass this as a callback to EvalJs so the navigation is initiated
154 // before we wait for the script result since it relies on events dispatched
155 // during the navigation.
156 auto trigger_navigation = base::BindOnce(
157 &ViewTransitionBrowserTestTraverse::TriggerBackNavigation,
David Bokan5ae9e452024-03-26 16:23:59158 base::Unretained(this), back_url, contents);
Khushal Sagar4e5aec432024-03-14 23:12:46159
160 auto result =
David Bokan5ae9e452024-03-26 16:23:59161 EvalJs(contents,
Khushal Sagar4e5aec432024-03-14 23:12:46162 JsReplace(
163 R"(
164 (async () => {
165 let navigateFired = false;
166 navigation.onnavigate = (event) => {
167 navigateFired = (event.navigationType === "traverse");
168 };
169 let pageswapfired = new Promise((resolve) => {
170 onpageswap = (e) => {
171 if (!navigateFired || e.viewTransition == null) {
172 resolve(null);
173 return;
174 }
175 activation = e.activation;
176 resolve(activation);
177 };
178 });
179 let result = await pageswapfired;
180 return result != null;
181 })();
182 )"),
183 EXECUTE_SCRIPT_DEFAULT_OPTIONS, ISOLATED_WORLD_ID_GLOBAL,
184 std::move(trigger_navigation));
185 return result.ExtractBool();
186 }
187
David Bokan5ae9e452024-03-26 16:23:59188 void TriggerBackNavigation(GURL back_url, WebContents* web_contents) {
Khushal Sagar4e5aec432024-03-14 23:12:46189 if (BFCacheEnabled()) {
David Bokan5ae9e452024-03-26 16:23:59190 TestActivationManager manager(web_contents, back_url);
191 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46192 manager.WaitForNavigationFinished();
193 } else {
David Bokan5ae9e452024-03-26 16:23:59194 TestNavigationManager manager(web_contents, back_url);
195 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46196 ASSERT_TRUE(manager.WaitForNavigationFinished());
197 }
198 }
199};
200
201IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
202 NavigateEventFiresBeforeCapture) {
203 if (!BFCacheEnabled()) {
204 DisableBackForwardCacheForTesting(
205 shell()->web_contents(),
206 BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);
207 } else if (!base::FeatureList::IsEnabled(features::kBackForwardCache)) {
208 GTEST_SKIP();
209 }
210
211 GURL test_url(
212 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
213 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
214
215 GURL second_url(embedded_test_server()->GetURL(
216 "/view_transitions/basic-vt-opt-in.html?new"));
217 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), second_url));
218 WaitForCopyableViewInWebContents(shell()->web_contents());
219
220 auto& nav_controller = static_cast<NavigationControllerImpl&>(
221 shell()->web_contents()->GetController());
222 ASSERT_TRUE(nav_controller.CanGoBack());
223 ASSERT_TRUE(NavigateBack(test_url));
224}
225
David Bokan5ae9e452024-03-26 16:23:59226// A session restore (e.g. "Duplicate Tab", "Undo Close Tab") uses RESTORE
227// navigation types when traversing the session history. Ensure these
228// navigations trigger a view transition.
229IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
230 TransitionOnSessionRestoreTraversal) {
231 // A restored session will never have its session history in BFCache so
232 // there's no need to run a BFCache version of the test.
233 if (BFCacheEnabled()) {
234 GTEST_SKIP();
235 }
236
237 // Start with a page which has an opt-in for VT.
238 GURL url_a(
239 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
240 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_a));
241
242 // Navigate to another page with an opt-in. (There's no transition due to
243 // being browser-initiated)
244 GURL url_b(embedded_test_server()->GetURL(
245 "/view_transitions/basic-vt-opt-in.html?next"));
246 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_b));
247
248 // Clone the tab and load the page. Note: the cloned web contents must be put
249 // into a window to generate BeginFrames which are required since a view
250 // transition will not trigger unless a frame has been generated and the page
251 // revealed.
252 std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
253 WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
254 shell()->AddNewContents(nullptr, std::move(new_tab), url_b,
255 WindowOpenDisposition::NEW_FOREGROUND_TAB,
256 blink::mojom::WindowFeatures(), false, nullptr);
257 NavigationController& new_controller = new_tab_impl->GetController();
258
259 {
260 TestNavigationObserver clone_observer(new_tab_impl);
261 new_controller.LoadIfNecessary();
262 clone_observer.Wait();
263 }
264
265 // Ensure the page has been revealed before navigating back so that a
266 // transition will be triggered.
267 WaitForCopyableViewInWebContents(new_tab_impl);
268
269 // TODO(crbug.com/331226127) Intentionally ignore the return value as the
270 // navigation API (erroneously?) doesn't fire events for restored traversals.
271 NavigateBack(url_a, new_tab_impl);
272
273 // Ensure a frame has been generated so that the reveal event would have been
274 // fired.
275 WaitForCopyableViewInWebContents(new_tab_impl);
276
277 EXPECT_EQ(true, EvalJs(new_tab_impl, "had_incoming_transition"));
278}
279
Khushal Sagar4e5aec432024-03-14 23:12:46280INSTANTIATE_TEST_SUITE_P(P,
281 ViewTransitionBrowserTestTraverse,
282 ::testing::Bool());
283
Khushal Sagar91b544222024-03-12 17:36:59284} // namespace content