chromium/ios/chrome/browser/contextual_panel/entrypoint/coordinator/contextual_panel_entrypoint_mediator_unittest.mm

// 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.

#import "ios/chrome/browser/contextual_panel/entrypoint/coordinator/contextual_panel_entrypoint_mediator.h"

#import "base/test/metrics/histogram_tester.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/feature_engagement/test/scoped_iph_feature_list.h"
#import "components/feature_engagement/test/test_tracker.h"
#import "ios/chrome/browser/contextual_panel/entrypoint/coordinator/contextual_panel_entrypoint_mediator_delegate.h"
#import "ios/chrome/browser/contextual_panel/entrypoint/ui/contextual_panel_entrypoint_consumer.h"
#import "ios/chrome/browser/contextual_panel/model/contextual_panel_item_type.h"
#import "ios/chrome/browser/contextual_panel/model/contextual_panel_model.h"
#import "ios/chrome/browser/contextual_panel/model/contextual_panel_tab_helper.h"
#import "ios/chrome/browser/contextual_panel/model/contextual_panel_tab_helper_observer.h"
#import "ios/chrome/browser/contextual_panel/sample/model/sample_panel_item_configuration.h"
#import "ios/chrome/browser/contextual_panel/utils/contextual_panel_metrics.h"
#import "ios/chrome/browser/infobars/model/infobar_badge_tab_helper.h"
#import "ios/chrome/browser/infobars/model/infobar_manager_impl.h"
#import "ios/chrome/browser/shared/model/web_state_list/test/fake_web_state_list_delegate.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/contextual_panel_entrypoint_iph_commands.h"
#import "ios/chrome/browser/shared/public/commands/contextual_sheet_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"

// A fake ContextualPanelEntrypointConsumer for use in tests.
@interface FakeEntrypointConsumer : NSObject <ContextualPanelEntrypointConsumer>

@property(nonatomic, assign) BOOL entrypointIsShown;

@property(nonatomic, assign) BOOL entrypointIsLarge;

@property(nonatomic, assign) BOOL contextualPanelIsOpen;

@property(nonatomic, assign) BOOL entrypointIsColored;

@property(nonatomic, assign) base::WeakPtr<ContextualPanelItemConfiguration>
    currentConfiguration;

@end

@implementation FakeEntrypointConsumer

- (void)setEntrypointConfig:
    (base::WeakPtr<ContextualPanelItemConfiguration>)config {
  self.currentConfiguration = config;
}

- (void)hideEntrypoint {
  self.entrypointIsShown = NO;
}

- (void)showEntrypoint {
  self.entrypointIsShown = YES;
}

- (void)transitionToLargeEntrypoint {
  self.entrypointIsLarge = YES;
}

- (void)transitionToSmallEntrypoint {
  self.entrypointIsLarge = NO;
}

- (void)transitionToContextualPanelOpenedState:(BOOL)opened {
  self.contextualPanelIsOpen = opened;
}

- (void)setInfobarBadgesCurrentlyShown:(BOOL)infobarBadgesCurrentlyShown {
}

- (void)setEntrypointColored:(BOOL)colored {
  self.entrypointIsColored = colored;
}

@end

// Fake test implementation of ContextualPanelEntrypointMediatorDelegate
@interface FakeContextualPanelEntrypointMediatorDelegate
    : NSObject <ContextualPanelEntrypointMediatorDelegate>

@property(nonatomic, assign) BOOL canShowLargeContextualPanelEntrypoint;

@end

@implementation FakeContextualPanelEntrypointMediatorDelegate

- (BOOL)canShowLargeContextualPanelEntrypoint:
    (ContextualPanelEntrypointMediator*)mediator {
  return self.canShowLargeContextualPanelEntrypoint;
}

- (void)setLocationBarLabelCenteredBetweenContent:
            (ContextualPanelEntrypointMediator*)mediator
                                         centered:(BOOL)centered {
  // No-op.
}

- (void)disableFullscreen {
  // No-op.
}

- (void)enableFullscreen {
  // No-op.
}

