chromium/ios/chrome/browser/crash_report/model/breadcrumbs/breadcrumb_manager_tab_helper_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/crash_report/model/breadcrumbs/breadcrumb_manager_tab_helper.h"

#import <string>

#import "base/containers/circular_deque.h"
#import "base/containers/contains.h"
#import "base/strings/string_split.h"
#import "base/strings/stringprintf.h"
#import "base/test/task_environment.h"
#import "components/breadcrumbs/core/breadcrumb_manager.h"
#import "components/infobars/core/infobar_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_delegate.h"
#import "ios/chrome/browser/infobars/model/test/fake_infobar_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/web/public/security/ssl_status.h"
#import "ios/web/public/test/error_test_util.h"
#import "ios/web/public/test/fakes/fake_navigation_context.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/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.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"

using infobars::InfoBarDelegate;

namespace {

const base::circular_deque<std::string>& GetEvents() {
  return breadcrumbs::BreadcrumbManager::GetInstance().GetEvents();
}

bool EventsEmpty() {
  return GetEvents().empty();
}

}  // namespace

// Test fixture for BreadcrumbManagerTabHelper class.
class BreadcrumbManagerTabHelperTest : public PlatformTest {
 protected:
  void SetUp() override {
    PlatformTest::SetUp();
    TestChromeBrowserState::Builder test_cbs_builder;
    chrome_browser_state_ = std::move(test_cbs_builder).Build();

    first_web_state_.SetBrowserState(chrome_browser_state_.get());
    second_web_state_.SetBrowserState(chrome_browser_state_.get());

    // Navigation manager is needed for InfobarManager.
    first_web_state_.SetNavigationManager(
        std::make_unique<web::FakeNavigationManager>());
    InfoBarManagerImpl::CreateForWebState(&first_web_state_);
    second_web_state_.SetNavigationManager(
        std::make_unique<web::FakeNavigationManager>());
    InfoBarManagerImpl::CreateForWebState(&second_web_state_);

    CRWWebViewScrollViewProxy* scroll_view_proxy =
        [[CRWWebViewScrollViewProxy alloc] init];
    scroll_view_ = [[UIScrollView alloc] init];
    [scroll_view_proxy setScrollView:scroll_view_];
    id web_view_proxy_mock = OCMProtocolMock(@protocol(CRWWebViewProxy));
    [[[web_view_proxy_mock stub] andReturn:scroll_view_proxy] scrollViewProxy];
    first_web_state_.SetWebViewProxy(web_view_proxy_mock);

    BreadcrumbManagerTabHelper::CreateForWebState(&first_web_state_);
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
  web::FakeWebState first_web_state_;
  web::FakeWebState second_web_state_;
  UIScrollView* scroll_view_ = nil;
};

// Tests that the identifier returned for a WebState is unique.
TEST_F(BreadcrumbManagerTabHelperTest, UniqueIdentifiers) {
  BreadcrumbManagerTabHelper::CreateForWebState(&second_web_state_);

  int first_tab_identifier =
      BreadcrumbManagerTabHelper::FromWebState(&first_web_state_)
          ->GetUniqueId();
  int second_tab_identifier =
      BreadcrumbManagerTabHelper::FromWebState(&second_web_state_)
          ->GetUniqueId();

  EXPECT_GT(first_tab_identifier, 0);
  EXPECT_GT(second_tab_identifier, 0);
  EXPECT_NE(first_tab_identifier, second_tab_identifier);
}

// Tests that BreadcrumbManagerTabHelper events are logged to the associated
// BreadcrumbManagerKeyedService. This test does not attempt to validate that
// every observer method is correctly called as that is done in the
// WebStateObserverTest tests.
TEST_F(BreadcrumbManagerTabHelperTest, EventsLogged) {
  EXPECT_TRUE(EventsEmpty());
  web::FakeNavigationContext context;
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();

  first_web_state_.OnNavigationFinished(&context);
  ASSERT_EQ(2u, events.size());
  EXPECT_TRUE(base::Contains(events.back(),
                             breadcrumbs::kBreadcrumbDidFinishNavigation))
      << events.back();
}

// Tests that BreadcrumbManagerTabHelper events logged from seperate WebStates
// are unique.
TEST_F(BreadcrumbManagerTabHelperTest, UniqueEvents) {
  web::FakeNavigationContext context;
  first_web_state_.OnNavigationStarted(&context);

  BreadcrumbManagerTabHelper::CreateForWebState(&second_web_state_);
  second_web_state_.OnNavigationStarted(&context);

  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_STRNE(events.front().c_str(), events.back().c_str());
  EXPECT_TRUE(base::Contains(events.front(),
                             breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.front();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
}

// Tests metadata for www.google.com navigation.
TEST_F(BreadcrumbManagerTabHelperTest, GoogleNavigationStart) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetUrl(GURL("https://www.google.com"));
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(
      base::Contains(events.front(), breadcrumbs::kBreadcrumbGoogleNavigation))
      << events.front();
}

// Tests metadata for https://play.google.com/ navigation.
TEST_F(BreadcrumbManagerTabHelperTest, GooglePlayNavigationStart) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetUrl(GURL("https://play.google.com/"));
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  // #google is useful to indicate SRP. There is no need to know URLs of other
  // visited google properties.
  EXPECT_FALSE(
      base::Contains(events.front(), breadcrumbs::kBreadcrumbGoogleNavigation))
      << events.front();
}

// Tests metadata for chrome://newtab NTP navigation.
TEST_F(BreadcrumbManagerTabHelperTest, ChromeNewTabNavigationStart) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetUrl(GURL(kChromeUINewTabURL));
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(
      events.front(),
      base::StringPrintf("%s%lld", breadcrumbs::kBreadcrumbDidStartNavigation,
                         context.GetNavigationId())))
      << events.front();
  EXPECT_TRUE(
      base::Contains(events.front(), breadcrumbs::kBreadcrumbNtpNavigation))
      << events.front();
}

