chromium/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java

// Copyright 2023 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.touch_to_fill.password_generation;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.touch_to_fill.password_generation.TouchToFillPasswordGenerationCoordinator.INTERACTION_RESULT_HISTOGRAM;
import static org.chromium.chrome.browser.touch_to_fill.password_generation.TouchToFillPasswordGenerationCoordinator.InteractionResult.DISMISSED_FROM_NATIVE;
import static org.chromium.chrome.browser.touch_to_fill.password_generation.TouchToFillPasswordGenerationCoordinator.InteractionResult.DISMISSED_SHEET;
import static org.chromium.chrome.browser.touch_to_fill.password_generation.TouchToFillPasswordGenerationCoordinator.InteractionResult.REJECTED_GENERATED_PASSWORD;
import static org.chromium.chrome.browser.touch_to_fill.password_generation.TouchToFillPasswordGenerationCoordinator.InteractionResult.USED_GENERATED_PASSWORD;

import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;

import androidx.test.ext.junit.rules.ActivityScenarioRule;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.prefs.PrefService;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.base.ViewAndroidDelegate;

/** Tests for {@link TouchToFillPasswordGenerationBridge} */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    ChromeSwitches.DISABLE_NATIVE_INITIALIZATION
})
public class TouchToFillPasswordGenerationModuleTest {
    private TouchToFillPasswordGenerationCoordinator mCoordinator;
    private final ArgumentCaptor<BottomSheetObserver> mBottomSheetObserverCaptor =
            ArgumentCaptor.forClass(BottomSheetObserver.class);

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

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    @Mock private WebContents mWebContents;
    @Mock private BottomSheetController mBottomSheetController;
    @Mock private TouchToFillPasswordGenerationCoordinator.Delegate mDelegate;
    @Mock private KeyboardVisibilityDelegate mKeyboardVisibilityDelegate;
    @Mock private PrefService mPrefService;

    private static final String sTestEmailAddress = "[email protected]";
    private static final String sGeneratedPassword = "Strong generated password";
    private ViewGroup mContent;
    private TestActivity mActivity;

    @Before
    public void setUp() {
        MockitoAnnotations.openMocks(this);

        mActivityScenarioRule
                .getScenario()
                .onActivity(
                        activity -> {
                            setUpBottomSheetController();
                            mCoordinator =
                                    new TouchToFillPasswordGenerationCoordinator(
                                            mWebContents,
                                            mPrefService,
                                            mBottomSheetController,
                                            mKeyboardVisibilityDelegate,
                                            mDelegate);
                            mActivity = activity;
                            mContent = new FrameLayout(mActivity);
                            mActivity.setContentView(mContent);
                        });
    }

    private void setUpBottomSheetController() {
        when(mBottomSheetController.requestShowContent(any(), anyBoolean())).thenReturn(true);
        doNothing().when(mBottomSheetController).addObserver(mBottomSheetObserverCaptor.capture());
    }

    private void show() {
        mCoordinator.show(sGeneratedPassword, sTestEmailAddress, mActivity);
        mContent.addView(mCoordinator.getContentViewForTesting());
    }

    @Test
    public void showsAndHidesBottomSheet() {
        show();
        verify(mBottomSheetController).requestShowContent(any(), anyBoolean());
        verify(mBottomSheetController).addObserver(any());

        mBottomSheetObserverCaptor.getValue().onSheetClosed(StateChangeReason.SWIPE);
        verify(mBottomSheetController).hideContent(any(), anyBoolean());
        verify(mBottomSheetController).removeObserver(mBottomSheetObserverCaptor.getValue());
    }

    @Test
    public void testBottomSheetForceHide() {
        show();
        verify(mBottomSheetController).requestShowContent(any(), anyBoolean());

        mCoordinator.hideFromNative();
        verify(mBottomSheetController).hideContent(any(), anyBoolean());
        verify(mDelegate, times(0)).onDismissed(/* passwordAccepted= */ anyBoolean());
    }

    @Test
    public void testGeneratedPasswordAcceptedCalled() {
        show();

        Button acceptPasswordButton = mContent.findViewById(R.id.use_password_button);
        acceptPasswordButton.performClick();
        verify(mDelegate).onGeneratedPasswordAccepted(sGeneratedPassword);
    }

