chromium/ios/chrome/browser/ntp/ui_bundled/new_tab_page_coordinator_unittest.mm

// Copyright 2018 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/ntp/ui_bundled/new_tab_page_coordinator.h"

#import "base/memory/raw_ptr.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "components/commerce/core/mock_shopping_service.h"
#import "components/variations/service/variations_service.h"
#import "components/variations/service/variations_service_client.h"
#import "components/variations/synthetic_trial_registry.h"
#import "ios/chrome/browser/commerce/model/shopping_service_factory.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_large_icon_service_factory.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_factory.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/segmentation_platform/model/segmentation_platform_service_factory.h"
#import "ios/chrome/browser/shared/model/application_context/application_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/profile/test/test_profile_manager_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/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/shared/public/commands/omnibox_commands.h"
#import "ios/chrome/browser/shared/public/commands/parcel_tracking_opt_in_commands.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_recent_tab_browser_agent.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_action_item.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.h"
#import "ios/chrome/browser/ntp/ui_bundled/feed_wrapper_view_controller.h"
#import "ios/chrome/browser/ntp/ui_bundled/incognito/incognito_view_controller.h"
#import "ios/chrome/browser/ntp/shared/metrics/home_metrics.h"
#import "ios/chrome/browser/ntp/shared/metrics/new_tab_page_metrics_constants.h"
#import "ios/chrome/browser/ntp/shared/metrics/new_tab_page_metrics_recorder.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_component_factory.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_controller_delegate.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_coordinator+Testing.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_feature.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_header_view_controller.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_mediator.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_metrics_delegate.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_view_controller.h"
#import "ios/chrome/browser/ui/toolbar/public/fakebox_focuser.h"
#import "ios/chrome/browser/url_loading/model/fake_url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_notifier_browser_agent.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/chrome/test/testing_application_context.h"
#import "ios/testing/scoped_block_swizzler.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/test/web_task_environment.h"
#import "services/network/test/test_network_connection_tracker.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"
#import "third_party/ocmock/gtest_support.h"

using variations::SyntheticTrialRegistry;
using variations::UIStringOverrider;
using variations::VariationsService;
using variations::VariationsServiceClient;

namespace {

// TODO(crbug.com/40742801): Remove when fake VariationsServiceClient created.
class TestVariationsServiceClient : public VariationsServiceClient {
 public:
  TestVariationsServiceClient() = default;
  TestVariationsServiceClient(const TestVariationsServiceClient&) = delete;
  TestVariationsServiceClient& operator=(const TestVariationsServiceClient&) =
      delete;
  ~TestVariationsServiceClient() override = default;

  // VariationsServiceClient:
  base::Version GetVersionForSimulation() override { return base::Version(); }
  scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory()
      override {
    return nullptr;
  }
  network_time::NetworkTimeTracker* GetNetworkTimeTracker() override {
    return nullptr;
  }
  bool OverridesRestrictParameter(std::string* parameter) override {
    return false;
  }
  bool IsEnterprise() override { return false; }
  void RemoveGoogleGroupsFromPrefsForDeletedProfiles(
      PrefService* local_state) override {}

 private:
  // VariationsServiceClient:
  version_info::Channel GetChannel() override {
    return version_info::Channel::UNKNOWN;
  }
};

// Creates a VariationsService and sets it as the TestingApplicationContext's
// VariationService for the life of the instance.
class ScopedVariationsService {
 public:
  ScopedVariationsService() {
    EXPECT_EQ(nullptr,
              TestingApplicationContext::GetGlobal()->GetVariationsService());
    synthetic_trial_registry_ = std::make_unique<SyntheticTrialRegistry>();

    variations_service_ = VariationsService::Create(
        std::make_unique<TestVariationsServiceClient>(),
        TestingApplicationContext::GetGlobal()->GetLocalState(),
        /*state_manager=*/nullptr, "dummy-disable-background-switch",
        UIStringOverrider(),
        network::TestNetworkConnectionTracker::CreateGetter(),
        synthetic_trial_registry_.get());
    TestingApplicationContext::GetGlobal()->SetVariationsService(
        variations_service_.get());
  }

