chromium/chrome/browser/tab_resumption/junit/src/org/chromium/chrome/browser/tab_resumption/TabResumptionModuleSuggestionsUnitTest.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.tab_resumption;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Size;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.robolectric.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_resumption.UrlImageProvider.UrlImageSource;
import org.chromium.chrome.browser.tab_ui.ThumbnailProvider;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.favicon.IconType;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class TabResumptionModuleSuggestionsUnitTest extends TestSupport {

    // Fake LargeIconBridge, which is explicitly faked for testing since it uses
    // a callback to pass results.
    class FakeLargeIconBridge extends LargeIconBridge {
        private static final int DEFAULT_FALLBACK_COLOR = 0xff0000ff;

        GURL mFakeCachedUrl;
        Bitmap mFakeCachedBitmap;

        FakeLargeIconBridge(GURL fakeCachedUrl, Bitmap fakeCachedBitmap) {
            mFakeCachedUrl = fakeCachedUrl;
            mFakeCachedBitmap = fakeCachedBitmap;
        }

        @Override
        public boolean getLargeIconForUrl(
                final GURL pageUrl,
                int minSizePx,
                int desiredSizePx,
                final LargeIconCallback callback) {
            // For simplicity, fake with a synchronous call.
            callback.onLargeIconAvailable(
                    pageUrl.equals(mFakeCachedUrl) ? mFakeCachedBitmap : /* tab= */ null,
                    DEFAULT_FALLBACK_COLOR,
                    /*isFallbackColorDefault*/ true,
                    IconType.FAVICON);
            return true;
        }
    }

    @Mock private Profile mProfile;

    // Various test value that satisfy FOO_LO < FOO_0 < FOO_HI.
    private static final String SOURCE_NAME_LO = "Desktop";
    private static final String SOURCE_NAME_0 = "My Phone";
    private static final String SOURCE_NAME_HI = "Phone 2";
    private static final GURL URL_LO = JUnitTestGURLs.BLUE_1;
    private static final GURL URL_0 = JUnitTestGURLs.GOOGLE_URL_DOG;
    private static final GURL URL_HI = JUnitTestGURLs.RED_2;
    private static final String TITLE_LO = "Blue 1";
    private static final String TITLE_0 = "Google";
    private static final String TITLE_HI = "Red 2";
    private static final long TIMESTAMP_LO = makeTimestamp(3, 2, 1);
    private static final long TIMESTAMP_0 = makeTimestamp(5, 5, 5);
    private static final long TIMESTAMP_HI = makeTimestamp(7, 8, 9);
    private static final int ID_LO = 1;
    private static final int ID_0 = 5;
    private static final int ID_HI = 10;

    private int mCallbackCounter;

    @Before
    public void setUp() {}

    @After
    public void tearDown() {}

    private static SuggestionEntry createSuggestionEntry(
            String source, GURL url, String title, long time, int id) {
        return new SuggestionEntry(
                SuggestionEntryType.LOCAL_TAB,
                source,
                url,
                title,
                time,
                id,
                null,
                null,
                /* needMatchLocalTab= */ false);
    }

    @Test
    @SmallTest
    public void testCompareSuggestions() {
        SuggestionEntry entry0 =
                createSuggestionEntry(SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_0, ID_0);
        Assert.assertEquals(
                0,
                entry0.compareTo(
                        createSuggestionEntry(SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_0, ID_0)));

        // Timestamps dominate source name.
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_LO, ID_0))
                        < 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_LO, URL_0, TITLE_0, TIMESTAMP_LO, ID_0))
                        < 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_HI, URL_0, TITLE_0, TIMESTAMP_LO, ID_0))
                        < 0);

        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_HI, ID_0))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_LO, URL_0, TITLE_0, TIMESTAMP_HI, ID_0))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_HI, URL_0, TITLE_0, TIMESTAMP_HI, ID_0))
                        > 0);

        // Source name dominates title.
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_LO, URL_0, TITLE_0, TIMESTAMP_0, ID_0))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_LO, URL_0, TITLE_LO, TIMESTAMP_0, ID_0))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_LO, URL_0, TITLE_HI, TIMESTAMP_0, ID_0))
                        > 0);

        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_HI, URL_0, TITLE_0, TIMESTAMP_0, ID_0))
                        < 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_HI, URL_0, TITLE_LO, TIMESTAMP_0, ID_0))
                        < 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_HI, URL_0, TITLE_HI, TIMESTAMP_0, ID_0))
                        < 0);

        // Title dominates id.
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_LO, TIMESTAMP_0, ID_0))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_LO, TIMESTAMP_0, ID_LO))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_LO, TIMESTAMP_0, ID_HI))
                        > 0);

        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_HI, TIMESTAMP_0, ID_0))
                        < 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_HI, TIMESTAMP_0, ID_LO))
                        < 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_HI, TIMESTAMP_0, ID_HI))
                        < 0);

        // Id as final tie-breaker.
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_0, ID_LO))
                        > 0);
        Assert.assertTrue(
                entry0.compareTo(
                                createSuggestionEntry(
                                        SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_0, ID_HI))
                        < 0);

        // URL doesn't matter.
        Assert.assertEquals(
                0,
                entry0.compareTo(
                        createSuggestionEntry(SOURCE_NAME_0, URL_LO, TITLE_0, TIMESTAMP_0, ID_0)));
        Assert.assertEquals(
                0,
                entry0.compareTo(
                        createSuggestionEntry(SOURCE_NAME_0, URL_HI, TITLE_0, TIMESTAMP_0, ID_0)));
    }

    @Test
    @SmallTest
    public void testCompareSuggestionsWithTraingIds() {
        SuggestionEntry entry =
                createSuggestionEntry(SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_0, ID_0);
        SuggestionEntry entryWithTrainingInfo =
                createSuggestionEntry(SOURCE_NAME_0, URL_0, TITLE_0, TIMESTAMP_0, ID_0);
        entryWithTrainingInfo.trainingInfo =
                new TrainingInfo(
                        /* nativeVisitedUrlRankingBackend= */ 0L,
                        /* visitId= */ "www.google.com",
                        /* requestId= */ 123L);

        // The presence of `trainingInfo` does not affect comparison.
        Assert.assertEquals(0, entry.compareTo(entryWithTrainingInfo));
    }

    @Test
    @SmallTest
    public void testUrlImageProvider() {
        GURL urlWithFavicon = URL_0;
        GURL urlWithoutFavicon = URL_LO;
        Bitmap expectedRealIcon = makeBitmap(64, 64);
        Bitmap expectedThumbnail = makeBitmap(32, 32);
        Bitmap expectedFallbackIcon = makeBitmap(64, 64);
        LargeIconBridge largeIconBridge = new FakeLargeIconBridge(urlWithFavicon, expectedRealIcon);
        ThumbnailProvider thumbnailProvider = Mockito.mock(ThumbnailProvider.class);
        doAnswer(
                        (InvocationOnMock invocation) -> {
                            ((Callback<Drawable>) invocation.getArguments()[3])
                                    .onResult(new BitmapDrawable(expectedThumbnail));
                            return null;
                        })
                .when(thumbnailProvider)
                .getTabThumbnailWithCallback(
                        /* tabId= */ anyInt(),
                        /* thumbnailSize= */ any(Size.class),
                        /* isSelected= */ anyBoolean(),
                        /* callback= */ any(Callback.class));
        RoundedIconGenerator roundedIconGenerator = Mockito.mock(RoundedIconGenerator.class);
        when(roundedIconGenerator.generateIconForUrl(urlWithoutFavicon))
                .thenReturn(expectedFallbackIcon);

        UrlImageSource urlImageSource = Mockito.mock(UrlImageSource.class);
        when(urlImageSource.createThumbnailProvider()).thenReturn(thumbnailProvider);
        when(urlImageSource.createIconGenerator()).thenReturn(roundedIconGenerator);
        Context context = ApplicationProvider.getApplicationContext();
        UrlImageProvider urlImageProvider =
                new UrlImageProvider(context, urlImageSource, null, largeIconBridge);

        urlImageProvider.fetchImageForUrl(
                urlWithFavicon,
                (Bitmap icon) -> {
                    Assert.assertEquals(icon, expectedRealIcon);
                    ++mCallbackCounter;
                });

        Assert.assertEquals(1, mCallbackCounter);

        urlImageProvider.getTabThumbnail(
                /* tabId= */ 0,
                /* thumbnailSize= */ new Size(32, 32),
                /* tabThumbnailCallback= */ (Drawable icon) -> {
                    Assert.assertEquals(((BitmapDrawable) icon).getBitmap(), expectedThumbnail);
                    ++mCallbackCounter;
                });
        Assert.assertEquals(2, mCallbackCounter);

        urlImageProvider.fetchImageForUrl(
                urlWithoutFavicon,
                (Bitmap icon) -> {
                    Assert.assertEquals(icon, expectedFallbackIcon);
                    ++mCallbackCounter;
                });

        Assert.assertEquals(3, mCallbackCounter);

        urlImageProvider.destroy();
        assertNull(urlImageProvider.getImageServiceBridgeForTesting());
        assertNull(urlImageProvider.getLargeIconBridgeForTesting());
        assertTrue(urlImageProvider.isDestroyed());
    }

    @Test
    public void testIsLocalTab() {
        SuggestionEntry entry =
                new SuggestionEntry(
                        SuggestionEntryType.LOCAL_TAB,
                        SOURCE_NAME_0,
                        URL_0,
                        TITLE_0,
                        TIMESTAMP_0,
                        ID_0,
                        null,
                        null,
                        /* needMatchLocalTab= */ false);
        assertTrue(entry.isLocalTab());

        entry =
                new SuggestionEntry(
                        SuggestionEntryType.HISTORY,
                        SOURCE_NAME_0,
                        URL_0,
                        TITLE_0,
                        TIMESTAMP_0,
                        ID_0,
                        null,
                        null,
                        /* needMatchLocalTab= */ false);
        assertTrue(entry.isLocalTab());

        SuggestionEntry invalidEntry =
                new SuggestionEntry(
                        SuggestionEntryType.LOCAL_TAB,
                        SOURCE_NAME_0,
                        URL_0,
                        TITLE_0,
                        TIMESTAMP_0,
                        Tab.INVALID_TAB_ID,
                        null,
                        null,
                        /* needMatchLocalTab= */ false);
        assertFalse(invalidEntry.isLocalTab());
    }
}