// Tests metadata for about://newtab/ NTP navigation.
TEST_F(BreadcrumbManagerTabHelperTest, AboutNewTabNavigationStart) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetUrl(GURL("about://newtab/"));
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(
      events.front(),
      base::StringPrintf("%s%lld", breadcrumbs::kBreadcrumbDidStartNavigation,
                         context.GetNavigationId())))
      << events.front();
  EXPECT_TRUE(
      base::Contains(events.front(), breadcrumbs::kBreadcrumbNtpNavigation))
      << events.front();
}

// Tests unique ID in DidStartNavigation and DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, NavigationUniqueId) {
  ASSERT_TRUE(EventsEmpty());

  // DidStartNavigation
  web::FakeNavigationContext context;
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(
      events.front(),
      base::StringPrintf("%s%lld", breadcrumbs::kBreadcrumbDidStartNavigation,
                         context.GetNavigationId())))
      << events.front();

  // DidFinishNavigation
  first_web_state_.OnNavigationFinished(&context);
  ASSERT_EQ(2u, events.size());
  EXPECT_TRUE(base::Contains(
      events.back(),
      base::StringPrintf("%s%lld", breadcrumbs::kBreadcrumbDidFinishNavigation,
                         context.GetNavigationId())))
      << events.back();
}

// Tests renderer initiated metadata in DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, RendererInitiatedByUser) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetIsRendererInitiated(true);
  context.SetHasUserGesture(true);
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), "#link")) << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
  EXPECT_TRUE(base::Contains(events.back(),
                             breadcrumbs::kBreadcrumbRendererInitiatedByUser))
      << events.back();
  EXPECT_FALSE(base::Contains(
      events.back(), breadcrumbs::kBreadcrumbRendererInitiatedByScript))
      << events.back();
}

// Tests renderer initiated metadata in DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, RendererInitiatedByScript) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetIsRendererInitiated(true);
  context.SetHasUserGesture(false);
  context.SetPageTransition(ui::PAGE_TRANSITION_RELOAD);
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), "#reload")) << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
  EXPECT_FALSE(base::Contains(events.back(),
                              breadcrumbs::kBreadcrumbRendererInitiatedByUser))
      << events.back();
  EXPECT_TRUE(base::Contains(events.back(),
                             breadcrumbs::kBreadcrumbRendererInitiatedByScript))
      << events.back();
}

