chromium/ios/chrome/browser/badges/ui_bundled/badge_mediator_unittest.mm

// Copyright 2019 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/badges/ui_bundled/badge_mediator.h"

#import <map>

#import "base/containers/contains.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/task_environment.h"
#import "ios/chrome/browser/badges/ui_bundled/badge_consumer.h"
#import "ios/chrome/browser/badges/ui_bundled/badge_item.h"
#import "ios/chrome/browser/badges/ui_bundled/badge_type.h"
#import "ios/chrome/browser/badges/ui_bundled/badge_type_util.h"
#import "ios/chrome/browser/infobars/model/badge_state.h"
#import "ios/chrome/browser/infobars/model/infobar_badge_tab_helper.h"
#import "ios/chrome/browser/infobars/model/infobar_badge_tab_helper_delegate.h"
#import "ios/chrome/browser/infobars/model/infobar_ios.h"
#import "ios/chrome/browser/infobars/model/infobar_manager_impl.h"
#import "ios/chrome/browser/infobars/model/test/fake_infobar_ios.h"
#import "ios/chrome/browser/overlays/model/public/common/infobars/infobar_overlay_request_config.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presenter.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_queue.h"
#import "ios/chrome/browser/overlays/model/test/fake_overlay_presentation_context.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/ui/infobars/test_infobar_delegate.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_user_data.h"
#import "testing/gtest/include/gtest/gtest.h"

namespace {
// The two infobar types used in tests.  Both support badges.
InfobarType kFirstInfobarType = InfobarType::kInfobarTypePasswordSave;
std::u16string kFirstInfobarMessageText = u"FakeInfobarDelegate1";
InfobarType kSecondInfobarType = InfobarType::kInfobarTypePasswordUpdate;
std::u16string kSecondInfobarMessageText = u"FakeInfobarDelegate2";
// Parameters used for BadgeMediator test fixtures.
enum class TestParam {
  kNormal,
  kOffTheRecord,
};
}  // namespace

// Fake of BadgeConsumer.
@interface FakeBadgeConsumer : NSObject <BadgeConsumer>
@property(nonatomic, strong) id<BadgeItem> displayedBadge;
@property(nonatomic, assign) BOOL hasFullscreenOffTheRecordBadge;
@property(nonatomic, assign) BOOL hasUnreadBadge;
@end

@implementation FakeBadgeConsumer
- (void)setupWithDisplayedBadge:(id<BadgeItem>)displayedBadgeItem
                fullScreenBadge:(id<BadgeItem>)fullscreenBadgeItem {
  self.hasFullscreenOffTheRecordBadge =
      fullscreenBadgeItem != nil &&
      fullscreenBadgeItem.badgeType == kBadgeTypeIncognito;
  self.displayedBadge = displayedBadgeItem;
}
- (void)updateDisplayedBadge:(id<BadgeItem>)displayedBadgeItem
             fullScreenBadge:(id<BadgeItem>)fullscreenBadgeItem
                     infoBar:(InfoBarIOS*)infoBar {
  self.hasFullscreenOffTheRecordBadge =
      fullscreenBadgeItem != nil &&
      fullscreenBadgeItem.badgeType == kBadgeTypeIncognito;
  self.displayedBadge = displayedBadgeItem;
}
- (void)markDisplayedBadgeAsRead:(BOOL)read {
  self.hasUnreadBadge = !read;
}
@end

class BadgeMediatorTest : public testing::TestWithParam<TestParam> {
 protected:
  BadgeMediatorTest()
      : badge_consumer_([[FakeBadgeConsumer alloc] init]),
        browser_state_(TestChromeBrowserState::Builder().Build()),
        browser_(std::make_unique<TestBrowser>(browser_state())) {
    overlay_presenter_ = OverlayPresenter::FromBrowser(
        browser(), OverlayModality::kInfobarBanner);
    overlay_presenter_->SetPresentationContext(&overlay_presentation_context_);
    badge_mediator_ =
        [[BadgeMediator alloc] initWithWebStateList:web_state_list()
                                   overlayPresenter:overlay_presenter_
                                        isIncognito:is_off_the_record()];
    badge_mediator_.consumer = badge_consumer_;
  }

  ~BadgeMediatorTest() override {
    overlay_presenter_->SetPresentationContext(nullptr);
    [badge_mediator_ disconnect];
  }

  // Appends a new WebState to the WebStateList and activates it.
  void AppendActivatedWebState() {
    auto web_state = std::make_unique<web::FakeWebState>();
    web_state->SetNavigationManager(
        std::make_unique<web::FakeNavigationManager>());
    web_state->SetBrowserState(browser_state());
    InfoBarManagerImpl::CreateForWebState(web_state.get());
    InfobarBadgeTabHelper::GetOrCreateForWebState(web_state.get());
    web_state_list()->InsertWebState(
        std::move(web_state),
        WebStateList::InsertionParams::Automatic().Activate());
  }

