// 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.
package org.chromium.chrome.browser.identity_disc;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.not;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.Activity;
import androidx.annotation.StringRes;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameterBefore;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.ApplicationTestUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.night_mode.ChromeNightModeTestUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.settings.SettingsActivity;
import org.chromium.chrome.browser.signin.SigninAndHistorySyncActivity;
import org.chromium.chrome.browser.signin.SyncConsentActivity;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider;
import org.chromium.chrome.browser.ui.signin.SigninUtils;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.NewTabPageTestUtils;
import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestUtil;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.signin.base.AccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.signin.identitymanager.PrimaryAccountChangeEvent;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.test.util.NightModeTestUtils;
import org.chromium.ui.test.util.ViewUtils;
import java.io.IOException;
/** Instrumentation test for Identity Disc. */
@RunWith(ParameterizedRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class IdentityDiscControllerTest {
private static final String EMAIL = "[email protected]";
private static final String NAME = "Email Emailson";
private static final String FULL_NAME = NAME + ".full";
private final ChromeTabbedActivityTestRule mActivityTestRule =
new ChromeTabbedActivityTestRule();
private final SigninTestRule mSigninTestRule = new SigninTestRule();
// Mock sign-in environment needs to be destroyed after ChromeTabbedActivity in case there are
// observers registered in the AccountManagerFacade mock.
@Rule
public final RuleChain mRuleChain =
RuleChain.outerRule(mSigninTestRule).around(mActivityTestRule);
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Rule
public final ChromeRenderTestRule mRenderTestRule =
ChromeRenderTestRule.Builder.withPublicCorpus()
.setBugComponent(ChromeRenderTestRule.Component.SERVICES_SIGN_IN)
.build();
private Tab mTab;
@Mock private IdentityServicesProvider mIdentityServicesProviderMock;
@Mock private SigninManager mSigninManagerMock;
@Mock private IdentityManager mIdentityManagerMock;
@Mock private ObservableSupplier<Profile> mProfileSupplier;
@Mock private ButtonDataProvider.ButtonDataObserver mButtonDataObserver;
@Mock private Tracker mTracker;
@Mock private ActivityLifecycleDispatcher mDispatcher;
@BeforeClass
public static void setUpBeforeActivityLaunched() {
ChromeNightModeTestUtils.setUpNightModeBeforeChromeActivityLaunched();
}
@UseMethodParameterBefore(NightModeTestUtils.NightModeParams.class)
public void setupNightMode(boolean nightModeEnabled) {
ChromeNightModeTestUtils.setUpNightModeForChromeActivity(nightModeEnabled);
mRenderTestRule.setNightModeEnabled(nightModeEnabled);
}
@AfterClass
public static void tearDownAfterActivityDestroyed() {
ChromeNightModeTestUtils.tearDownNightModeAfterChromeActivityDestroyed();
}
@Before
public void setUp() {
mActivityTestRule.startMainActivityWithURL(UrlConstants.NTP_URL);
mTab = mActivityTestRule.getActivity().getActivityTab();
NewTabPageTestUtils.waitForNtpLoaded(mTab);
}
@Test
@MediumTest
public void testIdentityDiscWithNavigation() {
// User is signed in.
mSigninTestRule.addTestAccountThenSigninAndEnableSync();
ViewUtils.waitForVisibleView(allOf(withId(R.id.optional_toolbar_button), isDisplayed()));
// Identity Disc should be hidden on navigation away from NTP.
leaveNtp();
onView(withId(R.id.optional_toolbar_button))
.check(
matches(
anyOf(
withEffectiveVisibility(ViewMatchers.Visibility.GONE),
not(
withContentDescription(
R.string
.accessibility_toolbar_btn_identity_disc)))));
}
@Test
@MediumTest
@DisableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
public void testIdentityDiscSignedOut_replaceSyncBySigninDisabled() {
// When user is signed out, a signed-out avatar should be visible on the NTP.
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(
R.string
.accessibility_toolbar_btn_signed_out_with_sync_identity_disc)));
// Clicking the signed-out avatar should lead to the sync consent screen.
ActivityTestUtils.waitForActivity(
InstrumentationRegistry.getInstrumentation(),
SyncConsentActivity.class,
() -> onView(withId(R.id.optional_toolbar_button)).perform(click()));
}
@Test
@MediumTest
@EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
public void testIdentityDiscSignedOut_replaceSyncBySigninEnabled() throws Exception {
// When user is signed out, a signed-out avatar should be visible on the NTP.
@StringRes int descriptionId = R.string.accessibility_toolbar_btn_signed_out_identity_disc;
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(descriptionId)));
// Clicking the signed-out avatar should lead to the correct sign-in screen.
Activity signinActivity =
ActivityTestUtils.waitForActivity(
InstrumentationRegistry.getInstrumentation(),
SigninAndHistorySyncActivity.class,
() -> onView(withId(R.id.optional_toolbar_button)).perform(click()));
if (signinActivity != null) {
ApplicationTestUtils.finishActivity(signinActivity);
}
}
@Test
@MediumTest
public void testIdentityDiscSignedOut_signinDisabledByPolicy() {
IdentityServicesProvider.setInstanceForTests(mIdentityServicesProviderMock);
ThreadUtils.runOnUiThreadBlocking(
() -> {
when(mIdentityServicesProviderMock.getSigninManager(Mockito.any()))
.thenReturn(mSigninManagerMock);
// This mock is required because the MainSettings class calls the
// IdentityManager.
when(mIdentityServicesProviderMock.getIdentityManager(Mockito.any()))
.thenReturn(mIdentityManagerMock);
});
when(mSigninManagerMock.isSigninDisabledByPolicy()).thenReturn(true);
// When user is signed out, a signed-out avatar should be visible on the NTP.
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(getSignedOutAvatarDescription())));
// Clicking the signed-out avatar should lead to the settings screen.
ActivityTestUtils.waitForActivity(
InstrumentationRegistry.getInstrumentation(),
SettingsActivity.class,
() -> onView(withId(R.id.optional_toolbar_button)).perform(click()));
}
@Test
@MediumTest
public void testIdentityDiscWithSignin() {
// Identity Disc should be shown on sign-in state change with a NTP refresh.
mSigninTestRule.addAccountThenSignin(EMAIL, NAME);
// TODO(crbug.com/40721874): Remove the reload once the sign-in without sync observer
// is implemented.
ThreadUtils.runOnUiThreadBlocking(mTab::reload);
String expectedContentDescription =
mActivityTestRule
.getActivity()
.getString(
R.string
.accessibility_toolbar_btn_identity_disc_with_name_and_email,
FULL_NAME,
EMAIL);
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(expectedContentDescription)));
mSigninTestRule.signOut();
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(getSignedOutAvatarDescription())));
}
@Test
@MediumTest
public void testIdentityDiscWithSignin_nonDisplayableEmail() {
// Identity Disc should be shown on sign-in state change with a NTP refresh.
AccountInfo accountInfo = addAndSigninAccountWithNonDisplayableEmail();
// TODO(crbug.com/40721874): Remove the reload once the sign-in without sync observer
// is implemented.
ThreadUtils.runOnUiThreadBlocking(mTab::reload);
String expectedContentDescription =
mActivityTestRule
.getActivity()
.getString(
R.string.accessibility_toolbar_btn_identity_disc_with_name,
accountInfo.getFullName());
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(expectedContentDescription)));
mSigninTestRule.forceSignOut();
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(getSignedOutAvatarDescription())));
}
@Test
@MediumTest
@SuppressWarnings("CheckReturnValue")
public void testIdentityDiscWithSigninAndEnableSync() {
// Identity Disc should be shown on sign-in state change without NTP refresh.
mSigninTestRule.addAccountThenSigninAndEnableSync(EMAIL, NAME);
String expectedContentDescription =
mActivityTestRule
.getActivity()
.getString(
R.string
.accessibility_toolbar_btn_identity_disc_with_name_and_email,
FULL_NAME,
EMAIL);
// TODO(crbug.com/40277716): This is a no-op, replace with ViewUtils.waitForVisibleView().
ViewUtils.isEventuallyVisible(
allOf(
withId(R.id.optional_toolbar_button),
withContentDescription(expectedContentDescription),
isDisplayed()));
mSigninTestRule.signOut();
// TODO(crbug.com/40277716): This is a no-op, replace with ViewUtils.waitForVisibleView().
ViewUtils.isEventuallyVisible(
allOf(
withId(R.id.optional_toolbar_button),
withContentDescription(getSignedOutAvatarDescription()),
isDisplayed()));
}
@Test
@MediumTest
public void testIdentityDiscWithSigninAndEnableSync_nonDisplayableEmail() {
// Identity Disc should be shown on sign-in state change without NTP refresh.
AccountInfo accountInfo = addAndSigninAccountWithNonDisplayableEmail();
SigninTestUtil.signinAndEnableSync(
accountInfo, SyncTestUtil.getSyncServiceForLastUsedProfile());
String expectedContentDescription =
mActivityTestRule
.getActivity()
.getString(
R.string.accessibility_toolbar_btn_identity_disc_with_name,
accountInfo.getFullName());
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
withContentDescription(expectedContentDescription),
isDisplayed()));
mSigninTestRule.forceSignOut();
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
withContentDescription(getSignedOutAvatarDescription()),
isDisplayed()));
}
@Test
@MediumTest
@SuppressWarnings("CheckReturnValue")
public void testIdentityDiscWithSwitchToIncognito() {
mSigninTestRule.addTestAccountThenSigninAndEnableSync();
// TODO(crbug.com/40277716): This is a no-op, replace with ViewUtils.waitForVisibleView().
ViewUtils.isEventuallyVisible(allOf(withId(R.id.optional_toolbar_button), isDisplayed()));
// Identity Disc should not be visible, when switched from sign in state to incognito NTP.
mActivityTestRule.newIncognitoTabFromMenu();
// TODO(crbug.com/40277716): This is a no-op, replace with ViewUtils.waitForVisibleView().
ViewUtils.isEventuallyVisible(
allOf(
withId(R.id.optional_toolbar_button),
withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
}
@Test
@SmallTest
public void onPrimaryAccountChanged_accountSet() {
IdentityDiscController identityDiscController =
buildControllerWithObserver(mButtonDataObserver);
PrimaryAccountChangeEvent accountSetEvent =
newSigninEvent(PrimaryAccountChangeEvent.Type.SET);
identityDiscController.onPrimaryAccountChanged(accountSetEvent);
verify(mButtonDataObserver).buttonDataChanged(true);
}
@Test
@SmallTest
public void onPrimaryAccountChanged_accountCleared() {
IdentityDiscController identityDiscController =
buildControllerWithObserver(mButtonDataObserver);
PrimaryAccountChangeEvent accountClearedEvent =
newSigninEvent(PrimaryAccountChangeEvent.Type.CLEARED);
identityDiscController.onPrimaryAccountChanged(accountClearedEvent);
verify(mButtonDataObserver).buttonDataChanged(false);
Assert.assertTrue(identityDiscController.isProfileDataCacheEmpty());
}
@Test
@MediumTest
public void onClick_profileSupplierNotYetInitialized_doesNothing() {
TrackerFactory.setTrackerForTests(mTracker);
IdentityDiscController identityDiscController =
new IdentityDiscController(
mActivityTestRule.getActivity(), mDispatcher, /* profileSupplier= */ null);
// If the button is tapped before the profile is set, the click shouldn't be recorded.
identityDiscController.onClick();
verifyNoMoreInteractions(mTracker);
}
@Test
@MediumTest
public void onClick_profileNotYetInitialized_doesNothing() {
TrackerFactory.setTrackerForTests(mTracker);
IdentityDiscController identityDiscController =
new IdentityDiscController(
mActivityTestRule.getActivity(), mDispatcher, mProfileSupplier);
// If the button is tapped before the profile is set, the click shouldn't be recorded.
identityDiscController.onClick();
verifyNoMoreInteractions(mTracker);
}
@Test
@MediumTest
@Feature("RenderTest")
@UseMethodParameter(NightModeTestUtils.NightModeParams.class)
public void testIdentityDisc_signedOut(boolean nightModeEnabled) throws IOException {
mRenderTestRule.render(
mActivityTestRule.getActivity().findViewById(R.id.optional_toolbar_button),
"identity_disc_signed_out");
}
@Test
@MediumTest
@Feature("RenderTest")
@UseMethodParameter(NightModeTestUtils.NightModeParams.class)
public void testIdentityDisc_signedIn(boolean nightModeEnabled) throws IOException {
// Sign-in and wait for the user profile image to appear.
mSigninTestRule.addAccountThenSignin(EMAIL, NAME);
String expectedContentDescription =
mActivityTestRule
.getActivity()
.getString(
R.string
.accessibility_toolbar_btn_identity_disc_with_name_and_email,
FULL_NAME,
EMAIL);
ViewUtils.waitForVisibleView(
allOf(
withId(R.id.optional_toolbar_button),
isDisplayed(),
withContentDescription(expectedContentDescription)));
// Test the profile image shown in signed-in state to ensure the image is not tinted
// accidentally.
mRenderTestRule.render(
mActivityTestRule.getActivity().findViewById(R.id.optional_toolbar_button),
"identity_disc_signed_in");
}
private void leaveNtp() {
mActivityTestRule.loadUrl(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
ChromeTabUtils.waitForTabPageLoaded(mTab, ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
}
private AccountInfo addAndSigninAccountWithNonDisplayableEmail() {
mSigninTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL);
mSigninTestRule.waitForSignin(AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL);
return AccountManagerTestRule.TEST_ACCOUNT_NON_DISPLAYABLE_EMAIL;
}
private IdentityDiscController buildControllerWithObserver(
ButtonDataProvider.ButtonDataObserver observer) {
IdentityDiscController controller =
new IdentityDiscController(
mActivityTestRule.getActivity(), mDispatcher, mProfileSupplier);
controller.addObserver(observer);
return controller;
}
private PrimaryAccountChangeEvent newSigninEvent(int eventType) {
return new PrimaryAccountChangeEvent(eventType, ConsentLevel.SIGNIN);
}
private @StringRes int getSignedOutAvatarDescription() {
return (SigninUtils.shouldShowNewSigninFlow())
? R.string.accessibility_toolbar_btn_signed_out_identity_disc
: R.string.accessibility_toolbar_btn_signed_out_with_sync_identity_disc;
}
}