// 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/model/price_insights_model.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "components/commerce/core/commerce_feature_list.h"
#import "components/commerce/core/commerce_types.h"
#import "components/commerce/core/mock_shopping_service.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/strings/grit/components_strings.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/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.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"
#import "ui/base/l10n/l10n_util.h"
using testing::_;
namespace {
const char kTestUrl[] = "https://www.merchant.com/price_drop_product";
const char kTestSecondUrl[] =
"https://www.merchant.com/second_price_drop_product";
std::string kTestTitle = "Product";
} // namespace
// Unittests related to the PriceInsightsModel.
class PriceInsightsModelTest : public PlatformTest {
public:
PriceInsightsModelTest() {}
~PriceInsightsModelTest() override {}
void SetUp() override {
TestChromeBrowserState::Builder builder;
builder.AddTestingFactory(
commerce::ShoppingServiceFactory::GetInstance(),
base::BindRepeating(
[](web::BrowserState*) -> std::unique_ptr<KeyedService> {
return commerce::MockShoppingService::Build();
}));
test_chrome_browser_state_ = std::move(builder).Build();
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));
web_state_ = std::make_unique<web::FakeWebState>();
web_state_->SetNavigationManager(std::move(navigation_manager));
web_state_->SetBrowserState(test_chrome_browser_state_.get());
web_state_->SetNavigationItemCount(1);
web_state_->SetCurrentURL(GURL(kTestUrl));
web_state_->SetBrowserState(test_chrome_browser_state_.get());
price_insights_model_ = std::make_unique<PriceInsightsModel>();
shopping_service_ = static_cast<commerce::MockShoppingService*>(
commerce::ShoppingServiceFactory::GetForBrowserState(
test_chrome_browser_state_.get()));
shopping_service_->SetResponseForGetProductInfoForUrl(std::nullopt);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(std::nullopt);
shopping_service_->SetIsSubscribedCallbackValue(false);
shopping_service_->SetIsShoppingListEligible(true);
fetch_configuration_callback_count = 0;
}
void FetchConfigurationCallback(
std::unique_ptr<ContextualPanelItemConfiguration> configuration) {
returned_configuration_ = std::move(configuration);
fetch_configuration_callback_count++;
}
int GetPriceInsightsCallbacksCount() {
return price_insights_model_->callbacks_.size();
}
int GetPriceInsightsCallbacksValueCountForUrl(const GURL& product_url) {
auto callbacks_it = price_insights_model_->callbacks_.find(product_url);
return callbacks_it->second.size();
}
int GetPriceInsightsExecutionsCount() {
return price_insights_model_->price_insights_executions_.size();
}
protected:
base::test::ScopedFeatureList features_;
base::test::TaskEnvironment task_environment_;
std::unique_ptr<PriceInsightsModel> price_insights_model_;
raw_ptr<commerce::MockShoppingService> shopping_service_;
std::unique_ptr<ContextualPanelItemConfiguration> returned_configuration_;
int fetch_configuration_callback_count;
std::unique_ptr<web::FakeWebState> web_state_;
std::unique_ptr<TestChromeBrowserState> test_chrome_browser_state_;
};
// Tests that fetching the configuration for the price insights model returns no
// data when there's any product info.
TEST_F(PriceInsightsModelTest, TestFetchConfigurationNoProductInfo) {
base::RunLoop run_loop;
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(nullptr, config);
}
// Tests that fetching the configuration for the price insights model returns no
// data when product info has no title and no product cluster title.
TEST_F(PriceInsightsModelTest, TestFetchConfigurationNoTitleNoClusterTitle) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(nullptr, config);
}
// Tests that fetching the configuration for the price insights model returns no
// data when product info is available without tracking.
TEST_F(PriceInsightsModelTest, TestFetchConfigurationProductInfoNoTracking) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
shopping_service_->SetIsShoppingListEligible(false);
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(nullptr, config);
}
// Test that GetProductInfoForUrl return data when the configuration is fetched.
TEST_F(PriceInsightsModelTest, TestFetchProductInfo) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
EXPECT_EQ(1, GetPriceInsightsCallbacksCount());
EXPECT_EQ(1, GetPriceInsightsExecutionsCount());
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(false, config->price_insights_info.has_value());
EXPECT_EQ(true, config->product_info.has_value());
commerce::ProductInfo info2 = config->product_info.value();
EXPECT_EQ(kTestTitle, info2.title);
EXPECT_EQ(0, GetPriceInsightsCallbacksCount());
EXPECT_EQ(0, GetPriceInsightsExecutionsCount());
}
// Test that GetPriceInsightsInfoForUrl return data when the configuration is
// fetched.
TEST_F(PriceInsightsModelTest, TestFetchPriceInsightsInfo) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
EXPECT_EQ(1, GetPriceInsightsCallbacksCount());
EXPECT_EQ(1, GetPriceInsightsExecutionsCount());
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(true, config->product_info.has_value());
EXPECT_EQ(true, config->price_insights_info.has_value());
commerce::PriceInsightsInfo info2 = config->price_insights_info.value();
EXPECT_EQ(123u, info2.product_cluster_id);
EXPECT_EQ(0, GetPriceInsightsCallbacksCount());
EXPECT_EQ(0, GetPriceInsightsExecutionsCount());
}
// Tests that GetProductInfoForUrl is only called once when multiple requests
// in-flight have the same URL.
TEST_F(PriceInsightsModelTest, TestMultipleRequestForTheSameURL) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> product_info;
product_info.emplace();
product_info->title = kTestTitle;
shopping_service_->SetResponseForGetProductInfoForUrl(
std::move(product_info));
std::optional<commerce::PriceInsightsInfo> price_insights_info;
price_insights_info.emplace();
price_insights_info->catalog_history_prices.emplace_back("2021-01-01",
3330000);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_insights_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
EXPECT_EQ(1, GetPriceInsightsCallbacksCount());
EXPECT_EQ(2, GetPriceInsightsCallbacksValueCountForUrl(GURL(kTestUrl)));
EXPECT_EQ(1, GetPriceInsightsExecutionsCount());
run_loop.Run();
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForActionTimeout, ^bool {
base::RunLoop().RunUntilIdle();
return fetch_configuration_callback_count == 2;
}));
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(true, config->product_info.has_value());
EXPECT_EQ(true, config->price_insights_info.has_value());
EXPECT_EQ(0, GetPriceInsightsCallbacksCount());
EXPECT_EQ(0, GetPriceInsightsExecutionsCount());
}
// Tests that GetProductInfoForUrl is called for each request in-flight that has
// a different URL.
TEST_F(PriceInsightsModelTest, TestMultipleRequestForDifferentURL) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> product_info;
product_info.emplace();
product_info->title = kTestTitle;
shopping_service_->SetResponseForGetProductInfoForUrl(
std::move(product_info));
std::optional<commerce::PriceInsightsInfo> price_insights_info;
price_insights_info.emplace();
price_insights_info->catalog_history_prices.emplace_back("2021-01-01",
3330000);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_insights_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(2);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(2);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
web_state_->SetCurrentURL(GURL(kTestSecondUrl));
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
EXPECT_EQ(2, GetPriceInsightsCallbacksCount());
EXPECT_EQ(2, GetPriceInsightsExecutionsCount());
run_loop.Run();
ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForActionTimeout, ^bool {
base::RunLoop().RunUntilIdle();
return fetch_configuration_callback_count == 2;
}));
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(true, config->product_info.has_value());
EXPECT_EQ(true, config->price_insights_info.has_value());
EXPECT_EQ(0, GetPriceInsightsCallbacksCount());
EXPECT_EQ(0, GetPriceInsightsExecutionsCount());
}
// Test that GetProductInfoForUrl return data when the configuration is fetched.
TEST_F(PriceInsightsModelTest, TestFetchIsSubscribed) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
shopping_service_->SetIsSubscribedCallbackValue(true);
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(true, config->is_subscribed);
}
// Test that GetProductInfoForUrl return data when the configuration is fetched.
TEST_F(PriceInsightsModelTest, TestFetchProductInfoWithPriceTrackAvailable) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
shopping_service_->SetIsSubscribedCallbackValue(false);
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(true, config->can_price_track);
}
// Test that price track is not available when the eligibility is not met.
TEST_F(PriceInsightsModelTest, TestFetchProductInfoWithPriceTrackUnavailable) {
base::RunLoop run_loop;
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
shopping_service_->SetIsSubscribedCallbackValue(false);
shopping_service_->SetIsShoppingListEligible(false);
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(false, config->can_price_track);
}
// Test that GetProductInfo, GetProductInfoForUrl, and IsSubscribed all return
// data for the config.
TEST_F(PriceInsightsModelTest, TestFetchCompleteConfig) {
base::RunLoop run_loop;
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(true, config->is_subscribed);
}
// Test that GetProductInfo, GetProductInfoForUrl, all return
// data for the config when the product cannot be tracked.
TEST_F(PriceInsightsModelTest, TestFetchPriceInsightsWhenTrackUnavailable) {
base::RunLoop run_loop;
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
shopping_service_->SetIsSubscribedCallbackValue(false);
shopping_service_->SetIsShoppingListEligible(false);
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::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);
EXPECT_EQ(false, config->can_price_track);
EXPECT_EQ(true, config->product_info.has_value());
EXPECT_EQ(true, config->price_insights_info.has_value());
}
// Test that when the price bucket is unknown, the entrypoint message is empty
// and the relevance is set to low.
TEST_F(PriceInsightsModelTest, TestPriceBucketUnknownEmptyMessageLowRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeatureWithParameters(
commerce::kPriceInsightsIos,
{{kLowPriceParam, kLowPriceParamPriceIsLow}});
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kUnknown;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_PRICE_INSIGHTS_ACCESSIBILITY),
config->accessibility_label);
EXPECT_EQ("", config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kDownTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::low_relevance, config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}
// Test that when the price bucket is low, the entrypoint message is set to a
// specific string, and the relevance is set to high.
TEST_F(PriceInsightsModelTest, TestPriceBucketLowLowPriceMessageHighRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeatureWithParameters(
commerce::kPriceInsightsIos,
{{kLowPriceParam, kLowPriceParamPriceIsLow}});
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kLowPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(l10n_util::GetStringUTF8(
IDS_SHOPPING_INSIGHTS_ICON_EXPANDED_TEXT_LOW_PRICE),
config->accessibility_label);
EXPECT_EQ(l10n_util::GetStringUTF8(
IDS_SHOPPING_INSIGHTS_ICON_EXPANDED_TEXT_LOW_PRICE),
config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kDownTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::high_relevance,
config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}
// Test that when the price bucket is low, the entrypoint message is set to a
// specific string, and the relevance is set to high.
TEST_F(PriceInsightsModelTest, TestPriceBucketLowGoodDealMessageHighRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeatureWithParameters(
commerce::kPriceInsightsIos,
{{kLowPriceParam, kLowPriceParamGoodDealNow}});
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kLowPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_INSIGHTS_ICON_EXPANDED_TEXT_GOOD_DEAL),
config->accessibility_label);
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_INSIGHTS_ICON_EXPANDED_TEXT_GOOD_DEAL),
config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kDownTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::high_relevance,
config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}
// Test that when the price bucket is low, the entrypoint message is set to a
// specific string, and the relevance is set to high.
TEST_F(PriceInsightsModelTest,
TestPriceBucketLowSeePriceHistoryMessageHighRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeatureWithParameters(
commerce::kPriceInsightsIos,
{{kLowPriceParam, kLowPriceParamSeePriceHistory}});
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kLowPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(
l10n_util::GetStringUTF8(IDS_INSIGHTS_ICON_EXPANDED_TEXT_PRICE_HISTORY),
config->accessibility_label);
EXPECT_EQ(
l10n_util::GetStringUTF8(IDS_INSIGHTS_ICON_EXPANDED_TEXT_PRICE_HISTORY),
config->entrypoint_message);
EXPECT_EQ(ContextualPanelItemConfiguration::high_relevance,
config->relevance);
}
// Test that when the price bucket is high and PriceInisghtsHighPrice is
// disabled, the relevance is set to low and the accessibility text are not
// empty.
TEST_F(PriceInsightsModelTest,
TestHighPriceDisabledPriceBucketHighEmptyMessageLowRelevance) {
base::RunLoop run_loop;
features_.InitAndDisableFeature(commerce::kPriceInsightsHighPriceIos);
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kHighPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_PRICE_INSIGHTS_ACCESSIBILITY),
config->accessibility_label);
EXPECT_EQ("", config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kDownTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::low_relevance, config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}
// Test that when the price bucket is high and the page is subscribed, the
// relevance is set to low and the accessibility text are not empty.
TEST_F(PriceInsightsModelTest, TestPriceBucketHighSubscribedLowRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeature(commerce::kPriceInsightsHighPriceIos);
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kHighPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_PRICE_INSIGHTS_ACCESSIBILITY),
config->accessibility_label);
EXPECT_EQ("", config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kUpTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::low_relevance, config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}
// Test that when the price bucket is high and the page can not be tracked, the
// relevance is set to low and the accessibility text is not empty.
TEST_F(PriceInsightsModelTest,
TestPriceBucketHighTrackNotAvailableLowRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeature(commerce::kPriceInsightsHighPriceIos);
shopping_service_->SetIsSubscribedCallbackValue(true);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kHighPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(0);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_PRICE_INSIGHTS_ACCESSIBILITY),
config->accessibility_label);
EXPECT_EQ("", config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kUpTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::low_relevance, config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}
// Test that when the price bucket is high and the page is currently not
// subscribed, the relevance is set to high and both the entrypoint text and
// accessibility text are not empty.
TEST_F(PriceInsightsModelTest, TestPriceBucketHighHighRelevance) {
base::RunLoop run_loop;
features_.InitAndEnableFeature(commerce::kPriceInsightsHighPriceIos);
shopping_service_->SetIsSubscribedCallbackValue(false);
std::optional<commerce::ProductInfo> info;
info.emplace();
info->title = kTestTitle;
info->product_cluster_id = 12345L;
shopping_service_->SetResponseForGetProductInfoForUrl(std::move(info));
std::optional<commerce::PriceInsightsInfo> price_info;
price_info.emplace();
price_info->product_cluster_id = 123u;
price_info->catalog_history_prices.emplace_back("2021-01-01", 3330000);
price_info->catalog_history_prices.emplace_back("2021-01-02", 4440000);
price_info->price_bucket = commerce::PriceBucket::kHighPrice;
shopping_service_->SetResponseForGetPriceInsightsInfoForUrl(
std::move(price_info));
EXPECT_CALL(*shopping_service_, GetProductInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, GetPriceInsightsInfoForUrl(_, _)).Times(1);
EXPECT_CALL(*shopping_service_, IsSubscribed(_, _)).Times(1);
price_insights_model_->FetchConfigurationForWebState(
web_state_.get(),
base::BindOnce(&PriceInsightsModelTest::FetchConfigurationCallback,
base::Unretained(this))
.Then(run_loop.QuitClosure()));
run_loop.Run();
PriceInsightsItemConfiguration* config =
static_cast<PriceInsightsItemConfiguration*>(
returned_configuration_.get());
EXPECT_EQ(
l10n_util::GetStringUTF8(IDS_INSIGHTS_ICON_PRICE_HIGH_EXPANDED_TEXT),
config->accessibility_label);
EXPECT_EQ(
l10n_util::GetStringUTF8(IDS_INSIGHTS_ICON_PRICE_HIGH_EXPANDED_TEXT),
config->entrypoint_message);
EXPECT_EQ(base::SysNSStringToUTF8(kUpTrendSymbol),
config->entrypoint_image_name);
EXPECT_EQ(ContextualPanelItemConfiguration::EntrypointImageType::SFSymbol,
config->image_type);
EXPECT_EQ(ContextualPanelItemConfiguration::high_relevance,
config->relevance);
EXPECT_EQ(&feature_engagement::kIPHiOSContextualPanelPriceInsightsFeature,
config->iph_feature);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointUsed,
config->iph_entrypoint_used_event_name);
EXPECT_EQ(feature_engagement::events::
kIOSContextualPanelPriceInsightsEntrypointExplicitlyDismissed,
config->iph_entrypoint_explicitly_dismissed);
}