// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h" #include "base/command_line.h" #import "base/mac/foundation_util.h" #include "base/mac/scoped_nsobject.h" #include "base/macros.h" #include "base/memory/scoped_ptr.h" #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/profiles/avatar_menu.h" #include "chrome/browser/profiles/profile_info_cache.h" #include "chrome/browser/services/gcm/fake_gcm_profile_service.h" #include "chrome/browser/services/gcm/gcm_profile_service_factory.h" #include "chrome/browser/signin/account_fetcher_service_factory.h" #include "chrome/browser/signin/account_tracker_service_factory.h" #include "chrome/browser/signin/chrome_signin_helper.h" #include "chrome/browser/signin/fake_account_fetcher_service_builder.h" #include "chrome/browser/signin/fake_profile_oauth2_token_service_builder.h" #include "chrome/browser/signin/profile_oauth2_token_service_factory.h" #include "chrome/browser/signin/signin_manager_factory.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/cocoa/cocoa_profile_test.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/pref_names.h" #include "components/signin/core/browser/fake_account_fetcher_service.h" #include "components/signin/core/browser/fake_profile_oauth2_token_service.h" #include "components/signin/core/browser/profile_oauth2_token_service.h" #include "components/signin/core/browser/signin_manager.h" #include "components/signin/core/common/profile_management_switches.h" #include "components/signin/core/common/signin_pref_names.h" #include "components/syncable_prefs/pref_service_syncable.h" const std::string kGaiaId = "gaiaid-user@gmail.com"; const std::string kEmail = "user@gmail.com"; const std::string kSecondaryEmail = "user2@gmail.com"; const std::string kSecondaryGaiaId = "gaiaid-user2@gmail.com"; const std::string kLoginToken = "oauth2_login_token"; class ProfileChooserControllerTest : public CocoaProfileTest { public: ProfileChooserControllerTest() { TestingProfile::TestingFactories factories; factories.push_back( std::make_pair(ProfileOAuth2TokenServiceFactory::GetInstance(), BuildFakeProfileOAuth2TokenService)); factories.push_back( std::make_pair(AccountFetcherServiceFactory::GetInstance(), FakeAccountFetcherServiceBuilder::BuildForTests)); AddTestingFactories(factories); } void SetUp() override { CocoaProfileTest::SetUp(); ASSERT_TRUE(browser()->profile()); gcm::GCMProfileServiceFactory::GetInstance()->SetTestingFactory( browser()->profile(), gcm::FakeGCMProfileService::Build); testing_profile_manager()->CreateTestingProfile( "test1", scoped_ptr(), base::ASCIIToUTF16("Test 1"), 0, std::string(), testing_factories()); testing_profile_manager()->CreateTestingProfile( "test2", scoped_ptr(), base::ASCIIToUTF16("Test 2"), 1, std::string(), TestingProfile::TestingFactories()); menu_ = new AvatarMenu(testing_profile_manager()->profile_info_cache(), NULL, NULL); menu_->RebuildMenu(); // There should be the default profile + two profiles we created. EXPECT_EQ(3U, menu_->GetNumberOfItems()); } void TearDown() override { [controller() close]; controller_.reset(); CocoaProfileTest::TearDown(); } void StartProfileChooserController() { StartProfileChooserControllerWithTutorialMode(profiles::TUTORIAL_MODE_NONE); } void StartProfileChooserControllerWithTutorialMode( profiles::TutorialMode mode) { NSRect frame = [test_window() frame]; NSPoint point = NSMakePoint(NSMidX(frame), NSMidY(frame)); controller_.reset([[ProfileChooserController alloc] initWithBrowser:browser() anchoredAt:point viewMode:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER tutorialMode:mode serviceType:signin::GAIA_SERVICE_TYPE_NONE accessPoint:signin_metrics::AccessPoint:: ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN]); [controller_ showWindow:nil]; } void AssertRightClickTutorialShown() { NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // There should be 4 views: the tutorial, the active profile card, a // separator and the options view. ASSERT_EQ(4U, [subviews count]); // The tutorial is the topmost view, so the last in the array. It should // contain 3 views: the title, the content text and the OK button. NSArray* tutorialSubviews = [[subviews objectAtIndex:3] subviews]; ASSERT_EQ(3U, [tutorialSubviews count]); NSTextField* tutorialTitle = base::mac::ObjCCastStrict( [tutorialSubviews objectAtIndex:2]); EXPECT_GT([[tutorialTitle stringValue] length], 0U); NSTextField* tutorialContent = base::mac::ObjCCastStrict( [tutorialSubviews objectAtIndex:1]); EXPECT_GT([[tutorialContent stringValue] length], 0U); NSButton* tutorialOKButton = base::mac::ObjCCastStrict( [tutorialSubviews objectAtIndex:0]); EXPECT_GT([[tutorialOKButton title] length], 0U); } void StartFastUserSwitcher() { NSRect frame = [test_window() frame]; NSPoint point = NSMakePoint(NSMidX(frame), NSMidY(frame)); controller_.reset([[ProfileChooserController alloc] initWithBrowser:browser() anchoredAt:point viewMode:profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER tutorialMode:profiles::TUTORIAL_MODE_NONE serviceType:signin::GAIA_SERVICE_TYPE_NONE accessPoint:signin_metrics::AccessPoint:: ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN]); [controller_ showWindow:nil]; } ProfileChooserController* controller() { return controller_; } AvatarMenu* menu() { return menu_; } private: base::scoped_nsobject controller_; // Weak; owned by |controller_|. AvatarMenu* menu_; DISALLOW_COPY_AND_ASSIGN(ProfileChooserControllerTest); }; TEST_F(ProfileChooserControllerTest, InitialLayoutWithNewMenu) { StartProfileChooserController(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // Three profiles means we should have one active card, one separator and // one option buttons view. We also have an update promo for the new avatar // menu. // TODO(noms): Enforcing 4U fails on the waterfall debug bots, but it's not // reproducible anywhere else. ASSERT_GE([subviews count], 3U); // There should be two buttons and a separator in the option buttons view. NSArray* buttonSubviews = [[subviews objectAtIndex:0] subviews]; ASSERT_EQ(3U, [buttonSubviews count]); // There should be an incognito button. NSButton* incognitoButton = base::mac::ObjCCast([buttonSubviews objectAtIndex:0]); EXPECT_EQ(@selector(goIncognito:), [incognitoButton action]); EXPECT_EQ(controller(), [incognitoButton target]); // There should be a separator. EXPECT_TRUE([[subviews objectAtIndex:1] isKindOfClass:[NSBox class]]); // There should be a user switcher button. NSButton* userSwitcherButton = base::mac::ObjCCast([buttonSubviews objectAtIndex:2]); EXPECT_EQ(@selector(showUserManager:), [userSwitcherButton action]); EXPECT_EQ(controller(), [userSwitcherButton target]); // There should be a separator. EXPECT_TRUE([[subviews objectAtIndex:1] isKindOfClass:[NSBox class]]); // There should be the profile avatar, name and links container in the active // card view. The links displayed in the container are checked separately. NSArray* activeCardSubviews = [[subviews objectAtIndex:2] subviews]; ASSERT_EQ(3U, [activeCardSubviews count]); // Profile icon. NSView* activeProfileImage = [activeCardSubviews objectAtIndex:2]; EXPECT_TRUE([activeProfileImage isKindOfClass:[NSButton class]]); // Profile name. NSView* activeProfileName = [activeCardSubviews objectAtIndex:1]; EXPECT_TRUE([activeProfileName isKindOfClass:[NSButton class]]); EXPECT_EQ(menu()->GetItemAt(0).name, base::SysNSStringToUTF16( [base::mac::ObjCCast(activeProfileName) title])); // Profile links. This is a local profile, so there should be a signin button // and a signin promo. NSArray* linksSubviews = [[activeCardSubviews objectAtIndex:0] subviews]; ASSERT_EQ(2U, [linksSubviews count]); NSButton* link = base::mac::ObjCCast( [linksSubviews objectAtIndex:0]); EXPECT_EQ(@selector(showInlineSigninPage:), [link action]); EXPECT_EQ(controller(), [link target]); NSTextField* promo = base::mac::ObjCCast( [linksSubviews objectAtIndex:1]); EXPECT_GT([[promo stringValue] length], 0U); } TEST_F(ProfileChooserControllerTest, RightClickTutorialShownAfterWelcome) { // The welcome upgrade tutorial takes precedence so show it then dismiss it. // The right click tutorial should be shown right away. StartProfileChooserControllerWithTutorialMode( profiles::TUTORIAL_MODE_WELCOME_UPGRADE); [controller() dismissTutorial:nil]; AssertRightClickTutorialShown(); } TEST_F(ProfileChooserControllerTest, RightClickTutorialShownAfterReopen) { // The welcome upgrade tutorial takes precedence so show it then close the // menu. Reopening the menu should show the tutorial. StartProfileChooserController(); [controller() close]; StartProfileChooserController(); AssertRightClickTutorialShown(); // The tutorial must be manually dismissed so it should still be shown after // closing and reopening the menu, [controller() close]; StartProfileChooserController(); AssertRightClickTutorialShown(); } TEST_F(ProfileChooserControllerTest, RightClickTutorialNotShownAfterDismiss) { // The welcome upgrade tutorial takes precedence so show it then close the // menu. Reopening the menu should show the tutorial. StartProfileChooserController(); [controller() close]; StartProfileChooserControllerWithTutorialMode( profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING); AssertRightClickTutorialShown(); // Dismissing the tutorial should prevent it from being shown forever. [controller() dismissTutorial:nil]; NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // There should be 3 views since there's no tutorial ASSERT_EQ(3U, [subviews count]); // Closing and reopening the menu shouldn't show the tutorial. [controller() close]; StartProfileChooserControllerWithTutorialMode( profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING); subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // There should be 3 views since there's no tutorial ASSERT_EQ(3U, [subviews count]); } TEST_F(ProfileChooserControllerTest, OtherProfilesSortedAlphabetically) { // Add two extra profiles, to make sure sorting is alphabetical and not // by order of creation. testing_profile_manager()->CreateTestingProfile( "test3", scoped_ptr(), base::ASCIIToUTF16("New Profile"), 1, std::string(), TestingProfile::TestingFactories()); testing_profile_manager()->CreateTestingProfile( "test4", scoped_ptr(), base::ASCIIToUTF16("Another Test"), 1, std::string(), TestingProfile::TestingFactories()); StartFastUserSwitcher(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; NSString* sortedNames[] = { @"Another Test", @"New Profile", @"Test 1", @"Test 2" }; // There are four "other" profiles, each with a button and a separator. ASSERT_EQ([subviews count], 8U); // There should be four "other profiles" items, sorted alphabetically. The // "other profiles" start at index 2 (after the option buttons view and its // separator), and each have a separator. We need to iterate through the // profiles in the order displayed in the bubble, which is opposite from the // drawn order. int sortedNameIndex = 0; for (int i = 7; i > 0; i -= 2) { // The item at index i is the separator. NSButton* button = base::mac::ObjCCast( [subviews objectAtIndex:i-1]); EXPECT_TRUE( [[button title] isEqualToString:sortedNames[sortedNameIndex++]]); } } TEST_F(ProfileChooserControllerTest, LocalProfileActiveCardLinksWithNewMenu) { StartProfileChooserController(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; NSArray* activeCardSubviews = [[subviews objectAtIndex:2] subviews]; NSArray* activeCardLinks = [[activeCardSubviews objectAtIndex:0] subviews]; ASSERT_EQ(2U, [activeCardLinks count]); // There should be a sign in button. NSButton* link = base::mac::ObjCCast( [activeCardLinks objectAtIndex:0]); EXPECT_EQ(@selector(showInlineSigninPage:), [link action]); EXPECT_EQ(controller(), [link target]); // Local profiles have a signin promo. NSTextField* promo = base::mac::ObjCCast( [activeCardLinks objectAtIndex:1]); EXPECT_GT([[promo stringValue] length], 0U); } TEST_F(ProfileChooserControllerTest, SignedInProfileActiveCardLinksWithAccountConsistency) { switches::EnableAccountConsistencyForTesting( base::CommandLine::ForCurrentProcess()); // Sign in the first profile. ProfileInfoCache* cache = testing_profile_manager()->profile_info_cache(); cache->SetAuthInfoOfProfileAtIndex(0, kGaiaId, base::ASCIIToUTF16(kEmail)); StartProfileChooserController(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; NSArray* activeCardSubviews = [[subviews objectAtIndex:2] subviews]; NSArray* activeCardLinks = [[activeCardSubviews objectAtIndex:0] subviews]; // There is one link: manage accounts. ASSERT_EQ(1U, [activeCardLinks count]); NSButton* manageAccountsLink = base::mac::ObjCCast([activeCardLinks objectAtIndex:0]); EXPECT_EQ(@selector(showAccountManagement:), [manageAccountsLink action]); EXPECT_EQ(controller(), [manageAccountsLink target]); } TEST_F(ProfileChooserControllerTest, SignedInProfileActiveCardLinksWithNewMenu) { // Sign in the first profile. ProfileInfoCache* cache = testing_profile_manager()->profile_info_cache(); cache->SetAuthInfoOfProfileAtIndex(0, kGaiaId, base::ASCIIToUTF16(kEmail)); StartProfileChooserController(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; NSArray* activeCardSubviews = [[subviews objectAtIndex:2] subviews]; NSArray* activeCardLinks = [[activeCardSubviews objectAtIndex:0] subviews]; // There is one disabled button with the user's email. ASSERT_EQ(1U, [activeCardLinks count]); NSButton* emailButton = base::mac::ObjCCast([activeCardLinks objectAtIndex:0]); EXPECT_EQ(kEmail, base::SysNSStringToUTF8([emailButton title])); EXPECT_EQ(nil, [emailButton action]); EXPECT_FALSE([emailButton isEnabled]); } TEST_F(ProfileChooserControllerTest, AccountManagementLayout) { switches::EnableAccountConsistencyForTesting( base::CommandLine::ForCurrentProcess()); // Sign in the first profile. ProfileInfoCache* cache = testing_profile_manager()->profile_info_cache(); cache->SetAuthInfoOfProfileAtIndex(0, kGaiaId, base::ASCIIToUTF16(kEmail)); // Mark that we are using the profile name on purpose, so that we don't // fallback to testing the algorithm that chooses which default name // should be used. cache->SetProfileIsUsingDefaultNameAtIndex(0, false); // Set up the AccountTrackerService, signin manager and the OAuth2Tokens. Profile* profile = browser()->profile(); AccountTrackerServiceFactory::GetForProfile(profile) ->SeedAccountInfo(kGaiaId, kEmail); AccountTrackerServiceFactory::GetForProfile(profile) ->SeedAccountInfo(kSecondaryGaiaId, kSecondaryEmail); SigninManagerFactory::GetForProfile(profile) ->SetAuthenticatedAccountInfo(kGaiaId, kEmail); std::string account_id = SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedAccountId(); ProfileOAuth2TokenServiceFactory::GetForProfile(profile) ->UpdateCredentials(account_id, kLoginToken); account_id = AccountTrackerServiceFactory::GetForProfile(profile) ->PickAccountIdForAccount(kSecondaryGaiaId, kSecondaryEmail); ProfileOAuth2TokenServiceFactory::GetForProfile(profile) ->UpdateCredentials(account_id, kLoginToken); StartProfileChooserController(); [controller() initMenuContentsWithView: profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // There should be one active card, one accounts container, two separators // and one option buttons view. ASSERT_EQ(5U, [subviews count]); // There should be three buttons and two separators in the option // buttons view. NSArray* buttonSubviews = [[subviews objectAtIndex:0] subviews]; ASSERT_EQ(3U, [buttonSubviews count]); // There should be a separator. EXPECT_TRUE([[buttonSubviews objectAtIndex:1] isKindOfClass:[NSBox class]]); // There should be an incognito button. NSButton* incognitoButton = base::mac::ObjCCast([buttonSubviews objectAtIndex:0]); EXPECT_EQ(@selector(goIncognito:), [incognitoButton action]); EXPECT_EQ(controller(), [incognitoButton target]); // There should be a separator. EXPECT_TRUE([[subviews objectAtIndex:3] isKindOfClass:[NSBox class]]); // There should be a user switcher button. NSButton* userSwitcherButton = base::mac::ObjCCast([buttonSubviews objectAtIndex:2]); EXPECT_EQ(@selector(showUserManager:), [userSwitcherButton action]); EXPECT_EQ(controller(), [userSwitcherButton target]); // In the accounts view, there should be the account list container // accounts and one "add accounts" button. NSArray* accountsSubviews = [[subviews objectAtIndex:2] subviews]; ASSERT_EQ(2U, [accountsSubviews count]); NSButton* addAccountsButton = base::mac::ObjCCast([accountsSubviews objectAtIndex:0]); EXPECT_EQ(@selector(addAccount:), [addAccountsButton action]); EXPECT_EQ(controller(), [addAccountsButton target]); // There should be two accounts in the account list container. NSArray* accountsListSubviews = [[accountsSubviews objectAtIndex:1] subviews]; ASSERT_EQ(2U, [accountsListSubviews count]); NSButton* genericAccount = base::mac::ObjCCast([accountsListSubviews objectAtIndex:0]); NSButton* genericAccountDelete = base::mac::ObjCCast( [[genericAccount subviews] objectAtIndex:0]); EXPECT_EQ(@selector(showAccountRemovalView:), [genericAccountDelete action]); EXPECT_EQ(controller(), [genericAccountDelete target]); EXPECT_NE(-1, [genericAccountDelete tag]); // Primary accounts are always last. NSButton* primaryAccount = base::mac::ObjCCast([accountsListSubviews objectAtIndex:1]); NSButton* primaryAccountDelete = base::mac::ObjCCast( [[primaryAccount subviews] objectAtIndex:0]); EXPECT_EQ(@selector(showAccountRemovalView:), [primaryAccountDelete action]); EXPECT_EQ(controller(), [primaryAccountDelete target]); EXPECT_EQ(-1, [primaryAccountDelete tag]); // There should be another separator. EXPECT_TRUE([[subviews objectAtIndex:3] isKindOfClass:[NSBox class]]); // There should be the profile avatar, name and a "hide accounts" link // container in the active card view. NSArray* activeCardSubviews = [[subviews objectAtIndex:4] subviews]; ASSERT_EQ(3U, [activeCardSubviews count]); // Profile icon. NSView* activeProfileImage = [activeCardSubviews objectAtIndex:2]; EXPECT_TRUE([activeProfileImage isKindOfClass:[NSButton class]]); // Profile name. NSView* activeProfileName = [activeCardSubviews objectAtIndex:1]; EXPECT_TRUE([activeProfileName isKindOfClass:[NSButton class]]); EXPECT_EQ(menu()->GetItemAt(0).name, base::SysNSStringToUTF16( [base::mac::ObjCCast(activeProfileName) title])); // Profile links. This is a local profile, so there should be a signin button. NSArray* linksSubviews = [[activeCardSubviews objectAtIndex:0] subviews]; ASSERT_EQ(1U, [linksSubviews count]); NSButton* link = base::mac::ObjCCast( [linksSubviews objectAtIndex:0]); EXPECT_EQ(@selector(hideAccountManagement:), [link action]); EXPECT_EQ(controller(), [link target]); } TEST_F(ProfileChooserControllerTest, SignedInProfileLockDisabled) { switches::EnableNewProfileManagementForTesting( base::CommandLine::ForCurrentProcess()); // Sign in the first profile. ProfileInfoCache* cache = testing_profile_manager()->profile_info_cache(); cache->SetAuthInfoOfProfileAtIndex(0, kGaiaId, base::ASCIIToUTF16(kEmail)); // The preference, not the email, determines whether the profile can lock. browser()->profile()->GetPrefs()->SetString( prefs::kGoogleServicesHostedDomain, "chromium.org"); StartProfileChooserController(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // There will be two buttons and one separators in the option buttons view. NSArray* buttonSubviews = [[subviews objectAtIndex:0] subviews]; ASSERT_EQ(3U, [buttonSubviews count]); // The last button should not be the lock button. NSButton* lastButton = base::mac::ObjCCast([buttonSubviews objectAtIndex:0]); ASSERT_TRUE(lastButton); EXPECT_NE(@selector(lockProfile:), [lastButton action]); } TEST_F(ProfileChooserControllerTest, SignedInProfileLockEnabled) { switches::EnableNewProfileManagementForTesting( base::CommandLine::ForCurrentProcess()); // Sign in the first profile. ProfileInfoCache* cache = testing_profile_manager()->profile_info_cache(); cache->SetAuthInfoOfProfileAtIndex(0, kGaiaId, base::ASCIIToUTF16(kEmail)); // The preference, not the email, determines whether the profile can lock. browser()->profile()->GetPrefs()->SetString( prefs::kGoogleServicesHostedDomain, "google.com"); // Lock is only available where a supervised user is present. cache->SetSupervisedUserIdOfProfileAtIndex(1, kEmail); StartProfileChooserController(); NSArray* subviews = [[[controller() window] contentView] subviews]; ASSERT_EQ(2U, [subviews count]); subviews = [[subviews objectAtIndex:0] subviews]; // There will be three buttons and two separators in the option buttons view. NSArray* buttonSubviews = [[subviews objectAtIndex:0] subviews]; ASSERT_EQ(5U, [buttonSubviews count]); // There should be a lock button. NSButton* lockButton = base::mac::ObjCCast([buttonSubviews objectAtIndex:0]); ASSERT_TRUE(lockButton); EXPECT_EQ(@selector(lockProfile:), [lockButton action]); EXPECT_EQ(controller(), [lockButton target]); EXPECT_TRUE([lockButton isEnabled]); }