- (CGPoint)helpAnchorUsingBottomOmnibox:(BOOL)isBottomOmnibox {
  return CGPointMake(0, 0);
}

- (BOOL)isBottomOmniboxActive {
  return NO;
}

@end

// Test fake to allow easier triggering of ContextualPanelTabHelperObserver
// methods.
class FakeContextualPanelTabHelper : public ContextualPanelTabHelper {
 public:
  explicit FakeContextualPanelTabHelper(
      web::WebState* web_state,
      std::map<ContextualPanelItemType, raw_ptr<ContextualPanelModel>> models)
      : ContextualPanelTabHelper(web_state, models) {}

  static void CreateForWebState(
      web::WebState* web_state,
      std::map<ContextualPanelItemType, raw_ptr<ContextualPanelModel>> models) {
    web_state->SetUserData(
        UserDataKey(),
        std::make_unique<FakeContextualPanelTabHelper>(web_state, models));
  }

  void AddObserver(ContextualPanelTabHelperObserver* observer) override {
    ContextualPanelTabHelper::AddObserver(observer);
    observers_.AddObserver(observer);
  }
  void RemoveObserver(ContextualPanelTabHelperObserver* observer) override {
    ContextualPanelTabHelper::RemoveObserver(observer);
    observers_.RemoveObserver(observer);
  }

  void CallContextualPanelTabHelperDestroyed() {
    for (auto& observer : observers_) {
      observer.ContextualPanelTabHelperDestroyed(this);
    }
  }

  void CallContextualPanelHasNewData(
      std::vector<base::WeakPtr<ContextualPanelItemConfiguration>>
          item_configurations) {
    for (auto& observer : observers_) {
      observer.ContextualPanelHasNewData(this, item_configurations);
    }
  }

  base::WeakPtr<ContextualPanelItemConfiguration> GetFirstCachedConfig()
      override {
    return !configs_.empty() ? configs_[0]->weak_ptr_factory.GetWeakPtr()
                             : nullptr;
  }

  // Helper to add configs to the front of the Fake tab helper cached
  // `configs_`.
  void AddToCachedConfigs(
      std::unique_ptr<SamplePanelItemConfiguration> configuration) {
    configs_.insert(configs_.begin(), std::move(configuration));
  }

  base::ObserverList<ContextualPanelTabHelperObserver, true> observers_;
  std::vector<std::unique_ptr<ContextualPanelItemConfiguration>> configs_;
};

class ContextualPanelEntrypointMediatorTest : public PlatformTest {
 public:
  ContextualPanelEntrypointMediatorTest()
      : web_state_list_(&web_state_list_delegate_) {
    auto web_state = std::make_unique<web::FakeWebState>();
    std::map<ContextualPanelItemType, raw_ptr<ContextualPanelModel>> models;
    FakeContextualPanelTabHelper::CreateForWebState(web_state.get(), models);
    InfoBarManagerImpl::CreateForWebState(web_state.get());
    InfobarBadgeTabHelper::GetOrCreateForWebState(web_state.get());
    web_state_list_.InsertWebState(
        std::move(web_state),
        WebStateList::InsertionParams::Automatic().Activate(true));

    mocked_entrypoint_help_handler_ =
        OCMStrictProtocolMock(@protocol(ContextualPanelEntrypointIPHCommands));
    mocked_contextual_sheet_handler_ =
        OCMStrictProtocolMock(@protocol(ContextualSheetCommands));

    feature_engagement::test::ScopedIphFeatureList list;
    list.InitAndEnableFeatures(
        {feature_engagement::kIPHiOSContextualPanelSampleModelFeature});

    tracker_ = feature_engagement::CreateTestTracker();

    // Make sure tracker is initialized.
    tracker_->AddOnInitializedCallback(BoolArgumentQuitClosure());
    run_loop_.Run();

    mediator_ = [[ContextualPanelEntrypointMediator alloc]
          initWithWebStateList:&web_state_list_
             engagementTracker:tracker_.get()
        contextualSheetHandler:mocked_contextual_sheet_handler_
         entrypointHelpHandler:mocked_entrypoint_help_handler_];

    entrypoint_consumer_ = [[FakeEntrypointConsumer alloc] init];
    mediator_.consumer = entrypoint_consumer_;

    delegate_ = [[FakeContextualPanelEntrypointMediatorDelegate alloc] init];
    mediator_.delegate = delegate_;
  }