// Tests browser initiated metadata in DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, BrowserInitiatedByScript) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetIsRendererInitiated(false);
  context.SetPageTransition(ui::PAGE_TRANSITION_TYPED);
  first_web_state_.OnNavigationStarted(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), "#typed")) << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
  EXPECT_FALSE(base::Contains(events.back(),
                              breadcrumbs::kBreadcrumbRendererInitiatedByUser))
      << events.back();
  EXPECT_FALSE(base::Contains(
      events.back(), breadcrumbs::kBreadcrumbRendererInitiatedByScript))
      << events.back();
}

// Tests download navigation.
TEST_F(BreadcrumbManagerTabHelperTest, Download) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  context.SetIsDownload(true);
  first_web_state_.OnNavigationFinished(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(),
                             breadcrumbs::kBreadcrumbDidFinishNavigation))
      << events.back();
  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbDownload))
      << events.back();
}

// Tests PDF load.
TEST_F(BreadcrumbManagerTabHelperTest, PdfLoad) {
  ASSERT_TRUE(EventsEmpty());

  first_web_state_.SetContentsMimeType("application/pdf");
  first_web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbPdfLoad))
      << events.back();
}

// Tests page load success.
TEST_F(BreadcrumbManagerTabHelperTest, PageLoadSuccess) {
  ASSERT_TRUE(EventsEmpty());

  first_web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_FALSE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoadFailure))
      << events.back();
}

// Tests page load failure.
TEST_F(BreadcrumbManagerTabHelperTest, PageLoadFailure) {
  ASSERT_TRUE(EventsEmpty());

  first_web_state_.OnPageLoaded(web::PageLoadCompletionStatus::FAILURE);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoadFailure))
      << events.back();
}

// Tests NTP page load.
TEST_F(BreadcrumbManagerTabHelperTest, NtpPageLoad) {
  ASSERT_TRUE(EventsEmpty());

  first_web_state_.SetCurrentURL(GURL(kChromeUINewTabURL));
  first_web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbNtpNavigation))
      << events.back();
  // NTP navigation can't fail, so there is no success/failure metadata.
  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbNtpNavigation))
      << events.back();
}

// Tests navigation error.
TEST_F(BreadcrumbManagerTabHelperTest, NavigationError) {
  ASSERT_TRUE(EventsEmpty());

  web::FakeNavigationContext context;
  NSError* error = web::testing::CreateTestNetError([NSError
      errorWithDomain:NSURLErrorDomain
                 code:NSURLErrorNotConnectedToInternet
             userInfo:nil]);
  context.SetError(error);
  first_web_state_.OnNavigationFinished(&context);
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());

  EXPECT_TRUE(base::Contains(events.back(),
                             breadcrumbs::kBreadcrumbDidFinishNavigation))
      << events.back();
  EXPECT_TRUE(base::Contains(
      events.back(), net::ErrorToShortString(net::ERR_INTERNET_DISCONNECTED)))
      << events.back();
}

// Tests changes in security states.
TEST_F(BreadcrumbManagerTabHelperTest, DidChangeVisibleSecurityState) {
  auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
  web::FakeNavigationManager* navigation_manager_ptr = navigation_manager.get();
  first_web_state_.SetNavigationManager(std::move(navigation_manager));
  ASSERT_TRUE(EventsEmpty());

  // Empty navigation manager.
  first_web_state_.OnVisibleSecurityStateChanged();
  ASSERT_TRUE(EventsEmpty());

  // Default navigation item.
  auto visible_item = web::NavigationItem::Create();
  navigation_manager_ptr->SetVisibleItem(visible_item.get());
  first_web_state_.OnVisibleSecurityStateChanged();
  ASSERT_TRUE(EventsEmpty());

  // Mixed content.
  web::SSLStatus& status = visible_item->GetSSL();
  status.content_status = web::SSLStatus::DISPLAYED_INSECURE_CONTENT;
  first_web_state_.OnVisibleSecurityStateChanged();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbMixedContent))
      << events.back();
  EXPECT_FALSE(base::Contains(events.back(),
                              breadcrumbs::kBreadcrumbAuthenticationBroken))
      << events.back();

  // Broken authentication.
  status.content_status = web::SSLStatus::NORMAL_CONTENT;
  status.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN;
  first_web_state_.OnVisibleSecurityStateChanged();
  ASSERT_EQ(2u, events.size());
  EXPECT_FALSE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbMixedContent))
      << events.back();
  EXPECT_TRUE(base::Contains(events.back(),
                             breadcrumbs::kBreadcrumbAuthenticationBroken))
      << events.back();
}

