Update recently visited page when navigation happens

ShoppingService::OnWebWrapperSwitched() is only called on focus
change. But if a navigation happens in the tab, this is not get called.
This CL calls OnWebWrapperSwitched() when a navigation happens to keep
recent viewed tab list up to date.

Bug: 355487445
Change-Id: I7f1939f1657a6d4649105ac11f95c484c156ef13
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5739997
Reviewed-by: Adam Rice <[email protected]>
Reviewed-by: Matthew Jones <[email protected]>
Commit-Queue: Min Qin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1339757}
diff --git a/components/commerce/content/browser/BUILD.gn b/components/commerce/content/browser/BUILD.gn
index 4a27670..795fd8b 100644
--- a/components/commerce/content/browser/BUILD.gn
+++ b/components/commerce/content/browser/BUILD.gn
@@ -29,11 +29,15 @@
 
 source_set("unit_tests") {
   testonly = true
-  sources = [ "web_extractor_impl_unittest.cc" ]
+  sources = [
+    "commerce_tab_helper_unittest.cc",
+    "web_extractor_impl_unittest.cc",
+  ]
   deps = [
     ":browser",
     "//base",
     "//base/test:test_support",
+    "//components/commerce/core:shopping_service_test_support",
     "//components/commerce/core/mojom:mojo_bindings",
     "//content/public/browser",
     "//content/test:test_support",
diff --git a/components/commerce/content/browser/commerce_tab_helper.cc b/components/commerce/content/browser/commerce_tab_helper.cc
index cc58ac77..f6fc513 100644
--- a/components/commerce/content/browser/commerce_tab_helper.cc
+++ b/components/commerce/content/browser/commerce_tab_helper.cc
@@ -50,6 +50,12 @@
   if (web_contents()->IsDocumentOnLoadCompletedInPrimaryMainFrame()) {
     shopping_service_->DidFinishLoad(web_wrapper_.get());
   }
+
+  if (navigation_handle->HasCommitted() &&
+      navigation_handle->ShouldUpdateHistory() &&
+      web_contents()->GetFocusedFrame()) {
+    shopping_service_->OnWebWrapperViewed(web_wrapper_.get());
+  }
 }
 
 void CommerceTabHelper::DidStopLoading() {
diff --git a/components/commerce/content/browser/commerce_tab_helper.h b/components/commerce/content/browser/commerce_tab_helper.h
index 54804ab8..84ab474 100644
--- a/components/commerce/content/browser/commerce_tab_helper.h
+++ b/components/commerce/content/browser/commerce_tab_helper.h
@@ -49,6 +49,7 @@
   void SetShoppingServiceForTesting(KeyedService* service);
 
  private:
+  friend class CommerceTabHelperTest;
   friend class content::WebContentsUserData<CommerceTabHelper>;
 
   CommerceTabHelper(content::WebContents* contents,
diff --git a/components/commerce/content/browser/commerce_tab_helper_unittest.cc b/components/commerce/content/browser/commerce_tab_helper_unittest.cc
new file mode 100644
index 0000000..a585b4d
--- /dev/null
+++ b/components/commerce/content/browser/commerce_tab_helper_unittest.cc
@@ -0,0 +1,89 @@
+// 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 "components/commerce/content/browser/commerce_tab_helper.h"
+
+#include "base/memory/ptr_util.h"
+#include "components/commerce/content/browser/web_contents_wrapper.h"
+#include "components/commerce/core/mock_shopping_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/mock_navigation_handle.h"
+#include "content/public/test/test_renderer_host.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace commerce {
+
+const char kLastMainFrameUrl[] = "https://foo.com";
+const char kNewMainFrameUrl[] = "https://foobar.com";
+
+class CommerceTabHelperTest : public content::RenderViewHostTestHarness {
+ public:
+  CommerceTabHelperTest() = default;
+  CommerceTabHelperTest(const CommerceTabHelperTest&) = delete;
+  CommerceTabHelperTest& operator=(const CommerceTabHelperTest&) = delete;
+  ~CommerceTabHelperTest() override = default;
+
+  void SetUp() override {
+    content::RenderViewHostTestHarness::SetUp();
+    tab_helper_ = base::WrapUnique(new CommerceTabHelper(
+        web_contents(), browser_context()->IsOffTheRecord(), &shopping_service_,
+        0));
+    NavigateAndCommit(GURL(kLastMainFrameUrl));
+  }
+
+  void TearDown() override { content::RenderViewHostTestHarness::TearDown(); }
+
+ protected:
+  std::unique_ptr<CommerceTabHelper> tab_helper_;
+  MockShoppingService shopping_service_;
+};
+
+TEST_F(CommerceTabHelperTest, FocusedMainFrameNavigation) {
+  FocusWebContentsOnMainFrame();
+  NavigateAndCommit(GURL(kNewMainFrameUrl));
+
+  EXPECT_CALL(shopping_service_, GetUrlInfosForRecentlyViewedWebWrappers)
+      .WillOnce([this]() {
+        return shopping_service_
+            .ShoppingService::GetUrlInfosForRecentlyViewedWebWrappers();
+      });
+  std::vector<UrlInfo> infos =
+      shopping_service_.GetUrlInfosForRecentlyViewedWebWrappers();
+  EXPECT_EQ(infos.size(), 1u);
+  EXPECT_EQ(infos[0].url, GURL(kNewMainFrameUrl));
+}
+
+TEST_F(CommerceTabHelperTest, NotFocusedMainFrameNavigation) {
+  NavigateAndCommit(GURL(kNewMainFrameUrl));
+
+  EXPECT_CALL(shopping_service_, GetUrlInfosForRecentlyViewedWebWrappers)
+      .WillOnce([this]() {
+        return shopping_service_
+            .ShoppingService::GetUrlInfosForRecentlyViewedWebWrappers();
+      });
+  std::vector<UrlInfo> infos =
+      shopping_service_.GetUrlInfosForRecentlyViewedWebWrappers();
+  EXPECT_EQ(infos.size(), 0u);
+}
+
+TEST_F(CommerceTabHelperTest, SubFrameNavigation) {
+  FocusWebContentsOnMainFrame();
+  content::MockNavigationHandle navigation_handle(GURL(kNewMainFrameUrl),
+                                                  main_rfh());
+  navigation_handle.set_is_in_primary_main_frame(false);
+  navigation_handle.set_has_committed(true);
+  tab_helper_->DidFinishNavigation(&navigation_handle);
+
+  EXPECT_CALL(shopping_service_, GetUrlInfosForRecentlyViewedWebWrappers)
+      .WillOnce([this]() {
+        return shopping_service_
+            .ShoppingService::GetUrlInfosForRecentlyViewedWebWrappers();
+      });
+  std::vector<UrlInfo> infos =
+      shopping_service_.GetUrlInfosForRecentlyViewedWebWrappers();
+  EXPECT_EQ(infos.size(), 0u);
+}
+
+}  // namespace commerce
diff --git a/components/commerce/core/shopping_service.cc b/components/commerce/core/shopping_service.cc
index 83035b0..9b48485 100644
--- a/components/commerce/core/shopping_service.cc
+++ b/components/commerce/core/shopping_service.cc
@@ -428,35 +428,11 @@
 }
 
 void ShoppingService::OnWebWrapperSwitched(WebWrapper* web) {
-  bool already_exists_in_recents = false;
-  for (auto it = recently_visited_tabs_.begin();
-       it != recently_visited_tabs_.end(); ++it) {
-    if (it->url == web->GetLastCommittedURL()) {
-      recently_visited_tabs_.erase(it);
+  UpdateRecentlyViewedURL(web);
+}
 
-      // Don't remove the item from the cache here in case it's the only
-      // reference to a particular URL (causing deletion from the cache) since
-      // we're only shifting it to the head of the list.
-      already_exists_in_recents = true;
-      break;
-    }
-  }
-
-  UrlInfo info;
-  info.url = web->GetLastCommittedURL();
-  info.title = web->GetTitle();
-
-  if (!already_exists_in_recents) {
-    commerce_info_cache_.AddRef(info.url);
-  }
-
-  recently_visited_tabs_.insert(recently_visited_tabs_.begin(),
-                                std::move(info));
-
-  if (recently_visited_tabs_.size() > kRecentTabsMaxSize) {
-    commerce_info_cache_.RemoveRef(recently_visited_tabs_.back().url);
-    recently_visited_tabs_.pop_back();
-  }
+void ShoppingService::OnWebWrapperViewed(WebWrapper* web) {
+  UpdateRecentlyViewedURL(web);
 }
 
 void ShoppingService::TryRunningLocalExtractionForProductInfo(
@@ -1827,6 +1803,38 @@
           std::move(callback)));
 }
 
