chromium/ios/chrome/browser/shared/coordinator/scene/scene_controller_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/shared/coordinator/scene/scene_controller.h"

#import "base/test/task_environment.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/identity_manager/identity_test_utils.h"
#import "components/supervised_user/test_support/kids_chrome_management_test_utils.h"
#import "components/variations/scoped_variations_ids_provider.h"
#import "components/variations/variations_ids_provider.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/favicon/model/favicon_service_factory.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_large_icon_service_factory.h"
#import "ios/chrome/browser/history/model/history_service_factory.h"
#import "ios/chrome/browser/intents/intents_constants.h"
#import "ios/chrome/browser/intents/user_activity_browser_agent.h"
#import "ios/chrome/browser/prerender/model/prerender_service_factory.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/sessions/model/test_session_restoration_service.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_controller_testing.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_util_test_support.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.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/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/identity_test_environment_browser_state_adaptor.h"
#import "ios/chrome/browser/sync/model/send_tab_to_self_sync_service_factory.h"
#import "ios/chrome/browser/ui/main/browser_view_wrangler.h"
#import "ios/chrome/browser/ui/main/wrangled_browser.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_data.h"
#import "ios/web/public/test/web_task_environment.h"
#import "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#import "services/network/test/test_url_loader_factory.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"

@interface InternalFakeSceneController : SceneController
// Browser and ChromeBrowserState used to mock the currentInterface.
@property(nonatomic, assign) Browser* browser;
@property(nonatomic, assign) ChromeBrowserState* chromeBrowserState;
// Mocked currentInterface.
@property(nonatomic, assign) WrangledBrowser* currentInterface;
// BrowserViewWrangler to provide test setup for main coordinator and interface.
@property(nonatomic, strong) BrowserViewWrangler* browserViewWrangler;
// Argument for
// -dismissModalsAndMaybeOpenSelectedTabInMode:withUrlLoadParams:dismissOmnibox:
//  completion:.
@property(nonatomic, readonly) ApplicationModeForTabOpening applicationMode;
@end

@implementation InternalFakeSceneController

- (BOOL)isIncognitoForced {
  return NO;
}

- (void)dismissModalsAndMaybeOpenSelectedTabInMode:
            (ApplicationModeForTabOpening)targetMode
                                 withUrlLoadParams:
                                     (const UrlLoadParams&)urlLoadParams
                                    dismissOmnibox:(BOOL)dismissOmnibox
                                        completion:(ProceduralBlock)completion {
  _applicationMode = targetMode;
}

- (BOOL)URLIsOpenedInRegularMode:(const GURL&)URL {
  return NO;
}
@end

namespace {

class SceneControllerTest : public PlatformTest {
 protected:
  SceneControllerTest() {
    base_view_controller_ = [[UIViewController alloc] init];

    fake_scene_ = FakeSceneWithIdentifier([[NSUUID UUID] UUIDString]);
    AppState* appState = CreateMockAppState(InitStageFinal);
    scene_state_ = [[SceneStateWithFakeScene alloc] initWithScene:fake_scene_
                                                         appState:appState];

    scene_controller_ =
        [[InternalFakeSceneController alloc] initWithSceneState:scene_state_];
    scene_state_.controller = scene_controller_;

    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        IdentityManagerFactory::GetInstance(),
        base::BindRepeating(IdentityTestEnvironmentBrowserStateAdaptor::
                                BuildIdentityManagerForTests));
    builder.AddTestingFactory(PrerenderServiceFactory::GetInstance(),
                              PrerenderServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        SendTabToSelfSyncServiceFactory::GetInstance(),
        SendTabToSelfSyncServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        ios::TemplateURLServiceFactory::GetInstance(),
        ios::TemplateURLServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        IOSChromeFaviconLoaderFactory::GetInstance(),
        IOSChromeFaviconLoaderFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        IOSChromeLargeIconServiceFactory::GetInstance(),
        IOSChromeLargeIconServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(ios::FaviconServiceFactory::GetInstance(),
                              ios::FaviconServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(ios::HistoryServiceFactory::GetInstance(),
                              ios::HistoryServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(ios::BookmarkModelFactory::GetInstance(),
                              ios::BookmarkModelFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        SessionRestorationServiceFactory::GetInstance(),
        TestSessionRestorationService::GetTestingFactory());
    browser_state_ = std::move(builder).Build();

    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        browser_state_.get(),
        std::make_unique<FakeAuthenticationServiceDelegate>());

    browser_ =
        std::make_unique<TestBrowser>(browser_state_.get(), scene_state_);
    UserActivityBrowserAgent::CreateForBrowser(browser_.get());

    browser_state_->SetSharedURLLoaderFactory(
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_loader_factory_));

    scene_controller_.browserViewWrangler =
        [[BrowserViewWrangler alloc] initWithBrowserState:browser_state_.get()
                                               sceneState:scene_state_
                                      applicationEndpoint:nil
                                         settingsEndpoint:nil];
    [scene_controller_.browserViewWrangler createMainCoordinatorAndInterface];

    scene_controller_.browser = browser_.get();
    scene_controller_.chromeBrowserState = browser_state_.get();
    scene_controller_.currentInterface = CreateMockCurrentInterface();
    connection_information_ = scene_state_.controller;
  }