// Tests that adding an infobar logs the expected breadcrumb.
TEST_F(BreadcrumbManagerTabHelperTest, AddInfobar) {
  ASSERT_TRUE(EventsEmpty());

  InfoBarDelegate::InfoBarIdentifier identifier =
      InfoBarDelegate::InfoBarIdentifier::SESSION_CRASHED_INFOBAR_DELEGATE_IOS;
  std::unique_ptr<FakeInfobarDelegate> delegate =
      std::make_unique<FakeInfobarDelegate>(identifier);
  std::unique_ptr<FakeInfobarIOS> infobar =
      std::make_unique<FakeInfobarIOS>(std::move(delegate));
  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::move(infobar));

  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_TRUE(base::Contains(
      events.back(),
      base::StringPrintf("%s%d", breadcrumbs::kBreadcrumbInfobarAdded,
                         identifier)))
      << events.back();
}

// Tests that infobar breadcrumbs specify the infobar type.
TEST_F(BreadcrumbManagerTabHelperTest, InfobarTypes) {
  ASSERT_TRUE(EventsEmpty());

  // Add and remove first infobar.
  InfoBarDelegate::InfoBarIdentifier first_identifier =
      InfoBarDelegate::InfoBarIdentifier::SESSION_CRASHED_INFOBAR_DELEGATE_IOS;
  std::unique_ptr<FakeInfobarDelegate> first_delegate =
      std::make_unique<FakeInfobarDelegate>(first_identifier);
  std::unique_ptr<FakeInfobarIOS> first_infobar =
      std::make_unique<FakeInfobarIOS>(std::move(first_delegate));
  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::move(first_infobar));
  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->RemoveAllInfoBars(/*animate=*/false);

  // Add second infobar.
  InfoBarDelegate::InfoBarIdentifier second_identifier =
      InfoBarDelegate::InfoBarIdentifier::SYNC_ERROR_INFOBAR_DELEGATE_IOS;
  std::unique_ptr<FakeInfobarDelegate> second_delegate =
      std::make_unique<FakeInfobarDelegate>(second_identifier);
  std::unique_ptr<FakeInfobarIOS> second_infobar =
      std::make_unique<FakeInfobarIOS>(std::move(second_delegate));
  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::move(second_infobar));

  const auto& events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(events.front(), events.back());
  EXPECT_TRUE(base::Contains(
      events.front(),
      base::StringPrintf("%s%d", breadcrumbs::kBreadcrumbInfobarAdded,
                         first_identifier)))
      << events.back();
  EXPECT_TRUE(base::Contains(
      events.back(),
      base::StringPrintf("%s%d", breadcrumbs::kBreadcrumbInfobarAdded,
                         second_identifier)))
      << events.back();
}

// Tests that removing an infobar without animation logs the expected breadcrumb
// event.
TEST_F(BreadcrumbManagerTabHelperTest, RemoveInfobarNotAnimated) {
  ASSERT_TRUE(EventsEmpty());

  InfoBarDelegate::InfoBarIdentifier identifier =
      InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  std::unique_ptr<FakeInfobarDelegate> delegate =
      std::make_unique<FakeInfobarDelegate>(identifier);
  std::unique_ptr<FakeInfobarIOS> infobar =
      std::make_unique<FakeInfobarIOS>(std::move(delegate));
  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::move(infobar));

  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->RemoveAllInfoBars(/*animate=*/false);

  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_TRUE(base::Contains(
      events.back(),
      base::StringPrintf("%s%d", breadcrumbs::kBreadcrumbInfobarRemoved,
                         identifier)))
      << events.back();
  EXPECT_TRUE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbInfobarNotAnimated))
      << events.back();
}