  ~ScopedVariationsService() {
    EXPECT_EQ(variations_service_.get(),
              TestingApplicationContext::GetGlobal()->GetVariationsService());
    TestingApplicationContext::GetGlobal()->SetVariationsService(nullptr);
    variations_service_.reset();
  }

  VariationsService* Get() { return variations_service_.get(); }

  std::unique_ptr<VariationsService> variations_service_;
  std::unique_ptr<SyntheticTrialRegistry> synthetic_trial_registry_;
};

}  // namespace

// Test fixture for testing NewTabPageCoordinator class.
class NewTabPageCoordinatorTest : public PlatformTest {
 protected:
  NewTabPageCoordinatorTest()
      : base_view_controller_([[UIViewController alloc] init]) {
    TestChromeBrowserState::Builder test_cbs_builder;
    test_cbs_builder.AddTestingFactory(
        ios::TemplateURLServiceFactory::GetInstance(),
        ios::TemplateURLServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        IOSChromeLargeIconServiceFactory::GetInstance(),
        IOSChromeLargeIconServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        commerce::ShoppingServiceFactory::GetInstance(),
        base::BindRepeating(
            [](web::BrowserState*) -> std::unique_ptr<KeyedService> {
              return std::make_unique<commerce::MockShoppingService>();
            }));
    test_cbs_builder.AddTestingFactory(
        segmentation_platform::SegmentationPlatformServiceFactory::
            GetInstance(),
        segmentation_platform::SegmentationPlatformServiceFactory::
            GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        IOSChromeSafetyCheckManagerFactory::GetInstance(),
        IOSChromeSafetyCheckManagerFactory::GetDefaultFactory());

    browser_state_ =
        profile_manager_.AddProfileWithBuilder(std::move(test_cbs_builder));

    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        GetBrowserState(),
        std::make_unique<FakeAuthenticationServiceDelegate>());
    toolbar_delegate_ =
        OCMProtocolMock(@protocol(NewTabPageControllerDelegate));
    histogram_tester_ = std::make_unique<base::HistogramTester>();