 protected:
  base::RepeatingCallback<void(bool)> BoolArgumentQuitClosure() {
    return base::IgnoreArgs<bool>(run_loop_.QuitClosure());
  }

  web::WebTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  base::RunLoop run_loop_;
  std::unique_ptr<feature_engagement::Tracker> tracker_;
  FakeWebStateListDelegate web_state_list_delegate_;
  WebStateList web_state_list_;
  ContextualPanelEntrypointMediator* mediator_;
  FakeEntrypointConsumer* entrypoint_consumer_;
  FakeContextualPanelEntrypointMediatorDelegate* delegate_;
  id mocked_contextual_sheet_handler_;
  id mocked_entrypoint_help_handler_;
};

// Tests that tapping the entrypoint opens the panel if it's closed and vice
// versa.
TEST_F(ContextualPanelEntrypointMediatorTest, TestEntrypointTapped) {
  const base::HistogramTester histogram_tester;
  ContextualPanelTabHelper* tab_helper = ContextualPanelTabHelper::FromWebState(
      web_state_list_.GetActiveWebState());

  // Set the metrics data for the current entrypoint appearing.
  ContextualPanelTabHelper::EntrypointMetricsData metrics_data;
  metrics_data.entrypoint_item_type = ContextualPanelItemType::SamplePanelItem;
  metrics_data.appearance_time = base::Time::Now() - base::Seconds(10);
  tab_helper->SetMetricsData(metrics_data);

  [[mocked_contextual_sheet_handler_ expect] openContextualSheet];
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:YES];

  [mediator_ entrypointTapped];
  tab_helper->OpenContextualPanel();
  EXPECT_TRUE(entrypoint_consumer_.contextualPanelIsOpen);

  [[mocked_contextual_sheet_handler_ expect] closeContextualSheet];
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:YES];

  [mediator_ entrypointTapped];
  tab_helper->CloseContextualPanel();
  EXPECT_FALSE(entrypoint_consumer_.contextualPanelIsOpen);

  [mocked_contextual_sheet_handler_ verify];
  [mocked_entrypoint_help_handler_ verify];

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.Entrypoint.Regular",
                                      EntrypointInteractionType::Tapped, 1);
  histogram_tester.ExpectUniqueSample(
      "IOS.ContextualPanel.Entrypoint.Regular.SamplePanelItem",
      EntrypointInteractionType::Tapped, 1);

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.EntrypointTapped",
                                      ContextualPanelItemType::SamplePanelItem,
                                      1);

  histogram_tester.ExpectTimeBucketCount(
      "IOS.ContextualPanel.Entrypoint.Regular.UptimeBeforeTap",
      base::Seconds(10), 1);
  histogram_tester.ExpectTimeBucketCount(
      "IOS.ContextualPanel.Entrypoint.Regular.SamplePanelItem.UptimeBeforeTap",
      base::Seconds(10), 1);
}

TEST_F(ContextualPanelEntrypointMediatorTest, TestTabHelperDestroyed) {
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:NO];

  // Start off with entrypoint showing.
  [entrypoint_consumer_ showEntrypoint];

  FakeContextualPanelTabHelper* tab_helper =
      static_cast<FakeContextualPanelTabHelper*>(
          FakeContextualPanelTabHelper::FromWebState(
              web_state_list_.GetActiveWebState()));
  tab_helper->CallContextualPanelTabHelperDestroyed();

  EXPECT_FALSE(entrypoint_consumer_.entrypointIsShown);
  [mocked_entrypoint_help_handler_ verify];
}

