[lensoverlay] Capture top-level navigations in side panel.

Change-Id: I0ef244df0202d1fe2887365e938036fde6ef78c5
Bug: b:379152175, 379338040
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6024467
Commit-Queue: Juan Mojica <juanmojica@google.com>
Reviewed-by: Duncan Mercer <mercerd@google.com>
Cr-Commit-Position: refs/heads/main@{#1383877}
diff --git a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
index aad412d..575df9c 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
@@ -150,6 +150,11 @@
     "(function() {const anchor = document.createElement('a');anchor.href = "
     "$1;document.body.appendChild(anchor);anchor.click();})();";
 
+constexpr char kTopLevelNavLinkClickScript[] =
+    "(function() {const anchor = document.createElement('a');anchor.href = "
+    "$1;anchor.target='_top';document.body.appendChild(anchor);anchor.click();}"
+    ")();";
+
 constexpr char kCheckSearchboxInput[] =
     "(function() {const root = "
     "document.getElementsByTagName('lens-side-panel-app')[0].shadowRoot;"
@@ -2047,6 +2052,80 @@
 }
 
 IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
+                       SidePanel_TopLevelSameOriginLinkClick) {
+  WaitForPaint();
+
+  // State should start in off.
+  auto* controller = GetLensOverlayController();
+  EXPECT_EQ(controller->state(), State::kOff);
+
+  // Showing UI should change the state to screenshot and eventually to overlay.
+  controller->ShowUI(LensOverlayInvocationSource::kAppMenu);
+  ASSERT_EQ(controller->state(), State::kScreenshot);
+  EXPECT_TRUE(base::test::RunUntil(
+      [&]() { return controller->state() == State::kOverlay; }));
+  EXPECT_TRUE(content::WaitForLoadStop(GetOverlayWebContents()));
+  EXPECT_TRUE(controller->GetOverlayViewForTesting()->GetVisible());
+
+  // Loading a url in the side panel should show the results page. This needs to
+  // be done to set up the WebContentsObserver.
+  const GURL search_url("https://www.google.com/search");
+  controller->LoadURLInResultsFrame(search_url);
+
+  // Expect the Lens Overlay results panel to open.
+  auto* coordinator = browser()->GetFeatures().side_panel_coordinator();
+  EXPECT_TRUE(coordinator->IsSidePanelShowing());
+  EXPECT_EQ(coordinator->GetCurrentEntryId(),
+            SidePanelEntry::Id::kLensOverlayResults);
+  EXPECT_TRUE(content::WaitForLoadStop(
+      controller->GetSidePanelWebContentsForTesting()));
+  int tabs = browser()->tab_strip_model()->count();
+
+  // The results frame should be the only child frame of the side panel web
+  // contents.
+  content::RenderFrameHost* results_frame = content::ChildFrameAt(
+      controller->GetSidePanelWebContentsForTesting()->GetPrimaryMainFrame(),
+      0);
+  const GURL nav_url("https://www.google.com/search?q=apples");
+  content::OverrideLastCommittedOrigin(results_frame,
+                                       url::Origin::Create(search_url));
+  EXPECT_TRUE(results_frame);
+
+  // Verify the fake controller exists and reset any loading that was done
+  // before as part of setup.
+  auto* fake_controller = static_cast<LensOverlayControllerFake*>(controller);
+  ASSERT_TRUE(fake_controller);
+  fake_controller->ResetSidePanelTracking();
+
+  // Simulate a top level same-origin navigation on the results frame.
+  content::TestNavigationObserver observer(
+      controller->GetSidePanelWebContentsForTesting());
+  EXPECT_TRUE(content::ExecJs(
+      results_frame, content::JsReplace(kTopLevelNavLinkClickScript, nav_url),
+      content::EvalJsOptions::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
+  observer.WaitForNavigationFinished();
+
+  // It should not open a new tab as this is a same-origin navigation.
+  EXPECT_EQ(tabs, browser()->tab_strip_model()->count());
+
+  VerifySearchQueryParameters(observer.last_navigation_url());
+  VerifyTextQueriesAreEqual(observer.last_navigation_url(), nav_url);
+
+  // Verify the loading state was set correctly.
+  EXPECT_EQ(fake_controller->is_side_panel_loading_set_to_true_, 1);
+  EXPECT_EQ(fake_controller->is_side_panel_loading_set_to_false_, 0);
+
+  // We should find that the input text on the searchbox is the same as the text
+  // query of the nav_url.
+  EXPECT_TRUE(content::EvalJs(
+                  controller->GetSidePanelWebContentsForTesting()
+                      ->GetPrimaryMainFrame(),
+                  content::JsReplace(kCheckSearchboxInput, "apples"),
+                  content::EvalJsOptions::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)
+                  .ExtractBool());
+}
+
+IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
                        SidePanel_NewTabCrossOriginLinkClick) {
   WaitForPaint();
 
diff --git a/chrome/browser/ui/lens/lens_overlay_side_panel_navigation_throttle.cc b/chrome/browser/ui/lens/lens_overlay_side_panel_navigation_throttle.cc
index 3c13dc6..455f240 100644
--- a/chrome/browser/ui/lens/lens_overlay_side_panel_navigation_throttle.cc
+++ b/chrome/browser/ui/lens/lens_overlay_side_panel_navigation_throttle.cc
@@ -21,12 +21,14 @@
 LensOverlaySidePanelNavigationThrottle::MaybeCreateFor(
     content::NavigationHandle* handle,
     ThemeService* theme_service) {
-  // We only want to handle navigations within the side panel results frame, so
-  // we can ignore all navigations to a primary main frame. We can also ignore
-  // all navigations that don't occur one level down (e.g. children of iframes
-  // in the WebUI).
-  if (handle->IsInPrimaryMainFrame() || !handle->GetParentFrame() ||
-      !handle->GetParentFrame()->IsInPrimaryMainFrame()) {
+  // We only want to handle navigations within the side panel results frame, we
+  // can ignore all navigations that don't occur one level down (e.g. children
+  // of iframes in the WebUI). However, since the top level frame hosts the
+  // WebUI, we should also handle those navigations within this throttle to
+  // prevent breakages.
+  if (!handle->IsInPrimaryMainFrame() &&
+      (!handle->GetParentFrame() ||
+       !handle->GetParentFrame()->IsInPrimaryMainFrame())) {
     return nullptr;
   }
 
diff --git a/chrome/browser/ui/lens/lens_overlay_url_builder.cc b/chrome/browser/ui/lens/lens_overlay_url_builder.cc
index 640a4b5..64e50a2 100644
--- a/chrome/browser/ui/lens/lens_overlay_url_builder.cc
+++ b/chrome/browser/ui/lens/lens_overlay_url_builder.cc
@@ -37,6 +37,10 @@
 
 // Query parameter for the mode.
 inline constexpr char kModeParameterKey[] = "udm";
+
+// Query parameter for the toolbelt mode.
+inline constexpr char kToolbeltModeParameterKey[] = "tbm";
+
 // Query parameter values for the mode.
 inline constexpr char kUnimodalModeParameterValue[] = "26";
 inline constexpr char kMultimodalModeParameterValue[] = "24";
@@ -78,7 +82,8 @@
 // The list of query parameters to ignore when comparing search URLs.
 inline constexpr std::string kIgnoredSearchUrlQueryParameters[] = {
     kViewportWidthQueryParamKey, kViewportHeightQueryParamKey,
-    kXSRFTokenQueryParamKey, kSecActQueryParamKey};
+    kXSRFTokenQueryParamKey,     kSecActQueryParamKey,
+    kModeParameterKey,           kToolbeltModeParameterKey};
 
 // Query parameter for dark mode.
 inline constexpr char kDarkModeParameterKey[] = "cs";