  // Adds an Infobar of `type` to the InfoBarManager and returns the infobar.
  // Pass in different `message_text` to avoid replacing existing infobar.
  InfoBarIOS* AddInfobar(InfobarType type, std::u16string message_text) {
    std::unique_ptr<InfoBarIOS> added_infobar =
        std::make_unique<FakeInfobarIOS>(type, message_text);
    InfoBarIOS* infobar = added_infobar.get();
    infobar_manager()->AddInfoBar(std::move(added_infobar));
    return infobar;
  }

  // Removes `infobar` from its manager.
  void RemoveInfobar(InfoBarIOS* infobar) {
    infobar_manager()->RemoveInfoBar(infobar);
  }

  // Returns whether the test fixture is for an incognito BrowserState.
  bool is_off_the_record() const {
    return GetParam() == TestParam::kOffTheRecord;
  }
  // Returns the BrowserState to use for the test fixture.
  ChromeBrowserState* browser_state() {
    return is_off_the_record()
               ? browser_state_->GetOffTheRecordChromeBrowserState()
               : browser_state_.get();
  }
  // Returns the Browser to use for the test fixture.
  Browser* browser() { return browser_.get(); }
  // Returns the Browser's WebStateList.
  WebStateList* web_state_list() { return browser()->GetWebStateList(); }
  // Returns the active WebState.
  web::WebState* web_state() { return web_state_list()->GetActiveWebState(); }
  // Returns the active WebState's InfoBarManagerImpl.
  InfoBarManagerImpl* infobar_manager() {
    return InfoBarManagerImpl::FromWebState(web_state());
  }
  // Returns the active WebState's InfobarBadgeTabHelper.
  InfobarBadgeTabHelper* tab_helper() {
    return InfobarBadgeTabHelper::GetOrCreateForWebState(web_state());
  }

  base::test::TaskEnvironment environment_;
  FakeBadgeConsumer* badge_consumer_;
  std::unique_ptr<ChromeBrowserState> browser_state_;
  std::unique_ptr<Browser> browser_;
  FakeOverlayPresentationContext overlay_presentation_context_;
  BadgeMediator* badge_mediator_ = nil;
  raw_ptr<OverlayPresenter> overlay_presenter_ = nullptr;
};

// Test that the BadgeMediator responds with no displayed and fullscreen badge
// when there are no Infobars added and the BrowserState is not OffTheRecord.
TEST_P(BadgeMediatorTest, BadgeMediatorTestNoInfobar) {
  AppendActivatedWebState();
  EXPECT_FALSE(badge_consumer_.displayedBadge);
  EXPECT_EQ(is_off_the_record(),
            badge_consumer_.hasFullscreenOffTheRecordBadge);
}

// Test that the BadgeMediator responds with one new badge when an infobar is
// added
TEST_P(BadgeMediatorTest, BadgeMediatorTestAddInfobar) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypePasswordSave);
}

// Test that the BadgeMediator handled the removal of the correct badge when two
// infobars are added and then one is removed.
TEST_P(BadgeMediatorTest, BadgeMediatorTestRemoveInfobar) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  InfoBarIOS* second_infobar =
      AddInfobar(kSecondInfobarType, kSecondInfobarMessageText);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypeOverflow);
  RemoveInfobar(second_infobar);
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypePasswordSave);
}

TEST_P(BadgeMediatorTest, BadgeMediatorTestMarkAsRead) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  // Since there is only one badge, it should be marked as read.
  EXPECT_FALSE(badge_consumer_.hasUnreadBadge);
  AddInfobar(kSecondInfobarType, kSecondInfobarMessageText);
  ASSERT_EQ(kBadgeTypeOverflow, badge_consumer_.displayedBadge.badgeType);
  // Second badge should be unread since the overflow badge is being shown as
  // the displayed badge.
  EXPECT_TRUE(badge_consumer_.hasUnreadBadge);
  tab_helper()->UpdateBadgeForInfobarAccepted(kSecondInfobarType);
  // Second badge should be read since its infobar is accepted.
  EXPECT_FALSE(badge_consumer_.hasUnreadBadge);
}

// Test that the BadgeMediator updates the current badges to none when switching
// to a second WebState after an infobar is added to the first WebState.
TEST_P(BadgeMediatorTest, BadgeMediatorTestSwitchWebState) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypePasswordSave);
  AppendActivatedWebState();
  EXPECT_FALSE(badge_consumer_.displayedBadge);
}

// Test that the BadgeMediator does not inform its consumer of a new infobar if
// the added infobar came from an inactive WebState.
TEST_P(BadgeMediatorTest,
       BadgeMediatorTestSwitchWebStateAndAddInfobarToInactiveWebState) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypePasswordSave);
  AppendActivatedWebState();
  std::unique_ptr<InfoBarIOS> added_infobar = std::make_unique<FakeInfobarIOS>(
      kSecondInfobarType, kSecondInfobarMessageText);
  InfoBarManagerImpl::FromWebState(web_state_list()->GetWebStateAt(0))
      ->AddInfoBar(std::move(added_infobar));
  EXPECT_FALSE(badge_consumer_.displayedBadge);
}

