chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/SearchEngineUtilsUnitTest.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.omnibox;

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.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;

import android.content.res.Resources;
import android.graphics.Bitmap;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.UmaRecorderHolder;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.locale.LocaleManagerDelegate;
import org.chromium.chrome.browser.omnibox.status.StatusProperties.StatusIconResource;
import org.chromium.chrome.browser.omnibox.test.R;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.theme.ThemeUtils;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.url.GURL;

/** Tests for SearchEngineUtils. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SearchEngineUtilsUnitTest {
    private static final String LOGO_URL = "https://www.search.com/";
    private static final String TEMPLATE_URL = "https://www.search.com/search?q={query}";
    private static final String EVENTS_HISTOGRAM = "AndroidSearchEngineLogo.Events";

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Captor ArgumentCaptor<FaviconHelper.FaviconImageCallback> mCallbackCaptor;
    @Mock FaviconHelper mFaviconHelper;
    @Mock TemplateUrlService mTemplateUrlService;
    @Mock TemplateUrl mTemplateUrl;
    @Mock LocaleManagerDelegate mLocaleManagerDelegate;
    @Mock Resources mResources;
    @Mock Profile mProfile;

    SearchEngineUtils mSearchEngineUtils;
    Bitmap mBitmap;

    @Before
    public void setUp() {
        mBitmap = Shadow.newInstanceOf(Bitmap.class);
        shadowOf(mBitmap).appendDescription("test");

        TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlService);

        doReturn(TEMPLATE_URL).when(mTemplateUrl).getURL();
        doReturn(mTemplateUrl).when(mTemplateUrlService).getDefaultSearchEngineTemplateUrl();
        doReturn(false).when(mTemplateUrlService).isDefaultSearchEngineGoogle();
        doReturn(true)
                .when(mFaviconHelper)
                .getLocalFaviconImageForURL(any(), any(), anyInt(), any());
        doReturn(false).when(mLocaleManagerDelegate).needToCheckForSearchEnginePromo();
        LocaleManager.getInstance().setDelegateForTest(mLocaleManagerDelegate);

        lenient()
                .doReturn(true)
                .when(mFaviconHelper)
                .getLocalFaviconImageForURL(any(), any(), anyInt(), mCallbackCaptor.capture());

        // Used when creating bitmaps, needs to be greater than 0.
        doReturn(1).when(mResources).getDimensionPixelSize(anyInt());

        mSearchEngineUtils = new SearchEngineUtils(mProfile, mFaviconHelper);
    }

    @After
    public void tearDown() {
        UmaRecorderHolder.resetForTesting();
    }

    @Test
    public void testDefaultEnabledBehavior() {
        // Show DSE logo when using regular profile.
        doReturn(false).when(mProfile).isOffTheRecord();
        mSearchEngineUtils = new SearchEngineUtils(mProfile, mFaviconHelper);
        assertTrue(mSearchEngineUtils.shouldShowSearchEngineLogo());

        // Suppress DSE logo when using incognito profile.
        doReturn(true).when(mProfile).isOffTheRecord();
        mSearchEngineUtils = new SearchEngineUtils(mProfile, mFaviconHelper);
        assertFalse(mSearchEngineUtils.shouldShowSearchEngineLogo());
    }

    @Test
    public void recordEvent() {
        UmaRecorderHolder.resetForTesting();

        mSearchEngineUtils.recordEvent(SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST);
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST));

        mSearchEngineUtils.recordEvent(SearchEngineUtils.Events.FETCH_SUCCESS_CACHE_HIT);
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_SUCCESS_CACHE_HIT));
    }

    @Test
    public void getSearchEngineLogo() {
        // SearchEngineUtils retrieves logo when it's first created, and whenever the DSE changes.
        verify(mFaviconHelper).getLocalFaviconImageForURL(any(), any(), anyInt(), any());
        mCallbackCaptor.getValue().onFaviconAvailable(mBitmap, new GURL(LOGO_URL));

        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST));
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_SUCCESS));

        var expected = new StatusIconResource(LOGO_URL, mBitmap, 0);
        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);
        assertEquals(icon, expected);
    }

    @Test
    public void getSearchEngineLogo_nullTemplateUrlService() {
        StatusIconResource expected =
                SearchEngineUtils.getFallbackSearchIcon(BrandedColorScheme.APP_DEFAULT);

        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);

        assertEquals(icon, expected);
    }

    @Test
    public void getSearchEngineLogo_searchEngineGoogle() {
        // Simulate DSE change to Google.
        doReturn(true).when(mTemplateUrlService).isDefaultSearchEngineGoogle();
        mSearchEngineUtils.onTemplateURLServiceChanged();

        var expected = new StatusIconResource(R.drawable.ic_logo_googleg_20dp, 0);
        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);
        assertEquals(icon, expected);
    }

    @Test
    public void getSearchEngineLogo_faviconCached() {
        verify(mFaviconHelper).getLocalFaviconImageForURL(any(), any(), anyInt(), any());
        mCallbackCaptor.getValue().onFaviconAvailable(mBitmap, new GURL(LOGO_URL));

        var expected = new StatusIconResource(LOGO_URL, mBitmap, 0);
        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);
        assertEquals(icon, expected);

        icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);
        assertEquals(icon, expected);

        // Expect only one actual fetch, that happens independently from get request.
        // All get requests always supply cached value.
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST));
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_SUCCESS));
        assertEquals(
                2,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_SUCCESS_CACHE_HIT));
    }

    @Test
    public void getSearchEngineLogo_nullUrl() {
        UmaRecorderHolder.resetForTesting();

        // Simulate DSE change - policy blocking searches
        doReturn(null).when(mTemplateUrlService).getDefaultSearchEngineTemplateUrl();
        mSearchEngineUtils.onTemplateURLServiceChanged();

        var expected = SearchEngineUtils.getFallbackSearchIcon(BrandedColorScheme.APP_DEFAULT);
        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);

        assertEquals(icon, expected);
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST));
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_FAILED_NULL_URL));
    }

    @Test
    public void getSearchEngineLogo_faviconHelperError() {
        UmaRecorderHolder.resetForTesting();

        // Simulate FaviconFetcher failure on the next TemplateUrl change.
        doReturn(false)
                .when(mFaviconHelper)
                .getLocalFaviconImageForURL(any(), any(), anyInt(), mCallbackCaptor.capture());
        mSearchEngineUtils.onTemplateURLServiceChanged();

        var expected = SearchEngineUtils.getFallbackSearchIcon(BrandedColorScheme.APP_DEFAULT);
        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);

        assertEquals(icon, expected);
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST));
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM,
                        SearchEngineUtils.Events.FETCH_FAILED_FAVICON_HELPER_ERROR));
    }

    @Test
    public void getSearchEngineLogo_returnedBitmapNull() {
        StatusIconResource expected =
                SearchEngineUtils.getFallbackSearchIcon(BrandedColorScheme.APP_DEFAULT);

        var icon = mSearchEngineUtils.getSearchEngineLogo(BrandedColorScheme.APP_DEFAULT);
        verify(mFaviconHelper)
                .getLocalFaviconImageForURL(any(), any(), anyInt(), mCallbackCaptor.capture());
        FaviconHelper.FaviconImageCallback faviconCallback = mCallbackCaptor.getValue();
        faviconCallback.onFaviconAvailable(null, new GURL(LOGO_URL));

        assertEquals(icon, expected);
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM, SearchEngineUtils.Events.FETCH_NON_GOOGLE_LOGO_REQUEST));
        assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        EVENTS_HISTOGRAM,
                        SearchEngineUtils.Events.FETCH_FAILED_RETURNED_BITMAP_NULL));
    }

    @Test
    public void getFallbackSearchIcon() {
        StatusIconResource expected =
                new StatusIconResource(
                        R.drawable.ic_search, R.color.default_icon_color_white_tint_list);
        Assert.assertEquals(
                expected,
                SearchEngineUtils.getFallbackSearchIcon(BrandedColorScheme.DARK_BRANDED_THEME));

        expected =
                new StatusIconResource(
                        R.drawable.ic_search,
                        ThemeUtils.getThemedToolbarIconTintRes(/* useLight= */ true));
        Assert.assertEquals(
                expected, SearchEngineUtils.getFallbackSearchIcon(BrandedColorScheme.INCOGNITO));
    }

    @Test
    public void getFallbackNavigationIcon() {
        StatusIconResource expected =
                new StatusIconResource(
                        R.drawable.ic_globe_24dp, R.color.default_icon_color_white_tint_list);

        Assert.assertEquals(
                expected,
                SearchEngineUtils.getFallbackNavigationIcon(BrandedColorScheme.DARK_BRANDED_THEME));

        expected =
                new StatusIconResource(
                        R.drawable.ic_globe_24dp,
                        ThemeUtils.getThemedToolbarIconTintRes(/* useLight= */ true));
        Assert.assertEquals(
                expected,
                SearchEngineUtils.getFallbackNavigationIcon(BrandedColorScheme.INCOGNITO));
    }

    @Test
    public void needToCheckForSearchEnginePromo_SecurityExceptionThrown() {
        doThrow(SecurityException.class)
                .when(mLocaleManagerDelegate)
                .needToCheckForSearchEnginePromo();

        try {
            mSearchEngineUtils.needToCheckForSearchEnginePromo();
        } catch (Exception e) {
            Assert.fail("No exception should be thrown.");
        }
    }

    @Test
    public void needToCheckForSearchEnginePromo_DeadObjectRuntimeExceptionThrown() {
        doThrow(RuntimeException.class)
                .when(mLocaleManagerDelegate)
                .needToCheckForSearchEnginePromo();

        try {
            mSearchEngineUtils.needToCheckForSearchEnginePromo();
        } catch (Exception e) {
            Assert.fail("No exception should be thrown.");
        }
    }

    @Test
    public void needToCheckForSearchEnginePromo_resultCached() {
        doThrow(RuntimeException.class)
                .when(mLocaleManagerDelegate)
                .needToCheckForSearchEnginePromo();
        assertFalse(mSearchEngineUtils.needToCheckForSearchEnginePromo());

        Mockito.reset(mLocaleManagerDelegate);

        doReturn(true).when(mLocaleManagerDelegate).needToCheckForSearchEnginePromo();

        assertTrue(mSearchEngineUtils.needToCheckForSearchEnginePromo());

        Mockito.reset(mLocaleManagerDelegate);

        doReturn(false).when(mLocaleManagerDelegate).needToCheckForSearchEnginePromo();

        assertFalse(mSearchEngineUtils.needToCheckForSearchEnginePromo());
        assertFalse(mSearchEngineUtils.needToCheckForSearchEnginePromo());
        assertFalse(mSearchEngineUtils.needToCheckForSearchEnginePromo());

        verify(mLocaleManagerDelegate, times(1)).needToCheckForSearchEnginePromo();
    }

    private static Bitmap createSolidImage(int width, int height, int color) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        for (int x = 0; x < bitmap.getWidth(); x++) {
            for (int y = 0; y < bitmap.getHeight(); y++) {
                bitmap.setPixel(x, y, color);
            }
        }
        return bitmap;
    }

    private static Bitmap createSolidImageWithDifferentInnerColor(
            int width, int height, int outerColor, int innerColor) {
        Bitmap bitmap = createSolidImage(width, height, outerColor);
        for (int x = 1; x < bitmap.getWidth() - 1; x++) {
            for (int y = 1; y < bitmap.getHeight() - 1; y++) {
                bitmap.setPixel(x, y, innerColor);
            }
        }
        return bitmap;
    }

    private static Bitmap createSolidImageWithSlighlyLargerEdgeCoverage(
            int width, int height, int largerColor, int smallerColor) {
        Bitmap bitmap = createSolidImage(width, height, largerColor);
        for (int x = 0; x < bitmap.getWidth(); x++) {
            for (int y = bitmap.getHeight() + 1; y < bitmap.getHeight(); y++) {
                bitmap.setPixel(x, y, smallerColor);
            }
        }
        return bitmap;
    }
}