    @Test
    public void testBottomSheetIsHiddenAfterAcceptingPassword() {
        show();

        Button acceptPasswordButton = mContent.findViewById(R.id.use_password_button);
        acceptPasswordButton.performClick();
        verify(mBottomSheetController).hideContent(any(), anyBoolean());
        verify(mDelegate).onDismissed(/* passwordAccepted= */ true);
    }

    @Test
    public void testGenerationBottomSheetDismissCountMustResetAfterAcceptance() {
        show();

        Button acceptPasswordButton = mContent.findViewById(R.id.use_password_button);
        acceptPasswordButton.performClick();
        verify(mPrefService).setInteger(Pref.PASSWORD_GENERATION_BOTTOM_SHEET_DISMISS_COUNT, 0);
    }

    @Test
    public void testGeneratedPasswordRejectedCalled() {
        show();

        Button rejectPasswordButton = mContent.findViewById(R.id.reject_password_button);
        rejectPasswordButton.performClick();
        verify(mDelegate).onGeneratedPasswordRejected();
    }

    @Test
    public void testGenerationBottomSheetDismissCountMustIncrementAfterRejection() {
        show();

        Button rejectPasswordButton = mContent.findViewById(R.id.reject_password_button);
        rejectPasswordButton.performClick();
        verify(mPrefService).setInteger(Pref.PASSWORD_GENERATION_BOTTOM_SHEET_DISMISS_COUNT, 1);
    }

    @Test
    public void testKeyboardIsHiddenWhenBottomSheetIsDisplayed() {
        ViewAndroidDelegate viewAndroidDelegate = ViewAndroidDelegate.createBasicDelegate(mContent);
        when(mWebContents.getViewAndroidDelegate()).thenReturn(viewAndroidDelegate);

        show();
        verify(mKeyboardVisibilityDelegate).hideKeyboard(mContent);
    }

    @Test
    public void testBottomSheetIsHiddenAfterRejectingPassword() {
        show();

        Button rejectPasswordButton = mContent.findViewById(R.id.reject_password_button);
        rejectPasswordButton.performClick();
        verify(mBottomSheetController).hideContent(any(), anyBoolean());
        verify(mDelegate).onDismissed(/* passwordAccepted= */ false);
    }

    @Test
    public void testGenerationBottomSheetDismissCountMustNotChangeWhenDismissedFromNative() {
        show();

        mCoordinator.hideFromNative();
        verify(mPrefService, never())
                .setInteger(eq(Pref.PASSWORD_GENERATION_BOTTOM_SHEET_DISMISS_COUNT), anyInt());
    }

    @Test
    public void recordsMetricWhenPasswordAccepted() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        INTERACTION_RESULT_HISTOGRAM, USED_GENERATED_PASSWORD);
        show();

        Button acceptPasswordButton = mContent.findViewById(R.id.use_password_button);
        acceptPasswordButton.performClick();

        histogramExpectation.assertExpected();
    }

    @Test
    public void recordsMetricWhenPasswordRejected() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        INTERACTION_RESULT_HISTOGRAM, REJECTED_GENERATED_PASSWORD);
        show();

        Button rejectPasswordButton = mContent.findViewById(R.id.reject_password_button);
        rejectPasswordButton.performClick();

        histogramExpectation.assertExpected();
    }

    @Test
    public void recordsMetricWhenDismissedFromNative() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        INTERACTION_RESULT_HISTOGRAM, DISMISSED_FROM_NATIVE);
        show();

        mCoordinator.hideFromNative();

        histogramExpectation.assertExpected();
    }

    @Test
    public void recordsMetricWhenDismissedByUser() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        INTERACTION_RESULT_HISTOGRAM, DISMISSED_SHEET);
        show();

        ArgumentCaptor<BottomSheetObserver> observer =
                ArgumentCaptor.forClass(BottomSheetObserver.class);
        verify(mBottomSheetController).addObserver(observer.capture());

        observer.getValue().onSheetClosed(StateChangeReason.TAP_SCRIM);
        histogramExpectation.assertExpected();
    }
}