+void ShoppingService::UpdateRecentlyViewedURL(WebWrapper* web) {
+  bool already_exists_in_recents = false;
+  for (auto it = recently_visited_tabs_.begin();
+       it != recently_visited_tabs_.end(); ++it) {
+    if (it->url == web->GetLastCommittedURL()) {
+      recently_visited_tabs_.erase(it);
+
+      // Don't remove the item from the cache here in case it's the only
+      // reference to a particular URL (causing deletion from the cache) since
+      // we're only shifting it to the head of the list.
+      already_exists_in_recents = true;
+      break;
+    }
+  }
+
+  UrlInfo info;
+  info.url = web->GetLastCommittedURL();
+  info.title = web->GetTitle();
+
+  if (!already_exists_in_recents) {
+    commerce_info_cache_.AddRef(info.url);
+  }
+
+  recently_visited_tabs_.insert(recently_visited_tabs_.begin(),
+                                std::move(info));
+
+  if (recently_visited_tabs_.size() > kRecentTabsMaxSize) {
+    commerce_info_cache_.RemoveRef(recently_visited_tabs_.back().url);
+    recently_visited_tabs_.pop_back();
+  }
+}
+
 const std::vector<ProductSpecificationsSet>
 ShoppingService::GetAllProductSpecificationSets() {
   if (!product_specifications_service_) {
diff --git a/components/commerce/core/shopping_service.h b/components/commerce/core/shopping_service.h
index 242082f3..e875fc08 100644
--- a/components/commerce/core/shopping_service.h
+++ b/components/commerce/core/shopping_service.h
@@ -478,6 +478,10 @@
   // analogous to switching tabs.
   void OnWebWrapperSwitched(WebWrapper* web);
 
+  // Called to signal that a WebWrapper is viewed. This happens when a new
+  // navigation is committed in a focused tab.
+  void OnWebWrapperViewed(WebWrapper* web);
+
   // Schedule (or reschedule) the on-page local extraction execution. Calling
   // this sequentially for the same web wrapper with the same URL will cancel
   // the pending task and schedule a new one. The script will, at most, run once
@@ -619,6 +623,8 @@
   void GetProductIdentifierForUrl(const GURL& url,
                                   UrlProductIdentifierTupleCallback callback);
 
+  void UpdateRecentlyViewedURL(WebWrapper* web);
+
   // Return all ProductSpecificationsSets from ProductSpecificationsService.
   virtual const std::vector<ProductSpecificationsSet>
   GetAllProductSpecificationSets();
diff --git a/components/commerce/ios/DEPS b/components/commerce/ios/DEPS
index d0e62f4..0dd9adc8 100644
--- a/components/commerce/ios/DEPS
+++ b/components/commerce/ios/DEPS
@@ -4,4 +4,5 @@
   "+ios/web/public",
   "+services/data_decoder/public/cpp",
   "+ui/base",
+  "+net/http/http_response_headers.h",
 ]
diff --git a/components/commerce/ios/browser/BUILD.gn b/components/commerce/ios/browser/BUILD.gn
index 7ffd4c6..e5b56e1 100644
--- a/components/commerce/ios/browser/BUILD.gn
+++ b/components/commerce/ios/browser/BUILD.gn
@@ -23,6 +23,7 @@
     "//ios/web/public",
     "//ios/web/public/js_messaging",
     "//ios/web/public/webui",
+    "//net",
     "//services/data_decoder/public/cpp",
   ]
 
