chromium/chrome/android/junit/src/org/chromium/chrome/browser/search_engines/SearchEngineChoiceNotificationTest.java

// 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.search_engines;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.content.Intent;

import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.version_info.VersionInfo;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.search_engines.TemplateUrlService;

/** Unit tests for {@link SearchEngineChoiceNotification}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@EnableFeatures({})
public final class SearchEngineChoiceNotificationTest {
    private static final String TEST_INITIAL_ENGINE = "google.com";
    private static final String TEST_ALTERNATIVE_ENGINE = "duckduckgo.com";

    @Spy private Context mContext = RuntimeEnvironment.application.getApplicationContext();
    @Mock private SnackbarManager mSnackbarManager;
    @Mock private TemplateUrlService mTemplateUrlService;
    @Mock private TemplateUrl mInitialSearchEngine;
    @Mock private TemplateUrl mAlternativeSearchEngine;
    @Mock private Profile mProfile;
    @Captor private ArgumentCaptor<Snackbar> mSnackbarArgument;
    @Mock private SettingsLauncher mSettingsLauncher;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        ContextUtils.initApplicationContextForTests(mContext);

        // Sets up appropriate responses from Template URL service.
        ProfileManager.setLastUsedProfileForTesting(mProfile);
        TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlService);
        SettingsLauncherFactory.setInstanceForTesting(mSettingsLauncher);
        doReturn(TEST_ALTERNATIVE_ENGINE).when(mAlternativeSearchEngine).getKeyword();
        doReturn(SearchEngineType.SEARCH_ENGINE_DUCKDUCKGO)
                .when(mTemplateUrlService)
                .getSearchEngineTypeFromTemplateUrl(TEST_ALTERNATIVE_ENGINE);
        doReturn(TEST_INITIAL_ENGINE).when(mInitialSearchEngine).getKeyword();
        doReturn(SearchEngineType.SEARCH_ENGINE_GOOGLE)
                .when(mTemplateUrlService)
                .getSearchEngineTypeFromTemplateUrl(TEST_INITIAL_ENGINE);
        doReturn(mInitialSearchEngine)
                .when(mTemplateUrlService)
                .getDefaultSearchEngineTemplateUrl();
    }

    @Test
    @SmallTest
    public void receiveSearchEngineChoiceRequest() {
        SharedPreferencesManager prefs = ChromeSharedPreferences.getInstance();
        assertFalse(prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_REQUESTED_TIMESTAMP));
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        assertTrue(prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_REQUESTED_TIMESTAMP));

        long firstTimestamp =
                prefs.readLong(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_REQUESTED_TIMESTAMP);
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        long secondTimestamp =
                prefs.readLong(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_REQUESTED_TIMESTAMP);

        assertEquals(firstTimestamp, secondTimestamp);
    }

    @Test
    @SmallTest
    public void handleSearchEngineChoice_ignoredWhenNotRequested() {
        SharedPreferencesManager prefs = ChromeSharedPreferences.getInstance();
        assertFalse(prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_PRESENTED_VERSION));

        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, null);

        assertFalse(
                "When not requested, the call should have been ignored.",
                prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_PRESENTED_VERSION));

        assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SNACKBAR_SHOWN));
    }

    @Test
    @SmallTest
    public void handleSearchEngineChoice_ignoredWhenDefaultSearchManaged() {
        doReturn(true).when(mTemplateUrlService).isDefaultSearchManaged();
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        SharedPreferencesManager prefs = ChromeSharedPreferences.getInstance();
        assertFalse(prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_PRESENTED_VERSION));

        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, null);

        assertFalse(
                "When search engine settings are controlled by policy, the call should be ignored.",
                prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_PRESENTED_VERSION));

        assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SNACKBAR_SHOWN));
    }

    @Test
    @SmallTest
    public void handleSearchEngineChoice_performedFirstTime() {
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        // TODO(fgorski): Snackbar content is scoped to its package, therefore cannot be verified
        // here at this time. See whether that can be fixed.
        verify(mSnackbarManager, times(1)).showSnackbar(any(Snackbar.class));

        assertEquals(
                "We are expecting exactly one snackbar shown event.",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SNACKBAR_SHOWN));

        SharedPreferencesManager prefs = ChromeSharedPreferences.getInstance();
        assertTrue(
                "Version of the app should be persisted upon prompting.",
                prefs.contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_PRESENTED_VERSION));

        assertEquals(
                "Presented version should be set to the current product version.",
                VersionInfo.getProductVersion(),
                prefs.readString(
                        ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_PRESENTED_VERSION, null));
    }

    @Test
    @SmallTest
    public void handleSearchEngineChoice_ignoredOnSubsequentCalls() {
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        verify(mSnackbarManager, times(1)).showSnackbar(any(Snackbar.class));

        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        assertFalse(
                "Second call removes the preference for search engine choice before.",
                ChromeSharedPreferences.getInstance()
                        .contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_DEFAULT_TYPE_BEFORE));

        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);

        // No increase in execution counter means it was not called again.
        verify(mSnackbarManager, times(1)).showSnackbar(any(Snackbar.class));
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SNACKBAR_SHOWN));
    }

    @Test
    @SmallTest
    public void snackbarClicked() {
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();

        // We do not use a mock for SettingsLauncher here since the test needs to
        // verify that the launcher actually starts an activity.
        SettingsLauncherFactory.setInstanceForTesting(null);
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        verify(mSnackbarManager, times(1)).showSnackbar(mSnackbarArgument.capture());

        mSnackbarArgument.getValue().getController().onAction(null);
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.PROMPT_FOLLOWED));
        verify(mContext, times(1)).startActivity(any(Intent.class), isNull());
    }

    @Test
    @SmallTest
    public void reportSearchEngineChanged_whenNoChange() {
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        verify(mSnackbarManager, times(1)).showSnackbar(mSnackbarArgument.capture());
        mSnackbarArgument.getValue().getController().onAction(null);

        // Simulates no change.
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);

        assertFalse(
                "First handleSearchEngineChoice call after prompt removes SE choice before pref.",
                ChromeSharedPreferences.getInstance()
                        .contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_DEFAULT_TYPE_BEFORE));

        assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SEARCH_ENGINE_CHANGED));
        assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.ChosenSearchEngine",
                        SearchEngineType.SEARCH_ENGINE_DUCKDUCKGO));
    }

    @Test
    @SmallTest
    public void reportSearchEngineChanged_whenNoChangeOnFirstVisitToSettings() {
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        verify(mSnackbarManager, times(1)).showSnackbar(mSnackbarArgument.capture());
        mSnackbarArgument.getValue().getController().onAction(null);

        // Simulates a change between the initialization, but reporting happens only the first time.
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        assertFalse(
                "First handleSearchEngineChoice call after prompt removes SE choice before pref.",
                ChromeSharedPreferences.getInstance()
                        .contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_DEFAULT_TYPE_BEFORE));

        doReturn(mAlternativeSearchEngine)
                .when(mTemplateUrlService)
                .getDefaultSearchEngineTemplateUrl();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);

        assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SEARCH_ENGINE_CHANGED));
        assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.ChosenSearchEngine",
                        SearchEngineType.SEARCH_ENGINE_DUCKDUCKGO));
    }

    @Test
    @SmallTest
    public void reportSearchEngineChanged_onlyFirstTime() {
        SearchEngineChoiceNotification.receiveSearchEngineChoiceRequest();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);
        verify(mSnackbarManager, times(1)).showSnackbar(mSnackbarArgument.capture());
        mSnackbarArgument.getValue().getController().onAction(null);

        // Simulates a change of search engine on the first visit to settings.
        doReturn(mAlternativeSearchEngine)
                .when(mTemplateUrlService)
                .getDefaultSearchEngineTemplateUrl();
        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);

        assertEquals(
                "Event is recorded when search engine was changed.",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SEARCH_ENGINE_CHANGED));
        assertEquals(
                "Newly chosen search engine type should be recoreded.",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.ChosenSearchEngine",
                        SearchEngineType.SEARCH_ENGINE_DUCKDUCKGO));

        assertFalse(
                "First handleSearchEngineChoice call after prompt removes SE choice before pref.",
                ChromeSharedPreferences.getInstance()
                        .contains(ChromePreferenceKeys.SEARCH_ENGINE_CHOICE_DEFAULT_TYPE_BEFORE));

        SearchEngineChoiceNotification.handleSearchEngineChoice(mContext, mSnackbarManager);

        assertEquals(
                "Event should only be recorded once, therefore count should be still 1.",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.Events",
                        SearchEngineChoiceMetrics.Events.SEARCH_ENGINE_CHANGED));
        assertEquals(
                "New Search Engine shoudl only be reported once, therefore count should be 1",
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.SearchEngineChoice.ChosenSearchEngine",
                        SearchEngineType.SEARCH_ENGINE_DUCKDUCKGO));
    }
}