// Tests that if one configuration is provided, the entrypoint becomes shown.
TEST_F(ContextualPanelEntrypointMediatorTest, TestOneConfiguration) {
  const base::HistogramTester histogram_tester;
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:NO];

  ContextualPanelItemConfiguration configuration(
      ContextualPanelItemType::SamplePanelItem);

  FakeContextualPanelTabHelper* tab_helper =
      static_cast<FakeContextualPanelTabHelper*>(
          FakeContextualPanelTabHelper::FromWebState(
              web_state_list_.GetActiveWebState()));

  std::vector<base::WeakPtr<ContextualPanelItemConfiguration>>
      item_configurations;
  item_configurations.push_back(configuration.weak_ptr_factory.GetWeakPtr());

  tab_helper->CallContextualPanelHasNewData(item_configurations);

  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);

  ASSERT_TRUE(entrypoint_consumer_.currentConfiguration);
  EXPECT_EQ(&configuration, entrypoint_consumer_.currentConfiguration.get());

  [mocked_entrypoint_help_handler_ verify];

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.EntrypointDisplayed",
                                      ContextualPanelItemType::SamplePanelItem,
                                      1);

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.Entrypoint.Regular",
                                      EntrypointInteractionType::Displayed, 1);
  histogram_tester.ExpectUniqueSample(
      "IOS.ContextualPanel.Entrypoint.Regular.SamplePanelItem",
      EntrypointInteractionType::Displayed, 1);
}

// Tests that -disconnect doesn't crash and that nothing is observing the tab
// helper after disconnecting.s
TEST_F(ContextualPanelEntrypointMediatorTest, TestDisconnect) {
  FakeContextualPanelTabHelper* tab_helper =
      static_cast<FakeContextualPanelTabHelper*>(
          FakeContextualPanelTabHelper::FromWebState(
              web_state_list_.GetActiveWebState()));

  EXPECT_FALSE(tab_helper->observers_.empty());
  [mediator_ disconnect];
  EXPECT_TRUE(tab_helper->observers_.empty());
}

TEST_F(ContextualPanelEntrypointMediatorTest, TestLargeEntrypointAppears) {
  const base::HistogramTester histogram_tester;
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:NO];

  std::unique_ptr<SamplePanelItemConfiguration> configuration =
      std::make_unique<SamplePanelItemConfiguration>();
  configuration->relevance = ContextualPanelItemConfiguration::high_relevance;
  configuration->entrypoint_message = "test";

  delegate_.canShowLargeContextualPanelEntrypoint = YES;

  FakeContextualPanelTabHelper* tab_helper =
      static_cast<FakeContextualPanelTabHelper*>(
          FakeContextualPanelTabHelper::FromWebState(
              web_state_list_.GetActiveWebState()));

  std::vector<base::WeakPtr<ContextualPanelItemConfiguration>>
      item_configurations;
  item_configurations.push_back(configuration->weak_ptr_factory.GetWeakPtr());
  tab_helper->AddToCachedConfigs(std::move(configuration));

  tab_helper->CallContextualPanelHasNewData(item_configurations);

  // At first, the small entrypoint should be displayed.
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);

  // Advance time so that the large entrypoint is displayed.
  task_environment_.FastForwardBy(
      base::Seconds(LargeContextualPanelEntrypointDelayInSeconds()));
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsLarge);

  // Advance time until the large entrypoint transitions back to small.
  task_environment_.FastForwardBy(
      base::Seconds(LargeContextualPanelEntrypointDisplayedInSeconds()));
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);

  [mocked_entrypoint_help_handler_ verify];

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.EntrypointDisplayed",
                                      ContextualPanelItemType::SamplePanelItem,
                                      1);

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.Entrypoint.Regular",
                                      EntrypointInteractionType::Displayed, 1);
  histogram_tester.ExpectUniqueSample(
      "IOS.ContextualPanel.Entrypoint.Regular.SamplePanelItem",
      EntrypointInteractionType::Displayed, 1);

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.Entrypoint.Large",
                                      EntrypointInteractionType::Displayed, 1);
  histogram_tester.ExpectUniqueSample(
      "IOS.ContextualPanel.Entrypoint.Large.SamplePanelItem",
      EntrypointInteractionType::Displayed, 1);
}

