Implement PDF page count signal for contextual cueing

Change-Id: Iea3689885283853f7f62d5c0da0b2829222472bf
Bug: 389750286
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6219781
Reviewed-by: Sophie Chang <[email protected]>
Commit-Queue: Raj T <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1415359}
diff --git a/chrome/browser/contextual_cueing/BUILD.gn b/chrome/browser/contextual_cueing/BUILD.gn
index bea7fe2..ae1a71c 100644
--- a/chrome/browser/contextual_cueing/BUILD.gn
+++ b/chrome/browser/contextual_cueing/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//chrome/common/features.gni")
+import("//pdf/features.gni")
 
 source_set("contextual_cueing") {
   sources = [
@@ -24,6 +25,8 @@
   sources = [
     "contextual_cueing_features.cc",
     "contextual_cueing_helper.cc",
+    "contextual_cueing_page_data.cc",
+    "contextual_cueing_page_data.h",
     "contextual_cueing_service.cc",
     "contextual_cueing_service_factory.cc",
     "nudge_cap_tracker.cc",
@@ -37,9 +40,34 @@
     "//components/keyed_service/core",
     "//components/optimization_guide/core:core",
     "//components/optimization_guide/proto:optimization_guide_proto",
+    "//components/pdf/common:constants",
+    "//pdf:buildflags",
     "//url",
   ]
   if (enable_glic) {
     deps += [ "//chrome/browser/glic" ]
   }
+  if (enable_pdf) {
+    deps += [ "//components/pdf/browser" ]
+  }
+}
+
+source_set("unit_tests") {
+  testonly = true
+  sources = [
+    "contextual_cueing_helper_unittest.cc",
+    "contextual_cueing_page_data_unittest.cc",
+    "contextual_cueing_service_unittest.cc",
+    "nudge_cap_tracker_unittest.cc",
+  ]
+  deps = [
+    ":contextual_cueing",
+    ":impl",
+    "//base/test:test_support",
+    "//chrome/browser/optimization_guide:test_support",
+    "//chrome/test:test_support",
+    "//pdf:buildflags",
+    "//testing/gmock",
+    "//testing/gtest",
+  ]
 }
diff --git a/chrome/browser/contextual_cueing/DEPS b/chrome/browser/contextual_cueing/DEPS
index f0bf3d9..48c19d8 100644
--- a/chrome/browser/contextual_cueing/DEPS
+++ b/chrome/browser/contextual_cueing/DEPS
@@ -1,3 +1,4 @@
 include_rules = [
   "+components/keyed_service/core",
+  "+pdf/mojom/pdf.mojom.h",
 ]
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_features.cc b/chrome/browser/contextual_cueing/contextual_cueing_features.cc
index f85707a9..b88d51a 100644
--- a/chrome/browser/contextual_cueing/contextual_cueing_features.cc
+++ b/chrome/browser/contextual_cueing/contextual_cueing_features.cc
@@ -46,4 +46,9 @@
                                                    "VisitedDomainsLimit",
                                                    20);
 
+const base::FeatureParam<base::TimeDelta> kPdfPageCountCaptureDelay(
+    &kContextualCueing,
+    "PdfPageCountCaptureDelay",
+    base::Seconds(4));
+
 }  // namespace contextual_cueing
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_features.h b/chrome/browser/contextual_cueing/contextual_cueing_features.h
index a4b74486..e24f492e 100644
--- a/chrome/browser/contextual_cueing/contextual_cueing_features.h
+++ b/chrome/browser/contextual_cueing/contextual_cueing_features.h
@@ -38,6 +38,9 @@
 // used to implement nudge constraints per-domain per 24 hour period.
 extern const base::FeatureParam<int> kVisitedDomainsLimit;
 
+// The amount of time to wait for capturing the page count for a PDF document.
+extern const base::FeatureParam<base::TimeDelta> kPdfPageCountCaptureDelay;
+
 }  // namespace contextual_cueing
 
 #endif  // CHROME_BROWSER_CONTEXTUAL_CUEING_CONTEXTUAL_CUEING_FEATURES_H_
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_helper.cc b/chrome/browser/contextual_cueing/contextual_cueing_helper.cc
index d5068f2b..dbf1a243 100644
--- a/chrome/browser/contextual_cueing/contextual_cueing_helper.cc
+++ b/chrome/browser/contextual_cueing/contextual_cueing_helper.cc
@@ -8,6 +8,7 @@
 #include "base/metrics/histogram_functions.h"
 #include "chrome/browser/contextual_cueing/contextual_cueing_enums.h"
 #include "chrome/browser/contextual_cueing/contextual_cueing_features.h"