// Test that the BadgeMediator does not inform its consumer of a new infobar it
// has already been disconnected.
TEST_P(BadgeMediatorTest, BadgeMediatorTestDoNotAddInfobarIfWebStateListGone) {
  AppendActivatedWebState();
  ASSERT_FALSE(badge_consumer_.displayedBadge);
  [badge_mediator_ disconnect];
  std::unique_ptr<InfoBarIOS> added_infobar = std::make_unique<FakeInfobarIOS>(
      kSecondInfobarType, kSecondInfobarMessageText);
  InfoBarManagerImpl::FromWebState(web_state_list()->GetActiveWebState())
      ->AddInfoBar(std::move(added_infobar));
  EXPECT_FALSE(badge_consumer_.displayedBadge);
}

// Test that the BadgeMediator updates the badge when it is accepted.
TEST_P(BadgeMediatorTest, BadgeMediatorTestAcceptedBadge) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_FALSE(badge_consumer_.displayedBadge.badgeState &= BadgeStateAccepted);

  tab_helper()->UpdateBadgeForInfobarAccepted(kFirstInfobarType);
  EXPECT_TRUE(badge_consumer_.displayedBadge.badgeState &= BadgeStateAccepted);
}

// Test that the BadgeMediator updates the current badges when the starting
// active WebState already has a badge. This simulates an app launch after an
// update when the WebStateList is preserved but the LocationBar (and therefore
// the BadgeMediator) is restarted from scratch.
TEST_P(BadgeMediatorTest, BadgeMediatorTestRestartWithInfobar) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);

  // Simulate reload of app, but preservation of WebStateList.
  [badge_mediator_ disconnect];
  badge_mediator_ = nil;
  badge_consumer_ = nil;

  badge_consumer_ = [[FakeBadgeConsumer alloc] init];
  badge_mediator_ =
      [[BadgeMediator alloc] initWithWebStateList:web_state_list()
                                 overlayPresenter:overlay_presenter_
                                      isIncognito:is_off_the_record()];
  badge_mediator_.consumer = badge_consumer_;
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypePasswordSave);
}

// Test that the BadgeMediator clears its badges when the last WebState is
// detached and a new WebState is added. This test also makes sure that closing
// the last WebState doesn't break anything.
TEST_P(BadgeMediatorTest, BadgeMediatorTestCloseLastTab) {
  AppendActivatedWebState();
  AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);
  ASSERT_TRUE(badge_consumer_.displayedBadge);
  EXPECT_EQ(badge_consumer_.displayedBadge.badgeType, kBadgeTypePasswordSave);
  web_state_list()->DetachWebStateAt(0);
  AppendActivatedWebState();
  ASSERT_FALSE(badge_consumer_.displayedBadge);
}

// Tests that the badge mediator successfully updates the InfobarBadgeTabHelper
// for the active WebState for infobar banner presentation and dismissal.
TEST_P(BadgeMediatorTest, InfobarBannerOverlayObserving) {
  // Add an active WebState at index 0 and add an InfoBar with `type` to the
  // WebState's InfoBarManager, checking that the badge item has been created
  // with the default BadgeState.
  AppendActivatedWebState();
  InfobarType type = kFirstInfobarType;
  InfobarBadgeTabHelper* tab_helper =
      InfobarBadgeTabHelper::GetOrCreateForWebState(web_state());
  InfoBarIOS* infobar = AddInfobar(kFirstInfobarType, kFirstInfobarMessageText);

  std::map<InfobarType, BadgeState> badge_states =
      tab_helper->GetInfobarBadgeStates();
  ASSERT_EQ(1U, badge_states.size());
  ASSERT_TRUE(base::Contains(badge_states, type));
  BadgeState state = badge_states[type];
  ASSERT_FALSE(state & BadgeStatePresented);

  // Simulate the presentation of the infobar banner via OverlayPresenter in the
  // fake presentation context, verifying that the badge state is updated
  // accordingly.
  OverlayRequestQueue* queue = OverlayRequestQueue::FromWebState(
      web_state(), OverlayModality::kInfobarBanner);
  queue->AddRequest(
      OverlayRequest::CreateWithConfig<InfobarOverlayRequestConfig>(
          infobar, InfobarOverlayType::kBanner, infobar->high_priority()));
  badge_states = tab_helper->GetInfobarBadgeStates();
  EXPECT_TRUE(badge_states[type] & BadgeStatePresented);

  // Simulate dismissal of the banner and verify that the badge state is no
  // longer presented.
  queue->CancelAllRequests();
  badge_states = tab_helper->GetInfobarBadgeStates();
  EXPECT_FALSE(badge_states[type] & BadgeStatePresented);
}

INSTANTIATE_TEST_SUITE_P(/* No InstantiationName */,
                         BadgeMediatorTest,
                         testing::Values(TestParam::kNormal,
                                         TestParam::kOffTheRecord));