chromium/ios/chrome/browser/price_insights/coordinator/price_insights_modulator_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/price_insights/coordinator/price_insights_modulator.h"

#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/task_environment.h"
#import "components/commerce/core/commerce_types.h"
#import "components/commerce/core/mock_shopping_service.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/commerce/model/shopping_service_factory.h"
#import "ios/chrome/browser/contextual_panel/model/contextual_panel_item_configuration.h"
#import "ios/chrome/browser/price_insights/model/price_insights_feature.h"
#import "ios/chrome/browser/price_insights/model/price_insights_model.h"
#import "ios/chrome/browser/price_insights/ui/price_insights_cell.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/test/fakes/fake_ui_view_controller.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/web_state.h"
#import "testing/platform_test.h"

using testing::_;

namespace {

const char kTestUrl[] = "https://www.merchant.com/price_drop_product";
const char kTestBuyingOptionsUrl[] =
    "https://www.merchant.com/price_drop_product/jackpot";
const char kTestTitle[] = "Product";
const char kVariant[] = "Variant";
const char kCurrency[] = "USD";
const char kCountry[] = "US";
const uint64_t kClusterId = 123u;

}  // namespace

// Unittests related to the PriceInsightsModulator.
class PriceInsightsModulatorTest : public PlatformTest {
 public:
  PriceInsightsModulatorTest() {}
  ~PriceInsightsModulatorTest() override {}

  void SetUp() override {
    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(ios::BookmarkModelFactory::GetInstance(),
                              ios::BookmarkModelFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        commerce::ShoppingServiceFactory::GetInstance(),
        base::BindRepeating(
            [](web::BrowserState*) -> std::unique_ptr<KeyedService> {
              return commerce::MockShoppingService::Build();
            }));

    TestChromeBrowserState* test_chrome_browser_state =
        profile_manager_.AddProfileWithBuilder(std::move(builder));
    browser_ = std::make_unique<TestBrowser>(test_chrome_browser_state);
    base_view_controller_ = [[FakeUIViewController alloc] init];
    std::unique_ptr<web::FakeNavigationManager> navigation_manager =
        std::make_unique<web::FakeNavigationManager>();
    navigation_manager->AddItem(GURL(kTestUrl), ui::PAGE_TRANSITION_LINK);
    navigation_manager->SetLastCommittedItem(
        navigation_manager->GetItemAtIndex(0));
    std::unique_ptr<web::FakeWebState> web_state =
        std::make_unique<web::FakeWebState>();
    web_state_ptr_ = web_state.get();
    raw_ptr<WebStateList> web_state_list = browser_->GetWebStateList();
    web_state_list->InsertWebState(
        std::move(web_state),
        WebStateList::InsertionParams::Automatic().Activate());
    web_state_ptr_->SetNavigationManager(std::move(navigation_manager));
    web_state_ptr_->SetBrowserState(test_chrome_browser_state);
    web_state_ptr_->SetNavigationItemCount(1);
    web_state_ptr_->SetCurrentURL(GURL(kTestUrl));
    web_state_ptr_->SetBrowserState(test_chrome_browser_state);
    price_insights_model_ = std::make_unique<PriceInsightsModel>();
    shopping_service_ = static_cast<commerce::MockShoppingService*>(
        commerce::ShoppingServiceFactory::GetForBrowserState(
            test_chrome_browser_state));
    shopping_service_->SetResponseForGetProductInfoForUrl(std::nullopt);
    shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(std::nullopt);
    shopping_service_->SetIsShoppingListEligible(true);
  }

  void FetchConfigurationCallback(
      std::unique_ptr<ContextualPanelItemConfiguration> configuration) {
    returned_configuration_ = std::move(configuration);
  }

 protected:
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  base::test::TaskEnvironment task_environment_;
  TestProfileManagerIOS profile_manager_;
  std::unique_ptr<TestBrowser> browser_;
  UIViewController* base_view_controller_;
  web::FakeWebState* web_state_ptr_;
  std::unique_ptr<PriceInsightsModel> price_insights_model_;
  raw_ptr<commerce::MockShoppingService> shopping_service_;
  std::unique_ptr<ContextualPanelItemConfiguration> returned_configuration_;
};

// Tests that when the subscription status of a page changes after querying the
// model, the modulator assigns the latest subscription status.
TEST_F(PriceInsightsModulatorTest, TestSubscriptionStatusChange) {
  base::RunLoop run_loop;

  commerce::ProductInfo info;
  info.title = kTestTitle;
  info.product_cluster_id = kClusterId;
  shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));

  commerce::PriceInsightsInfo price_info;
  price_info.product_cluster_id = kClusterId;
  price_info.catalog_history_prices.emplace_back("2021-01-01", 3330000);
  price_info.catalog_history_prices.emplace_back("2021-01-02", 4440000);
  shopping_service_->SetIsSubscribedCallbackValue(false);
  shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
      std::move(price_info));

  // Fetch data from the model.
  price_insights_model_->FetchConfigurationForWebState(
      web_state_ptr_,
      base::BindOnce(&PriceInsightsModulatorTest::FetchConfigurationCallback,
                     base::Unretained(this))
          .Then(run_loop.QuitClosure()));

  run_loop.Run();

  PriceInsightsItemConfiguration* config =
      static_cast<PriceInsightsItemConfiguration*>(
          returned_configuration_.get());

  EXPECT_EQ(false, config->is_subscribed);

  shopping_service_->SetIsSubscribedCallbackValue(true);

  PriceInsightsModulator* modulator = [[PriceInsightsModulator alloc]
      initWithBaseViewController:base_view_controller_
                         browser:browser_.get()
               itemConfiguration:config->weak_ptr_factory.GetWeakPtr()];

  // Start the modulator.
  [modulator start];

  UICollectionView* collection_view = [[UICollectionView alloc]
             initWithFrame:CGRectZero
      collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
  PriceInsightsCell* cell = static_cast<PriceInsightsCell*>([collection_view
      dequeueConfiguredReusableCellWithRegistration:modulator.panelBlockData
                                                        .cellRegistration
                                       forIndexPath:[NSIndexPath
                                                        indexPathForRow:0
                                                              inSection:0]
                                               item:@"id"]);
  PriceInsightsItem* item = cell.priceInsightsItem;

  EXPECT_EQ(true, item.isPriceTracked);
}