TEST_F(ContextualPanelEntrypointMediatorTest, TestIPHEntrypointAppears) {
  const base::HistogramTester histogram_tester;
  std::unique_ptr<SamplePanelItemConfiguration> configuration =
      std::make_unique<SamplePanelItemConfiguration>();
  configuration->relevance = ContextualPanelItemConfiguration::high_relevance;
  configuration->entrypoint_message = "test";
  configuration->iph_entrypoint_used_event_name = "testUsedEvent";
  configuration->iph_entrypoint_explicitly_dismissed =
      "testExplicitlyDismissedEvent";
  configuration->iph_feature =
      &feature_engagement::kIPHiOSContextualPanelSampleModelFeature;
  configuration->iph_text = "test_text";
  configuration->iph_title = "test_title";
  configuration->iph_image_name = "test_image";

  auto weak_config = configuration->weak_ptr_factory.GetWeakPtr();

  OCMStub([mocked_entrypoint_help_handler_
              maybeShowContextualPanelEntrypointIPHWithConfig:weak_config
                                                  anchorPoint:CGPointMake(0, 0)
                                              isBottomOmnibox:NO])
      .andReturn(YES);

  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:NO];

  delegate_.canShowLargeContextualPanelEntrypoint = YES;

  FakeContextualPanelTabHelper* tab_helper =
      static_cast<FakeContextualPanelTabHelper*>(
          FakeContextualPanelTabHelper::FromWebState(
              web_state_list_.GetActiveWebState()));

  std::vector<base::WeakPtr<ContextualPanelItemConfiguration>>
      item_configurations;
  item_configurations.push_back(configuration->weak_ptr_factory.GetWeakPtr());
  tab_helper->AddToCachedConfigs(std::move(configuration));

  tab_helper->CallContextualPanelHasNewData(item_configurations);

  // At first, the small entrypoint should be displayed.
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsColored);

  // Advance time so that the IPH entrypoint is displayed.
  task_environment_.FastForwardBy(
      base::Seconds(LargeContextualPanelEntrypointDelayInSeconds()));
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsColored);

  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:YES];

  // Advance time until the IPH is dismissed.
  task_environment_.FastForwardBy(
      base::Seconds(LargeContextualPanelEntrypointDisplayedInSeconds()));
  EXPECT_TRUE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsColored);

  [mocked_entrypoint_help_handler_ verify];

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.EntrypointDisplayed",
                                      ContextualPanelItemType::SamplePanelItem,
                                      1);

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.Entrypoint.Regular",
                                      EntrypointInteractionType::Displayed, 1);
  histogram_tester.ExpectUniqueSample(
      "IOS.ContextualPanel.Entrypoint.Regular.SamplePanelItem",
      EntrypointInteractionType::Displayed, 1);

  histogram_tester.ExpectUniqueSample("IOS.ContextualPanel.Entrypoint.IPH",
                                      EntrypointInteractionType::Displayed, 1);
  histogram_tester.ExpectUniqueSample(
      "IOS.ContextualPanel.Entrypoint.IPH.SamplePanelItem",
      EntrypointInteractionType::Displayed, 1);
}

// Tests a change in the active WebState.
TEST_F(ContextualPanelEntrypointMediatorTest, TestWebStateListChanged) {
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:NO];
  [[mocked_entrypoint_help_handler_ expect]
      dismissContextualPanelEntrypointIPHAnimated:NO];

  auto web_state = std::make_unique<web::FakeWebState>();
  std::map<ContextualPanelItemType, raw_ptr<ContextualPanelModel>> models;
  FakeContextualPanelTabHelper::CreateForWebState(web_state.get(), models);
  InfoBarManagerImpl::CreateForWebState(web_state.get());
  InfobarBadgeTabHelper::GetOrCreateForWebState(web_state.get());

  web_state_list_.InsertWebState(
      std::move(web_state),
      WebStateList::InsertionParams::Automatic().Activate(true));

  EXPECT_FALSE(entrypoint_consumer_.entrypointIsShown);
  EXPECT_FALSE(entrypoint_consumer_.entrypointIsLarge);

  [mocked_entrypoint_help_handler_ verify];
}