blob: bf579fdd57f2a60caa5e2027e664bf4938946030 [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"
Noam Rosenthal09dd02b2024-09-21 21:05:526#include "base/test/test_future.h"
7#include "cc/test/pixel_comparator.h"
8#include "cc/test/pixel_test_utils.h"
Khushal Sagar91b544222024-03-12 17:36:599#include "components/viz/host/host_frame_sink_manager.h"
10#include "content/browser/compositor/surface_utils.h"
11#include "content/browser/renderer_host/frame_tree_node.h"
Khushal Sagar04e638c4b2024-05-13 17:43:1112#include "content/browser/renderer_host/view_transition_opt_in_state.h"
David Bokan5ae9e452024-03-26 16:23:5913#include "content/browser/web_contents/web_contents_impl.h"
Noam Rosenthal09dd02b2024-09-21 21:05:5214#include "content/public/browser/web_contents_observer.h"
Khushal Sagar4e5aec432024-03-14 23:12:4615#include "content/public/test/back_forward_cache_util.h"
Khushal Sagar91b544222024-03-12 17:36:5916#include "content/public/test/browser_test.h"
17#include "content/public/test/browser_test_utils.h"
18#include "content/public/test/content_browser_test.h"
19#include "content/shell/browser/shell.h"
Khushal Sagar04e638c4b2024-05-13 17:43:1120#include "content/shell/browser/shell_download_manager_delegate.h"
Khushal Sagar4e5aec432024-03-14 23:12:4621#include "content/test/content_browser_test_utils_internal.h"
Khushal Sagar91b544222024-03-12 17:36:5922#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
23#include "net/dns/mock_host_resolver.h"
24#include "net/test/embedded_test_server/default_handlers.h"
Zoraiz Naeemfe5b6552024-08-08 19:49:0325#include "services/viz/privileged/mojom/compositing/features.mojom-features.h"
Khushal Sagar91b544222024-03-12 17:36:5926#include "third_party/blink/public/common/features.h"
Noam Rosenthal09dd02b2024-09-21 21:05:5227#include "third_party/blink/public/common/features_generated.h"
28#include "ui/gfx/geometry/size.h"
Khushal Sagar91b544222024-03-12 17:36:5929
30namespace content {
31
32class ViewTransitionBrowserTest : public ContentBrowserTest {
33 public:
34 class TestCondition : public CommitDeferringCondition {
35 public:
36 TestCondition(NavigationRequest& request, base::RunLoop* run_loop)
37 : CommitDeferringCondition(request), run_loop_(run_loop) {}
38 ~TestCondition() override = default;
39
40 Result WillCommitNavigation(base::OnceClosure resume) override {
41 GetUIThreadTaskRunner()->PostTask(FROM_HERE, run_loop_->QuitClosure());
42 return Result::kDefer;
43 }
44
45 private:
46 raw_ptr<base::RunLoop> run_loop_;
47 };
48
49 ViewTransitionBrowserTest() {
50 feature_list_.InitWithFeatures(
Khushal Sagarf1b0c972024-03-27 15:31:5151 /*enabled_features=*/
Zoraiz Naeemfe5b6552024-08-08 19:49:0352 {blink::features::kViewTransitionOnNavigation,
53 viz::mojom::EnableVizTestApis},
Khushal Sagar91b544222024-03-12 17:36:5954 /*disabled_features=*/{});
55 }
56
57 void SetUpOnMainThread() override {
58 ContentBrowserTest::SetUpOnMainThread();
59 host_resolver()->AddRule("*", "127.0.0.1");
60 embedded_test_server()->ServeFilesFromSourceDirectory(
61 GetTestDataFilePath());
62 net::test_server::RegisterDefaultHandlers(embedded_test_server());
63 ASSERT_TRUE(embedded_test_server()->Start());
64 }
65
66 void WaitForConditionsDone(NavigationRequest* request) {
67 // Inject a condition to know when the VT response has been received but
68 // before the NavigationRequest is notified.
69 run_loop_ = std::make_unique<base::RunLoop>();
70 request->RegisterCommitDeferringConditionForTesting(
71 std::make_unique<TestCondition>(*request, run_loop_.get()));
72 run_loop_->Run();
73 }
74
Khushal Sagar04e638c4b2024-05-13 17:43:1175 bool HasVTOptIn(RenderFrameHost* rfh) {
76 auto* opt_in_state = ViewTransitionOptInState::GetForCurrentDocument(
77 static_cast<RenderFrameHostImpl*>(rfh));
78 return opt_in_state &&
79 opt_in_state->same_origin_opt_in() ==
80 blink::mojom::ViewTransitionSameOriginOptIn::kEnabled;
81 }
82
Khushal Sagar91b544222024-03-12 17:36:5983 private:
84 base::test::ScopedFeatureList feature_list_;
85 std::unique_ptr<base::RunLoop> run_loop_;
86};
87
88IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
Michael Thiessena74065a32024-06-28 18:48:3589 NavigationCancelledAfterScreenshot) {
Khushal Sagar91b544222024-03-12 17:36:5990 // Start with a page which has an opt-in for VT.
91 GURL test_url(
92 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
93 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Michael Thiessena74065a32024-06-28 18:48:3594 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagar91b544222024-03-12 17:36:5995
96 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
97 ASSERT_TRUE(
98 ExecJs(shell()->web_contents(), "location.href = location.href;"));
99
100 // Wait for response and resume. The navigation should be blocked by the view
101 // transition condition.
102 ASSERT_TRUE(navigation_manager.WaitForResponse());
103 navigation_manager.ResumeNavigation();
104
105 auto* navigation_request =
106 NavigationRequest::From(navigation_manager.GetNavigationHandle());
107 ASSERT_TRUE(navigation_request);
108 ASSERT_TRUE(
109 navigation_request->IsCommitDeferringConditionDeferredForTesting());
110 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
111
112 WaitForConditionsDone(navigation_request);
113 ASSERT_TRUE(navigation_request->commit_params().view_transition_state);
114
115 mojo::ScopedAllowSyncCallForTesting allow_sync;
116
Zoraiz Naeemfe5b6552024-08-08 19:49:03117 bool has_resources = false;
118 GetHostFrameSinkManager()
119 ->GetFrameSinkManagerTestApi()
120 .HasUnclaimedViewTransitionResources(&has_resources);
121 ASSERT_TRUE(has_resources);
Khushal Sagar91b544222024-03-12 17:36:59122
123 shell()->web_contents()->Stop();
124 ASSERT_FALSE(navigation_manager.was_committed());
Zoraiz Naeemfe5b6552024-08-08 19:49:03125 GetHostFrameSinkManager()
126 ->GetFrameSinkManagerTestApi()
127 .HasUnclaimedViewTransitionResources(&has_resources);
128 ASSERT_FALSE(has_resources);
Khushal Sagarf1b0c972024-03-27 15:31:51129
130 // Ensure the old renderer discards the outgoing transition.
131 EXPECT_TRUE(ExecJs(
132 shell()->web_contents()->GetPrimaryMainFrame(),
133 "(async () => { await document.startViewTransition().ready; })()"));
134}
135
136IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
137 NavigationCancelledBeforeScreenshot) {
138 // Start with a page which has an opt-in for VT.
139 GURL test_url(
140 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
141 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Michael Thiessena74065a32024-06-28 18:48:35142 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagarf1b0c972024-03-27 15:31:51143
144 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
145 ASSERT_TRUE(
146 ExecJs(shell()->web_contents(), "location.href = location.href;"));
147
148 // Wait for response and resume. The navigation should be blocked by the view
149 // transition condition.
150 ASSERT_TRUE(navigation_manager.WaitForResponse());
151 navigation_manager.ResumeNavigation();
152
153 auto* navigation_request =
154 NavigationRequest::From(navigation_manager.GetNavigationHandle());
155 ASSERT_TRUE(navigation_request);
156 ASSERT_TRUE(
157 navigation_request->IsCommitDeferringConditionDeferredForTesting());
158 ASSERT_FALSE(navigation_request->commit_params().view_transition_state);
159
160 // Stop the navigation while the screenshot request is in flight.
161 shell()->web_contents()->Stop();
162 ASSERT_FALSE(navigation_manager.was_committed());
163
164 // Ensure the old renderer discards the outgoing transition.
165 EXPECT_TRUE(ExecJs(
166 shell()->web_contents()->GetPrimaryMainFrame(),
167 "(async () => { await document.startViewTransition().ready; })()"));
Khushal Sagar91b544222024-03-12 17:36:59168}
169
170IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
Gaston Rodriguez318b7392024-06-28 14:51:17171 OwnershipTransferredToNewRenderer) {
Khushal Sagar91b544222024-03-12 17:36:59172 // Start with a page which has an opt-in for VT.
173 GURL test_url(
174 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
175 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Michael Thiessen517e2542024-06-27 14:02:44176 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagar91b544222024-03-12 17:36:59177
178 TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
179 ASSERT_TRUE(
180 ExecJs(shell()->web_contents(), "location.href = location.href;"));
181 ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
182 ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(
183 shell()->web_contents()->GetRenderWidgetHostView())
184 ->HasViewTransitionResourcesForTesting());
185}
186
David Bokan5ae9e452024-03-26 16:23:59187// Ensure a browser-initiated navigation (i.e. typing URL into omnibox) does
188// not trigger a view transitions.
189IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
190 NoOpOnBrowserInitiatedNavigations) {
191 // Start with a page which has an opt-in for VT.
192 GURL test_url(
193 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
194 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
195
196 GURL test_url_next(embedded_test_server()->GetURL(
197 "/view_transitions/basic-vt-opt-in.html?next"));
198 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url_next));
199 WaitForCopyableViewInWebContents(shell()->web_contents());
200
201 EXPECT_EQ(false, EvalJs(shell()->web_contents(), "had_incoming_transition"));
202}
203
Khushal Sagar04e638c4b2024-05-13 17:43:11204class ViewTransitionDownloadBrowserTest : public ViewTransitionBrowserTest {
205 public:
206 void SetUpOnMainThread() override {
207 ViewTransitionBrowserTest::SetUpOnMainThread();
208
209 // Set up a test download directory, in order to prevent prompting for
210 // handling downloads.
211 ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir());
212 ShellDownloadManagerDelegate* delegate =
213 static_cast<ShellDownloadManagerDelegate*>(
214 shell()
215 ->web_contents()
216 ->GetBrowserContext()
217 ->GetDownloadManagerDelegate());
218 delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath());
219 }
220
221 private:
222 base::ScopedTempDir downloads_directory_;
223};
224
225IN_PROC_BROWSER_TEST_F(ViewTransitionDownloadBrowserTest,
226 NavigationToDownloadLink) {
227 GURL test_url(
228 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
229 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
230 WaitForCopyableViewInWebContents(shell()->web_contents());
231
232 GURL download_url(embedded_test_server()->GetURL("/download-test1.lib"));
233 TestNavigationManager navigation_manager(shell()->web_contents(),
234 download_url);
235 ASSERT_TRUE(ExecJs(shell()->web_contents(),
236 JsReplace("location.href = $1", download_url)));
237
238 // Wait for response and resume. The navigation should not be blocked by the
239 // view transition condition.
240 ASSERT_TRUE(navigation_manager.WaitForRequestStart());
241
242 ASSERT_TRUE(HasVTOptIn(shell()->web_contents()->GetPrimaryMainFrame()));
243 auto* navigation_request =
244 NavigationRequest::From(navigation_manager.GetNavigationHandle());
245 ASSERT_EQ(
246 shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
247 navigation_request->GetTentativeOriginAtRequestTime());
248
249 ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
250 ASSERT_FALSE(navigation_manager.was_committed());
251}
252
Khushal Sagar4e5aec432024-03-14 23:12:46253class ViewTransitionBrowserTestTraverse
254 : public ViewTransitionBrowserTest,
255 public testing::WithParamInterface<bool> {
256 public:
257 bool BFCacheEnabled() const { return GetParam(); }
258
David Bokan5ae9e452024-03-26 16:23:59259 bool NavigateBack(GURL back_url, WebContents* contents = nullptr) {
260 if (!contents) {
261 contents = shell()->web_contents();
262 }
Khushal Sagar4e5aec432024-03-14 23:12:46263 // We need to trigger the navigation *after* executing the script below so
264 // the event handlers the script relies on are set before they're dispatched
265 // by the navigation.
266 //
267 // We pass this as a callback to EvalJs so the navigation is initiated
268 // before we wait for the script result since it relies on events dispatched
269 // during the navigation.
270 auto trigger_navigation = base::BindOnce(
271 &ViewTransitionBrowserTestTraverse::TriggerBackNavigation,
David Bokan5ae9e452024-03-26 16:23:59272 base::Unretained(this), back_url, contents);
Khushal Sagar4e5aec432024-03-14 23:12:46273
274 auto result =
David Bokan5ae9e452024-03-26 16:23:59275 EvalJs(contents,
Khushal Sagar4e5aec432024-03-14 23:12:46276 JsReplace(
277 R"(
278 (async () => {
279 let navigateFired = false;
280 navigation.onnavigate = (event) => {
281 navigateFired = (event.navigationType === "traverse");
282 };
283 let pageswapfired = new Promise((resolve) => {
284 onpageswap = (e) => {
285 if (!navigateFired || e.viewTransition == null) {
286 resolve(null);
287 return;
288 }
289 activation = e.activation;
290 resolve(activation);
291 };
292 });
293 let result = await pageswapfired;
294 return result != null;
295 })();
296 )"),
297 EXECUTE_SCRIPT_DEFAULT_OPTIONS, ISOLATED_WORLD_ID_GLOBAL,
298 std::move(trigger_navigation));
299 return result.ExtractBool();
300 }
301
David Bokan5ae9e452024-03-26 16:23:59302 void TriggerBackNavigation(GURL back_url, WebContents* web_contents) {
Khushal Sagar4e5aec432024-03-14 23:12:46303 if (BFCacheEnabled()) {
David Bokan5ae9e452024-03-26 16:23:59304 TestActivationManager manager(web_contents, back_url);
305 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46306 manager.WaitForNavigationFinished();
307 } else {
David Bokan5ae9e452024-03-26 16:23:59308 TestNavigationManager manager(web_contents, back_url);
309 web_contents->GetController().GoBack();
Khushal Sagar4e5aec432024-03-14 23:12:46310 ASSERT_TRUE(manager.WaitForNavigationFinished());
311 }
312 }
313};
314
315IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
316 NavigateEventFiresBeforeCapture) {
317 if (!BFCacheEnabled()) {
318 DisableBackForwardCacheForTesting(
319 shell()->web_contents(),
320 BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);
321 } else if (!base::FeatureList::IsEnabled(features::kBackForwardCache)) {
322 GTEST_SKIP();
323 }
324
325 GURL test_url(
326 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
327 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
Khushal Sagar4e5aec432024-03-14 23:12:46328 GURL second_url(embedded_test_server()->GetURL(
329 "/view_transitions/basic-vt-opt-in.html?new"));
330 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), second_url));
331 WaitForCopyableViewInWebContents(shell()->web_contents());
Khushal Sagar4e5aec432024-03-14 23:12:46332 auto& nav_controller = static_cast<NavigationControllerImpl&>(
333 shell()->web_contents()->GetController());
334 ASSERT_TRUE(nav_controller.CanGoBack());
335 ASSERT_TRUE(NavigateBack(test_url));
336}
337
David Bokan5ae9e452024-03-26 16:23:59338// A session restore (e.g. "Duplicate Tab", "Undo Close Tab") uses RESTORE
339// navigation types when traversing the session history. Ensure these
340// navigations trigger a view transition.
341IN_PROC_BROWSER_TEST_P(ViewTransitionBrowserTestTraverse,
342 TransitionOnSessionRestoreTraversal) {
343 // A restored session will never have its session history in BFCache so
344 // there's no need to run a BFCache version of the test.
345 if (BFCacheEnabled()) {
346 GTEST_SKIP();
347 }
348
349 // Start with a page which has an opt-in for VT.
350 GURL url_a(
351 embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
352 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_a));
353
354 // Navigate to another page with an opt-in. (There's no transition due to
355 // being browser-initiated)
356 GURL url_b(embedded_test_server()->GetURL(
357 "/view_transitions/basic-vt-opt-in.html?next"));
358 ASSERT_TRUE(NavigateToURL(shell()->web_contents(), url_b));
359
360 // Clone the tab and load the page. Note: the cloned web contents must be put
361 // into a window to generate BeginFrames which are required since a view
362 // transition will not trigger unless a frame has been generated and the page
363 // revealed.
364 std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
365 WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
366 shell()->AddNewContents(nullptr, std::move(new_tab), url_b,
367 WindowOpenDisposition::NEW_FOREGROUND_TAB,
368 blink::mojom::WindowFeatures(), false, nullptr);
369 NavigationController& new_controller = new_tab_impl->GetController();
370
371 {
372 TestNavigationObserver clone_observer(new_tab_impl);
373 new_controller.LoadIfNecessary();
374 clone_observer.Wait();
375 }
376
377 // Ensure the page has been revealed before navigating back so that a
378 // transition will be triggered.
379 WaitForCopyableViewInWebContents(new_tab_impl);
380
381 // TODO(crbug.com/331226127) Intentionally ignore the return value as the
382 // navigation API (erroneously?) doesn't fire events for restored traversals.
383 NavigateBack(url_a, new_tab_impl);
384
385 // Ensure a frame has been generated so that the reveal event would have been
386 // fired.
387 WaitForCopyableViewInWebContents(new_tab_impl);
388
389 EXPECT_EQ(true, EvalJs(new_tab_impl, "had_incoming_transition"));
390}
391
Khushal Sagar4e5aec432024-03-14 23:12:46392INSTANTIATE_TEST_SUITE_P(P,
393 ViewTransitionBrowserTestTraverse,
394 ::testing::Bool());
395
Noam Rosenthal09dd02b2024-09-21 21:05:52396class ViewTransitionCaptureTest
397 : public ContentBrowserTest,
398 public ::testing::WithParamInterface<std::string> {
399 public:
400 ViewTransitionCaptureTest() { EnablePixelOutput(); }
401
402 void SetUpOnMainThread() override {
403 ContentBrowserTest::SetUpOnMainThread();
404 host_resolver()->AddRule("*", "127.0.0.1");
405 embedded_test_server()->ServeFilesFromSourceDirectory(
406 GetTestDataFilePath());
407 net::test_server::RegisterDefaultHandlers(embedded_test_server());
408 ASSERT_TRUE(embedded_test_server()->Start());
409 }
410
411 protected:
412 SkBitmap TakeScreenshot() {
413 WaitForCopyableViewInWebContents(shell()->web_contents());
414 base::test::TestFuture<const SkBitmap&> future_bitmap;
415 shell()->web_contents()->GetRenderWidgetHostView()->CopyFromSurface(
416 gfx::Rect(), gfx::Size(), future_bitmap.GetCallback());
417 return future_bitmap.Take();
418 }
419
420 private:
421 base::test::ScopedFeatureList feature_list_;
422};
423
424IN_PROC_BROWSER_TEST_P(ViewTransitionCaptureTest,
425 ViewTransitionNoArtifactDuringCapture) {
426 GURL test_url(embedded_test_server()->GetURL(GetParam()));
427 auto* web_contents = shell()->web_contents();
428 web_contents->Resize({0, 0, 20, 20});
429 ASSERT_TRUE(NavigateToURL(web_contents, test_url));
430 ASSERT_EQ(EvalJs(web_contents, JsReplace(R"(
431 new Promise(resolve => {
432 requestAnimationFrame(() => resolve("ok"));
433 }))")),
434 "ok");
435 SkBitmap before_bitmap = TakeScreenshot();
436
437 // Sanity to see that we've captured something.
438 ASSERT_NE(before_bitmap.getColor(5, 5), 0u);
439 // This starts a view transition with a "hanging" promise that never resolves.
440 // When the view-transition callback is called, we resolve the external
441 // promise that signals us that it's time to capture.
442 ASSERT_EQ(EvalJs(web_contents, JsReplace(R"(
443 new Promise(ready_to_capture => {
444 document.startViewTransition(() => new Promise(() => {
445 ready_to_capture('ok');
446 }));
447 }))")),
448 "ok");
449 auto after_bitmap = TakeScreenshot();
450 ASSERT_EQ(before_bitmap.width(), after_bitmap.width());
451 ASSERT_EQ(before_bitmap.height(), after_bitmap.height());
452 EXPECT_TRUE(cc::MatchesBitmap(before_bitmap, after_bitmap,
453 cc::ExactPixelComparator()));
454}
455
456INSTANTIATE_TEST_SUITE_P(
457 P,
458 ViewTransitionCaptureTest,
459 testing::Values("/view_transitions/parent-child.html",
460 "/view_transitions/parent-child-opacity.html"));
461
Khushal Sagar91b544222024-03-12 17:36:59462} // namespace content