// Tests that PriceInsightsItem has the correct data from
// PriceInsightsItemConfiguration.
TEST_F(PriceInsightsModulatorTest, TestPriceInsightsItemDataFromConfig) {
  base::RunLoop run_loop;

  commerce::ProductInfo info;
  info.product_cluster_title = kTestTitle;
  info.product_cluster_id = kClusterId;
  info.currency_code = kCurrency;
  info.country_code = kCountry;

  shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));

  commerce::PriceInsightsInfo price_info;
  price_info.product_cluster_id = kClusterId;
  price_info.catalog_history_prices.emplace_back("2021-01-01", 3330000);
  price_info.catalog_history_prices.emplace_back("2021-01-02", 4440000);
  price_info.catalog_attributes = kVariant;
  price_info.currency_code = kCurrency;
  price_info.jackpot_url = GURL(kTestBuyingOptionsUrl);
  price_info.has_multiple_catalogs = true;

  shopping_service_->SetIsSubscribedCallbackValue(true);
  shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
      std::move(price_info));

  // Fetch data from the model.
  price_insights_model_->FetchConfigurationForWebState(
      web_state_ptr_,
      base::BindOnce(&PriceInsightsModulatorTest::FetchConfigurationCallback,
                     base::Unretained(this))
          .Then(run_loop.QuitClosure()));

  run_loop.Run();

  PriceInsightsItemConfiguration* config =
      static_cast<PriceInsightsItemConfiguration*>(
          returned_configuration_.get());

  shopping_service_->SetIsSubscribedCallbackValue(true);

  PriceInsightsModulator* modulator = [[PriceInsightsModulator alloc]
      initWithBaseViewController:base_view_controller_
                         browser:browser_.get()
               itemConfiguration:config->weak_ptr_factory.GetWeakPtr()];

  // Start the modulator.
  [modulator start];

  UICollectionView* collection_view = [[UICollectionView alloc]
             initWithFrame:CGRectZero
      collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
  PriceInsightsCell* cell = static_cast<PriceInsightsCell*>([collection_view
      dequeueConfiguredReusableCellWithRegistration:modulator.panelBlockData
                                                        .cellRegistration
                                       forIndexPath:[NSIndexPath
                                                        indexPathForRow:0
                                                              inSection:0]
                                               item:@"id"]);
  PriceInsightsItem* item = cell.priceInsightsItem;

  EXPECT_EQ(kTestTitle, base::SysNSStringToUTF8(item.title));
  EXPECT_EQ(kVariant, base::SysNSStringToUTF8(item.variants));
  EXPECT_EQ(kCurrency, item.currency);
  EXPECT_EQ(kCountry, item.country);
  EXPECT_EQ(2ul, [item.priceHistory count]);
  EXPECT_EQ(GURL(kTestBuyingOptionsUrl), item.buyingOptionsURL);
  EXPECT_EQ(true, item.canPriceTrack);
  EXPECT_EQ(true, item.isPriceTracked);
  EXPECT_EQ(GURL(kTestUrl), item.productURL);
  EXPECT_EQ(kClusterId, item.clusterId);
}

// Tests that PriceInsightsItem displays a valid title when
// product_cluster_title is empty.
TEST_F(PriceInsightsModulatorTest, TestPriceInsightsItemTitle) {
  base::RunLoop run_loop;

  commerce::ProductInfo info;
  info.title = kTestTitle;
  info.product_cluster_title = "";
  info.product_cluster_id = kClusterId;
  info.currency_code = kCurrency;
  info.country_code = kCountry;

  shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
  shopping_service_->SetIsSubscribedCallbackValue(true);

  // Fetch data from the model.
  price_insights_model_->FetchConfigurationForWebState(
      web_state_ptr_,
      base::BindOnce(&PriceInsightsModulatorTest::FetchConfigurationCallback,
                     base::Unretained(this))
          .Then(run_loop.QuitClosure()));

  run_loop.Run();

  PriceInsightsItemConfiguration* config =
      static_cast<PriceInsightsItemConfiguration*>(
          returned_configuration_.get());

  shopping_service_->SetIsSubscribedCallbackValue(true);

  PriceInsightsModulator* modulator = [[PriceInsightsModulator alloc]
      initWithBaseViewController:base_view_controller_
                         browser:browser_.get()
               itemConfiguration:config->weak_ptr_factory.GetWeakPtr()];

  // Start the modulator.
  [modulator start];

  UICollectionView* collection_view = [[UICollectionView alloc]
             initWithFrame:CGRectZero
      collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
  PriceInsightsCell* cell = static_cast<PriceInsightsCell*>([collection_view
      dequeueConfiguredReusableCellWithRegistration:modulator.panelBlockData
                                                        .cellRegistration
                                       forIndexPath:[NSIndexPath
                                                        indexPathForRow:0
                                                              inSection:0]
                                               item:@"id"]);
  PriceInsightsItem* item = cell.priceInsightsItem;

  EXPECT_EQ(kTestTitle, base::SysNSStringToUTF8(item.title));
}