chromium/chrome/browser/password_manager/android/account_storage_notice/javatests/src/org/chromium/chrome/browser/password_manager/account_storage_notice/AccountStorageNoticeCoordinatorIntegrationTest.java

// Copyright 2024 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.password_manager.account_storage_notice;

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.withText;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.password_manager.account_storage_notice.AccountStorageNoticeCoordinator.CLOSE_REASON_METRIC;

import androidx.test.espresso.Espresso;
import androidx.test.filters.MediumTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.password_manager.account_storage_notice.AccountStorageNoticeCoordinator.CloseReason;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.user_prefs.UserPrefs;

import java.io.IOException;

/**
 * Tests that verify AccountStorageNoticeCoordinator's interaction with the view, e.g. click
 * handling. These do not test the logic for when to show the view or not, see
 * AccountStorageNoticeCoordinatorUnitTest for that. They also do not test the integration with
 * embedders (saving and filling flows).
 */
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
@RunWith(ChromeJUnit4ClassRunner.class)
@EnableFeatures(ChromeFeatureList.ENABLE_PASSWORDS_ACCOUNT_STORAGE_FOR_NON_SYNCING_USERS)
public class AccountStorageNoticeCoordinatorIntegrationTest {
    @Rule public ChromeTabbedActivityTestRule mActivityRule = new ChromeTabbedActivityTestRule();

    @Rule
    public final ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setBugComponent(ChromeRenderTestRule.Component.SERVICES_SYNC)
                    .setRevision(1)
                    .build();

    @Rule
    public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private AccountStorageNoticeCoordinator.Natives mJniMock;

    private static final long NATIVE_OBSERVER_PTR = 42;
    // IDS_PASSWORDS_ACCOUNT_STORAGE_NOTICE_SUBTITLE without the <link> tags. It seems best for the
    // test to be explicit rather than implement string manipulation.
    private static final String RAW_SUBTITLE_TEXT =
            "When you’re signed in to Chrome, passwords you save will go in your Google Account. To"
                    + " turn this off, go to settings.";

    @Before
    public void setUp() {
        mJniMocker.mock(AccountStorageNoticeCoordinatorJni.TEST_HOOKS, mJniMock);
        mActivityRule.startMainActivityOnBlankPage();
    }

    @Test
    @MediumTest
    @Feature({"RenderTest"})
    public void testView() throws IOException {
        AccountStorageNoticeCoordinator coordinator = createCoordinator();
        waitSheetVisible(true);

        mRenderTestRule.render(
                coordinator.getBottomSheetViewForTesting(), "account_storage_notice_view");
    }

    @Test
    @MediumTest
    public void testMarksShown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Tests are batched, so the pref might have been set by a previous one.
                    UserPrefs.get(ProfileManager.getLastUsedRegularProfile())
                            .clearPref(Pref.ACCOUNT_STORAGE_NOTICE_SHOWN);
                });

        createCoordinator();
        waitSheetVisible(true);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(
                            UserPrefs.get(ProfileManager.getLastUsedRegularProfile())
                                    .getBoolean(Pref.ACCOUNT_STORAGE_NOTICE_SHOWN));
                });
    }

    @Test
    @MediumTest
    public void testStrings() {
        createCoordinator();
        waitSheetVisible(true);

        onView(withText(R.string.passwords_account_storage_notice_title))
                .check(matches(isDisplayed()));
        onView(withText(R.string.passwords_account_storage_notice_button_text))
                .check(matches(isDisplayed()));
        onView(withText(RAW_SUBTITLE_TEXT)).check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    public void testClickButton() {
        createCoordinator();
        waitSheetVisible(true);
        verify(mJniMock, never()).onClosed(NATIVE_OBSERVER_PTR);
        HistogramWatcher.newBuilder()
                .expectIntRecord(CLOSE_REASON_METRIC, CloseReason.USER_CLICKED_GOT_IT)
                .build();

        onView(withText(R.string.passwords_account_storage_notice_button_text)).perform(click());

        waitSheetVisible(false);
        verify(mJniMock).onClosed(NATIVE_OBSERVER_PTR);
    }

    // TODO(crbug.com/346747486): Add test clicking on settings link. There seems to be some
    // limitation on ViewUtils.clickOnClickableSpan(). Test the metric too.

    @Test
    @MediumTest
    public void testDismissWithBackPress() {
        createCoordinator();
        waitSheetVisible(true);
        verify(mJniMock, never()).onClosed(NATIVE_OBSERVER_PTR);
        HistogramWatcher.newBuilder()
                .expectIntRecord(CLOSE_REASON_METRIC, CloseReason.USER_DISMISSED)
                .build();

        Espresso.pressBack();

        waitSheetVisible(false);
        verify(mJniMock).onClosed(NATIVE_OBSERVER_PTR);
    }

    @Test
    @MediumTest
    public void testHideImmediatelyIfShowing() {
        AccountStorageNoticeCoordinator coordinator = createCoordinator();
        waitSheetVisible(true);
        verify(mJniMock, never()).onClosed(NATIVE_OBSERVER_PTR);
        HistogramWatcher.newBuilder()
                .expectIntRecord(CLOSE_REASON_METRIC, CloseReason.EMBEDDER_REQUESTED)
                .build();

        ThreadUtils.runOnUiThreadBlocking(() -> coordinator.hideImmediatelyIfShowing());

        waitSheetVisible(false);
        verify(mJniMock).onClosed(NATIVE_OBSERVER_PTR);
    }

    @Test
    @MediumTest
    public void testHideWithoutObserver() {
        AccountStorageNoticeCoordinator coordinator = createCoordinator();
        waitSheetVisible(true);
        verify(mJniMock, never()).onClosed(NATIVE_OBSERVER_PTR);
        HistogramWatcher.newBuilder()
                .expectIntRecord(CLOSE_REASON_METRIC, CloseReason.EMBEDDER_REQUESTED)
                .build();

        ThreadUtils.runOnUiThreadBlocking(() -> coordinator.setObserver(0));
        ThreadUtils.runOnUiThreadBlocking(() -> coordinator.hideImmediatelyIfShowing());

        waitSheetVisible(false);
        verify(mJniMock, never()).onClosed(NATIVE_OBSERVER_PTR);
    }

    private AccountStorageNoticeCoordinator createCoordinator() {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Profile profile = ProfileManager.getLastUsedRegularProfile();
                    // The logic for when to show the coordinator is tested in the UnitTest.java.
                    // Tests here only care about the UI interaction.
                    AccountStorageNoticeCoordinator coordinator =
                            AccountStorageNoticeCoordinator.createAndShow(
                                    mActivityRule.getActivity().getWindowAndroid(),
                                    UserPrefs.get(profile));
                    coordinator.setObserver(NATIVE_OBSERVER_PTR);
                    return coordinator;
                });
    }

    private void waitSheetVisible(boolean visible) {
        CriteriaHelper.pollUiThread(
                () -> {
                    @SheetState
                    int state =
                            BottomSheetControllerProvider.from(
                                            mActivityRule.getActivity().getWindowAndroid())
                                    .getSheetState();
                    // The sheet opens at half height or full height depending on the screen size.
                    return visible
                            ? (state == SheetState.HALF || state == SheetState.FULL)
                            : state == SheetState.HIDDEN;
                });
    }
}