diff --git a/components/commerce/ios/browser/commerce_tab_helper.mm b/components/commerce/ios/browser/commerce_tab_helper.mm
index 967593e7..a287aa8 100644
--- a/components/commerce/ios/browser/commerce_tab_helper.mm
+++ b/components/commerce/ios/browser/commerce_tab_helper.mm
@@ -6,10 +6,42 @@
 
 #include "base/memory/ptr_util.h"
 #include "ios/web/public/navigation/navigation_context.h"
+#include "ios/web/public/navigation/navigation_item.h"
+#include "ios/web/public/navigation/navigation_manager.h"
 #include "ios/web/public/web_state.h"
+#include "net/http/http_response_headers.h"
 
 namespace commerce {
 
+namespace {
+bool ShouldUpdateRecentlyViewedURL(web::WebState* web_state,
+                                   web::NavigationContext* navigation_context) {
+  if (navigation_context->GetError()) {
+    return false;
+  }
+
+  if (navigation_context->GetResponseHeaders() &&
+      navigation_context->GetResponseHeaders()->response_code() == 404) {
+    return false;
+  }
+
+  if (!navigation_context->HasCommitted() ||
+      !web_state->GetNavigationManager()->GetLastCommittedItem()) {
+    // Navigation was replaced or aborted.
+    return false;
+  }
+
+  web::NavigationItem* last_committed_item =
+      web_state->GetNavigationManager()->GetLastCommittedItem();
+  const GURL& url = last_committed_item->GetURL();
+  if (url.SchemeIs(url::kDataScheme)) {
+    return false;
+  }
+
+  return true;
+}
+}  // namespace
+
 CommerceTabHelper::CommerceTabHelper(web::WebState* state,
                                      bool is_off_the_record,
                                      ShoppingService* shopping_service)
@@ -44,6 +76,11 @@
   }
 
   shopping_service_->DidNavigatePrimaryMainFrame(web_wrapper_.get());
+
+  if (web_state->IsVisible() &&
+      ShouldUpdateRecentlyViewedURL(web_state, navigation_context)) {
+    shopping_service_->OnWebWrapperViewed(web_wrapper_.get());
+  }
 }
 
 void CommerceTabHelper::DidStopLoading(web::WebState* web_state) {