blob: 2d0352882187b273bd21184d836ae1d8e0b19e81 [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"
Khushal Sagar04e638c4b2024-05-13 17:43:119#include "content/browser/renderer_host/view_transition_opt_in_state.h"
David Bokan5ae9e452024-03-26 16:23:5910#include "content/browser/web_contents/web_contents_impl.h"
Khushal Sagar4e5aec432024-03-14 23:12:4611#include "content/public/test/back_forward_cache_util.h"
Khushal Sagar91b544222024-03-12 17:36:5912#include "content/public/test/browser_test.h"
13#include "content/public/test/browser_test_utils.h"
14#include "content/public/test/content_browser_test.h"
15#include "content/shell/browser/shell.h"
Khushal Sagar04e638c4b2024-05-13 17:43:1116#include "content/shell/browser/shell_download_manager_delegate.h"
Khushal Sagar4e5aec432024-03-14 23:12:4617#include "content/test/content_browser_test_utils_internal.h"
Khushal Sagar91b544222024-03-12 17:36:5918#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
19#include "net/dns/mock_host_resolver.h"
20#include "net/test/embedded_test_server/default_handlers.h"
Zoraiz Naeemfe5b6552024-08-08 19:49:0321#include "services/viz/privileged/mojom/compositing/features.mojom-features.h"
Khushal Sagar91b544222024-03-12 17:36:5922#include "third_party/blink/public/common/features.h"
23
24namespace content {
25
26class ViewTransitionBrowserTest : public ContentBrowserTest {
27 public:
28 class TestCondition : public CommitDeferringCondition {
29 public:
30 TestCondition(NavigationRequest& request, base::RunLoop* run_loop)
31 : CommitDeferringCondition(request), run_loop_(run_loop) {}
32 ~TestCondition() override = default;
33
34 Result WillCommitNavigation(base::OnceClosure resume) override {
35 GetUIThreadTaskRunner()->PostTask(FROM_HERE, run_loop_->QuitClosure());
36 return Result::kDefer;
37 }
38
39 private:
40 raw_ptr<base::RunLoop> run_loop_;
41 };
42
43 ViewTransitionBrowserTest() {
44 feature_list_.InitWithFeatures(
Khushal Sagarf1b0c972024-03-27 15:31:5145 /*enabled_features=*/
Zoraiz Naeemfe5b6552024-08-08 19:49:0346 {blink::features::kViewTransitionOnNavigation,
47 viz::mojom::EnableVizTestApis},
Khushal Sagar91b544222024-03-12 17:36:5948 /*disabled_features=*/{});
49 }
50
51 void SetUpOnMainThread() override {
52 ContentBrowserTest::SetUpOnMainThread();
53 host_resolver()->AddRule("*", "127.0.0.1");
54 embedded_test_server()->ServeFilesFromSourceDirectory(
55 GetTestDataFilePath());
56 net::test_server::RegisterDefaultHandlers(embedded_test_server());
57 ASSERT_TRUE(embedded_test_server()->Start());
58 }
59
60 void WaitForConditionsDone(NavigationRequest* request) {
61 // Inject a condition to know when the VT response has been received but
62 // before the NavigationRequest is notified.
63 run_loop_ = std::make_unique<base::RunLoop>();
64 request->RegisterCommitDeferringConditionForTesting(
65 std::make_unique<TestCondition>(*request, run_loop_.get()));
66 run_loop_->Run();
67 }
68
Khushal Sagar04e638c4b2024-05-13 17:43:1169 bool HasVTOptIn(RenderFrameHost* rfh) {
70 auto* opt_in_state = ViewTransitionOptInState::GetForCurrentDocument(
71 static_cast<RenderFrameHostImpl*>(rfh));
72 return opt_in_state &&
73 opt_in_state->same_origin_opt_in() ==
74 blink::mojom::ViewTransitionSameOriginOptIn::kEnabled;
75 }
76
Khushal Sagar91b544222024-03-12 17:36:5977 private:
78 base::test::ScopedFeatureList feature_list_;
79 std::unique_ptr<base::RunLoop> run_loop_;
80};
81
Khushal Sagar91b544222024-03-12 17:36:5982IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
Michael Thiessena74065a32024-06-28 18:48:3583 NavigationCancelledAfterScreenshot) {
Khushal Sagar91b544222024-03-12 17:36:5984 // Start with a page which has an opt-in for VT.
85 GURL test_url(
86 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
87 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Michael Thiessena74065a32024-06-28 18:48:3588 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagar91b544222024-03-12 17:36:5989
90 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
91 ASSERT_TRUE(
92 ExecJs(shell()->web_contents(), "location.href = location.href;"));
93
94 // Wait for response and resume. The navigation should be blocked by the view
95 // transition condition.
96 ASSERT_TRUE(navigation_manager.WaitForResponse());
97 navigation_manager.ResumeNavigation();
98
99 auto* navigation_request =
100 NavigationRequest::From(navigation_manager.GetNavigationHandle());
101 ASSERT_TRUE(navigation_request);
102 ASSERT_TRUE(
103 navigation_request->IsCommitDeferringConditionDeferredForTesting());
104 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
105
106 WaitForConditionsDone(navigation_request);
107 ASSERT_TRUE(navigation_request->commit_params().view_transition_state);
108
109 mojo::ScopedAllowSyncCallForTesting allow_sync;
110
Zoraiz Naeemfe5b6552024-08-08 19:49:03111 bool has_resources = false;
112 GetHostFrameSinkManager()
113 ->GetFrameSinkManagerTestApi()
114 .HasUnclaimedViewTransitionResources(&has_resources);
115 ASSERT_TRUE(has_resources);
Khushal Sagar91b544222024-03-12 17:36:59116
117 shell()->web_contents()->Stop();
118 ASSERT_FALSE(navigation_manager.was_committed());
Zoraiz Naeemfe5b6552024-08-08 19:49:03119 GetHostFrameSinkManager()
120 ->GetFrameSinkManagerTestApi()
121 .HasUnclaimedViewTransitionResources(&has_resources);
122 ASSERT_FALSE(has_resources);
Khushal Sagarf1b0c972024-03-27 15:31:51123
124 // Ensure the old renderer discards the outgoing transition.
125 EXPECT_TRUE(ExecJs(
126 shell()->web_contents()->GetPrimaryMainFrame(),
127 "(async () => { await document.startViewTransition().ready; })()"));
128}
129
130IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
131 NavigationCancelledBeforeScreenshot) {
132 // Start with a page which has an opt-in for VT.
133 GURL test_url(
134 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
135 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Michael Thiessena74065a32024-06-28 18:48:35136 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagarf1b0c972024-03-27 15:31:51137
138 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
139 ASSERT_TRUE(
140 ExecJs(shell()->web_contents(), "location.href = location.href;"));
141
142 // Wait for response and resume. The navigation should be blocked by the view
143 // transition condition.
144 ASSERT_TRUE(navigation_manager.WaitForResponse());
145 navigation_manager.ResumeNavigation();
146
147 auto* navigation_request =
148 NavigationRequest::From(navigation_manager.GetNavigationHandle());
149 ASSERT_TRUE(navigation_request);
150 ASSERT_TRUE(
151 navigation_request->IsCommitDeferringConditionDeferredForTesting());
152 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
153
154 // Stop the navigation while the screenshot request is in flight.
155 shell()->web_contents()->Stop();
156 ASSERT_FALSE(navigation_manager.was_committed());
157
158 // Ensure the old renderer discards the outgoing transition.
159 EXPECT_TRUE(ExecJs(
160 shell()->web_contents()->GetPrimaryMainFrame(),
161 "(async () => { await document.startViewTransition().ready; })()"));
Khushal Sagar91b544222024-03-12 17:36:59162}
163
164IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
Gaston Rodriguez318b7392024-06-28 14:51:17165 OwnershipTransferredToNewRenderer) {
Khushal Sagar91b544222024-03-12 17:36:59166 // Start with a page which has an opt-in for VT.
167 GURL test_url(
168 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
169 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Michael Thiessen517e2542024-06-27 14:02:44170 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagar91b544222024-03-12 17:36:59171
172 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
173 ASSERT_TRUE(
174 ExecJs(shell()->web_contents(), "location.href = location.href;"));
175 ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
176 ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(
177 shell()->web_contents()->GetRenderWidgetHostView())
178 ->HasViewTransitionResourcesForTesting());
179}
180
David Bokan5ae9e452024-03-26 16:23:59181// Ensure a browser-initiated navigation (i.e. typing URL into omnibox) does
182// not trigger a view transitions.
183IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
184 NoOpOnBrowserInitiatedNavigations) {
185 // Start with a page which has an opt-in for VT.
186 GURL test_url(
187 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
188 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
189
190 GURL test_url_next(embedded_test_server()->GetURL(
191 "/view_transitions/basic-vt-opt-in.html?next"));
192 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url_next));
193 WaitForCopyableViewInWebContents(shell()->web_contents());
194
195 EXPECT_EQ(false, EvalJs(shell()->web_contents(), "had_incoming_transition"));
196}
197
Khushal Sagar04e638c4b2024-05-13 17:43:11198class ViewTransitionDownloadBrowserTest : public ViewTransitionBrowserTest {
199 public:
200 void SetUpOnMainThread() override {
201 ViewTransitionBrowserTest::SetUpOnMainThread();
202
203 // Set up a test download directory, in order to prevent prompting for
204 // handling downloads.
205 ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir());
206 ShellDownloadManagerDelegate* delegate =
207 static_cast<ShellDownloadManagerDelegate*>(
208 shell()
209 ->web_contents()
210 ->GetBrowserContext()
211 ->GetDownloadManagerDelegate());
212 delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath());
213 }
214
215 private:
216 base::ScopedTempDir downloads_directory_;
217};
218
219IN_PROC_BROWSER_TEST_F(ViewTransitionDownloadBrowserTest,
220 NavigationToDownloadLink) {
221 GURL test_url(
222 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
223 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
224 WaitForCopyableViewInWebContents(shell()->web_contents());
225
226 GURL download_url(embedded_test_server()->GetURL("/download-test1.lib"));
227 TestNavigationManager navigation_manager(shell()->web_contents(),
228 download_url);
229 ASSERT_TRUE(ExecJs(shell()->web_contents(),
230 JsReplace("location.href = $1", download_url)));
231
232 // Wait for response and resume. The navigation should not be blocked by the
233 // view transition condition.
234 ASSERT_TRUE(navigation_manager.WaitForRequestStart());
235
236 ASSERT_TRUE(HasVTOptIn(shell()->web_contents()->GetPrimaryMainFrame()));
237 auto* navigation_request =
238 NavigationRequest::From(navigation_manager.GetNavigationHandle());
239 ASSERT_EQ(
240 shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
241 navigation_request->GetTentativeOriginAtRequestTime());
242
243 ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
244 ASSERT_FALSE(navigation_manager.was_committed());
245}
246
Khushal Sagar4e5aec432024-03-14 23:12:46247class ViewTransitionBrowserTestTraverse
248 : public ViewTransitionBrowserTest,
249 public testing::WithParamInterface<bool> {
250 public:
251 bool BFCacheEnabled() const { return GetParam(); }
252
David Bokan5ae9e452024-03-26 16:23:59253 bool NavigateBack(GURL back_url, WebContents* contents = nullptr) {
254 if (!contents) {
255 contents = shell()->web_contents();
256 }
Khushal Sagar4e5aec432024-03-14 23:12:46257 // We need to trigger the navigation *after* executing the script below so
258 // the event handlers the script relies on are set before they're dispatched
259 // by the navigation.
260 //
261 // We pass this as a callback to EvalJs so the navigation is initiated
262 // before we wait for the script result since it relies on events dispatched
263 // during the navigation.
264 auto trigger_navigation = base::BindOnce(
265 &ViewTransitionBrowserTestTraverse::TriggerBackNavigation,
David Bokan5ae9e452024-03-26 16:23:59266 base::Unretained(this), back_url, contents);
Khushal Sagar4e5aec432024-03-14 23:12:46267
268 auto result =
David Bokan5ae9e452024-03-26 16:23:59269 EvalJs(contents,
Khushal Sagar4e5aec432024-03-14 23:12:46270 JsReplace(
271 R"(
272 (async () => {
273 let navigateFired = false;
274 navigation.onnavigate = (event) => {
275 navigateFired = (event.navigationType === "traverse");
276 };
277 let pageswapfired = new Promise((resolve) => {
278 onpageswap = (e) => {
279 if (!navigateFired || e.viewTransition == null) {
280 resolve(null);
281 return;
282 }
283 activation = e.activation;
284 resolve(activation);
285 };
286 });
287 let result = await pageswapfired;
288 return result != null;
289 })();
290 )"),
291 EXECUTE_SCRIPT_DEFAULT_OPTIONS, ISOLATED_WORLD_ID_GLOBAL,
292 std::move(trigger_navigation));
293 return result.ExtractBool();
294 }
295
David Bokan5ae9e452024-03-26 16:23:59296 void TriggerBackNavigation(GURL back_url, WebContents* web_contents) {
Khushal Sagar4e5aec432024-03-14 23:12:46297 if (BFCacheEnabled()) {
David Bokan5ae9e452024-03-26 16:23:59298 TestActivationManager manager(web_contents, back_url);
299 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46300 manager.WaitForNavigationFinished();
301 } else {
David Bokan5ae9e452024-03-26 16:23:59302 TestNavigationManager manager(web_contents, back_url);
303 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46304 ASSERT_TRUE(manager.WaitForNavigationFinished());
305 }
306 }
307};
308
309IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
310 NavigateEventFiresBeforeCapture) {
311 if (!BFCacheEnabled()) {
312 DisableBackForwardCacheForTesting(
313 shell()->web_contents(),
314 BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);
315 } else if (!base::FeatureList::IsEnabled(features::kBackForwardCache)) {
316 GTEST_SKIP();
317 }
318
319 GURL test_url(
320 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
321 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
322
323 GURL second_url(embedded_test_server()->GetURL(
324 "/view_transitions/basic-vt-opt-in.html?new"));
325 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), second_url));
326 WaitForCopyableViewInWebContents(shell()->web_contents());
327
328 auto& nav_controller = static_cast<NavigationControllerImpl&>(
329 shell()->web_contents()->GetController());
330 ASSERT_TRUE(nav_controller.CanGoBack());
331 ASSERT_TRUE(NavigateBack(test_url));
332}
333
David Bokan5ae9e452024-03-26 16:23:59334// A session restore (e.g. "Duplicate Tab", "Undo Close Tab") uses RESTORE
335// navigation types when traversing the session history. Ensure these
336// navigations trigger a view transition.
337IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
338 TransitionOnSessionRestoreTraversal) {
339 // A restored session will never have its session history in BFCache so
340 // there's no need to run a BFCache version of the test.
341 if (BFCacheEnabled()) {
342 GTEST_SKIP();
343 }
344
345 // Start with a page which has an opt-in for VT.
346 GURL url_a(
347 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
348 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_a));
349
350 // Navigate to another page with an opt-in. (There's no transition due to
351 // being browser-initiated)
352 GURL url_b(embedded_test_server()->GetURL(
353 "/view_transitions/basic-vt-opt-in.html?next"));
354 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_b));
355
356 // Clone the tab and load the page. Note: the cloned web contents must be put
357 // into a window to generate BeginFrames which are required since a view
358 // transition will not trigger unless a frame has been generated and the page
359 // revealed.
360 std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
361 WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
362 shell()->AddNewContents(nullptr, std::move(new_tab), url_b,
363 WindowOpenDisposition::NEW_FOREGROUND_TAB,
364 blink::mojom::WindowFeatures(), false, nullptr);
365 NavigationController& new_controller = new_tab_impl->GetController();
366
367 {
368 TestNavigationObserver clone_observer(new_tab_impl);
369 new_controller.LoadIfNecessary();
370 clone_observer.Wait();
371 }
372
373 // Ensure the page has been revealed before navigating back so that a
374 // transition will be triggered.
375 WaitForCopyableViewInWebContents(new_tab_impl);
376
377 // TODO(crbug.com/331226127) Intentionally ignore the return value as the
378 // navigation API (erroneously?) doesn't fire events for restored traversals.
379 NavigateBack(url_a, new_tab_impl);
380
381 // Ensure a frame has been generated so that the reveal event would have been
382 // fired.
383 WaitForCopyableViewInWebContents(new_tab_impl);
384
385 EXPECT_EQ(true, EvalJs(new_tab_impl, "had_incoming_transition"));
386}
387
Khushal Sagar4e5aec432024-03-14 23:12:46388INSTANTIATE_TEST_SUITE_P(P,
389 ViewTransitionBrowserTestTraverse,
390 ::testing::Bool());
391
Khushal Sagar91b544222024-03-12 17:36:59392} // namespace content