  ~SceneControllerTest() override { [scene_controller_ teardownUI]; }

  // Mock & stub an AppState object with an arbitrary `init_stage` property.
  id CreateMockAppState(InitStage init_stage) {
    id mock_app_state = OCMClassMock([AppState class]);
    OCMStub([(AppState*)mock_app_state initStage]).andReturn(init_stage);
    return mock_app_state;
  }

  // Mock & stub a WrangledBrowser object.
  id CreateMockCurrentInterface() {
    id mock_wrangled_browser = OCMClassMock(WrangledBrowser.class);
    OCMStub([mock_wrangled_browser browserState])
        .andReturn(browser_state_.get());
    OCMStub([mock_wrangled_browser browser]).andReturn(browser_.get());
    return mock_wrangled_browser;
  }

  signin::IdentityManager* GetIdentityManager() {
    return IdentityManagerFactory::GetForBrowserState(browser_state_.get());
  }

  void MakePrimaryAccountAvailable(const std::string& email) {
    signin::MakePrimaryAccountAvailable(
        IdentityManagerFactory::GetForBrowserState(browser_state_.get()), email,
        signin::ConsentLevel::kSignin);
  }

  void SetAutomaticIssueOfAccessTokens(bool grant) {
    signin::SetAutomaticIssueOfAccessTokens(GetIdentityManager(), grant);
  }

  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO,
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  variations::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
      variations::VariationsIdsProvider::Mode::kUseSignedInState};

  std::unique_ptr<Browser> browser_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  InternalFakeSceneController* scene_controller_;
  SceneState* scene_state_;
  id fake_scene_;
  id<ConnectionInformation> connection_information_;

  UIViewController* base_view_controller_;
  network::TestURLLoaderFactory test_loader_factory_;
};

// TODO(crbug.com/40693350): Add a test for keeping validity of detecting a
// fresh open in new window coming from ios dock. 'Dock' is considered the
// default when the new window opening request is external to chrome and
// unknown.

// Tests that scene controller updates scene state's incognitoContentVisible
// when the relevant application command is called.
TEST_F(SceneControllerTest, UpdatesIncognitoContentVisibility) {
  [scene_controller_ setIncognitoContentVisible:NO];
  EXPECT_FALSE(scene_state_.incognitoContentVisible);
  [scene_controller_ setIncognitoContentVisible:YES];
  EXPECT_TRUE(scene_state_.incognitoContentVisible);
  [scene_controller_ setIncognitoContentVisible:NO];
  EXPECT_FALSE(scene_state_.incognitoContentVisible);
}

// Tests that scene controller correctly handles an external intent to
// OpenIncognitoSearch.
// TODO(crbug.com/40947630): re-enabled the test.
TEST_F(SceneControllerTest, DISABLED_TestOpenIncognitoSearchForShortcutItem) {
  UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
        initWithType:kShortcutNewIncognitoSearch
      localizedTitle:kShortcutNewIncognitoSearch];
  [scene_controller_ performActionForShortcutItem:shortcut
                                completionHandler:nil];
  EXPECT_TRUE(scene_state_.startupHadExternalIntent);
  EXPECT_EQ(ApplicationModeForTabOpening::INCOGNITO,
            [scene_controller_ applicationMode]);
  EXPECT_EQ(FOCUS_OMNIBOX,
            [connection_information_ startupParameters].postOpeningAction);
}

// Tests that scene controller correctly handles an external intent to
// OpenNewSearch.
TEST_F(SceneControllerTest, TestOpenNewSearchForShortcutItem) {
  UIApplicationShortcutItem* shortcut =
      [[UIApplicationShortcutItem alloc] initWithType:kShortcutNewSearch
                                       localizedTitle:kShortcutNewSearch];
  [scene_controller_ performActionForShortcutItem:shortcut
                                completionHandler:nil];
  EXPECT_TRUE(scene_state_.startupHadExternalIntent);
  EXPECT_EQ(ApplicationModeForTabOpening::NORMAL,
            [scene_controller_ applicationMode]);
  EXPECT_EQ(FOCUS_OMNIBOX,
            [connection_information_ startupParameters].postOpeningAction);
}