+#include "chrome/browser/contextual_cueing/contextual_cueing_page_data.h"
 #include "chrome/browser/contextual_cueing/contextual_cueing_service.h"
 #include "chrome/browser/contextual_cueing/contextual_cueing_service_factory.h"
 #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
@@ -33,29 +34,9 @@
 
 namespace contextual_cueing {
 
-namespace {
-
-// Returns whether the `config` matches all the current cueing condition.
-bool DidMatchCueingConditions(
-    const optimization_guide::proto::GlicCueingConfiguration& config) {
-  for (const auto& condition : config.conditions()) {
-    switch (condition.signal()) {
-      case optimization_guide::proto::
-          CONTEXTUAL_CUEING_CLIENT_SIGNAL_UNSPECIFIED:
-      case optimization_guide::proto::
-          CONTEXTUAL_CUEING_CLIENT_SIGNAL_PDF_PAGE_COUNT:
-      case optimization_guide::proto::
-          CONTEXTUAL_CUEING_CLIENT_SIGNAL_CONTENT_LENGTH_WORD_COUNT:
-        // TODO: crbug.com/389751174 - Implement checking the client signals.
-        return false;
-    }
-  }
-  return true;
-}
-
 class ScopedNudgeDecisionRecorder {
  public:
-  ScopedNudgeDecisionRecorder(
+  explicit ScopedNudgeDecisionRecorder(
       optimization_guide::proto::OptimizationType optimization_type)
       : optimization_type_(optimization_type) {}
   ~ScopedNudgeDecisionRecorder() {
@@ -78,8 +59,6 @@
   NudgeDecision nudge_decision_ = NudgeDecision::kUnknown;
 };
 
-}  // namespace
-
 ContextualCueingHelper::ContextualCueingHelper(
     content::WebContents* web_contents,
     OptimizationGuideKeyedService* ogks,
@@ -122,52 +101,58 @@
 }
 
 void ContextualCueingHelper::DocumentOnLoadCompletedInPrimaryMainFrame() {
-  last_navigation_cue_label_.clear();
-
   auto* glic_nudge_controller = GetGlicNudgeController();
   if (!glic_nudge_controller) {
     return;
   }
 
-  ScopedNudgeDecisionRecorder recorder(
-      optimization_guide::proto::GLIC_CONTEXTUAL_CUEING);
-  const GURL& url = web_contents()->GetLastCommittedURL();
+  std::unique_ptr<ScopedNudgeDecisionRecorder> recorder =
+      std::make_unique<ScopedNudgeDecisionRecorder>(
+          optimization_guide::proto::GLIC_CONTEXTUAL_CUEING);
   optimization_guide::OptimizationMetadata metadata;
   auto decision = optimization_guide_keyed_service_->CanApplyOptimization(
-      url, optimization_guide::proto::GLIC_CONTEXTUAL_CUEING, &metadata);
-  if (decision == optimization_guide::OptimizationGuideDecision::kTrue &&
-      !metadata.empty()) {
-    auto parsed = metadata.ParsedMetadata<
-        optimization_guide::proto::GlicContextualCueingMetadata>();
-    if (parsed) {
-      for (const auto& config : parsed->cueing_configurations()) {
-        if (!config.has_cue_label()) {
-          continue;
-        }
+      web_contents()->GetLastCommittedURL(),
+      optimization_guide::proto::GLIC_CONTEXTUAL_CUEING, &metadata);
+  if (decision != optimization_guide::OptimizationGuideDecision::kTrue ||
+      metadata.empty()) {
+    recorder->set_nudge_decision(NudgeDecision::kServerDataUnavailable);
+    return;
+  }
+  auto parsed = metadata.ParsedMetadata<
+      optimization_guide::proto::GlicContextualCueingMetadata>();
+  if (!parsed) {
+    recorder->set_nudge_decision(NudgeDecision::kServerDataMalformed);
+    return;
+  }
+  ContextualCueingPageData::CreateForPage(
+      web_contents()->GetPrimaryPage(), std::move(*parsed),
+      base::BindOnce(&ContextualCueingHelper::OnCueingDecision,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(recorder)));
+}
 
-        if (DidMatchCueingConditions(config)) {
-          last_navigation_cue_label_ = config.cue_label();
-          break;
-        }
-      }
-
-      recorder.set_nudge_decision(
-          last_navigation_cue_label_.empty()
-              ? NudgeDecision::kClientConditionsUnmet
-              : contextual_cueing_service_->CanShowNudge(url));
-    } else {
-      recorder.set_nudge_decision(NudgeDecision::kServerDataMalformed);
-    }
-  } else {
-    recorder.set_nudge_decision(NudgeDecision::kServerDataUnavailable);
+void ContextualCueingHelper::OnCueingDecision(
+    std::unique_ptr<ScopedNudgeDecisionRecorder> decision_recorder,
+    const std::string& cue_label) {
+  CHECK_EQ(NudgeDecision::kUnknown, decision_recorder->nudge_decision());
+  if (ContextualCueingPageData::GetForPage(web_contents()->GetPrimaryPage())) {
+    ContextualCueingPageData::DeleteForPage(web_contents()->GetPrimaryPage());
   }
 
-  if (recorder.nudge_decision() != NudgeDecision::kSuccess) {
-    // Clear out the label since we didn't show it.
-    last_navigation_cue_label_.clear();
+  if (cue_label.empty()) {
+    decision_recorder->set_nudge_decision(
+        NudgeDecision::kClientConditionsUnmet);
+    return;
   }
-  glic_nudge_controller->UpdateNudgeLabel(
-      web_contents(), last_navigation_cue_label_,
+
+  const GURL& url = web_contents()->GetLastCommittedURL();
+  auto can_show_decision = contextual_cueing_service_->CanShowNudge(url);
+  decision_recorder->set_nudge_decision(can_show_decision);
+  if (can_show_decision != NudgeDecision::kSuccess) {
+    return;
+  }
+
+  GetGlicNudgeController()->UpdateNudgeLabel(
+      web_contents(), cue_label,
       base::BindRepeating(
           &ContextualCueingService::OnNudgeActivity,
           contextual_cueing_service_->GetWeakPtr(), url,
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_helper.h b/chrome/browser/contextual_cueing/contextual_cueing_helper.h
index 525316663..3ae3ba0e 100644
--- a/chrome/browser/contextual_cueing/contextual_cueing_helper.h
+++ b/chrome/browser/contextual_cueing/contextual_cueing_helper.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_CONTEXTUAL_CUEING_CONTEXTUAL_CUEING_HELPER_H_
 #define CHROME_BROWSER_CONTEXTUAL_CUEING_CONTEXTUAL_CUEING_HELPER_H_
 
+#include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "content/public/browser/web_contents_observer.h"
 #include "content/public/browser/web_contents_user_data.h"
@@ -18,6 +19,7 @@
 namespace contextual_cueing {
 
 class ContextualCueingService;
+class ScopedNudgeDecisionRecorder;
 
 class ContextualCueingHelper
     : public content::WebContentsObserver,
@@ -38,15 +40,15 @@
 
   tabs::GlicNudgeController* GetGlicNudgeController();
 
-  const std::string& last_navigation_cue_label() const {
-    return last_navigation_cue_label_;
-  }
-
  private:
   ContextualCueingHelper(content::WebContents* contents,
                          OptimizationGuideKeyedService* ogks,
                          ContextualCueingService* ccs);
 
+  void OnCueingDecision(
+      std::unique_ptr<ScopedNudgeDecisionRecorder> decision_recorder,
+      const std::string& cue_label);
+
   // Not owned and guaranteed to outlive `this`.
   raw_ptr<OptimizationGuideKeyedService> optimization_guide_keyed_service_ =
       nullptr;
@@ -54,8 +56,7 @@
   // Not owned and guaranteed to outlive `this`.
   raw_ptr<ContextualCueingService> contextual_cueing_service_ = nullptr;
 
-  // Holds the cue label for the last navigation in `this`.
-  std::string last_navigation_cue_label_;
+  base::WeakPtrFactory<ContextualCueingHelper> weak_ptr_factory_{this};
 
   friend WebContentsUserData<ContextualCueingHelper>;
   WEB_CONTENTS_USER_DATA_KEY_DECL();
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_page_data.cc b/chrome/browser/contextual_cueing/contextual_cueing_page_data.cc
new file mode 100644
index 0000000..9aca96d1
--- /dev/null
+++ b/chrome/browser/contextual_cueing/contextual_cueing_page_data.cc
@@ -0,0 +1,144 @@
+// Copyright 2025 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/browser/contextual_cueing/contextual_cueing_page_data.h"
+
+#include "base/task/single_thread_task_runner.h"
+#include "chrome/browser/contextual_cueing/contextual_cueing_features.h"
+#include "content/public/browser/web_contents.h"
+#include "pdf/buildflags.h"
+
+#if BUILDFLAG(ENABLE_PDF)
+#include "components/pdf/browser/pdf_document_helper.h"
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+namespace contextual_cueing {
+
+namespace {
+
+bool DidMatchCueingCondition(
+    const optimization_guide::proto::ContextualCueingConditions& condition,
+    int64_t value) {
+  if (!condition.has_cueing_operator()) {
+    return false;
+  }
+  if (!condition.has_int64_threshold()) {
+    return false;
+  }
+  switch (condition.cueing_operator()) {
+    case optimization_guide::proto::CONTEXTUAL_CUEING_OPERATOR_UNSPECIFIED:
+      return false;
+    case optimization_guide::proto::
+        CONTEXTUAL_CUEING_OPERATOR_GREATER_THAN_OR_EQUAL_TO:
+      return value >= condition.int64_threshold();
+    case optimization_guide::proto::
+        CONTEXTUAL_CUEING_OPERATOR_LESS_THAN_OR_EQUAL_TO:
+      return value <= condition.int64_threshold();
+  }
+}
+
+}  // namespace
+
+ContextualCueingPageData::ContextualCueingPageData(
+    content::Page& page,
+    optimization_guide::proto::GlicContextualCueingMetadata metadata,
+    CueingDecisionCallback cueing_decision_callback)
+    : content::PageUserData<ContextualCueingPageData>(page),
+      metadata_(metadata),
+      cueing_decision_callback_(std::move(cueing_decision_callback)) {
+  FindMatchingConfig();
+}
+
+ContextualCueingPageData::~ContextualCueingPageData() = default;
+
+PAGE_USER_DATA_KEY_IMPL(ContextualCueingPageData);
+
+// Attempts to find the matching cueing configuration.
+void ContextualCueingPageData::FindMatchingConfig() {
+  CHECK(cueing_decision_callback_);
+  bool needs_pdf_page_count = false;
+  for (const auto& config : metadata_.cueing_configurations()) {
+    if (!config.has_cue_label()) {
+      continue;
+    }
+    auto decision = DidMatchCueingConditions(config);
+    if (decision == kAllowed) {
+      std::move(cueing_decision_callback_).Run(config.cue_label());
+      return;
+    }
+    if (decision == kNeedsPdfPageCount) {
+      needs_pdf_page_count = true;
+    }
+  }
+  if (needs_pdf_page_count) {
+#if BUILDFLAG(ENABLE_PDF)
+    CHECK_EQ(pdf::kPDFMimeType, page().GetContentsMimeType());
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+        FROM_HERE,
+        base::BindOnce(&ContextualCueingPageData::RequestPdfPageCount,
+                       weak_factory_.GetWeakPtr()),
+        kPdfPageCountCaptureDelay.Get());
+    return;
+#endif  // BUILDFLAG(ENABLE_PDF)
+  }
+  // None of the config matched, and no client-signals were requested.
+  std::move(cueing_decision_callback_).Run(std::string());
+}
+
+ContextualCueingPageData::CueingConfigurationDecision
+ContextualCueingPageData::DidMatchCueingConditions(
+    const optimization_guide::proto::GlicCueingConfiguration& config) {
+  for (const auto& condition : config.conditions()) {
+    switch (condition.signal()) {
+      case optimization_guide::proto::
+          CONTEXTUAL_CUEING_CLIENT_SIGNAL_UNSPECIFIED:
+        return kDisallowed;
+      case optimization_guide::proto::
+          CONTEXTUAL_CUEING_CLIENT_SIGNAL_PDF_PAGE_COUNT:
+        if (page().GetContentsMimeType() != pdf::kPDFMimeType) {
+          return kDisallowed;
+        }
+        if (!pdf_page_count_) {
+          return kNeedsPdfPageCount;
+        }
+        return DidMatchCueingCondition(condition, *pdf_page_count_)
+                   ? kAllowed
+                   : kDisallowed;
+      case optimization_guide::proto::
+          CONTEXTUAL_CUEING_CLIENT_SIGNAL_CONTENT_LENGTH_WORD_COUNT:
+        // TODO: crbug.com/389751174 - Implement checking the client signals.
+        return kDisallowed;
+    }
+  }
+  return kAllowed;
+}
+
+#if BUILDFLAG(ENABLE_PDF)
+void ContextualCueingPageData::RequestPdfPageCount() {
+  CHECK(page().GetContentsMimeType() == pdf::kPDFMimeType);
+  pdf::PDFDocumentHelper* pdf_helper =
+      pdf::PDFDocumentHelper::MaybeGetForWebContents(
+          content::WebContents::FromRenderFrameHost(&page().GetMainDocument()));
+  if (pdf_helper) {
+    // Fetch zero PDF bytes to just receive the total page count.
+    pdf_helper->GetPdfBytes(
+        /*size_limit=*/0,
+        base::BindOnce(&ContextualCueingPageData::OnPdfPageCountReceived,
+                       weak_factory_.GetWeakPtr()));
+  }
+}
+
+void ContextualCueingPageData::OnPdfPageCountReceived(
+    pdf::mojom::PdfListener::GetPdfBytesStatus status,
+    const std::vector<uint8_t>& bytes,
+    uint32_t page_count) {
+  if (status == pdf::mojom::PdfListener::GetPdfBytesStatus::kFailed) {
+    return;
+  }
+  pdf_page_count_ = page_count;
+  FindMatchingConfig();
+}
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+}  // namespace contextual_cueing
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_page_data.h b/chrome/browser/contextual_cueing/contextual_cueing_page_data.h
new file mode 100644
index 0000000..0355d79
--- /dev/null
+++ b/chrome/browser/contextual_cueing/contextual_cueing_page_data.h
@@ -0,0 +1,74 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CONTEXTUAL_CUEING_CONTEXTUAL_CUEING_PAGE_DATA_H_
+#define CHROME_BROWSER_CONTEXTUAL_CUEING_CONTEXTUAL_CUEING_PAGE_DATA_H_
+
+#include "components/optimization_guide/proto/contextual_cueing_metadata.pb.h"
+#include "components/pdf/common/constants.h"
+#include "content/public/browser/page_user_data.h"
+#include "pdf/buildflags.h"
+#include "pdf/mojom/pdf.mojom.h"
+
+namespace contextual_cueing {
+
+// Decider for contextual cueing that is scoped to `Page`.
+class ContextualCueingPageData
+    : public content::PageUserData<ContextualCueingPageData> {
+ public:
+  using CueingDecisionCallback = base::OnceCallback<void(const std::string&)>;
+
+  ContextualCueingPageData(const ContextualCueingPageData&) = delete;
+  ContextualCueingPageData& operator=(const ContextualCueingPageData&) = delete;
+  ~ContextualCueingPageData() override;
+
+ private:
+  friend class content::PageUserData<ContextualCueingPageData>;
+  friend class ContextualCueingPageDataTest;
+
+  // Holds the cueing condition decision for one cueing configuration.
+  enum CueingConfigurationDecision {
+    kAllowed,
+    kDisallowed,
+    kNeedsPdfPageCount,
+  };
+
+  ContextualCueingPageData(
+      content::Page& page,
+      optimization_guide::proto::GlicContextualCueingMetadata metadata,
+      CueingDecisionCallback cueing_decision_callback);
+
+  // Finds the matching config that passes all the conditions. Requests and
+  // waits for any client-signals when needed.
+  void FindMatchingConfig();
+
+  // Returns whether the `config` matches all the current cueing condition.
+  CueingConfigurationDecision DidMatchCueingConditions(
+      const optimization_guide::proto::GlicCueingConfiguration& config);
+
+#if BUILDFLAG(ENABLE_PDF)
+  void RequestPdfPageCount();
+
+  void OnPdfPageCountReceived(pdf::mojom::PdfListener::GetPdfBytesStatus status,
+                              const std::vector<uint8_t>& bytes,
+                              uint32_t page_count);
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+  const optimization_guide::proto::GlicContextualCueingMetadata metadata_;
+
+  // Holds the page count of PDF. Populated only when the mainframe of the page
+  // has a PDF renderer, and the page count has been successfully retrieved from
+  // it.
+  std::optional<size_t> pdf_page_count_;
+
+  CueingDecisionCallback cueing_decision_callback_;
+
+  base::WeakPtrFactory<ContextualCueingPageData> weak_factory_{this};
+
+  PAGE_USER_DATA_KEY_DECL();
+};
+
+}  // namespace contextual_cueing
+
+#endif  // CHROME_BROWSER_CONTEXTUAL_CUEING_CONTEXTUAL_CUEING_PAGE_DATA_H_
diff --git a/chrome/browser/contextual_cueing/contextual_cueing_page_data_unittest.cc b/chrome/browser/contextual_cueing/contextual_cueing_page_data_unittest.cc
new file mode 100644
index 0000000..48d0a52
--- /dev/null
+++ b/chrome/browser/contextual_cueing/contextual_cueing_page_data_unittest.cc
@@ -0,0 +1,151 @@
+// Copyright 2025 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/browser/contextual_cueing/contextual_cueing_page_data.h"
+
+#include "base/test/test_future.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/test_utils.h"
+#include "content/public/test/web_contents_tester.h"
+
+namespace contextual_cueing {
+
+class ContextualCueingPageDataTest : public content::RenderViewHostTestHarness {
+ public:
+  void SetUp() override {
+    content::RenderViewHostTestHarness::SetUp();
+    web_contents_ = CreateTestWebContents();
+  }
+
+  void TearDown() override {
+    web_contents_.reset();
+    content::RenderViewHostTestHarness::TearDown();
+  }
+
+  std::unique_ptr<content::BrowserContext> CreateBrowserContext() override {
+    return std::make_unique<TestingProfile>();
+  }
+
+  void InvokePdfPageCountReceived(size_t page_count) {
+    auto* page_data =
+        ContextualCueingPageData::GetForPage(web_contents_->GetPrimaryPage());
+    page_data->OnPdfPageCountReceived(
+        pdf::mojom::PdfListener::GetPdfBytesStatus::kSuccess, {}, page_count);
+  }
+
+ protected:
+  std::unique_ptr<content::WebContents> web_contents_;
+};
+
+TEST_F(ContextualCueingPageDataTest, Basic) {
+  base::test::TestFuture<const std::string&> future;
+  optimization_guide::proto::GlicContextualCueingMetadata metadata;
+  auto* config = metadata.add_cueing_configurations();
+  config->set_cue_label("basic label");
+
+  ContextualCueingPageData::CreateForPage(web_contents_->GetPrimaryPage(),
+                                          std::move(metadata),
+                                          future.GetCallback());
+  ASSERT_TRUE(future.Wait());
+  EXPECT_EQ("basic label", future.Get());
+}
+
+TEST_F(ContextualCueingPageDataTest, NonPdfPageFails) {
+  base::test::TestFuture<const std::string&> future;
+  optimization_guide::proto::GlicContextualCueingMetadata metadata;
+  auto* config = metadata.add_cueing_configurations();
+  config->set_cue_label("basic label");
+  auto* pdf_condition = config->add_conditions();
+  pdf_condition->set_cueing_operator(
+      optimization_guide::proto::ContextualCueingOperator::
+          CONTEXTUAL_CUEING_OPERATOR_GREATER_THAN_OR_EQUAL_TO);
+  pdf_condition->set_int64_threshold(2);
+
+  ContextualCueingPageData::CreateForPage(web_contents_->GetPrimaryPage(),
+                                          std::move(metadata),
+                                          future.GetCallback());
+  ASSERT_TRUE(future.Wait());
+  EXPECT_TRUE(future.Get().empty());
+}
+
+TEST_F(ContextualCueingPageDataTest, PdfPageCountFails) {
+  content::WebContentsTester::For(web_contents_.get())
+      ->SetMainFrameMimeType(pdf::kPDFMimeType);
+
+  base::test::TestFuture<const std::string&> future;
+  optimization_guide::proto::GlicContextualCueingMetadata metadata;
+  auto* config = metadata.add_cueing_configurations();
+  config->set_cue_label("pdf label");
+  auto* pdf_condition = config->add_conditions();
+  pdf_condition->set_signal(optimization_guide::proto::
+                                CONTEXTUAL_CUEING_CLIENT_SIGNAL_PDF_PAGE_COUNT);
+  pdf_condition->set_cueing_operator(
+      optimization_guide::proto::ContextualCueingOperator::
+          CONTEXTUAL_CUEING_OPERATOR_GREATER_THAN_OR_EQUAL_TO);
+  pdf_condition->set_int64_threshold(2);
+
+  ContextualCueingPageData::CreateForPage(web_contents_->GetPrimaryPage(),
+                                          std::move(metadata),
+                                          future.GetCallback());
+  InvokePdfPageCountReceived(1);
+
+  ASSERT_TRUE(future.Wait());
+  EXPECT_TRUE(future.Get().empty());
+}
+
+TEST_F(ContextualCueingPageDataTest, PdfPageCountPasses) {
+  content::WebContentsTester::For(web_contents_.get())
+      ->SetMainFrameMimeType(pdf::kPDFMimeType);
+
+  base::test::TestFuture<const std::string&> future;
+  optimization_guide::proto::GlicContextualCueingMetadata metadata;
+  auto* config = metadata.add_cueing_configurations();
+  config->set_cue_label("pdf label");
+  auto* pdf_condition = config->add_conditions();
+  pdf_condition->set_signal(optimization_guide::proto::
+                                CONTEXTUAL_CUEING_CLIENT_SIGNAL_PDF_PAGE_COUNT);
+  pdf_condition->set_cueing_operator(
+      optimization_guide::proto::ContextualCueingOperator::
+          CONTEXTUAL_CUEING_OPERATOR_GREATER_THAN_OR_EQUAL_TO);
+  pdf_condition->set_int64_threshold(2);
+
+  ContextualCueingPageData::CreateForPage(web_contents_->GetPrimaryPage(),
+                                          std::move(metadata),
+                                          future.GetCallback());
+  InvokePdfPageCountReceived(4);
+
+  ASSERT_TRUE(future.Wait());
+  EXPECT_EQ("pdf label", future.Get());
+}
+
+TEST_F(ContextualCueingPageDataTest, BasicAndPdfPageCountCondition) {
+  content::WebContentsTester::For(web_contents_.get())
+      ->SetMainFrameMimeType(pdf::kPDFMimeType);
+
+  base::test::TestFuture<const std::string&> future;
+  optimization_guide::proto::GlicContextualCueingMetadata metadata;
+  auto* config = metadata.add_cueing_configurations();
+  config->set_cue_label("pdf label");
+  auto* pdf_condition = config->add_conditions();
+  pdf_condition->set_signal(optimization_guide::proto::
+                                CONTEXTUAL_CUEING_CLIENT_SIGNAL_PDF_PAGE_COUNT);
+  pdf_condition->set_cueing_operator(
+      optimization_guide::proto::ContextualCueingOperator::
+          CONTEXTUAL_CUEING_OPERATOR_GREATER_THAN_OR_EQUAL_TO);
+  pdf_condition->set_int64_threshold(2);
+
+  // Second basic condition should get picked.
+  config = metadata.add_cueing_configurations();
+  config->set_cue_label("basic label");
+
+  ContextualCueingPageData::CreateForPage(web_contents_->GetPrimaryPage(),
+                                          std::move(metadata),
+                                          future.GetCallback());
+  ASSERT_TRUE(future.Wait());
+  EXPECT_EQ("basic label", future.Get());
+}
+
+}  // namespace contextual_cueing