// 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 "base/test/test_future.h"
#include "base/trace_event/trace_config.h"
#include "build/buildflag.h"
#include "cc/base/features.h"
#include "cc/test/pixel_comparator.h"
#include "cc/test/pixel_test_utils.h"
#include "components/viz/common/features.h"
#include "components/viz/common/frame_sinks/copy_output_result.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/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/view_transition_opt_in_state.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/tracing_controller.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.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/shell/browser/shell_download_manager_delegate.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 "services/viz/privileged/mojom/compositing/features.mojom-features.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "ui/gfx/geometry/size.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;
    }

    const char* TraceEventName() const override { return "TestCondition"; }

   private:
    raw_ptr<base::RunLoop> run_loop_;
  };

  ViewTransitionBrowserTest() {
    feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {viz::mojom::EnableVizTestApis},
        /*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();
  }

  bool HasVTOptIn(RenderFrameHost* rfh) {
    auto* opt_in_state = ViewTransitionOptInState::GetForCurrentDocument(
        static_cast<RenderFrameHostImpl*>(rfh));
    return opt_in_state &&
           opt_in_state->same_origin_opt_in() ==
               blink::mojom::ViewTransitionSameOriginOptIn::kEnabled;
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  std::unique_ptr<base::RunLoop> run_loop_;
};

// TODO(crbug.com/468211765): Flaky on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_NavigationCancelledAfterScreenshot \
  DISABLED_NavigationCancelledAfterScreenshot
#else
#define MAYBE_NavigationCancelledAfterScreenshot \
  NavigationCancelledAfterScreenshot
#endif
IN_PROC_BROWSER_TEST_F(ViewTransitionBrowserTest,
                       MAYBE_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));
  WaitForCopyableViewInWebContents(shell()->web_contents());

  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;

  bool has_resources = false;
  GetHostFrameSinkManager()
      ->GetFrameSinkManagerTestApi()
      .HasUnclaimedViewTransitionResources(&has_resources);
  ASSERT_TRUE(has_resources);

  shell()->web_contents()->Stop();
  ASSERT_FALSE(navigation_manager.was_committed());
  GetHostFrameSinkManager()
      ->GetFrameSinkManagerTestApi()
      .HasUnclaimedViewTransitionResources(&has_resources);
  ASSERT_FALSE(has_resources);

  // Ensure the old renderer discards the outgoing transition.
  EXPECT_TRUE(ExecJs(
      shell()->web_contents()->GetPrimaryMainFrame(),
      "(async () => { await document.startViewTransition().ready; })()"));
}

class ViewTransitionEarlyFinalFrameTest : public ViewTransitionBrowserTest {
 public:
  ViewTransitionEarlyFinalFrameTest() {
    feature_list_.InitAndEnableFeature(
        ::features::kSendEarlyFinalBeginMainFrame);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(ViewTransitionEarlyFinalFrameTest,
                       SendEarlyFinalBeginMainFrameTriggers) {
  GURL test_url(
      embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));

  ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
  WaitForCopyableViewInWebContents(shell()->web_contents());

  // Start tracing for the "cc" category.
  base::RunLoop tracing_run_loop;
  base::trace_event::TraceConfig trace_config("cc", "");
  content::TracingController::GetInstance()->StartTracing(
      trace_config, tracing_run_loop.QuitClosure());
  tracing_run_loop.Run();

  // Trigger the cross-document view transition.
  TestNavigationManager navigation_manager(shell()->web_contents(), test_url);
  ASSERT_TRUE(
      ExecJs(shell()->web_contents(), "location.href = location.href;"));
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

  // Stop tracing.
  std::string trace_output;
  base::RunLoop stop_tracing_run_loop;
  content::TracingController::GetInstance()->StopTracing(
      content::TracingController::CreateStringEndpoint(base::BindOnce(
          [](std::string* output, base::OnceClosure quit,
             std::unique_ptr<std::string> trace_str) {
            *output = std::move(*trace_str);
            std::move(quit).Run();
          },
          &trace_output, stop_tracing_run_loop.QuitClosure())));
  stop_tracing_run_loop.Run();

  // Check that the function which requests an urgent frame was called and
  // traced.
  EXPECT_TRUE(trace_output.find("Scheduler::SendEarlyFinalBeginMainFrame") !=
              std::string::npos);
}

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));
  WaitForCopyableViewInWebContents(shell()->web_contents());

  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));
  WaitForCopyableViewInWebContents(shell()->web_contents());

  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 ViewTransitionDownloadBrowserTest : public ViewTransitionBrowserTest {
 public:
  void SetUpOnMainThread() override {
    ViewTransitionBrowserTest::SetUpOnMainThread();

    // Set up a test download directory, in order to prevent prompting for
    // handling downloads.
    ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir());
    ShellDownloadManagerDelegate* delegate =
        static_cast<ShellDownloadManagerDelegate*>(
            shell()
                ->web_contents()
                ->GetBrowserContext()
                ->GetDownloadManagerDelegate());
    delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath());
  }

 private:
  base::ScopedTempDir downloads_directory_;
};

IN_PROC_BROWSER_TEST_F(ViewTransitionDownloadBrowserTest,
                       NavigationToDownloadLink) {
  GURL test_url(
      embedded_test_server()->GetURL("/view_transitions/basic-vt-opt-in.html"));
  ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
  WaitForCopyableViewInWebContents(shell()->web_contents());

  GURL download_url(embedded_test_server()->GetURL("/download-test1.lib"));
  TestNavigationManager navigation_manager(shell()->web_contents(),
                                           download_url);
  ASSERT_TRUE(ExecJs(shell()->web_contents(),
                     JsReplace("location.href = $1", download_url)));

  // Wait for response and resume. The navigation should not be blocked by the
  // view transition condition.
  ASSERT_TRUE(navigation_manager.WaitForRequestStart());

  ASSERT_TRUE(HasVTOptIn(shell()->web_contents()->GetPrimaryMainFrame()));
  auto* navigation_request =
      NavigationRequest::From(navigation_manager.GetNavigationHandle());
  ASSERT_EQ(
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
      navigation_request->GetTentativeOriginAtRequestTime());

  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
  ASSERT_FALSE(navigation_manager.was_committed());
}

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());

class ViewTransitionCaptureTest
    : public ContentBrowserTest,
      public ::testing::WithParamInterface<std::pair<bool, std::string>> {
 public:
  ViewTransitionCaptureTest() {
    EnablePixelOutput(1.f);
    feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {viz::mojom::EnableVizTestApis,
         features::kViewTransitionCaptureAndDisplay},
        /*disabled_features=*/
        {blink::features::kPaintHolding});
  }

  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());
  }

 protected:
  SkBitmap TakeScreenshot() {
    base::test::TestFuture<const content::CopyFromSurfaceResult&> future_bitmap;
    shell()->web_contents()->GetRenderWidgetHostView()->CopyFromSurface(
        gfx::Rect(), gfx::Size(), base::TimeDelta(),
        future_bitmap.GetCallback());
    return future_bitmap.Take()
        .value_or(viz::CopyOutputBitmapWithMetadata())
        .bitmap;
  }

  void WaitForSurfaceAnimationManager(RenderFrameHost* render_frame_host) {
    mojo::ScopedAllowSyncCallForTesting sync_scope;
    GetHostFrameSinkManager()
        ->GetFrameSinkManagerTestApi()
        .WaitForSurfaceAnimationManager(
            static_cast<RenderFrameHostImpl*>(render_frame_host)
                ->GetRenderWidgetHost()
                ->GetFrameSinkId());
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// TODO(https://crbug.com/400187507): Disabled due to continuous flakiness.
IN_PROC_BROWSER_TEST_P(ViewTransitionCaptureTest,
                       DISABLED_ViewTransitionNoArtifactDuringCapture) {
  const auto& [frametest, url] = GetParam();
  GURL test_url(embedded_test_server()->GetURL(url));
  auto* web_contents = shell()->web_contents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_url));
  shell()->ResizeWebContentForTests(gfx::Size(100, 100));

  ASSERT_EQ(EvalJs(web_contents, JsReplace(R"(
            new Promise(resolve => {
              requestAnimationFrame(() => {
                requestAnimationFrame(() => resolve("ok"));
              });
            }))")),
            "ok");
  WaitForCopyableViewInWebContents(shell()->web_contents());
  SkBitmap before_bitmap = TakeScreenshot();

  // Sanity to see that we've captured something.
  ASSERT_NE(before_bitmap.getColor(5, 5), 0u);
  // This starts a view transition with a callback that signals that we're ok
  // to capture, but otherwise never finishes running the callback.
  if (frametest) {
    ASSERT_EQ(EvalJs(web_contents, JsReplace(R"(
                new Promise(dom_callback_started => {
                  frame.contentDocument.startViewTransition(async () => {
                    dom_callback_started('ok');
                    await new Promise(() => {});
                  });
                }))")),
              "ok");
  } else {
    ASSERT_EQ(EvalJs(web_contents, JsReplace(R"(
                new Promise(dom_callback_started => {
                  document.startViewTransition(async () => {
                    dom_callback_started('ok');
                    await new Promise(() => {});
                  });
                }))")),
              "ok");
  }
  WaitForSurfaceAnimationManager(
      shell()->web_contents()->GetPrimaryMainFrame());
  auto after_bitmap = TakeScreenshot();
  ASSERT_EQ(before_bitmap.width(), after_bitmap.width());
  ASSERT_EQ(before_bitmap.height(), after_bitmap.height());

  cc::FuzzyPixelComparator comparator;
  // Allow 50% of pixels to different by at most 3 in all channels.
  // The small differences on some platforms seem to be noise, and don't
  // invalidate the test intent.
  comparator.SetAbsErrorLimit(255, 3);
  comparator.SetErrorPixelsPercentageLimit(0, 50);
  EXPECT_TRUE(cc::MatchesBitmap(after_bitmap, before_bitmap, comparator));
}

INSTANTIATE_TEST_SUITE_P(
    P,
    ViewTransitionCaptureTest,
    // The pair parameter has the following meaning:
    //  - The first boolean indicates whether this test should invoke
    //    startViewTransition() on the `frame` DOM element's contentDocument (if
    //    true) or on the main frame's document (if false).
    //    - The second string indicates the location of the test to load.
    testing::Values(
        std::make_pair(false, "/view_transitions/parent-child.html"),
        std::make_pair(false, "/view_transitions/parent-child-opacity.html"),
        std::make_pair(true,
                       "/view_transitions/parent-child-opacity-iframe.html")));

class ViewTransitionProcessShutdownTest : public ViewTransitionBrowserTest {
 public:
  ViewTransitionProcessShutdownTest() {
    EnablePixelOutput(1.f);
    feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {blink::features::kDelayLayerTreeViewDeletionOnLocalSwap,
         ::features::kRenderDocument},
        /*disabled_features=*/{});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(ViewTransitionProcessShutdownTest,
                       ResourcesAvailableAfterCrossProcessNavigation) {
  DisableBackForwardCacheForTesting(
      shell()->web_contents(),
      BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);

  GURL test_url(embedded_test_server()->GetURL(
      "a.com", "/view_transitions/basic-vt-opt-in.html"));
  ASSERT_TRUE(NavigateToURL(shell()->web_contents(), test_url));
  WaitForCopyableViewInWebContents(shell()->web_contents());

  // Navigate to another page with an opt-in. Use a.com to ensure same-origin
  // (triggers VT). Use COOP to force a new BrowsingInstance and process swap.
  GURL second_url(embedded_test_server()->GetURL(
      "a.com",
      "/view_transitions/"
      "basic-vt-opt-in.html?new&pipe=header(Cross-Origin-Opener-Policy,same-"
      "origin)"));
  TestNavigationManager navigation_manager(shell()->web_contents(), second_url);
  ASSERT_TRUE(ExecJs(shell()->web_contents(),
                     JsReplace("location.href = $1", second_url)));
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

  WaitForCopyableViewInWebContents(shell()->web_contents());
  auto& nav_controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  ASSERT_EQ(nav_controller.GetLastCommittedEntry()->GetURL(), second_url);

  // Ensure the new renderer has the resources.
  ASSERT_TRUE(static_cast<RenderWidgetHostViewBase*>(
                  shell()->web_contents()->GetRenderWidgetHostView())
                  ->HasViewTransitionResourcesForTesting());
}
}  // namespace content
