Skip to content

Commit

Permalink
[116][PriceInsights] Add icon animation
Browse files Browse the repository at this point in the history
The icon will show the expand animation if all the following conditions are met:
  * Icon has not expanded today and has not expanded 3 times in the last
    28 days.
  * The price range and price history data both are available
  * The price is detected as a low or high price.

(cherry picked from commit a784997)

Change-Id: Ib0d0b985ca98e063d6c035ee0a497d6fda3d7d42
Bug: 1458274, b/285996573
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4623018
Reviewed-by: Emily Shack <emshack@chromium.org>
Reviewed-by: Matthew Jones <mdjones@chromium.org>
Reviewed-by: Dana Fried <dfried@chromium.org>
Reviewed-by: Zhiyuan Cai <zhiyuancai@chromium.org>
Commit-Queue: Mei Liang <meiliang@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1161991}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4657782
Reviewed-by: Tommy Nyquist <nyquist@chromium.org>
Cr-Commit-Position: refs/branch-heads/5845@{#264}
Cr-Branched-From: 5a5dff6-refs/heads/main@{#1160321}
  • Loading branch information
Mei Liang authored and Chromium LUCI CQ committed Jun 30, 2023
1 parent a165eee commit 80ebf4f
Show file tree
Hide file tree
Showing 15 changed files with 376 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class MockShoppingListUiTabHelper : public commerce::ShoppingListUiTabHelper {
CreateShoppingInsightsWebView,
(),
(override));
MOCK_METHOD(const absl::optional<commerce::PriceInsightsInfo>&,
GetPriceInsightsInfo,
());

private:
gfx::Image valid_product_image_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ ShoppingListUiTabHelper::GetPendingTrackingStateForTesting() {
return pending_tracking_state_;
}

const absl::optional<PriceInsightsInfo>&
ShoppingListUiTabHelper::GetPriceInsightsInfo() {
return price_insights_info_;
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(ShoppingListUiTabHelper);

} // namespace commerce
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class ShoppingListUiTabHelper
base::OnceCallback<void(bool)> callback);
void ShowShoppingInsightsSidePanel();

// Return the PriceInsightsInfo for the last fetched product URL. A reference
// to this object should not be kept directly, if one is needed, a copy should
// be made.
virtual const absl::optional<PriceInsightsInfo>& GetPriceInsightsInfo();

protected:
ShoppingListUiTabHelper(content::WebContents* contents,
ShoppingService* shopping_service,
Expand Down
107 changes: 105 additions & 2 deletions chrome/browser/ui/views/commerce/price_insights_icon_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@

#include "chrome/browser/ui/views/commerce/price_insights_icon_view.h"

#include "base/timer/timer.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/commerce/price_tracking/shopping_list_ui_tab_helper.h"
#include "chrome/browser/ui/side_panel/side_panel_entry_id.h"
#include "chrome/browser/ui/side_panel/side_panel_ui.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/shopping_service.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
Expand All @@ -18,15 +24,18 @@

PriceInsightsIconView::PriceInsightsIconView(
IconLabelBubbleView::Delegate* icon_label_bubble_delegate,
PageActionIconView::Delegate* page_action_icon_delegate)
PageActionIconView::Delegate* page_action_icon_delegate,
Profile* profile)
: PageActionIconView(nullptr,
0,
icon_label_bubble_delegate,
page_action_icon_delegate,
"PriceInsights"),
profile_(profile),
icon_(OmniboxFieldTrial::IsChromeRefreshIconsEnabled()
? &vector_icons::kShoppingBagRefreshIcon
: &vector_icons::kShoppingBagIcon) {
SetUpForInOutAnimation();
SetProperty(views::kElementIdentifierKey, kPriceInsightsChipElementId);
SetAccessibilityProperties(
/*role*/ absl::nullopt,
Expand All @@ -45,7 +54,97 @@ const gfx::VectorIcon& PriceInsightsIconView::GetVectorIcon() const {
}

void PriceInsightsIconView::UpdateImpl() {
SetVisible(ShouldShow());
bool should_show = ShouldShow();

if (should_show) {
if (!GetVisible() && QualifiesForPageActionLabel()) {
MaybeShowPageActionLabel();
}
} else {
HidePageActionLabel();
}

SetVisible(should_show);
}

void PriceInsightsIconView::HidePageActionLabel() {
UnpauseAnimation();
ResetSlideAnimation(false);
}

void PriceInsightsIconView::MaybeShowPageActionLabel() {
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(profile_);

if (!tracker ||
!tracker->ShouldTriggerHelpUI(
feature_engagement::kIPHPriceInsightsPageActionIconLabelFeature)) {
return;
}

should_extend_label_shown_duration_ = true;
AnimateIn(absl::nullopt);

// Note that `Dismiss()` in this case does not dismiss the UI. It's telling
// the FE backend that the promo is done so that other promos can run. Showing
// the label should not block other promos from displaying.
tracker->Dismissed(
feature_engagement::kIPHPriceInsightsPageActionIconLabelFeature);
}

bool PriceInsightsIconView::QualifiesForPageActionLabel() {
auto* web_contents = GetWebContents();

if (!web_contents) {
return false;
}
auto* tab_helper =
commerce::ShoppingListUiTabHelper::FromWebContents(web_contents);
CHECK(tab_helper);

auto& price_insights_info = tab_helper->GetPriceInsightsInfo();

if (!price_insights_info.has_value()) {
return false;
}

if (price_insights_info->price_bucket == commerce::PriceBucket::kLowPrice) {
SetLabel(l10n_util::GetStringUTF16(
IDS_SHOPPING_INSIGHTS_ICON_EXPANDED_TEXT_LOW_PRICE));
} else if (price_insights_info->price_bucket ==
commerce::PriceBucket::kHighPrice) {
SetLabel(l10n_util::GetStringUTF16(
IDS_SHOPPING_INSIGHTS_ICON_EXPANDED_TEXT_HIGH_PRICE));
} else {
return false;
}

return price_insights_info->typical_low_price_micros.has_value() &&
price_insights_info->typical_high_price_micros.has_value() &&
!price_insights_info->catalog_history_prices.empty();
}

void PriceInsightsIconView::AnimationProgressed(
const gfx::Animation* animation) {
PageActionIconView::AnimationProgressed(animation);
// When the label is fully revealed pause the animation for
// kLabelPersistDuration before resuming the animation and allowing the label
// to animate out. This is currently set to show for 12s including the in/out
// animation.
// TODO(crbug.com/1314206): This approach of inspecting the animation progress
// to extend the animation duration is quite hacky. This should be removed and
// the IconLabelBubbleView API expanded to support a finer level of control.
constexpr double kAnimationValueWhenLabelFullyShown = 0.5;
constexpr base::TimeDelta kLabelPersistDuration = base::Seconds(10.8);
if (should_extend_label_shown_duration_ &&
GetAnimationValue() >= kAnimationValueWhenLabelFullyShown) {
should_extend_label_shown_duration_ = false;
PauseAnimation();
animate_out_timer_.Start(
FROM_HERE, kLabelPersistDuration,
base::BindRepeating(&PriceInsightsIconView::UnpauseAnimation,
base::Unretained(this)));
}
}

void PriceInsightsIconView::OnExecuting(
Expand Down Expand Up @@ -75,5 +174,9 @@ bool PriceInsightsIconView::ShouldShow() const {
return tab_helper && tab_helper->ShouldShowPriceInsightsIconView();
}

const std::u16string& PriceInsightsIconView::GetIconLabelForTesting() {
return label()->GetText();
}

BEGIN_METADATA(PriceInsightsIconView, PageActionIconView)
END_METADATA
30 changes: 29 additions & 1 deletion chrome/browser/ui/views/commerce/price_insights_icon_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
#ifndef CHROME_BROWSER_UI_VIEWS_COMMERCE_PRICE_INSIGHTS_ICON_VIEW_H_
#define CHROME_BROWSER_UI_VIEWS_COMMERCE_PRICE_INSIGHTS_ICON_VIEW_H_

#include "base/timer/timer.h"
#include "chrome/browser/ui/views/page_action/page_action_icon_view.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/gfx/vector_icon_types.h"

class Profile;

// This icon appears in the location bar when the current page qualifies for
// price insight information. Upon clicking, it opens the side panel with more
// price information.
Expand All @@ -17,22 +20,47 @@ class PriceInsightsIconView : public PageActionIconView {
METADATA_HEADER(PriceInsightsIconView);
PriceInsightsIconView(
IconLabelBubbleView::Delegate* icon_label_bubble_delegate,
PageActionIconView::Delegate* page_action_icon_delegate);
PageActionIconView::Delegate* page_action_icon_delegate,
Profile* profile);
~PriceInsightsIconView() override;

// PageActionIconView:
views::BubbleDialogDelegate* GetBubble() const override;

const std::u16string& GetIconLabelForTesting();

protected:
// PageActionIconView:
const gfx::VectorIcon& GetVectorIcon() const override;
void UpdateImpl() override;
void OnExecuting(PageActionIconView::ExecuteSource execute_source) override;

private:
// IconLabelBubbleView:
void AnimationProgressed(const gfx::Animation* animation) override;

// Return whether the icon need to be shown.
bool ShouldShow() const;

// Show page action label if it meets the feature engagement requirements.
void MaybeShowPageActionLabel();

// Return whether the page is qualified for the page action label.
bool QualifiesForPageActionLabel();

// Hides the page action label.
void HidePageActionLabel();

const raw_ptr<Profile> profile_;
raw_ptr<const gfx::VectorIcon> icon_;

// Animates out the price tracking icon label after a fixed period of time.
// This keeps the label visible for long enough to give users an opportunity
// to read the label text.
base::RetainingOneShotTimer animate_out_timer_;
// Boolean that tracks whether we should extend the duration for which the
// label is shown when it animates in.
bool should_extend_label_shown_duration_ = false;
};

#endif // CHROME_BROWSER_UI_VIEWS_COMMERCE_PRICE_INSIGHTS_ICON_VIEW_H_
116 changes: 104 additions & 12 deletions chrome/browser/ui/views/commerce/price_insights_icon_view_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "chrome/test/base/ui_test_utils.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/test_utils.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/user_education/test/feature_promo_test_util.h"
#include "content/public/test/browser_test.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
Expand All @@ -37,15 +39,20 @@ class PriceInsightsIconViewBrowserTest : public UiBrowserTest {
.Times(testing::AnyNumber());
ON_CALL(*mock_tab_helper, ShouldShowPriceInsightsIconView)
.WillByDefault(testing::Return(true));

EXPECT_CALL(*mock_tab_helper, GetPriceInsightsInfo)
.Times(testing::AnyNumber());
ON_CALL(*mock_tab_helper, GetPriceInsightsInfo)
.WillByDefault(testing::ReturnRef(price_insights_info_));
}

void ShowUi(const std::string& name) override {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kTestURL)));
}

