chromium/chrome/browser/quick_delete/android/javatests/src/org/chromium/chrome/browser/quick_delete/QuickDeleteControllerTest.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.quick_delete;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBack;
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.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Instrumentation;
import android.view.View;
import android.widget.Spinner;

import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;

import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
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.base.test.util.Restriction;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.browsing_data.BrowsingDataBridge;
import org.chromium.chrome.browser.browsing_data.BrowsingDataBridgeJni;
import org.chromium.chrome.browser.browsing_data.BrowsingDataType;
import org.chromium.chrome.browser.browsing_data.ClearBrowsingDataFragmentAdvanced;
import org.chromium.chrome.browser.browsing_data.TimePeriod;
import org.chromium.chrome.browser.browsing_data.TimePeriodUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.layouts.LayoutTestUtils;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.settings.SettingsActivity;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabTestUtils;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.browsing_data.DeleteBrowsingDataAction;
import org.chromium.ui.modaldialog.ModalDialogProperties;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/** Tests for quick delete controller. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@EnableFeatures(ChromeFeatureList.QUICK_DELETE_FOR_ANDROID)
@Batch(Batch.PER_CLASS)
public class QuickDeleteControllerTest {
    private static final long ACTIVITY_WAIT_LONG_MS = TimeUnit.SECONDS.toMillis(10);
    private static final long FIFTEEN_MINUTES_IN_MS = TimeUnit.MINUTES.toMillis(15);

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private BrowsingDataBridge.Natives mBrowsingDataBridgeMock;

    private final CallbackHelper mCallbackHelper = new CallbackHelper();

    private ChromeTabbedActivity mActivity;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mActivityTestRule.startMainActivityOnBlankPage();
        mActivity = mActivityTestRule.getActivity();

        mJniMocker.mock(BrowsingDataBridgeJni.TEST_HOOKS, mBrowsingDataBridgeMock);
        // Ensure that whenever the mock is asked to clear browsing data, the callback is
        // immediately called.
        doAnswer(
                        (Answer<Void>)
                                invocation -> {
                                    ((BrowsingDataBridge.OnClearBrowsingDataListener)
                                                    invocation.getArgument(2))
                                            .onBrowsingDataCleared();
                                    mCallbackHelper.notifyCalled();
                                    return null;
                                })
                .when(mBrowsingDataBridgeMock)
                .clearBrowsingData(
                        any(), any(), any(), any(), anyInt(), any(), any(), any(), any());

        // Set the time for the initial tab to be outside of the quick delete time span.
        Tab initialTab = mActivity.getActivityTab();
        runOnUiThreadBlocking(
                () -> {
                    TabTestUtils.setLastNavigationCommittedTimestampMillis(
                            initialTab, System.currentTimeMillis() - FIFTEEN_MINUTES_IN_MS);
                });

        // Open new tab for tests.
        mActivityTestRule.loadUrlInNewTab("about:blank");
    }

    @After
    public void tearDown() {
        // Close all tabs
        runOnUiThreadBlocking(
                () ->
                        mActivity
                                .getCurrentTabModel()
                                .closeTabs(TabClosureParams.closeAllTabs().build()));
    }

    private void openQuickDeleteDialog() {
        // Open 3 dot menu.
        runOnUiThreadBlocking(
                () -> {
                    AppMenuTestSupport.showAppMenu(
                            mActivityTestRule.getAppMenuCoordinator(), null, false);
                });
        onViewWaiting(withId(R.id.app_menu_list))
                .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));

        // Click on quick delete menu item.
        runOnUiThreadBlocking(
                () -> {
                    AppMenuTestSupport.callOnItemClick(
                            mActivityTestRule.getAppMenuCoordinator(), R.id.quick_delete_menu_id);
                });
        onViewWaiting(withId(R.id.quick_delete_spinner), true)
                .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

    private void assertDataTypesCleared(@TimePeriod int timePeriod, int... types) {
        verify(mBrowsingDataBridgeMock)
                .clearBrowsingData(
                        any(), any(), any(), eq(types), eq(timePeriod), any(), any(), any(), any());
    }

    @Test
    @MediumTest
    @DisabledTest(message = "b/322945246")
    public void testNavigateToTabSwitcher_WhenClickingDelete() throws TimeoutException {
        openQuickDeleteDialog();
        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        LayoutTestUtils.waitForLayout(mActivity.getLayoutManager(), LayoutType.TAB_SWITCHER);
    }

    @Test
    @MediumTest
    public void testSnackbarShown_WhenClickingDelete() throws TimeoutException {
        openQuickDeleteDialog();
        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        onViewWaiting(
                        withText(
                                mActivity.getString(
                                        R.string.quick_delete_snackbar_message,
                                        TimePeriodUtils.getTimePeriodString(
                                                mActivity, TimePeriod.LAST_15_MINUTES))))
                .check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    public void testSnackbarShown_WhenClickingDelete_AllTimeSelected() throws TimeoutException {
        openQuickDeleteDialog();

        onView(withId(R.id.quick_delete_spinner)).check(matches(isDisplayed()));
        View dialogView =
                mActivity
                        .getModalDialogManager()
                        .getCurrentDialogForTest()
                        .get(ModalDialogProperties.CUSTOM_VIEW);

        runOnUiThreadBlocking(
                () -> {
                    Spinner spinnerView = dialogView.findViewById(R.id.quick_delete_spinner);
                    spinnerView.setSelection(5);
                    var option =
                            (TimePeriodUtils.TimePeriodSpinnerOption) spinnerView.getSelectedItem();
                    assertEquals(TimePeriod.ALL_TIME, option.getTimePeriod());
                });

        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        onViewWaiting(withText(R.string.quick_delete_snackbar_all_time_message))
                .check(matches(isDisplayed()));
    }

    @Test
    @MediumTest
    public void testQuickDeleteHistogram_WhenClickingDelete() throws TimeoutException {
        openQuickDeleteDialog();

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newBuilder()
                        .expectBooleanRecord("Privacy.QuickDelete.TabsEnabled", true)
                        .expectIntRecord(
                                QuickDeleteMetricsDelegate.HISTOGRAM_NAME,
                                QuickDeleteMetricsDelegate.QuickDeleteAction.DELETE_CLICKED)
                        .build();

        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        histogramWatcher.assertExpected();
    }

    @Test
    @MediumTest
    public void testDeleteBrowsingDataHistogram_WhenClickingDelete() throws TimeoutException {
        openQuickDeleteDialog();

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Privacy.DeleteBrowsingData.Action", DeleteBrowsingDataAction.QUICK_DELETE);

        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        histogramWatcher.assertExpected();
    }

    @Test
    @MediumTest
    public void testBrowsingDataDeletion_DefaultTimePeriodSelected() throws TimeoutException {
        openQuickDeleteDialog();

        View dialogView =
                mActivity
                        .getModalDialogManager()
                        .getCurrentDialogForTest()
                        .get(ModalDialogProperties.CUSTOM_VIEW);

        runOnUiThreadBlocking(
                () -> {
                    Spinner spinnerView = dialogView.findViewById(R.id.quick_delete_spinner);
                    var option =
                            (TimePeriodUtils.TimePeriodSpinnerOption) spinnerView.getSelectedItem();
                    assertEquals(TimePeriod.LAST_15_MINUTES, option.getTimePeriod());
                });

        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        assertDataTypesCleared(
                TimePeriod.LAST_15_MINUTES,
                BrowsingDataType.HISTORY,
                BrowsingDataType.SITE_DATA,
                BrowsingDataType.CACHE);
    }

    @Test
    @MediumTest
    public void testBrowsingDataDeletion_LastHourSelected() throws TimeoutException {
        openQuickDeleteDialog();

        View dialogView =
                mActivity
                        .getModalDialogManager()
                        .getCurrentDialogForTest()
                        .get(ModalDialogProperties.CUSTOM_VIEW);

        runOnUiThreadBlocking(
                () -> {
                    Spinner spinnerView = dialogView.findViewById(R.id.quick_delete_spinner);
                    // Set the time selection for LAST_HOUR.
                    spinnerView.setSelection(1);
                    var option =
                            (TimePeriodUtils.TimePeriodSpinnerOption) spinnerView.getSelectedItem();
                    assertEquals(TimePeriod.LAST_HOUR, option.getTimePeriod());
                });

        onViewWaiting(withId(R.id.positive_button)).perform(click());

        // Wait for browsing data deletion to complete.
        mCallbackHelper.waitForOnly();

        assertDataTypesCleared(
                TimePeriod.LAST_HOUR,
                BrowsingDataType.HISTORY,
                BrowsingDataType.SITE_DATA,
                BrowsingDataType.CACHE);
    }

    @Test
    @MediumTest
    public void testQuickDeleteHistogram_WhenClickingCancel() {
        openQuickDeleteDialog();

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        QuickDeleteMetricsDelegate.HISTOGRAM_NAME,
                        QuickDeleteMetricsDelegate.QuickDeleteAction.CANCEL_CLICKED);

        onViewWaiting(withId(R.id.negative_button)).perform(click());

        verify(mBrowsingDataBridgeMock, never())
                .clearBrowsingData(
                        any(), any(), any(), any(), anyInt(), any(), any(), any(), any());

        histogramWatcher.assertExpected();
    }

    @Test
    @MediumTest
    public void testQuickDeleteHistogram_WhenClickingMoreOptions() {
        openQuickDeleteDialog();

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newBuilder()
                        .expectIntRecord(
                                QuickDeleteMetricsDelegate.HISTOGRAM_NAME,
                                QuickDeleteMetricsDelegate.QuickDeleteAction.MORE_OPTIONS_CLICKED)
                        .expectIntRecord(
                                QuickDeleteMetricsDelegate.HISTOGRAM_NAME,
                                QuickDeleteMetricsDelegate.QuickDeleteAction
                                        .DIALOG_DISMISSED_IMPLICITLY)
                        .build();

        onViewWaiting(withId(R.id.quick_delete_more_options)).perform(click());

        histogramWatcher.assertExpected();
    }

    @Test
    @MediumTest
    public void testQuickDeleteHistogram_WhenClickingBackButton() {
        openQuickDeleteDialog();

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        QuickDeleteMetricsDelegate.HISTOGRAM_NAME,
                        QuickDeleteMetricsDelegate.QuickDeleteAction.DIALOG_DISMISSED_IMPLICITLY);

        // Implicitly dismiss pop up by pressing Clank's back button.
        pressBack();

        histogramWatcher.assertExpected();
    }

    @Test
    @MediumTest
    public void testMoreOptions_Triggers_ClearBrowsingData_Advanced() {
        final Instrumentation.ActivityMonitor activityMonitor =
                new Instrumentation.ActivityMonitor(SettingsActivity.class.getName(), null, false);
        InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor);

        openQuickDeleteDialog();
        // Wait for the dialog to show-up so we can retrieve the dialog in the next line.
        onViewWaiting(withId(R.id.quick_delete_spinner)).check(matches(isDisplayed()));
        onView(withId(R.id.quick_delete_more_options)).perform(click());

        SettingsActivity activity =
                (SettingsActivity)
                        InstrumentationRegistry.getInstrumentation()
                                .waitForMonitorWithTimeout(activityMonitor, ACTIVITY_WAIT_LONG_MS);

        assertTrue(activity.getMainFragment() instanceof ClearBrowsingDataFragmentAdvanced);
    }

    @Test
    @MediumTest
    @Restriction(Restriction.RESTRICTION_TYPE_INTERNET)
    public void testQuickDeleteTabsNotClosed_WithMultiInstance() {
        MultiWindowUtils.setInstanceCountForTesting(3);
        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher("Privacy.QuickDelete.TabsEnabled", false);

        mActivityTestRule.loadUrl("https://www.google.com/");
        assertEquals(1, mActivity.getCurrentTabModel().getCount());

        openQuickDeleteDialog();

        onViewWaiting(withId(R.id.positive_button)).perform(click());
        assertEquals(1, mActivity.getCurrentTabModel().getCount());
        histogramWatcher.assertExpected();
    }
}