// Tests that scene controller correctly handles an external intent to
// OpenVoiceSearch.
TEST_F(SceneControllerTest, TestOpenVoiceSearchForShortcutItem) {
  UIApplicationShortcutItem* shortcut =
      [[UIApplicationShortcutItem alloc] initWithType:kShortcutVoiceSearch
                                       localizedTitle:kShortcutVoiceSearch];
  [scene_controller_ performActionForShortcutItem:shortcut
                                completionHandler:nil];
  EXPECT_TRUE(scene_state_.startupHadExternalIntent);
  EXPECT_EQ(ApplicationModeForTabOpening::NORMAL,
            [scene_controller_ applicationMode]);
  EXPECT_EQ(START_VOICE_SEARCH,
            [connection_information_ startupParameters].postOpeningAction);
}

// Tests that scene controller correctly handles an external intent to
// OpenQRScanner.
TEST_F(SceneControllerTest, TestOpenQRScannerForShortcutItem) {
  UIApplicationShortcutItem* shortcut =
      [[UIApplicationShortcutItem alloc] initWithType:kShortcutQRScanner
                                       localizedTitle:kShortcutQRScanner];
  [scene_controller_ performActionForShortcutItem:shortcut
                                completionHandler:nil];
  EXPECT_TRUE(scene_state_.startupHadExternalIntent);
  EXPECT_EQ(ApplicationModeForTabOpening::NORMAL,
            [scene_controller_ applicationMode]);
  EXPECT_EQ(START_QR_CODE_SCANNER,
            [connection_information_ startupParameters].postOpeningAction);
}

// Tests that "Report an issue" populates user feedback data with available
// information on the family member role for the signed-in user.
TEST_F(SceneControllerTest, TestReportAnIssueViewControllerWithFamilyResponse) {
  MakePrimaryAccountAvailable("[email protected]");
  SetAutomaticIssueOfAccessTokens(/*grant=*/true);

  base::RunLoop run_loop;
  UserFeedbackDataCallback completion =
      base::BindRepeating(^(UserFeedbackData* data) {
        EXPECT_EQ(UserFeedbackSender::ToolsMenu, data.origin);
        EXPECT_FALSE(data.currentPageIsIncognito);
        EXPECT_TRUE([data.familyMemberRole isEqualToString:@"child"]);
      }).Then(run_loop.QuitClosure());

  [scene_controller_
      presentReportAnIssueViewController:base_view_controller_
                                  sender:UserFeedbackSender::ToolsMenu
                        userFeedbackData:[[UserFeedbackData alloc] init]
                                 timeout:base::Seconds(1)
                              completion:std::move(completion)];

  // Create the family members fetch response.
  kidsmanagement::ListMembersResponse list_family_members_response;
  supervised_user::SetFamilyMemberAttributesForTesting(
      list_family_members_response.add_members(), kidsmanagement::CHILD, "foo");
  test_loader_factory_.SimulateResponseForPendingRequest(
      "https://kidsmanagement-pa.googleapis.com/kidsmanagement/v1/families/"
      "mine/members?alt=proto&allow_empty_family=true",
      list_family_members_response.SerializeAsString());

  run_loop.Run();
}

// Tests that "Report an issue" populates user feedback data for signed-in user.
TEST_F(SceneControllerTest, TestReportAnIssueViewControllerForSignedInUser) {
  MakePrimaryAccountAvailable("[email protected]");

  base::RunLoop run_loop;
  UserFeedbackDataCallback completion =
      base::BindRepeating(^(UserFeedbackData* data) {
        EXPECT_EQ(UserFeedbackSender::ToolsMenu, data.origin);
        EXPECT_FALSE(data.currentPageIsIncognito);
        EXPECT_EQ(nil, data.familyMemberRole);
      }).Then(run_loop.QuitClosure());

  [scene_controller_
      presentReportAnIssueViewController:base_view_controller_
                                  sender:UserFeedbackSender::ToolsMenu
                        userFeedbackData:[[UserFeedbackData alloc] init]
                                 timeout:base::Seconds(0)
                              completion:std::move(completion)];
  run_loop.Run();
}

// Tests that "Report an issue" populates user feedback data for signed-out
// user.
TEST_F(SceneControllerTest, TestReportAnIssueViewControllerForSignedOutUser) {
  base::RunLoop run_loop;
  UserFeedbackDataCallback completion =
      base::BindRepeating(^(UserFeedbackData* data) {
        EXPECT_EQ(UserFeedbackSender::ToolsMenu, data.origin);
        EXPECT_FALSE(data.currentPageIsIncognito);
        EXPECT_EQ(nil, data.familyMemberRole);
      }).Then(run_loop.QuitClosure());

  [scene_controller_
      presentReportAnIssueViewController:base_view_controller_
                                  sender:UserFeedbackSender::ToolsMenu
                        userFeedbackData:[[UserFeedbackData alloc] init]
                                 timeout:base::Seconds(1)
                              completion:std::move(completion)];
  run_loop.Run();
}

}  // namespace