bool VerifyUi() override {
auto* price_tracking_chip = GetChip();
if (!price_tracking_chip) {
auto* price_insights_chip = GetChip();
if (!price_insights_chip) {
return false;
}

Expand All @@ -59,16 +66,8 @@ class PriceInsightsIconViewBrowserTest : public UiBrowserTest {
ui_test_utils::WaitForBrowserToClose();
}

private:
base::test::ScopedFeatureList test_features_{commerce::kPriceInsights};

BrowserView* GetBrowserView() {
return BrowserView::GetBrowserViewForBrowser(browser());
}

LocationBarView* GetLocationBarView() {
return GetBrowserView()->toolbar()->location_bar();
}
protected:
absl::optional<commerce::PriceInsightsInfo> price_insights_info_;

PriceInsightsIconView* GetChip() {
const ui::ElementContext context =
Expand All @@ -81,9 +80,102 @@ class PriceInsightsIconViewBrowserTest : public UiBrowserTest {
? views::AsViewClass<PriceInsightsIconView>(matched_view)
: nullptr;
}

private:
base::test::ScopedFeatureList test_features_{commerce::kPriceInsights};

BrowserView* GetBrowserView() {
return BrowserView::GetBrowserViewForBrowser(browser());
}

LocationBarView* GetLocationBarView() {
return GetBrowserView()->toolbar()->location_bar();
}
};

IN_PROC_BROWSER_TEST_F(PriceInsightsIconViewBrowserTest,
InvokeUi_show_price_insights_icon) {
ShowAndVerifyUi();
}

class PriceInsightsIconViewWithLabelBrowserTest
: public PriceInsightsIconViewBrowserTest {
public:
PriceInsightsIconViewWithLabelBrowserTest() {
test_features_.InitAndEnableFeatures(
{commerce::kPriceInsights,
feature_engagement::kIPHPriceInsightsPageActionIconLabelFeature},
{});
}
// UiBrowserTest:
void PreShow() override {
PriceInsightsIconViewBrowserTest::PreShow();

std::string test_name =
testing::UnitTest::GetInstance()->current_test_info()->name();
if (test_name == "InvokeUi_show_price_insights_icon_with_low_price_label") {
price_insights_info_ = commerce::CreateValidPriceInsightsInfo(
true, true, commerce::PriceBucket::kLowPrice);
} else if (test_name ==
"InvokeUi_show_price_insights_icon_with_high_price_label") {
price_insights_info_ = commerce::CreateValidPriceInsightsInfo(
true, true, commerce::PriceBucket::kHighPrice);
} else {
price_insights_info_ = commerce::CreateValidPriceInsightsInfo(
true, true, commerce::PriceBucket::kTypicalPrice);
}
}

bool VerifyUi() override {
auto* price_insights_chip = GetChip();
if (!price_insights_chip) {
return false;
}

std::string test_name =
testing::UnitTest::GetInstance()->current_test_info()->name();
if (test_name == "InvokeUi_show_price_insights_icon_with_low_price_label") {
EXPECT_TRUE(price_insights_chip->ShouldShowLabel());
EXPECT_EQ(
base::ToLowerASCII(price_insights_chip->GetIconLabelForTesting()),
u"price is low");

// TODO(meiliang): Add pixel test.
} else if (test_name ==
"InvokeUi_show_price_insights_icon_with_high_price_label") {
EXPECT_TRUE(price_insights_chip->ShouldShowLabel());
EXPECT_EQ(
base::ToLowerASCII(price_insights_chip->GetIconLabelForTesting()),
u"price is high");

// TODO(meiliang): Add pixel test.
}
return true;
}

void SetupFeatureEngagementTracker() {
BrowserFeaturePromoController* const promo_controller =
BrowserView::GetBrowserViewForBrowser(browser())
->GetFeaturePromoController();
EXPECT_TRUE(
user_education::test::WaitForFeatureEngagementReady(promo_controller));
}

private:
feature_engagement::test::ScopedIphFeatureList test_features_;
};

IN_PROC_BROWSER_TEST_F(PriceInsightsIconViewWithLabelBrowserTest,
InvokeUi_show_price_insights_icon_with_low_price_label) {
SetupFeatureEngagementTracker();

ShowAndVerifyUi();
}

IN_PROC_BROWSER_TEST_F(
PriceInsightsIconViewWithLabelBrowserTest,
InvokeUi_show_price_insights_icon_with_high_price_label) {
SetupFeatureEngagementTracker();

ShowAndVerifyUi();
}

0 comments on commit 80ebf4f

Please sign in to comment.