blob: 32370483896f1a910b95d147e0e85d792dee7ff1 [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(
Khushal Sagarf1b0c972024-03-27 15:31:5142 /*enabled_features=*/
43 {blink::features::kViewTransitionOnNavigation},
Khushal Sagar91b544222024-03-12 17:36:5944 /*disabled_features=*/{});
45 }
46
47 void SetUpOnMainThread() override {
48 ContentBrowserTest::SetUpOnMainThread();
49 host_resolver()->AddRule("*", "127.0.0.1");
50 embedded_test_server()->ServeFilesFromSourceDirectory(
51 GetTestDataFilePath());
52 net::test_server::RegisterDefaultHandlers(embedded_test_server());
53 ASSERT_TRUE(embedded_test_server()->Start());
54 }
55
56 void WaitForConditionsDone(NavigationRequest* request) {
57 // Inject a condition to know when the VT response has been received but
58 // before the NavigationRequest is notified.
59 run_loop_ = std::make_unique<base::RunLoop>();
60 request->RegisterCommitDeferringConditionForTesting(
61 std::make_unique<TestCondition>(*request, run_loop_.get()));
62 run_loop_->Run();
63 }
64
65 private:
66 base::test::ScopedFeatureList feature_list_;
67 std::unique_ptr<base::RunLoop> run_loop_;
68};
69
70IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
71 NavigationCancelledAfterScreenshot) {
72 // Start with a page which has an opt-in for VT.
73 GURL test_url(
74 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
75 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
76
77 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
78 ASSERT_TRUE(
79 ExecJs(shell()->web_contents(), "location.href = location.href;"));
80
81 // Wait for response and resume. The navigation should be blocked by the view
82 // transition condition.
83 ASSERT_TRUE(navigation_manager.WaitForResponse());
84 navigation_manager.ResumeNavigation();
85
86 auto* navigation_request =
87 NavigationRequest::From(navigation_manager.GetNavigationHandle());
88 ASSERT_TRUE(navigation_request);
89 ASSERT_TRUE(
90 navigation_request->IsCommitDeferringConditionDeferredForTesting());
91 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
92
93 WaitForConditionsDone(navigation_request);
94 ASSERT_TRUE(navigation_request->commit_params().view_transition_state);
95
96 mojo::ScopedAllowSyncCallForTesting allow_sync;
97
98 ASSERT_TRUE(
99 GetHostFrameSinkManager()->HasUnclaimedViewTransitionResourcesForTest());
100
101 shell()->web_contents()->Stop();
102 ASSERT_FALSE(navigation_manager.was_committed());
103 ASSERT_FALSE(
104 GetHostFrameSinkManager()->HasUnclaimedViewTransitionResourcesForTest());
Khushal Sagarf1b0c972024-03-27 15:31:51105
106 // Ensure the old renderer discards the outgoing transition.
107 EXPECT_TRUE(ExecJs(
108 shell()->web_contents()->GetPrimaryMainFrame(),
109 "(async () => { await document.startViewTransition().ready; })()"));
110}
111
112IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
113 NavigationCancelledBeforeScreenshot) {
114 // Start with a page which has an opt-in for VT.
115 GURL test_url(
116 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
117 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
118
119 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
120 ASSERT_TRUE(
121 ExecJs(shell()->web_contents(), "location.href = location.href;"));
122
123 // Wait for response and resume. The navigation should be blocked by the view
124 // transition condition.
125 ASSERT_TRUE(navigation_manager.WaitForResponse());
126 navigation_manager.ResumeNavigation();
127
128 auto* navigation_request =
129 NavigationRequest::From(navigation_manager.GetNavigationHandle());
130 ASSERT_TRUE(navigation_request);
131 ASSERT_TRUE(
132 navigation_request->IsCommitDeferringConditionDeferredForTesting());
133 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
134
135 // Stop the navigation while the screenshot request is in flight.
136 shell()->web_contents()->Stop();
137 ASSERT_FALSE(navigation_manager.was_committed());
138
139 // Ensure the old renderer discards the outgoing transition.
140 EXPECT_TRUE(ExecJs(
141 shell()->web_contents()->GetPrimaryMainFrame(),
142 "(async () => { await document.startViewTransition().ready; })()"));
Khushal Sagar91b544222024-03-12 17:36:59143}
144
145IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
146 OwnershipTransferredToNewRenderer) {
147 // Start with a page which has an opt-in for VT.
148 GURL test_url(
149 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
150 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
151
152 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
153 ASSERT_TRUE(
154 ExecJs(shell()->web_contents(), "location.href = location.href;"));
155 ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
156 ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(
157 shell()->web_contents()->GetRenderWidgetHostView())
158 ->HasViewTransitionResourcesForTesting());
159}
160
David Bokan5ae9e452024-03-26 16:23:59161// Ensure a browser-initiated navigation (i.e. typing URL into omnibox) does
162// not trigger a view transitions.
163IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
164 NoOpOnBrowserInitiatedNavigations) {
165 // Start with a page which has an opt-in for VT.
166 GURL test_url(
167 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
168 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
169
170 GURL test_url_next(embedded_test_server()->GetURL(
171 "/view_transitions/basic-vt-opt-in.html?next"));
172 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url_next));
173 WaitForCopyableViewInWebContents(shell()->web_contents());
174
175 EXPECT_EQ(false, EvalJs(shell()->web_contents(), "had_incoming_transition"));
176}
177
Khushal Sagar4e5aec432024-03-14 23:12:46178class ViewTransitionBrowserTestTraverse
179 : public ViewTransitionBrowserTest,
180 public testing::WithParamInterface<bool> {
181 public:
182 bool BFCacheEnabled() const { return GetParam(); }
183
David Bokan5ae9e452024-03-26 16:23:59184 bool NavigateBack(GURL back_url, WebContents* contents = nullptr) {
185 if (!contents) {
186 contents = shell()->web_contents();
187 }
Khushal Sagar4e5aec432024-03-14 23:12:46188 // We need to trigger the navigation *after* executing the script below so
189 // the event handlers the script relies on are set before they're dispatched
190 // by the navigation.
191 //
192 // We pass this as a callback to EvalJs so the navigation is initiated
193 // before we wait for the script result since it relies on events dispatched
194 // during the navigation.
195 auto trigger_navigation = base::BindOnce(
196 &ViewTransitionBrowserTestTraverse::TriggerBackNavigation,
David Bokan5ae9e452024-03-26 16:23:59197 base::Unretained(this), back_url, contents);
Khushal Sagar4e5aec432024-03-14 23:12:46198
199 auto result =
David Bokan5ae9e452024-03-26 16:23:59200 EvalJs(contents,
Khushal Sagar4e5aec432024-03-14 23:12:46201 JsReplace(
202 R"(
203 (async () => {
204 let navigateFired = false;
205 navigation.onnavigate = (event) => {
206 navigateFired = (event.navigationType === "traverse");
207 };
208 let pageswapfired = new Promise((resolve) => {
209 onpageswap = (e) => {
210 if (!navigateFired || e.viewTransition == null) {
211 resolve(null);
212 return;
213 }
214 activation = e.activation;
215 resolve(activation);
216 };
217 });
218 let result = await pageswapfired;
219 return result != null;
220 })();
221 )"),
222 EXECUTE_SCRIPT_DEFAULT_OPTIONS, ISOLATED_WORLD_ID_GLOBAL,
223 std::move(trigger_navigation));
224 return result.ExtractBool();
225 }
226
David Bokan5ae9e452024-03-26 16:23:59227 void TriggerBackNavigation(GURL back_url, WebContents* web_contents) {
Khushal Sagar4e5aec432024-03-14 23:12:46228 if (BFCacheEnabled()) {
David Bokan5ae9e452024-03-26 16:23:59229 TestActivationManager manager(web_contents, back_url);
230 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46231 manager.WaitForNavigationFinished();
232 } else {
David Bokan5ae9e452024-03-26 16:23:59233 TestNavigationManager manager(web_contents, back_url);
234 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46235 ASSERT_TRUE(manager.WaitForNavigationFinished());
236 }
237 }
238};
239
240IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
241 NavigateEventFiresBeforeCapture) {
242 if (!BFCacheEnabled()) {
243 DisableBackForwardCacheForTesting(
244 shell()->web_contents(),
245 BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);
246 } else if (!base::FeatureList::IsEnabled(features::kBackForwardCache)) {
247 GTEST_SKIP();
248 }
249
250 GURL test_url(
251 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
252 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
253
254 GURL second_url(embedded_test_server()->GetURL(
255 "/view_transitions/basic-vt-opt-in.html?new"));
256 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), second_url));
257 WaitForCopyableViewInWebContents(shell()->web_contents());
258
259 auto& nav_controller = static_cast<NavigationControllerImpl&>(
260 shell()->web_contents()->GetController());
261 ASSERT_TRUE(nav_controller.CanGoBack());
262 ASSERT_TRUE(NavigateBack(test_url));
263}
264
David Bokan5ae9e452024-03-26 16:23:59265// A session restore (e.g. "Duplicate Tab", "Undo Close Tab") uses RESTORE
266// navigation types when traversing the session history. Ensure these
267// navigations trigger a view transition.
268IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
269 TransitionOnSessionRestoreTraversal) {
270 // A restored session will never have its session history in BFCache so
271 // there's no need to run a BFCache version of the test.
272 if (BFCacheEnabled()) {
273 GTEST_SKIP();
274 }
275
276 // Start with a page which has an opt-in for VT.
277 GURL url_a(
278 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
279 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_a));
280
281 // Navigate to another page with an opt-in. (There's no transition due to
282 // being browser-initiated)
283 GURL url_b(embedded_test_server()->GetURL(
284 "/view_transitions/basic-vt-opt-in.html?next"));
285 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_b));
286
287 // Clone the tab and load the page. Note: the cloned web contents must be put
288 // into a window to generate BeginFrames which are required since a view
289 // transition will not trigger unless a frame has been generated and the page
290 // revealed.
291 std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
292 WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
293 shell()->AddNewContents(nullptr, std::move(new_tab), url_b,
294 WindowOpenDisposition::NEW_FOREGROUND_TAB,
295 blink::mojom::WindowFeatures(), false, nullptr);
296 NavigationController& new_controller = new_tab_impl->GetController();
297
298 {
299 TestNavigationObserver clone_observer(new_tab_impl);
300 new_controller.LoadIfNecessary();
301 clone_observer.Wait();
302 }
303
304 // Ensure the page has been revealed before navigating back so that a
305 // transition will be triggered.
306 WaitForCopyableViewInWebContents(new_tab_impl);
307
308 // TODO(crbug.com/331226127) Intentionally ignore the return value as the
309 // navigation API (erroneously?) doesn't fire events for restored traversals.
310 NavigateBack(url_a, new_tab_impl);
311
312 // Ensure a frame has been generated so that the reveal event would have been
313 // fired.
314 WaitForCopyableViewInWebContents(new_tab_impl);
315
316 EXPECT_EQ(true, EvalJs(new_tab_impl, "had_incoming_transition"));
317}
318
Khushal Sagar4e5aec432024-03-14 23:12:46319INSTANTIATE_TEST_SUITE_P(P,
320 ViewTransitionBrowserTestTraverse,
321 ::testing::Bool());
322
Khushal Sagar91b544222024-03-12 17:36:59323} // namespace content