[PDP] Implement Commerce meta info extraction in Blink
Before this CL, for all the commerce meta info extraction, we inject
a JS script. This approach has proven to be performance regressing. In
this CL, we implement the extraction using Blink APIs when available.
We still use the JS approach for platforms where Blink is not yet
available.
Bug: 1491256
Change-Id: I4e6609279f97f6901046fe291e382d852b7ab527
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4990661
Reviewed-by: Jeremy Roman <[email protected]>
Auto-Submit: Yue Zhang <[email protected]>
Reviewed-by: Ken Rockot <[email protected]>
Reviewed-by: David Trainor <[email protected]>
Commit-Queue: Ken Rockot <[email protected]>
Reviewed-by: Matthew Jones <[email protected]>
Reviewed-by: Robert Sesek <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1221100}
diff --git a/chrome/renderer/BUILD.gn b/chrome/renderer/BUILD.gn
index d6a0aa3..4f0b91f9 100644
--- a/chrome/renderer/BUILD.gn
+++ b/chrome/renderer/BUILD.gn
@@ -149,6 +149,7 @@
"//components/autofill/core/common:features",
"//components/base32",
"//components/cdm/renderer",
+ "//components/commerce/content/renderer",
"//components/commerce/core:commerce_heuristics_data",
"//components/commerce/core:feature_list",
"//components/commerce/core:heuristics_provider",
diff --git a/chrome/renderer/DEPS b/chrome/renderer/DEPS
index 6a1e056..b3da9e0 100644
--- a/chrome/renderer/DEPS
+++ b/chrome/renderer/DEPS
@@ -9,6 +9,7 @@
"+components/autofill/core/common",
"+components/cdm/renderer",
"+components/client_hints/common",
+ "+components/commerce/content/renderer",
"+components/content_capture/common",
"+components/content_capture/renderer",
"+components/content_settings/core/common",
diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc
index 8fe7b4a..44a36c9 100644
--- a/chrome/renderer/chrome_content_renderer_client.cc
+++ b/chrome/renderer/chrome_content_renderer_client.cc
@@ -71,6 +71,7 @@
#include "components/autofill/content/renderer/password_autofill_agent.h"
#include "components/autofill/content/renderer/password_generation_agent.h"
#include "components/autofill/core/common/autofill_features.h"
+#include "components/commerce/content/renderer/commerce_web_extractor.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/content_capture/common/content_capture_features.h"
#include "components/content_capture/renderer/content_capture_sender.h"
@@ -764,6 +765,10 @@
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
MultilineDetector::InstallIfNecessary(render_frame);
#endif
+
+ if (render_frame->IsMainFrame()) {
+ new commerce::CommerceWebExtractor(render_frame, registry);
+ }
}
void ChromeContentRendererClient::WebViewCreated(
diff --git a/chrome/renderer/commerce/DEPS b/chrome/renderer/commerce/DEPS
new file mode 100644
index 0000000..dcfb9c0e
--- /dev/null
+++ b/chrome/renderer/commerce/DEPS
@@ -0,0 +1 @@
+include_rules = ["+components/commerce/core"]
\ No newline at end of file
diff --git a/chrome/renderer/commerce/OWNERS b/chrome/renderer/commerce/OWNERS
new file mode 100644
index 0000000..ddde9c60
--- /dev/null
+++ b/chrome/renderer/commerce/OWNERS
@@ -0,0 +1,3 @@
[email protected]
+
+file://components/commerce/OWNERS
\ No newline at end of file
diff --git a/chrome/renderer/commerce/commerce_web_extractor_browsertest.cc b/chrome/renderer/commerce/commerce_web_extractor_browsertest.cc
new file mode 100644
index 0000000..6ea9e6ad
--- /dev/null
+++ b/chrome/renderer/commerce/commerce_web_extractor_browsertest.cc
@@ -0,0 +1,56 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/test/base/chrome_render_view_test.h"
+#include "components/commerce/content/renderer/commerce_web_extractor.h"
+#include "components/commerce/core/commerce_constants.h"
+
+namespace {
+class CommerceWebExtractorTest : public ChromeRenderViewTest {
+ public:
+ CommerceWebExtractorTest() = default;
+};
+
+TEST_F(CommerceWebExtractorTest, TestValidMetaExtraction) {
+ std::unique_ptr<commerce::CommerceWebExtractor> extractor =
+ std::make_unique<commerce::CommerceWebExtractor>(GetMainRenderFrame(),
+ registry_.get());
+ LoadHTML(
+ "<html>"
+ "<head>"
+ "<meta content=\"product\" property=\"og:type\">"
+ "<meta content=\"product\" property=\"og:type\">"
+ "</head>"
+ "<body>"
+ "</body></html>");
+
+ extractor->ExtractMetaInfo(base::BindOnce([](base::Value result) {
+ ASSERT_TRUE(result.is_dict());
+ auto* str = result.GetDict().FindString(commerce::kOgType);
+ ASSERT_TRUE(str);
+ ASSERT_EQ(*str, commerce::kOgTypeOgProduct);
+ }));
+ base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(CommerceWebExtractorTest, TestInvalidMetaExtraction) {
+ std::unique_ptr<commerce::CommerceWebExtractor> extractor =
+ std::make_unique<commerce::CommerceWebExtractor>(GetMainRenderFrame(),
+ registry_.get());
+ LoadHTML(
+ "<html>"
+ "<head>"
+ "<meta content=\"product\" property=\"type\">"
+ "<meta content=\"product\" type=\"og:type\">"
+ "</head>"
+ "<body>"
+ "</body></html>");
+
+ extractor->ExtractMetaInfo(base::BindOnce([](base::Value result) {
+ ASSERT_TRUE(result.is_dict());
+ ASSERT_TRUE(result.GetDict().empty());
+ }));
+ base::RunLoop().RunUntilIdle();
+}
+} // namespace
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index c775e98..b828dae 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1291,6 +1291,7 @@
"../renderer/autofill/password_generation_test_utils.h",
"../renderer/cart/commerce_hint_agent_browsertest.cc",
"../renderer/cart/commerce_hint_agent_renderer_browsertest.cc",
+ "../renderer/commerce/commerce_web_extractor_browsertest.cc",
"../renderer/safe_browsing/phishing_classifier_browsertest.cc",
"android/browsertests_apk/android_browsertests_jni_onload.cc",
"base/android/android_browser_test_browsertest_android.cc",
@@ -1301,6 +1302,8 @@
"//components/autofill/content/browser:test_support",
"//components/back_forward_cache:back_forward_cache",
"//components/browsing_data/core:core",
+ "//components/commerce/content/renderer",
+ "//components/commerce/core:commerce_constants",
"//components/enterprise:enterprise",
"//components/enterprise:test_support",
"//components/invalidation/impl:test_support",
@@ -1718,7 +1721,9 @@
"//components/certificate_transparency",
"//components/certificate_transparency:proto",
"//components/commerce/content/browser",
+ "//components/commerce/content/renderer",
"//components/commerce/core:cart_db_content_proto",
+ "//components/commerce/core:commerce_constants",
"//components/commerce/core:commerce_heuristics_data",
"//components/commerce/core:coupon_db_content_proto",
"//components/commerce/core:feature_list",
@@ -2796,6 +2801,7 @@
"../renderer/chrome_content_renderer_client_browsertest.cc",
"../renderer/chrome_content_settings_agent_delegate_browsertest.cc",
"../renderer/chrome_render_frame_observer_browsertest.cc",
+ "../renderer/commerce/commerce_web_extractor_browsertest.cc",
"../renderer/translate/translate_agent_browsertest.cc",
"../renderer/translate/translate_script_browsertest.cc",
"../services/qrcode_generator/qrcode_generator_service_pixeltest.cc",
diff --git a/components/BUILD.gn b/components/BUILD.gn
index 7c5788d..75a4fc67 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -503,6 +503,7 @@
":components_tests_pak_bundle_data",
"//components/autofill/ios/browser:unit_tests",
"//components/autofill/ios/form_util:unit_tests",
+ "//components/commerce/ios/browser:unit_tests",
"//components/crash/core/app:unit_tests",
"//components/feed/core/v2/public/ios:feed_ios_unit_tests",
"//components/image_fetcher/ios:unit_tests",
@@ -525,6 +526,7 @@
deps += [
"//components/background_sync:unit_tests",
"//components/blocked_content:unit_tests",
+ "//components/commerce/content/browser:unit_tests",
"//components/content_settings/browser:unit_tests",
"//components/content_settings/browser/ui:unit_tests",
"//components/feed/core/v2:core_unit_tests",
diff --git a/components/commerce/content/DEPS b/components/commerce/content/DEPS
index af61530..6909c59 100644
--- a/components/commerce/content/DEPS
+++ b/components/commerce/content/DEPS
@@ -3,6 +3,8 @@
"+components/optimization_guide",
"+components/prefs",
"+content/public",
+ "+mojo/public/cpp/bindings",
+ "+services/service_manager/public/cpp",
"+ui/base",
"+ui/webui",
]
diff --git a/components/commerce/content/browser/BUILD.gn b/components/commerce/content/browser/BUILD.gn
index 5e78906c..4a27670 100644
--- a/components/commerce/content/browser/BUILD.gn
+++ b/components/commerce/content/browser/BUILD.gn
@@ -17,14 +17,30 @@
deps = [
"//base",
"//components/commerce/core:shopping_service",
+ "//components/commerce/core/mojom:mojo_bindings",
"//components/resources:components_resources_grit",
"//content/public/browser",
+ "//mojo/public/cpp/bindings",
"//ui/webui",
]
public_deps = [ "//components/commerce/core/internals" ]
}
+source_set("unit_tests") {
+ testonly = true
+ sources = [ "web_extractor_impl_unittest.cc" ]
+ deps = [
+ ":browser",
+ "//base",
+ "//base/test:test_support",
+ "//components/commerce/core/mojom:mojo_bindings",
+ "//content/public/browser",
+ "//content/test:test_support",
+ "//testing/gtest",
+ ]
+}
+
if (!is_android) {
static_library("hint") {
sources = [
diff --git a/components/commerce/content/browser/web_contents_wrapper.cc b/components/commerce/content/browser/web_contents_wrapper.cc
index b3111b02..dce3cc1 100644
--- a/components/commerce/content/browser/web_contents_wrapper.cc
+++ b/components/commerce/content/browser/web_contents_wrapper.cc
@@ -54,4 +54,11 @@
web_contents_ = nullptr;
}
+content::RenderFrameHost* WebContentsWrapper::GetPrimaryMainFrame() {
+ if (!web_contents_) {
+ return nullptr;
+ }
+ return web_contents_->GetPrimaryMainFrame();
+}
+
} // namespace commerce
diff --git a/components/commerce/content/browser/web_contents_wrapper.h b/components/commerce/content/browser/web_contents_wrapper.h
index 67f9825..68034034 100644
--- a/components/commerce/content/browser/web_contents_wrapper.h
+++ b/components/commerce/content/browser/web_contents_wrapper.h
@@ -18,6 +18,10 @@
class Value;
} // namespace base
+namespace content {
+class RenderFrameHost;
+} // namespace content
+
namespace commerce {
// A WebWrapper backed by content::WebContents.
@@ -43,6 +47,8 @@
void ClearWebContentsPointer();
+ content::RenderFrameHost* GetPrimaryMainFrame();
+
private:
raw_ptr<content::WebContents> web_contents_;
diff --git a/components/commerce/content/browser/web_extractor_impl.cc b/components/commerce/content/browser/web_extractor_impl.cc
index dc62e76..a2f7dcc 100644
--- a/components/commerce/content/browser/web_extractor_impl.cc
+++ b/components/commerce/content/browser/web_extractor_impl.cc
@@ -5,7 +5,11 @@
#include "components/commerce/content/browser/web_extractor_impl.h"
#include "base/strings/utf_string_conversions.h"
+#include "components/commerce/content/browser/web_contents_wrapper.h"
#include "components/grit/components_resources.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/service_worker_context.h"
#include "ui/base/resource/resource_bundle.h"
namespace commerce {
@@ -16,11 +20,33 @@
void WebExtractorImpl::ExtractMetaInfo(
WebWrapper* web_wrapper,
base::OnceCallback<void(const base::Value)> callback) {
- std::string script =
- ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
- IDR_QUERY_SHOPPING_META_JS);
+ commerce::WebContentsWrapper* wrapper =
+ static_cast<commerce::WebContentsWrapper*>(web_wrapper);
+ content::RenderFrameHost* rfh = wrapper->GetPrimaryMainFrame();
+ if (!rfh) {
+ return;
+ }
+ mojo::Remote<commerce_web_extractor::mojom::CommerceWebExtractor>
+ remote_extractor;
+ rfh->GetRemoteInterfaces()->GetInterface(
+ remote_extractor.BindNewPipeAndPassReceiver());
+ commerce_web_extractor::mojom::CommerceWebExtractor* raw_commerce_extractor =
+ remote_extractor.get();
+ if (!raw_commerce_extractor) {
+ return;
+ }
+ // Pass along the remote_extractor to keep the pipe alive until the callback
+ // returns.
+ raw_commerce_extractor->ExtractMetaInfo(base::BindOnce(
+ &WebExtractorImpl::OnExtractionMetaInfo, weak_ptr_factory_.GetWeakPtr(),
+ std::move(remote_extractor), std::move(callback)));
+}
- web_wrapper->RunJavascript(base::UTF8ToUTF16(script), std::move(callback));
+void WebExtractorImpl::OnExtractionMetaInfo(
+ mojo::Remote<commerce_web_extractor::mojom::CommerceWebExtractor> extractor,
+ base::OnceCallback<void(base::Value)> callback,
+ base::Value result) {
+ std::move(callback).Run(std::move(result));
}
} // namespace commerce
diff --git a/components/commerce/content/browser/web_extractor_impl.h b/components/commerce/content/browser/web_extractor_impl.h
index 7239bd02..6d70c210 100644
--- a/components/commerce/content/browser/web_extractor_impl.h
+++ b/components/commerce/content/browser/web_extractor_impl.h
@@ -5,8 +5,10 @@
#ifndef COMPONENTS_COMMERCE_CONTENT_BROWSER_WEB_EXTRACTOR_IMPL_H_
#define COMPONENTS_COMMERCE_CONTENT_BROWSER_WEB_EXTRACTOR_IMPL_H_
+#include "components/commerce/core/mojom/commerce_web_extractor.mojom.h"
#include "components/commerce/core/web_extractor.h"
#include "components/commerce/core/web_wrapper.h"
+#include "mojo/public/cpp/bindings/remote.h"
namespace commerce {
@@ -23,6 +25,12 @@
base::OnceCallback<void(const base::Value)> callback) override;
private:
+ void OnExtractionMetaInfo(
+ mojo::Remote<commerce_web_extractor::mojom::CommerceWebExtractor>
+ extractor,
+ base::OnceCallback<void(const base::Value)> callback,
+ const base::Value result);
+
base::WeakPtrFactory<WebExtractorImpl> weak_ptr_factory_{this};
};
diff --git a/components/commerce/content/browser/web_extractor_impl_unittest.cc b/components/commerce/content/browser/web_extractor_impl_unittest.cc
new file mode 100644
index 0000000..1b359c4b
--- /dev/null
+++ b/components/commerce/content/browser/web_extractor_impl_unittest.cc
@@ -0,0 +1,93 @@
+// Copyright 2023 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/web_extractor_impl.h"
+
+#include "base/test/gmock_callback_support.h"
+#include "base/test/mock_callback.h"
+#include "components/commerce/content/browser/web_contents_wrapper.h"
+#include "components/commerce/core/mojom/commerce_web_extractor.mojom.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/test_renderer_host.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "services/service_manager/public/cpp/interface_provider.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace commerce {
+
+class WebExtractorImplTest : public content::RenderViewHostTestHarness {
+ public:
+ WebExtractorImplTest() = default;
+ WebExtractorImplTest(const WebExtractorImplTest&) = delete;
+ WebExtractorImplTest& operator=(const WebExtractorImplTest&) = delete;
+ ~WebExtractorImplTest() override = default;
+};
+
+class MockCommerceWebExtractor
+ : public commerce_web_extractor::mojom::CommerceWebExtractor {
+ public:
+ MockCommerceWebExtractor() = default;
+ MockCommerceWebExtractor(const MockCommerceWebExtractor&) = delete;
+ MockCommerceWebExtractor& operator=(const MockCommerceWebExtractor&) = delete;
+ ~MockCommerceWebExtractor() override = default;
+
+ void Bind(mojo::ScopedMessagePipeHandle handle) {
+ receiver_.Bind(mojo::PendingReceiver<
+ commerce_web_extractor::mojom::CommerceWebExtractor>(
+ std::move(handle)));
+ }
+
+ MOCK_METHOD1(ExtractMetaInfo, void(ExtractMetaInfoCallback callback));
+
+ private:
+ mojo::Receiver<commerce_web_extractor::mojom::CommerceWebExtractor> receiver_{
+ this};
+};
+
+TEST_F(WebExtractorImplTest, TestExtraction) {
+ std::unique_ptr<MockCommerceWebExtractor> extractor =
+ std::make_unique<MockCommerceWebExtractor>();
+
+ // Set up mock CommerceWebExtractor.
+ std::unique_ptr<content::WebContents> wc = CreateTestWebContents();
+ content::RenderFrameHostTester::For(wc->GetPrimaryMainFrame())
+ ->InitializeRenderFrameIfNeeded();
+ std::unique_ptr<service_manager::InterfaceProvider::TestApi> test_api =
+ std::make_unique<service_manager::InterfaceProvider::TestApi>(
+ wc->GetPrimaryMainFrame()->GetRemoteInterfaces());
+ test_api->SetBinderForName(
+ commerce_web_extractor::mojom::CommerceWebExtractor::Name_,
+ base::BindRepeating(&MockCommerceWebExtractor::Bind,
+ base::Unretained(extractor.get())));
+
+ // Initialize WebExtractor.
+ std::unique_ptr<commerce::WebExtractorImpl> web_extractor =
+ std::make_unique<commerce::WebExtractorImpl>();
+ std::unique_ptr<commerce::WebContentsWrapper> web_wrapper =
+ std::make_unique<commerce::WebContentsWrapper>(wc.get(), 0u);
+ EXPECT_CALL(*extractor, ExtractMetaInfo(testing::_))
+ .WillOnce(testing::Invoke(
+ [](MockCommerceWebExtractor::ExtractMetaInfoCallback callback) {
+ std::move(callback).Run(base::Value("123"));
+ }));
+ base::MockCallback<base::OnceCallback<void(const base::Value)>> callback;
+ EXPECT_CALL(callback, Run(testing::_))
+ .Times(1)
+ .WillOnce(testing::Invoke(
+ [](base::Value value) { ASSERT_EQ(value.GetString(), "123"); }));
+
+ web_extractor->ExtractMetaInfo(web_wrapper.get(), callback.Get());
+ base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(WebExtractorImplTest, TestNullWebContents) {
+ std::unique_ptr<content::WebContents> wc = CreateTestWebContents();
+ std::unique_ptr<commerce::WebContentsWrapper> web_wrapper =
+ std::make_unique<commerce::WebContentsWrapper>(wc.get(), 0u);
+
+ web_wrapper->ClearWebContentsPointer();
+
+ ASSERT_FALSE(web_wrapper->GetPrimaryMainFrame());
+}
+} // namespace commerce
diff --git a/components/commerce/content/renderer/BUILD.gn b/components/commerce/content/renderer/BUILD.gn
new file mode 100644
index 0000000..136b031
--- /dev/null
+++ b/components/commerce/content/renderer/BUILD.gn
@@ -0,0 +1,16 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+static_library("renderer") {
+ sources = [
+ "commerce_web_extractor.cc",
+ "commerce_web_extractor.h",
+ ]
+ deps = [
+ "//base",
+ "//components/commerce/core/mojom:mojo_bindings",
+ "//content/public/renderer",
+ "//ipc",
+ "//third_party/blink/public:blink",
+ ]
+}
diff --git a/components/commerce/content/renderer/DEPS b/components/commerce/content/renderer/DEPS
new file mode 100644
index 0000000..f07dab8
--- /dev/null
+++ b/components/commerce/content/renderer/DEPS
@@ -0,0 +1 @@
+include_rules = ["+third_party/blink/public"]
\ No newline at end of file
diff --git a/components/commerce/content/renderer/OWNERS b/components/commerce/content/renderer/OWNERS
new file mode 100644
index 0000000..255dc86
--- /dev/null
+++ b/components/commerce/content/renderer/OWNERS
@@ -0,0 +1 @@
[email protected]
\ No newline at end of file
diff --git a/components/commerce/content/renderer/commerce_web_extractor.cc b/components/commerce/content/renderer/commerce_web_extractor.cc
new file mode 100644
index 0000000..d26cdc9
--- /dev/null
+++ b/components/commerce/content/renderer/commerce_web_extractor.cc
@@ -0,0 +1,60 @@
+// Copyright 2023 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/renderer/commerce_web_extractor.h"
+
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "third_party/blink/public/web/web_element.h"
+#include "third_party/blink/public/web/web_element_collection.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace commerce {
+
+namespace {
+const char kOgContent[] = "content";
+const char kOgPrefix[] = "og:";
+const char kOgProperty[] = "property";
+const char kPageMeta[] = "meta";
+} // namespace
+
+CommerceWebExtractor::CommerceWebExtractor(
+ content::RenderFrame* render_frame,
+ service_manager::BinderRegistry* registry)
+ : render_frame_(render_frame) {
+ registry->AddInterface(base::BindRepeating(
+ &CommerceWebExtractor::BindReceiver, base::Unretained(this)));
+}
+
+CommerceWebExtractor::~CommerceWebExtractor() = default;
+
+void CommerceWebExtractor::ExtractMetaInfo(ExtractMetaInfoCallback callback) {
+ auto result = base::Value::Dict();
+ blink::WebDocument doc = render_frame_->GetWebFrame()->GetDocument();
+ blink::WebElementCollection collection =
+ doc.GetElementsByHTMLTagName(kPageMeta);
+ for (blink::WebElement element = collection.FirstItem(); !element.IsNull();
+ element = collection.NextItem()) {
+ if (!element.HasAttribute(kOgProperty) ||
+ !element.HasAttribute(kOgContent)) {
+ continue;
+ }
+ std::string name =
+ base::UTF16ToUTF8(element.GetAttribute(kOgProperty).Utf16());
+ std::string value =
+ base::UTF16ToUTF8(element.GetAttribute(kOgContent).Utf16());
+ if (base::StartsWith(name, kOgPrefix)) {
+ result.Set(name.substr(3), value);
+ }
+ }
+ std::move(callback).Run(base::Value(std::move(result)));
+}
+
+void CommerceWebExtractor::BindReceiver(
+ mojo::PendingReceiver<commerce_web_extractor::mojom::CommerceWebExtractor>
+ receiver) {
+ receiver_.reset();
+ receiver_.Bind(std::move(receiver));
+}
+} // namespace commerce
diff --git a/components/commerce/content/renderer/commerce_web_extractor.h b/components/commerce/content/renderer/commerce_web_extractor.h
new file mode 100644
index 0000000..3252621e
--- /dev/null
+++ b/components/commerce/content/renderer/commerce_web_extractor.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_COMMERCE_CONTENT_RENDERER_COMMERCE_WEB_EXTRACTOR_H_
+#define COMPONENTS_COMMERCE_CONTENT_RENDERER_COMMERCE_WEB_EXTRACTOR_H_
+
+#include "components/commerce/core/mojom/commerce_web_extractor.mojom.h"
+#include "content/public/renderer/render_frame.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "services/service_manager/public/cpp/binder_registry.h"
+
+namespace commerce {
+class CommerceWebExtractor
+ : public commerce_web_extractor::mojom::CommerceWebExtractor {
+ public:
+ CommerceWebExtractor(const CommerceWebExtractor&) = delete;
+ CommerceWebExtractor& operator=(const CommerceWebExtractor&) = delete;
+ CommerceWebExtractor(content::RenderFrame* render_frame,
+ service_manager::BinderRegistry* registry);
+ ~CommerceWebExtractor() override;
+
+ // commerce_web_extractor::mojom::CommerceWebExtractor:
+ void ExtractMetaInfo(ExtractMetaInfoCallback callback) override;
+
+ private:
+ void BindReceiver(
+ mojo::PendingReceiver<commerce_web_extractor::mojom::CommerceWebExtractor>
+ receiver);
+
+ raw_ptr<content::RenderFrame, ExperimentalRenderer> render_frame_;
+ mojo::Receiver<commerce_web_extractor::mojom::CommerceWebExtractor> receiver_{
+ this};
+};
+} // namespace commerce
+
+#endif // COMPONENTS_COMMERCE_CONTENT_RENDERER_COMMERCE_WEB_EXTRACTOR_H_
diff --git a/components/commerce/core/commerce_constants.cc b/components/commerce/core/commerce_constants.cc
index 9e59f94..c1d9fe33 100644
--- a/components/commerce/core/commerce_constants.cc
+++ b/components/commerce/core/commerce_constants.cc
@@ -43,4 +43,14 @@
const char kUTMPrefix[] = "utm_";
+const char kOgImage[] = "image";
+const char kOgPriceAmount[] = "price:amount";
+const char kOgPriceCurrency[] = "price:currency";
+const char kOgProductLink[] = "product_link";
+const char kOgTitle[] = "title";
+const char kOgType[] = "type";
+
+const char kOgTypeOgProduct[] = "product";
+const char kOgTypeProductItem[] = "product.item";
+
} // namespace commerce
diff --git a/components/commerce/core/commerce_constants.h b/components/commerce/core/commerce_constants.h
index 82859c2..19cabb95 100644
--- a/components/commerce/core/commerce_constants.h
+++ b/components/commerce/core/commerce_constants.h
@@ -58,6 +58,18 @@
// Prefix of UTM labels, including the underscore.
extern const char kUTMPrefix[];
+
+// Open graph keys.
+extern const char kOgImage[];
+extern const char kOgPriceAmount[];
+extern const char kOgPriceCurrency[];
+extern const char kOgProductLink[];
+extern const char kOgTitle[];
+extern const char kOgType[];
+
+// Specific open graph values we're interested in.
+extern const char kOgTypeOgProduct[];
+extern const char kOgTypeProductItem[];
} // namespace commerce
#endif // COMPONENTS_COMMERCE_CORE_COMMERCE_CONSTANTS_H_
diff --git a/components/commerce/core/mojom/BUILD.gn b/components/commerce/core/mojom/BUILD.gn
index 9c78607..5f1c219 100644
--- a/components/commerce/core/mojom/BUILD.gn
+++ b/components/commerce/core/mojom/BUILD.gn
@@ -5,7 +5,10 @@
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojo_bindings") {
- sources = [ "shopping_list.mojom" ]
+ sources = [
+ "commerce_web_extractor.mojom",
+ "shopping_list.mojom",
+ ]
webui_module_path = "/"
public_deps = [
"//mojo/public/mojom/base",
diff --git a/components/commerce/core/mojom/commerce_web_extractor.mojom b/components/commerce/core/mojom/commerce_web_extractor.mojom
new file mode 100644
index 0000000..68faccc
--- /dev/null
+++ b/components/commerce/core/mojom/commerce_web_extractor.mojom
@@ -0,0 +1,16 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module commerce_web_extractor.mojom;
+
+import "mojo/public/mojom/base/values.mojom";
+
+// Class for extracting commerce-related information from web page. This
+// is implemented by class in renderer process and called by browser
+// process.
+interface CommerceWebExtractor {
+ // Extract commerce-related meta info from the page and return the
+ // extracted value.
+ ExtractMetaInfo() => (mojo_base.mojom.Value result);
+};
\ No newline at end of file
diff --git a/components/commerce/core/shopping_service.cc b/components/commerce/core/shopping_service.cc
index 9a69c36..a416392 100644
--- a/components/commerce/core/shopping_service.cc
+++ b/components/commerce/core/shopping_service.cc
@@ -20,6 +20,7 @@
#include "base/values.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/commerce/core/bookmark_update_manager.h"
+#include "components/commerce/core/commerce_constants.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/commerce_utils.h"
#include "components/commerce/core/discounts_storage.h"
@@ -62,18 +63,6 @@
namespace commerce {
-// Open graph keys.
-const char kOgImage[] = "image";
-const char kOgPriceAmount[] = "price:amount";
-const char kOgPriceCurrency[] = "price:currency";
-const char kOgProductLink[] = "product_link";
-const char kOgTitle[] = "title";
-const char kOgType[] = "type";
-
-// Specific open graph values we're interested in.
-const char kOgTypeOgProduct[] = "product";
-const char kOgTypeProductItem[] = "product.item";
-
const long kToMicroCurrency = 1e6;
const char kImageAvailabilityHistogramName[] =
@@ -352,12 +341,15 @@
void ShoppingService::OnProductInfoLocalExtractionResult(const GURL url,
base::Value result) {
- // We should only ever get a string result from the script execution.
- if (!result.is_string()) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+ // We should only ever get a dict result from the script execution.
+ if (!result.is_dict() || result.GetDict().empty()) {
return;
}
- // Look up the entry again in case it was deleted (ex. by navigation).
+ // If there was no entry, do nothing. Most likely this means the page
+ // navigated before the script finished running.
auto it = product_info_cache_.find(url.spec());
if (it == product_info_cache_.end()) {
return;
@@ -367,28 +359,6 @@
kProductInfoLocalExtractionTime,
base::Time::Now() - it->second->local_extraction_execution_start_time);
- data_decoder::DataDecoder::ParseJsonIsolated(
- result.GetString(),
- base::BindOnce(&ShoppingService::OnProductInfoJsonSanitizationCompleted,
- weak_ptr_factory_.GetWeakPtr(), url));
-}
-
-void ShoppingService::OnProductInfoJsonSanitizationCompleted(
- const GURL url,
- data_decoder::DataDecoder::ValueOrError result) {
- DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-
- if (!result.has_value() || !result.value().is_dict())
- return;
-
- auto it = product_info_cache_.find(url.spec());
-
- // If there was no entry, do nothing. Most likely this means the page
- // navigated before the script finished running.
- if (it == product_info_cache_.end()) {
- return;
- }
-
ProductInfo* cached_info = it->second->product_info.get();
bool pdp_detected_by_client = false;
@@ -398,11 +368,12 @@
// that the server didn't detect the page as a PDP, so we should try to
// determine whether it is using the meta extracted from the page. This will
// only happen if the |kCommerceLocalPDPDetection| flag is enabled.
- pdp_detected_by_client = CheckIsPDPFromMetaOnly(result.value().GetDict());
+ pdp_detected_by_client = CheckIsPDPFromMetaOnly(result.GetDict());
+
if (cached_info) {
pdp_detected_by_server = true;
- MergeProductInfoData(cached_info, result.value().GetDict());
+ MergeProductInfoData(cached_info, result.GetDict());
}
if (base::FeatureList::IsEnabled(kCommerceLocalPDPDetection)) {
diff --git a/components/commerce/core/shopping_service.h b/components/commerce/core/shopping_service.h
index 2ab4222..75153e56 100644
--- a/components/commerce/core/shopping_service.h
+++ b/components/commerce/core/shopping_service.h
@@ -73,18 +73,6 @@
namespace commerce {
-// Open graph keys.
-extern const char kOgImage[];
-extern const char kOgPriceAmount[];
-extern const char kOgPriceCurrency[];
-extern const char kOgProductLink[];
-extern const char kOgTitle[];
-extern const char kOgType[];
-
-// Specific open graph values we're interested in.
-extern const char kOgTypeOgProduct[];
-extern const char kOgTypeProductItem[];
-
// The conversion multiplier to go from standard currency units to
// micro-currency units.
extern const long kToMicroCurrency;
@@ -543,12 +531,6 @@
// info.
void OnProductInfoLocalExtractionResult(const GURL url, base::Value result);
- // Handle the result of JSON parsing obtained from running local extraction on
- // the product info page.
- void OnProductInfoJsonSanitizationCompleted(
- const GURL url,
- data_decoder::DataDecoder::ValueOrError result);
-
// Tries to determine whether a page is a PDP only from information in meta
// tags extracted from the page. If enough information is present to call the
// page a PDP, this function returns true.
diff --git a/components/commerce/core/shopping_service_metrics_unittest.cc b/components/commerce/core/shopping_service_metrics_unittest.cc
index 844561f..6b162eb 100644
--- a/components/commerce/core/shopping_service_metrics_unittest.cc
+++ b/components/commerce/core/shopping_service_metrics_unittest.cc
@@ -7,6 +7,7 @@
#include "base/functional/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/values.h"
+#include "components/commerce/core/commerce_constants.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/metrics/metrics_utils.h"
#include "components/commerce/core/shopping_service.h"
@@ -61,7 +62,9 @@
test_features_.InitWithFeatures({kShoppingList, kCommerceAllowLocalImages},
{kCommerceAllowServerImages});
- base::Value js_result("{\"image\": \"" + std::string(kImageUrl2) + "\"}");
+ auto result = base::Value::Dict();
+ result.Set("image", std::string(kImageUrl2));
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -95,7 +98,9 @@
test_features_.InitWithFeatures({kShoppingList, kCommerceAllowServerImages},
{kCommerceAllowLocalImages});
- base::Value js_result("{\"image\": \"" + std::string(kImageUrl2) + "\"}");
+ auto result = base::Value::Dict();
+ result.Set("image", std::string(kImageUrl2));
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -129,7 +134,9 @@
{kShoppingList, kCommerceAllowLocalImages, kCommerceAllowServerImages},
{});
- base::Value js_result("{\"image\": \"" + std::string(kImageUrl2) + "\"}");
+ auto result = base::Value::Dict();
+ result.Set("image", std::string(kImageUrl2));
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -162,7 +169,9 @@
{kShoppingList, kCommerceAllowLocalImages, kCommerceAllowServerImages},
{});
- base::Value js_result("{\"irrelevant\": \"value\"}");
+ auto result = base::Value::Dict();
+ result.Set("irrelevant", std::string("value"));
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -216,7 +225,9 @@
{});
// Set the type as a non-product.
- base::Value js_result("{\"" + std::string(kOgType) + "\": \"article\"}");
+ auto result = base::Value::Dict();
+ result.Set(std::string(kOgType), "article");
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -237,8 +248,9 @@
test_features_.InitWithFeatures({kShoppingList, kCommerceLocalPDPDetection},
{});
- base::Value js_result("{\"" + std::string(kOgType) + "\": \"" +
- kOgTypeOgProduct + "\"}");
+ auto result = base::Value::Dict();
+ result.Set(std::string(commerce::kOgType), commerce::kOgTypeOgProduct);
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -259,8 +271,9 @@
test_features_.InitWithFeatures({kShoppingList, kCommerceLocalPDPDetection},
{});
- base::Value js_result("{\"" + std::string(kOgType) + "\": \"" +
- kOgTypeOgProduct + "\"}");
+ auto result = base::Value::Dict();
+ result.Set(commerce::kOgType, commerce::kOgTypeOgProduct);
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -281,8 +294,9 @@
test_features_.InitWithFeatures({kShoppingList, kCommerceLocalPDPDetection},
{});
- base::Value js_result("{\"" + std::string(kOgType) + "\": \"" +
- kOgTypeOgProduct + "\"}");
+ auto result = base::Value::Dict();
+ result.Set(kOgType, kOgTypeOgProduct);
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL("chrome://internal-page"), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -301,8 +315,9 @@
test_features_.InitWithFeatures({kShoppingList},
{kCommerceLocalPDPDetection});
- base::Value js_result("{\"" + std::string(kOgType) + "\": \"" +
- kOgTypeOgProduct + "\"}");
+ auto result = base::Value::Dict();
+ result.Set(commerce::kOgType, commerce::kOgTypeOgProduct);
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
@@ -321,7 +336,9 @@
// The content of the javascript result only needs to be json for this text,
// the actual fields don't matter.
- base::Value js_result("{\"success\": true}");
+ auto result = base::Value::Dict();
+ result.Set("success", "true");
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
opt_guide_->SetResponse(GURL(kProductUrl), OptimizationType::PRICE_TRACKING,
diff --git a/components/commerce/core/shopping_service_unittest.cc b/components/commerce/core/shopping_service_unittest.cc
index 3790d509..958b7c4 100644
--- a/components/commerce/core/shopping_service_unittest.cc
+++ b/components/commerce/core/shopping_service_unittest.cc
@@ -360,8 +360,9 @@
{kShoppingList, kCommerceAllowLocalImages, kCommerceAllowServerImages},
{});
- std::string json("{\"image\": \"" + std::string(kImageUrl) + "\"}");
- base::Value js_result(json);
+ auto result = base::Value::Dict();
+ result.Set("image", std::string(kImageUrl));
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
// Assume the page hasn't finished loading.
@@ -433,8 +434,9 @@
test_features_.InitWithFeatures(
{kCommerceAllowLocalImages, kCommerceAllowServerImages}, {});
- std::string json("{\"image\": \"" + std::string(kImageUrl) + "\"}");
- base::Value js_result(json);
+ auto result = base::Value::Dict();
+ result.Set("image", std::string(kImageUrl));
+ base::Value js_result(std::move(result));
MockWebWrapper web(GURL(kProductUrl), false, &js_result);
// Assume the page has already loaded for the navigation. This is usually the
diff --git a/components/commerce/core/web_extractor.h b/components/commerce/core/web_extractor.h
index 3a3ad7c..daf3f9e 100644
--- a/components/commerce/core/web_extractor.h
+++ b/components/commerce/core/web_extractor.h
@@ -23,7 +23,7 @@
// results via `callback`.
virtual void ExtractMetaInfo(
WebWrapper* web_wrapper,
- base::OnceCallback<void(const base::Value)> callback) = 0;
+ base::OnceCallback<void(base::Value)> callback) = 0;
private:
base::WeakPtrFactory<WebExtractor> weak_ptr_factory_{this};
diff --git a/components/commerce/ios/DEPS b/components/commerce/ios/DEPS
index dbdd5f6..6e52228 100644
--- a/components/commerce/ios/DEPS
+++ b/components/commerce/ios/DEPS
@@ -1,5 +1,6 @@
include_rules = [
"+components/grit",
"+ios/web/public",
+ "+services/data_decoder/public/cpp",
"+ui/base",
]
diff --git a/components/commerce/ios/browser/BUILD.gn b/components/commerce/ios/browser/BUILD.gn
index a8730253..248d3ee3 100644
--- a/components/commerce/ios/browser/BUILD.gn
+++ b/components/commerce/ios/browser/BUILD.gn
@@ -22,7 +22,21 @@
"//ios/web/public",
"//ios/web/public/js_messaging",
"//ios/web/public/webui",
+ "//services/data_decoder/public/cpp",
]
public_deps = [ "//components/commerce/core/internals" ]
}
+
+source_set("unit_tests") {
+ testonly = true
+ sources = [ "web_extractor_impl_unittest.mm" ]
+ deps = [
+ ":browser",
+ "//base/test:test_support",
+ "//components/commerce/core:commerce_constants",
+ "//ios/web/public/test",
+ "//ios/web/public/test:test_fixture",
+ "//services/data_decoder/public/cpp:test_support",
+ ]
+}
diff --git a/components/commerce/ios/browser/web_extractor_impl.h b/components/commerce/ios/browser/web_extractor_impl.h
index 723a99b9..7785eac 100644
--- a/components/commerce/ios/browser/web_extractor_impl.h
+++ b/components/commerce/ios/browser/web_extractor_impl.h
@@ -7,6 +7,7 @@
#include "components/commerce/core/web_extractor.h"
#include "components/commerce/core/web_wrapper.h"
+#include "services/data_decoder/public/cpp/data_decoder.h"
namespace commerce {
@@ -21,6 +22,17 @@
void ExtractMetaInfo(
WebWrapper* web_wrapper,
base::OnceCallback<void(const base::Value)> callback) override;
+
+ private:
+ void OnExtractionMetaInfo(
+ base::OnceCallback<void(const base::Value)> callback,
+ const base::Value result);
+
+ void OnProductInfoJsonSanitizationCompleted(
+ base::OnceCallback<void(const base::Value)> callback,
+ data_decoder::DataDecoder::ValueOrError result);
+
+ base::WeakPtrFactory<WebExtractorImpl> weak_ptr_factory_{this};
};
} // namespace commerce
diff --git a/components/commerce/ios/browser/web_extractor_impl.mm b/components/commerce/ios/browser/web_extractor_impl.mm
index 98d1934..fd858f9 100644
--- a/components/commerce/ios/browser/web_extractor_impl.mm
+++ b/components/commerce/ios/browser/web_extractor_impl.mm
@@ -15,12 +15,40 @@
void WebExtractorImpl::ExtractMetaInfo(
WebWrapper* web_wrapper,
- base::OnceCallback<void(const base::Value)> callback) {
+ base::OnceCallback<void(base::Value)> callback) {
std::string script =
ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_QUERY_SHOPPING_META_JS);
- web_wrapper->RunJavascript(base::UTF8ToUTF16(script), std::move(callback));
+ web_wrapper->RunJavascript(
+ base::UTF8ToUTF16(script),
+ base::BindOnce(&WebExtractorImpl::OnExtractionMetaInfo,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void WebExtractorImpl::OnExtractionMetaInfo(
+ base::OnceCallback<void(base::Value)> callback,
+ base::Value result) {
+ if (!result.is_string()) {
+ std::move(callback).Run(base::Value());
+ return;
+ }
+
+ data_decoder::DataDecoder::ParseJsonIsolated(
+ result.GetString(),
+ base::BindOnce(&WebExtractorImpl::OnProductInfoJsonSanitizationCompleted,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void WebExtractorImpl::OnProductInfoJsonSanitizationCompleted(
+ base::OnceCallback<void(base::Value)> callback,
+ data_decoder::DataDecoder::ValueOrError result) {
+ if (!result.has_value() || !result.value().is_dict()) {
+ std::move(callback).Run(base::Value());
+ return;
+ }
+
+ std::move(callback).Run(std::move(result.value()));
}
} // namespace commerce
diff --git a/components/commerce/ios/browser/web_extractor_impl_unittest.mm b/components/commerce/ios/browser/web_extractor_impl_unittest.mm
new file mode 100644
index 0000000..6ba44759
--- /dev/null
+++ b/components/commerce/ios/browser/web_extractor_impl_unittest.mm
@@ -0,0 +1,92 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "components/commerce/ios/browser/web_extractor_impl.h"
+
+#import "base/test/ios/wait_util.h"
+#import "base/values.h"
+#import "components/commerce/core/commerce_constants.h"
+#import "components/commerce/ios/browser/web_extractor_impl.h"
+#import "components/commerce/ios/browser/web_state_wrapper.h"
+#import "ios/web/public/test/web_test_with_web_state.h"
+#import "ios/web/public/web_state.h"
+#import "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
+
+using base::test::ios::kWaitForActionTimeout;
+using base::test::ios::WaitUntilConditionOrTimeout;
+
+namespace web {
+
+class WebExtractorImplTest : public web::WebTestWithWebState {
+ public:
+ WebExtractorImplTest() = default;
+
+ WebExtractorImplTest(const WebExtractorImplTest&) = delete;
+ WebExtractorImplTest& operator=(const WebExtractorImplTest&) = delete;
+
+ private:
+ // This is required to make sure that all DataDecoders constructed during its
+ // lifetime will connect to this instance rather than launching a separate
+ // process.
+ data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
+};
+
+TEST_F(WebExtractorImplTest, TestValidMetaExtraction) {
+ ASSERT_TRUE(LoadHtml("<html>"
+ "<head>"
+ "<meta content=\"product\" property=\"og:type\">"
+ "</head>"
+ "<body>"
+ "</body></html>"));
+
+ std::unique_ptr<commerce::WebStateWrapper> web_wrapper =
+ std::make_unique<commerce::WebStateWrapper>(web_state());
+ std::unique_ptr<commerce::WebExtractorImpl> web_extractor =
+ std::make_unique<commerce::WebExtractorImpl>();
+ __block bool callback_called = false;
+
+ web_extractor->ExtractMetaInfo(
+ web_wrapper.get(), base::BindOnce(^(const base::Value result) {
+ ASSERT_TRUE(result.is_dict());
+ auto* str = result.GetDict().FindString(commerce::kOgType);
+ ASSERT_TRUE(str);
+ ASSERT_EQ(*str, commerce::kOgTypeOgProduct);
+ callback_called = true;
+ }));
+
+ ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
+ base::RunLoop().RunUntilIdle();
+ return callback_called;
+ }));
+}
+
+TEST_F(WebExtractorImplTest, TestInvalidMetaExtraction) {
+ ASSERT_TRUE(LoadHtml("<html>"
+ "<head>"
+ "<meta content=\"product\" property=\"type\">"
+ "<meta content=\"product\" type=\"og:type\">"
+ "</head>"
+ "<body>"
+ "</body></html>"));
+
+ std::unique_ptr<commerce::WebStateWrapper> web_wrapper =
+ std::make_unique<commerce::WebStateWrapper>(web_state());
+ std::unique_ptr<commerce::WebExtractorImpl> web_extractor =
+ std::make_unique<commerce::WebExtractorImpl>();
+ __block bool callback_called = false;
+
+ web_extractor->ExtractMetaInfo(web_wrapper.get(),
+ base::BindOnce(^(const base::Value result) {
+ ASSERT_TRUE(result.is_dict());
+ ASSERT_TRUE(result.GetDict().empty());
+ callback_called = true;
+ }));
+
+ ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
+ base::RunLoop().RunUntilIdle();
+ return callback_called;
+ }));
+}
+
+} // namespace web