// Tests that removing an infobar with animation logs the expected breadcrumb
// event.
TEST_F(BreadcrumbManagerTabHelperTest, RemoveInfobarAnimated) {
  ASSERT_TRUE(EventsEmpty());

  InfoBarDelegate::InfoBarIdentifier identifier =
      InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  std::unique_ptr<FakeInfobarDelegate> delegate =
      std::make_unique<FakeInfobarDelegate>(identifier);
  std::unique_ptr<FakeInfobarIOS> infobar =
      std::make_unique<FakeInfobarIOS>(std::move(delegate));
  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::move(infobar));

  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->RemoveAllInfoBars(/*animate=*/true);

  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_TRUE(base::Contains(
      events.back(),
      base::StringPrintf("%s%d", breadcrumbs::kBreadcrumbInfobarRemoved,
                         identifier)))
      << events.back();
  EXPECT_FALSE(
      base::Contains(events.back(), breadcrumbs::kBreadcrumbInfobarNotAnimated))
      << events.back();
}

// Tests that replacing an infobar logs the expected breadcrumb event.
TEST_F(BreadcrumbManagerTabHelperTest, ReplaceInfobar) {
  ASSERT_TRUE(EventsEmpty());

  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::make_unique<FakeInfobarIOS>());

  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::make_unique<FakeInfobarIOS>(),
                   /*replace_existing=*/true);

  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());

  InfoBarDelegate::InfoBarIdentifier identifier =
      InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  EXPECT_TRUE(base::Contains(
      events.back(),
      base::StringPrintf("%s%d", breadcrumbs::kBreadcrumbInfobarReplaced,
                         identifier)))
      << events.back();
}

// Tests that replacing an infobar many times only logs the replaced infobar
// breadcrumb at major increments.
TEST_F(BreadcrumbManagerTabHelperTest, SequentialInfobarReplacements) {
  ASSERT_TRUE(EventsEmpty());

  InfoBarManagerImpl::FromWebState(&first_web_state_)
      ->AddInfoBar(std::make_unique<FakeInfobarIOS>());

  for (int replacements = 0; replacements < 500; replacements++) {
    InfoBarManagerImpl::FromWebState(&first_web_state_)
        ->AddInfoBar(std::make_unique<FakeInfobarIOS>(),
                     /*replace_existing=*/true);
  }

  const auto& events = GetEvents();
  // Replacing the infobar 500 times should only log breadcrumbs on the 1st,
  // 2nd, 5th, 20th, 100th, 200th replacement.
  ASSERT_EQ(7u, events.size());

  // The events should contain the number of times the info has been replaced.
  // Validate the last one, which occurs at the 200th replacement.
  std::string expected_event =
      base::StringPrintf("%s%d %d", breadcrumbs::kBreadcrumbInfobarReplaced,
                         InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR, 200);
  EXPECT_TRUE(base::Contains(events.back(), expected_event)) << events.back();
}

// Tests Zoom event.
TEST_F(BreadcrumbManagerTabHelperTest, Zoom) {
  ASSERT_TRUE(EventsEmpty());

  [scroll_view_.delegate scrollViewDidEndZooming:scroll_view_
                                        withView:nil
                                         atScale:0];

  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbZoom))
      << events.back();
}

// Tests Scroll event.
TEST_F(BreadcrumbManagerTabHelperTest, Scroll) {
  ASSERT_TRUE(EventsEmpty());

  [scroll_view_.delegate scrollViewDidEndDragging:scroll_view_
                                   willDecelerate:YES];

  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_TRUE(base::Contains(events.back(), breadcrumbs::kBreadcrumbScroll))
      << events.back();
}

// Tests batching sequential Scroll events.
TEST_F(BreadcrumbManagerTabHelperTest, MultipleScrolls) {
  ASSERT_TRUE(EventsEmpty());

  for (int scroll = 0; scroll < 500; scroll++) {
    [scroll_view_.delegate scrollViewDidEndDragging:scroll_view_
                                     willDecelerate:YES];
  }

  // Scrolling 500 times should only log breadcrumbs on the 1st, 2nd, 5th, 20th,
  // 100th and 200th time.
  const auto& events = GetEvents();
  ASSERT_EQ(6u, events.size());

  // The events should contain the number of times the info has been replaced.
  // Validate the last one, which occurs at the 200th scroll completion.
  std::string expected =
      base::StringPrintf("%s %d", breadcrumbs::kBreadcrumbScroll, 200);
  EXPECT_TRUE(base::Contains(events.back(), expected)) << events.back();
}