    std::vector<base::test::FeatureRef> enabled;
    enabled.push_back(kEnableDiscoverFeedTopSyncPromo);
    enabled.push_back(kEnableWebChannels);
    std::vector<base::test::FeatureRef> disabled;
    scoped_feature_list_.InitWithFeatures(enabled, disabled);
  }

  ChromeBrowserState* GetBrowserState() { return browser_state_.get(); }

  std::unique_ptr<web::FakeWebState> CreateWebState(const char* url) {
    auto test_web_state = std::make_unique<web::FakeWebState>();
    test_web_state->SetBrowserState(GetBrowserState());
    NewTabPageTabHelper::CreateForWebState(test_web_state.get());
    test_web_state->SetCurrentURL(GURL(url));
    test_web_state->SetNavigationManager(
        std::make_unique<web::FakeNavigationManager>());
    return test_web_state;
  }

  void CreateCoordinator(bool off_the_record) {
    if (off_the_record) {
      ChromeBrowserState* otr_state =
          GetBrowserState()->GetOffTheRecordChromeBrowserState();
      browser_ = std::make_unique<TestBrowser>(otr_state);
    } else {
      browser_ = std::make_unique<TestBrowser>(GetBrowserState());
      StartSurfaceRecentTabBrowserAgent::CreateForBrowser(browser_.get());
      // Create non-NTP WebState
      browser_.get()->GetWebStateList()->InsertWebState(
          CreateWebState("http://chromium.org"),
          WebStateList::InsertionParams::Automatic().Activate());
      favicon::WebFaviconDriver::CreateForWebState(
          browser_.get()->GetWebStateList()->GetActiveWebState(),
          /*favicon_service=*/nullptr);
      StartSurfaceRecentTabBrowserAgent* browser_agent =
          StartSurfaceRecentTabBrowserAgent::FromBrowser(browser_.get());
      browser_agent->SaveMostRecentTab();
    }

    // Mocks the component factory so that the NTP is tested with a fake feed.
    // This allows testing of feed-dependent views, such as the feed top
    // section.
    // TODO(crbug.com/40266435): Replace this with a
    // FakeNewTabPageComponentFactory implementation.
    component_factory_mock_ =
        OCMPartialMock([[NewTabPageComponentFactory alloc] init]);
    fake_feed_view_controller_ = [[UIViewController alloc] init];
    UICollectionView* fakeFeedCollectionView = [[UICollectionView alloc]
               initWithFrame:CGRectZero
        collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
    fakeFeedCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
    [fake_feed_view_controller_.view addSubview:fakeFeedCollectionView];
    FeedWrapperViewController* feedWrapperViewController =
        [[FeedWrapperViewController alloc]
              initWithDelegate:coordinator_
            feedViewController:fake_feed_view_controller_];
    OCMExpect([component_factory_mock_ discoverFeedForBrowser:browser_.get()
                                  viewControllerConfiguration:[OCMArg any]])
        .andReturn(fake_feed_view_controller_);
    OCMStub([component_factory_mock_
                feedWrapperViewControllerWithDelegate:[OCMArg any]
                                   feedViewController:[OCMArg any]])
        .andReturn(feedWrapperViewController);

    coordinator_ =
        [[NewTabPageCoordinator alloc] initWithBrowser:browser_.get()
                                      componentFactory:component_factory_mock_];
    coordinator_.baseViewController = base_view_controller_;
    coordinator_.toolbarDelegate = toolbar_delegate_;

    NTPMetricsRecorder_ = [[NewTabPageMetricsRecorder alloc] init];
    coordinator_.NTPMetricsRecorder = NTPMetricsRecorder_;

    InsertWebState(CreateWebStateWithURL(GURL("chrome://newtab")));
  }

  // Inserts a FakeWebState into the browser's WebStateList.
  void InsertWebState(std::unique_ptr<web::WebState> web_state) {
    browser_->GetWebStateList()->InsertWebState(
        std::move(web_state),
        WebStateList::InsertionParams::Automatic().Activate());
    web_state_ = browser_->GetWebStateList()->GetActiveWebState();
  }

  // Creates a FakeWebState and simulates that it is loaded with a given `url`.
  std::unique_ptr<web::WebState> CreateWebStateWithURL(const GURL& url) {
    std::unique_ptr<web::FakeWebState> web_state =
        std::make_unique<web::FakeWebState>();
    web_state->SetBrowserState(GetBrowserState());
    NewTabPageTabHelper::CreateForWebState(web_state.get());
    web_state->SetVisibleURL(url);
    auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
    navigation_manager->AddItem(url, ui::PAGE_TRANSITION_LINK);
    web_state->SetNavigationManager(std::move(navigation_manager));

    // Force the URL load callbacks.
    web::FakeNavigationContext navigation_context;
    web_state->OnNavigationStarted(&navigation_context);
    web_state->OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
    return std::move(web_state);
  }

  // Simulates loading the NTP in `web_state_`.
  void SetNTPAsCurrentURL() {
    web::FakeNavigationContext navigation_context;
    navigation_context.SetUrl(GURL("chrome://newtab"));
    web::FakeWebState* fake_web_state =
        static_cast<web::FakeWebState*>(web_state_);
    fake_web_state->SetVisibleURL(GURL("chrome://newtab"));
    fake_web_state->OnNavigationStarted(&navigation_context);
    fake_web_state->OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
  }

  void SetupCommandHandlerMocks() {
    help_commands_handler_mock = OCMProtocolMock(@protocol(HelpCommands));
    omnibox_commands_handler_mock = OCMProtocolMock(@protocol(OmniboxCommands));
    snackbar_commands_handler_mock =
        OCMProtocolMock(@protocol(SnackbarCommands));
    fakebox_focuser_handler_mock = OCMProtocolMock(@protocol(FakeboxFocuser));
    parcel_tracking_commands_handler_mock_ =
        OCMProtocolMock(@protocol(ParcelTrackingOptInCommands));
    [browser_.get()->GetCommandDispatcher()
        startDispatchingToTarget:help_commands_handler_mock
                     forProtocol:@protocol(HelpCommands)];
    [browser_.get()->GetCommandDispatcher()
        startDispatchingToTarget:omnibox_commands_handler_mock
                     forProtocol:@protocol(OmniboxCommands)];
    [browser_.get()->GetCommandDispatcher()
        startDispatchingToTarget:snackbar_commands_handler_mock
                     forProtocol:@protocol(SnackbarCommands)];
    [browser_.get()->GetCommandDispatcher()
        startDispatchingToTarget:fakebox_focuser_handler_mock
                     forProtocol:@protocol(FakeboxFocuser)];
    [browser_.get()->GetCommandDispatcher()
        startDispatchingToTarget:parcel_tracking_commands_handler_mock_
                     forProtocol:@protocol(ParcelTrackingOptInCommands)];
  }

  // Dynamically calls a selector on an object.
  void DynamicallyCallSelector(id object, SEL selector, Class klass) {
    NSMethodSignature* signature =
        [klass instanceMethodSignatureForSelector:selector];
    // Note: numberOfArguments is always at least 2 (self and _cmd).
    ASSERT_EQ(int(signature.numberOfArguments), 2);
    NSInvocation* invocation =
        [NSInvocation invocationWithMethodSignature:signature];
    invocation.selector = selector;
    [invocation invokeWithTarget:object];
  }

  // Expects a coordinator method call to call a view controller method.
  void ExpectMethodToProxyToVC(SEL coordinator_selector,
                               SEL view_controller_selector) {
    NewTabPageViewController* original_vc = coordinator_.NTPViewController;
    id view_controller_mock = OCMClassMock([NewTabPageViewController class]);
    coordinator_.NTPViewController =
        (NewTabPageViewController*)view_controller_mock;

    // Expect the call on the view controller.
    DynamicallyCallSelector([view_controller_mock expect],
                            view_controller_selector,
                            [NewTabPageViewController class]);

    // Call the method on the coordinator.
    DynamicallyCallSelector(coordinator_, coordinator_selector,
                            [coordinator_ class]);

    [view_controller_mock verify];
    coordinator_.NTPViewController = original_vc;
  }

  // Signs in a fake identity.
  void SignIn() {
    FakeSystemIdentity* fake_identity = [FakeSystemIdentity fakeIdentity1];
    FakeSystemIdentityManager* system_identity_manager =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    system_identity_manager->AddIdentity(fake_identity);
    AuthenticationServiceFactory::GetForBrowserState(GetBrowserState())
        ->SignIn(fake_identity,
                 signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);
  }

  web::WebTaskEnvironment task_environment_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  TestProfileManagerIOS profile_manager_;
  raw_ptr<ChromeBrowserState> browser_state_;
  raw_ptr<web::WebState> web_state_;
  id toolbar_delegate_;
  id delegate_;
  std::unique_ptr<Browser> browser_;
  UIViewController* fake_feed_view_controller_;
  NewTabPageCoordinator* coordinator_;
  NewTabPageMetricsRecorder* NTPMetricsRecorder_;
  id component_factory_mock_;
  UIViewController* base_view_controller_;
  id help_commands_handler_mock;
  id omnibox_commands_handler_mock;
  id snackbar_commands_handler_mock;
  id fakebox_focuser_handler_mock;
  id parcel_tracking_commands_handler_mock_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that the coordinator doesn't vend an IncognitoViewController VC on the
// record.
TEST_F(NewTabPageCoordinatorTest, StartOnTheRecord) {
  CreateCoordinator(/*off_the_record=*/false);
  SetupCommandHandlerMocks();
  [coordinator_ start];
  UIViewController* viewController = [coordinator_ viewController];
  EXPECT_FALSE([viewController isKindOfClass:[IncognitoViewController class]]);
  [coordinator_ stop];
}

// Tests that the coordinator vends an incognito VC off the record.
TEST_F(NewTabPageCoordinatorTest, StartOffTheRecord) {
  CreateCoordinator(/*off_the_record=*/true);
  [coordinator_ start];
  UIViewController* viewController = [coordinator_ viewController];
  EXPECT_TRUE([viewController isKindOfClass:[IncognitoViewController class]]);
  [coordinator_ stop];
}

// Tests that if the NTPCoordinator properly configures
// NewTabPageHeaderViewController and NewTabPageTabHelper correctly for
// Start depending on public lifecycle API calls.
TEST_F(NewTabPageCoordinatorTest, StartIsStartShowing) {
  CreateCoordinator(/*off_the_record=*/false);
  SetupCommandHandlerMocks();
  void (^swizzle_block)() = ^void() {
    // no-op
  };
  // Swizzle out `-configureNTPViewController` since UI code does not need to be
  // spun up for this test.
  std::unique_ptr<ScopedBlockSwizzler> configureNTPVCSwizzler =
      std::make_unique<ScopedBlockSwizzler>(
          [NewTabPageCoordinator class], @selector(configureNTPViewController),
          swizzle_block);
  // Swizzle out `-restoreNTPState` to prevent NTP VC's view from being loaded.
  std::unique_ptr<ScopedBlockSwizzler> restoreNTPStateSwizzler =
      std::make_unique<ScopedBlockSwizzler>([NewTabPageCoordinator class],
                                            @selector(restoreNTPState),
                                            swizzle_block);
  // Swizzle out the mediator's setUp method to prevent more VC loading.
  std::unique_ptr<ScopedBlockSwizzler> mediator_swizzler =
      std::make_unique<ScopedBlockSwizzler>([NewTabPageMediator class],
                                            @selector(setUp), swizzle_block);

  NewTabPageTabHelper::FromWebState(web_state_)->SetShowStartSurface(true);
  [coordinator_ start];
  [coordinator_ didNavigateToNTPInWebState:web_state_];
  // Test `-didNavigateAwayFromNTPWithinWebState` when currently showing Start
  // resets the configuration.
  [coordinator_ didNavigateAwayFromNTP];
  EXPECT_FALSE(
      NewTabPageTabHelper::FromWebState(web_state_)->ShouldShowStartSurface());
  [coordinator_ stop];

  // Test the active WebState updates NTP Start state to false if it
  // began as true.
  NewTabPageTabHelper::FromWebState(web_state_)->SetShowStartSurface(true);
  [coordinator_ start];
  [coordinator_ didNavigateToNTPInWebState:web_state_];
  // Save reference before `web_state_` is set to new active WebState.
  web::WebState* start_web_state = web_state_;
  // Simulate the active WebState change callback.
  InsertWebState(CreateWebStateWithURL(GURL("chrome://version")));
  [coordinator_ didNavigateAwayFromNTP];
  // Moved away from Start surface to a different WebState, Start config for
  // original WebState's TabHelper should be NO.
  EXPECT_FALSE(NewTabPageTabHelper::FromWebState(start_web_state)
                   ->ShouldShowStartSurface());
  [coordinator_ stop];
}

// Test that in response to tapping on Shortcuts while on the Start Surface, the
// NTPTabHelper, NTPCoordinator, and ContentSuggestionsMediator perform as
// expected, leading to logging the correct NTP metric and resets NTPTabHelper's
// ShouldShowStartSurface() property.
TEST_F(NewTabPageCoordinatorTest, ShortcutsStartMetricLogging) {
  CreateCoordinator(/*off_the_record=*/false);
  UrlLoadingNotifierBrowserAgent::CreateForBrowser(browser_.get());
  FakeUrlLoadingBrowserAgent::CreateForBrowser(browser_.get());
  SetupCommandHandlerMocks();
  NewTabPageTabHelper::FromWebState(web_state_)->SetShowStartSurface(true);
  [coordinator_ start];
  [coordinator_ didNavigateToNTPInWebState:web_state_];

  histogram_tester_->ExpectUniqueSample("IOS.Start.Click",
                                        IOSHomeActionType::kShortcuts, 0);
  histogram_tester_->ExpectUniqueSample(
      "IOS.MagicStack.Module.Click.OnStart",
      ContentSuggestionsModuleType::kShortcuts, 0);
  histogram_tester_->ExpectTotalCount(kStartTimeSpentHistogram, 0);
  histogram_tester_->ExpectTotalCount(kStartImpressionHistogram, 1);

  ContentSuggestionsMostVisitedActionItem* item =
      [[ContentSuggestionsMostVisitedActionItem alloc] init];
  item.title = @"Bookmarks 0";
  [coordinator_ shortcutTileOpened];
  // Force the URL load callback to simulate the NavigationManager receiving the
  // URL load signal from the URLLoadingBrowserAgent.
  web::FakeNavigationContext navigation_context;
  navigation_context.SetUrl(GURL("chrome://version"));
  static_cast<web::FakeWebState*>(web_state_)
      ->OnNavigationStarted(&navigation_context);
  // Simulate BrowserCoordinator receiving NTPTabHelper's
  // newTabPageHelperDidChangeVisibility: callback.
  [coordinator_ didNavigateAwayFromNTP];

  // Verify that ActionOnStartSurface metric was logged, meaning that
  // NewTabPageMetricsRecorder logged the metric before NewTabPageTabHelper
  // received the DidStartNavigation() WebStateObserver callback to reset
  // ShouldShowStartSurface() to false.
  histogram_tester_->ExpectUniqueSample("IOS.Start.Click",
                                        IOSHomeActionType::kShortcuts, 1);
  histogram_tester_->ExpectUniqueSample(
      "IOS.MagicStack.Module.Click.OnStart",
      ContentSuggestionsModuleType::kShortcuts, 1);
  histogram_tester_->ExpectTotalCount(kStartTimeSpentHistogram, 1);
  histogram_tester_->ExpectTotalCount(kStartImpressionHistogram, 1);
  EXPECT_FALSE(
      NewTabPageTabHelper::FromWebState(web_state_)->ShouldShowStartSurface());
  [coordinator_ stop];
}

TEST_F(NewTabPageCoordinatorTest, DidNavigateWithinWebState) {
  // Test normal and incognito modes.
  for (bool off_the_record : {false, true}) {
    CreateCoordinator(off_the_record);
    SetupCommandHandlerMocks();

    // Starting the NTP coordinator should not make it visible.
    [coordinator_ start];
    EXPECT_TRUE(coordinator_.started);
    EXPECT_FALSE(coordinator_.visible);

    // Navigate to NTP within the web state and check that NTP is visible.
    [coordinator_ didNavigateToNTPInWebState:web_state_];
    EXPECT_TRUE(coordinator_.started);
    EXPECT_TRUE(coordinator_.visible);

    // Simulate navigating away from the NTP.
    web::FakeNavigationContext navigation_context;
    navigation_context.SetUrl(GURL("chrome://version"));
    static_cast<web::FakeWebState*>(web_state_)
        ->OnNavigationStarted(&navigation_context);
    [coordinator_ didNavigateAwayFromNTP];

    // Remove one of the tabs so that NTPCoordinator will actually stop.
    browser_->GetWebStateList()->CloseWebStateAt(
        /*index=*/0, /* close_flags= */ 0);
    [coordinator_ stopIfNeeded];
    EXPECT_FALSE(coordinator_.started);
    EXPECT_FALSE(coordinator_.visible);
  }
}

TEST_F(NewTabPageCoordinatorTest, DidNavigateBetweenWebStates) {
  // Test normal and incognito modes.
  for (bool off_the_record : {false, true}) {
    CreateCoordinator(off_the_record);
    SetupCommandHandlerMocks();

    // Starting the NTP coordinator should not make it visible.
    [coordinator_ start];
    EXPECT_TRUE(coordinator_.started);
    EXPECT_FALSE(coordinator_.visible);
    if (!off_the_record) {
      histogram_tester_->ExpectTotalCount(kNTPImpressionHistogram, 0);
    }

    // Open an NTP in a new web state.
    InsertWebState(CreateWebStateWithURL(GURL("chrome://newtab")));
    [coordinator_ didNavigateToNTPInWebState:web_state_];
    if (!off_the_record) {
      histogram_tester_->ExpectTotalCount(kNTPTimeSpentHistogram, 0);
      histogram_tester_->ExpectTotalCount(kNTPImpressionHistogram, 1);
    }
    EXPECT_TRUE(coordinator_.started);
    EXPECT_TRUE(coordinator_.visible);

    // Insert a non-NTP WebState.
    InsertWebState(CreateWebStateWithURL(GURL("chrome://version")));
    [coordinator_ didNavigateAwayFromNTP];
    if (!off_the_record) {
      histogram_tester_->ExpectTotalCount(kNTPTimeSpentHistogram, 1);
      histogram_tester_->ExpectTotalCount(kNTPImpressionHistogram, 1);
    }
    EXPECT_TRUE(coordinator_.started);
    EXPECT_FALSE(coordinator_.visible);

    // Close non-NTP web state to get back to NTP web state.
    browser_->GetWebStateList()->CloseWebStateAt(
        /*index=*/1, /* close_flags= */ 0);
    [coordinator_ didNavigateToNTPInWebState:web_state_];
    if (!off_the_record) {
      histogram_tester_->ExpectTotalCount(kNTPTimeSpentHistogram, 1);
      histogram_tester_->ExpectTotalCount(kNTPImpressionHistogram, 2);
    }
    EXPECT_TRUE(coordinator_.started);
    EXPECT_TRUE(coordinator_.visible);

    // Close all web states.
    [coordinator_ didNavigateAwayFromNTP];
    CloseAllWebStates(*browser_->GetWebStateList(),
                      WebStateList::CLOSE_NO_FLAGS);
    [coordinator_ stopIfNeeded];
    if (!off_the_record) {
      histogram_tester_->ExpectTotalCount(kNTPTimeSpentHistogram, 2);
      histogram_tester_->ExpectTotalCount(kNTPImpressionHistogram, 2);
    }
    EXPECT_FALSE(coordinator_.visible);
    EXPECT_FALSE(coordinator_.started);
  }
}

// Tests that various NTPCoordinator methods correctly proxy method calls to
// the NTPViewController.
TEST_F(NewTabPageCoordinatorTest, ProxiesNTPViewControllerMethods) {
  CreateCoordinator(/*off_the_record=*/false);
  SetupCommandHandlerMocks();
  [coordinator_ start];
  [coordinator_ didNavigateToNTPInWebState:web_state_];

  ExpectMethodToProxyToVC(@selector(isScrolledToTop),
                          @selector(isNTPScrolledToTop));
  ExpectMethodToProxyToVC(@selector(willUpdateSnapshot),
                          @selector(willUpdateSnapshot));
  ExpectMethodToProxyToVC(@selector(focusFakebox), @selector(focusOmnibox));
  ExpectMethodToProxyToVC(@selector(locationBarDidResignFirstResponder),
                          @selector(omniboxDidResignFirstResponder));

  [coordinator_ stop];
}

// Tests the state of the NTP coordinator after starting and stopping it. This
// mainly ensures that all strongly references properties are created and
// released, but also checks that the NTP state is correct for each scnenario.
TEST_F(NewTabPageCoordinatorTest, IsNTPCleanOnStop) {
  CreateCoordinator(/*off_the_record=*/false);
  SetupCommandHandlerMocks();

  [coordinator_ start];
  EXPECT_NE(nil, coordinator_.NTPViewController);
  EXPECT_NE(nil, coordinator_.contentSuggestionsCoordinator.viewController);
  EXPECT_NE(nil, coordinator_.contentSuggestionsCoordinator);
  EXPECT_NE(nil, coordinator_.headerViewController);
  EXPECT_NE(nil, coordinator_.NTPMediator);
  EXPECT_NE(nil, coordinator_.feedWrapperViewController);
  EXPECT_NE(nil, coordinator_.feedTopSectionCoordinator);
  EXPECT_NE(nil, coordinator_.feedHeaderViewController);
  EXPECT_TRUE(coordinator_.started);

  [coordinator_ stop];
  EXPECT_EQ(nil, coordinator_.NTPViewController);
  EXPECT_EQ(nil, coordinator_.contentSuggestionsCoordinator.viewController);
  EXPECT_EQ(nil, coordinator_.contentSuggestionsCoordinator);
  EXPECT_EQ(nil, coordinator_.headerViewController);
  EXPECT_EQ(nil, coordinator_.NTPMediator);
  EXPECT_EQ(nil, coordinator_.feedWrapperViewController);
  EXPECT_EQ(nil, coordinator_.feedTopSectionCoordinator);
  EXPECT_EQ(nil, coordinator_.feedHeaderViewController);
  EXPECT_FALSE(coordinator_.started);
}

// Tests that the state of an NTP is saved and restored when leaving an NTP and
// navigating back to it in the same web state.
TEST_F(NewTabPageCoordinatorTest, TestSaveNTPState) {
  CreateCoordinator(/*off_the_record=*/false);
  SetupCommandHandlerMocks();
  [coordinator_ start];
  [coordinator_ didNavigateToNTPInWebState:web_state_];

  // Check that initial NTP is scrolled to top.
  CGFloat scrollPosition = coordinator_.NTPViewController.scrollPosition;
  EXPECT_NEAR(scrollPosition, -[coordinator_.NTPViewController heightAboveFeed],
              1);

  // Change the selected feed and set some scroll position.
  [coordinator_ selectFeedType:FeedTypeFollowing];
  [coordinator_.NTPViewController
      setContentOffsetToTopOfFeedOrLess:scrollPosition + 100];

  FeedType selectedFeed = coordinator_.selectedFeed;
  scrollPosition = coordinator_.NTPViewController.scrollPosition;

  // Navigate away from the NTP and stop the coordinator.
  [coordinator_ didNavigateAwayFromNTP];
  [coordinator_ stop];

  // Navigate to another NTP in the same web state.
  [coordinator_ start];
  [coordinator_ didNavigateToNTPInWebState:web_state_];

  // Check that newly opened NTP restores saved state.
  EXPECT_EQ(coordinator_.selectedFeed, selectedFeed);
  EXPECT_NEAR(coordinator_.NTPViewController.scrollPosition, scrollPosition, 1);

  [coordinator_ stop];
}

// Tests that following feed and discover feed can be selected.
TEST_F(NewTabPageCoordinatorTest, SelectFeedType) {
  // Following feed is only available in the US, so we need to override the
  // VariationsService's stored permenant country to test.
  ScopedVariationsService scoped_variations_service;
  scoped_variations_service.Get()->OverrideStoredPermanentCountry("us");

  CreateCoordinator(/*off_the_record=*/false);
  SetupCommandHandlerMocks();
  [coordinator_ start];
  // Simulate the view appearing.
  [coordinator_.NTPViewController beginAppearanceTransition:YES animated:NO];
  [coordinator_.NTPViewController endAppearanceTransition];
  SignIn();
  // Scroll down slightly.
  CGFloat scrollPosition =
      round(coordinator_.NTPViewController.scrollPosition + 100);
  [coordinator_.NTPViewController
      setContentOffsetToTopOfFeedOrLess:scrollPosition];

  // Expect the Following feed to be loaded, and scroll position to be
  // maintained.
  OCMExpect([component_factory_mock_
                    followingFeedForBrowser:browser_.get()
                viewControllerConfiguration:[OCMArg any]
                                   sortType:FollowingFeedSortTypeByLatest])
      .andReturn(fake_feed_view_controller_);
  [coordinator_ selectFeedType:FeedTypeFollowing];
  EXPECT_OCMOCK_VERIFY(component_factory_mock_);
  EXPECT_EQ(coordinator_.selectedFeed, FeedTypeFollowing);
  EXPECT_EQ(coordinator_.NTPViewController.scrollPosition, scrollPosition);

  // Expect the Discover feed to be loaded, and scroll position to be
  // maintained.
  OCMExpect([component_factory_mock_ discoverFeedForBrowser:browser_.get()
                                viewControllerConfiguration:[OCMArg any]])
      .andReturn(fake_feed_view_controller_);
  [coordinator_ selectFeedType:FeedTypeDiscover];
  EXPECT_OCMOCK_VERIFY(component_factory_mock_);
  EXPECT_EQ(coordinator_.selectedFeed, FeedTypeDiscover);
  EXPECT_EQ(coordinator_.NTPViewController.scrollPosition, scrollPosition);

  [coordinator_ stop];
}