// Copyright 2022 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.styles;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
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.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.favicon.LargeIconBridge.LargeIconCallback;
import org.chromium.components.favicon.LargeIconBridgeJni;
import org.chromium.components.image_fetcher.ImageFetcher;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;
/** Tests for {@link OmniboxImageSupplier}. */
@RunWith(BaseRobolectricTestRunner.class)
public final class OmniboxImageSupplierUnitTest {
private static final GURL NAV_URL = JUnitTestGURLs.URL_1;
private static final GURL NAV_URL_2 = JUnitTestGURLs.URL_2;
private static final int FALLBACK_COLOR = 0xACE0BA5E;
public @Rule MockitoRule mockitoRule = MockitoJUnit.rule();
public @Rule JniMocker mJniMocker = new JniMocker();
private ArgumentCaptor<LargeIconCallback> mIconCallbackCaptor =
ArgumentCaptor.forClass(LargeIconCallback.class);
private OmniboxImageSupplier mSupplier;
private @Mock Bitmap mBitmap1;
private @Mock Bitmap mBitmap2;
private @Mock RoundedIconGenerator mIconGenerator;
private @Mock LargeIconBridge.Natives mLargeIconBridgeJni;
private @Mock Callback<Bitmap> mCallback1;
private @Mock Callback<Bitmap> mCallback2;
private @Mock Profile mProfile;
private @Mock ImageFetcher mImageFetcher;
private @Px int mFaviconSize;
@Before
public void setUp() {
mJniMocker.mock(LargeIconBridgeJni.TEST_HOOKS, mLargeIconBridgeJni);
var context = ContextUtils.getApplicationContext();
mFaviconSize =
context.getResources()
.getDimensionPixelSize(R.dimen.omnibox_suggestion_favicon_size);
assert mFaviconSize != 0;
mSupplier = new OmniboxImageSupplier(context);
mSupplier.setRoundedIconGeneratorForTesting(mIconGenerator);
doReturn(1L).when(mLargeIconBridgeJni).init();
doReturn(true)
.when(mLargeIconBridgeJni)
.getLargeIconForURL(anyLong(), any(), any(), anyInt(), anyInt(), any());
}
/**
* Confirm that icon of expected size was requested from LargeIconBridge, and report a supplied
* bitmap back to the caller.
*
* @param url the url to expect a lookup for
* @param bitmap the bitmap to return to the caller (may be null)
*/
private void verifyLargeIconBridgeRequest(@NonNull GURL url, @Nullable Bitmap bitmap) {
ShadowLooper.runUiThreadTasks();
verify(mLargeIconBridgeJni)
.getLargeIconForURL(
anyLong(),
eq(mProfile),
eq(url),
eq(mFaviconSize / 2),
eq(mFaviconSize),
mIconCallbackCaptor.capture());
mIconCallbackCaptor.getValue().onLargeIconAvailable(bitmap, FALLBACK_COLOR, true, 0);
}
/**
* Confirm the type of icon reported to the caller.
*
* @param bitmap The expected bitmap.
* @param type The expected favicon type.
*/
private void verifyReturnedIcon(@Nullable Bitmap bitmap) {
verify(mCallback1, times(1)).onResult(eq(bitmap));
}
/**
* Confirm no unexpected calls were made to any of our data producers or consumers and clear all
* counters.
*/
private void verifyNoOtherInteractionsAndClearInteractions() {
verifyNoMoreInteractions(mLargeIconBridgeJni);
clearInvocations(mLargeIconBridgeJni);
verifyNoMoreInteractions(mIconGenerator, mCallback1);
clearInvocations(mIconGenerator, mCallback1);
}
@Test
public void fetchFavicon_noLargeIconBridge() {
// Favicon service does not exist, so we should expect a _single_ call to
// RoundedIconGenerator.
mSupplier.fetchFavicon(NAV_URL, mCallback1);
verifyReturnedIcon(null);
verifyNoOtherInteractionsAndClearInteractions();
}
@Test
public void generateFavicon_beforeNativeInitialized() {
doReturn(mBitmap1).when(mIconGenerator).generateIconForUrl(NAV_URL);
mSupplier.generateFavicon(NAV_URL, mCallback1);
ShadowLooper.runUiThreadTasks();
verifyReturnedIcon(null);
verifyNoOtherInteractionsAndClearInteractions();
}
@Test
public void generateFavicon_afterNativeInitialized() {
doReturn(mBitmap1).when(mIconGenerator).generateIconForUrl(NAV_URL);
mSupplier.onNativeInitialized();
mSupplier.generateFavicon(NAV_URL, mCallback1);
ShadowLooper.runUiThreadTasks();
verify(mIconGenerator, times(1)).generateIconForUrl(NAV_URL);
verifyReturnedIcon(mBitmap1);
verifyNoOtherInteractionsAndClearInteractions();
}
@Test
public void testIconRetrieval_largeIconAvailableWithNoBackoff() {
mSupplier.setProfile(mProfile);
verify(mLargeIconBridgeJni, times(1)).init();
mSupplier.fetchFavicon(NAV_URL, mCallback1);
verifyLargeIconBridgeRequest(NAV_URL, mBitmap1);
verifyReturnedIcon(mBitmap1);
verifyNoOtherInteractionsAndClearInteractions();
// Try again, expect icon to be served from LargeIconBridge cache.
mSupplier.fetchFavicon(NAV_URL, mCallback1);
verifyReturnedIcon(mBitmap1);
verifyNoOtherInteractionsAndClearInteractions();
}
@Test
public void testIconRetrieval_differentUrlsDontCollide() {
mSupplier.setProfile(mProfile);
verify(mLargeIconBridgeJni, times(1)).init();
mSupplier.fetchFavicon(NAV_URL, mCallback1);
verifyLargeIconBridgeRequest(NAV_URL, mBitmap1);
verifyReturnedIcon(mBitmap1);
verifyNoOtherInteractionsAndClearInteractions();
// Try another URL. Expect icon to *not* be served from any caches.
mSupplier.fetchFavicon(NAV_URL_2, mCallback1);
verifyLargeIconBridgeRequest(NAV_URL_2, mBitmap1);
verifyReturnedIcon(mBitmap1);
verifyNoOtherInteractionsAndClearInteractions();
}
@Test
public void testIconRetrieval_clearingCacheRestartsEntireFlow() {
mSupplier.setProfile(mProfile);
verify(mLargeIconBridgeJni, times(1)).init();
mSupplier.fetchFavicon(NAV_URL, mCallback1);
verifyLargeIconBridgeRequest(NAV_URL, mBitmap1);
verifyReturnedIcon(mBitmap1);
verifyNoOtherInteractionsAndClearInteractions();
mSupplier.resetCache();
// Retry. Expect the exact same flow with that same URL.
mSupplier.fetchFavicon(NAV_URL, mCallback1);
verifyLargeIconBridgeRequest(NAV_URL, mBitmap2);
verifyReturnedIcon(mBitmap2);
verifyNoOtherInteractionsAndClearInteractions();
}
@Test
public void destroy_releasesLargeIconBridgeIfSet() {
mSupplier.setProfile(mProfile);
verify(mLargeIconBridgeJni, times(1)).init();
verifyNoMoreInteractions(mLargeIconBridgeJni);
mSupplier.destroy();
verify(mLargeIconBridgeJni, times(1)).destroy(anyLong());
verifyNoMoreInteractions(mLargeIconBridgeJni);
}
@Test
public void setProfile_destroysOldLargeIconBridgeIfPresent() {
mSupplier.setProfile(mProfile);
verify(mLargeIconBridgeJni, times(1)).init();
verifyNoMoreInteractions(mLargeIconBridgeJni);
clearInvocations(mLargeIconBridgeJni);
// We technically don't expect the change to be "to the same profile", we don't check for
// this.
mSupplier.setProfile(mProfile);
verify(mLargeIconBridgeJni, times(1)).destroy(anyLong());
verify(mLargeIconBridgeJni, times(1)).init();
verifyNoMoreInteractions(mLargeIconBridgeJni);
}
@Test
public void resetCache_safeWhenBridgeAndFetcherNotAvailable() {
mSupplier.resetCache();
}
@Test
public void resetCache_clearsPendingQueues() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
mSupplier.resetCache();
verify(mImageFetcher, times(1)).clear();
}
@Test
public void fetchImage_aggregateMultipleRequestsForSameUrl_successfulFetch() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
var url = JUnitTestGURLs.RED_1;
// Issue 2 requests for the same URL.
mSupplier.fetchImage(url, mCallback1);
mSupplier.fetchImage(url, mCallback2);
// Observe only one interaction with ImageFetcher.
ArgumentCaptor<ImageFetcher.Params> paramCaptor =
ArgumentCaptor.forClass(ImageFetcher.Params.class);
ArgumentCaptor<Callback<Bitmap>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
verify(mImageFetcher, times(1)).fetchImage(paramCaptor.capture(), callbackCaptor.capture());
verifyNoMoreInteractions(mImageFetcher);
// Confirm the URL and no callbacks emitted to registered callbacks.
assertEquals(JUnitTestGURLs.RED_1.getSpec(), paramCaptor.getValue().url);
verifyNoMoreInteractions(mCallback1, mCallback2);
// Emit reply.
callbackCaptor.getValue().onResult(mBitmap1);
// Observe all listeners receiving notification.
verify(mCallback1, times(1)).onResult(mBitmap1);
verify(mCallback2, times(1)).onResult(mBitmap1);
}
@Test
public void fetchImage_aggregateMultipleRequestsForSameUrl_failingFetch() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
var url = JUnitTestGURLs.RED_1;
// Issue 2 requests for the same URL.
mSupplier.fetchImage(url, mCallback1);
mSupplier.fetchImage(url, mCallback2);
// Observe only one interaction with ImageFetcher.
ArgumentCaptor<ImageFetcher.Params> paramCaptor =
ArgumentCaptor.forClass(ImageFetcher.Params.class);
ArgumentCaptor<Callback<Bitmap>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
verify(mImageFetcher, times(1)).fetchImage(paramCaptor.capture(), callbackCaptor.capture());
verifyNoMoreInteractions(mImageFetcher);
// Confirm the URL and no callbacks emitted to registered callbacks.
assertEquals(JUnitTestGURLs.RED_1.getSpec(), paramCaptor.getValue().url);
verifyNoMoreInteractions(mCallback1, mCallback2);
// Emit reply.
callbackCaptor.getValue().onResult(null);
// Observe no listeners receiving notification.
verifyNoMoreInteractions(mCallback1, mCallback2);
// At this point listeners should be removed. Confirm this by emitting a fake second reply
// and confirm callbacks receiving no call.
callbackCaptor.getValue().onResult(mBitmap1);
verifyNoMoreInteractions(mCallback1, mCallback2);
}
@Test
public void fetchImage_aggregateMultipleRequestsForSameUrl_noFetcher() {
mSupplier.setImageFetcherForTesting(null);
var url = JUnitTestGURLs.RED_1;
// Issue 2 requests for the same URL.
mSupplier.fetchImage(url, mCallback1);
mSupplier.fetchImage(url, mCallback2);
verifyNoMoreInteractions(mImageFetcher);
// Observe listeners receiving no notification.
verifyNoMoreInteractions(mCallback1, mCallback2);
}
@Test
public void fetchImage_callbacksAreNotRetainedAfterCompletion() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
ArgumentCaptor<Callback<Bitmap>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
var url = JUnitTestGURLs.RED_1;
// Issue first request and observe the interaction with ImageFetcher.
mSupplier.fetchImage(url, mCallback1);
verify(mImageFetcher, times(1)).fetchImage(any(), callbackCaptor.capture());
// Resolve the image. Observe only the first callback receives notification.
callbackCaptor.getValue().onResult(mBitmap1);
verify(mCallback1, times(1)).onResult(mBitmap1);
verifyNoMoreInteractions(mImageFetcher, mCallback1, mCallback2);
// Issue second request. Observe second interaction with ImageFetcher.
mSupplier.fetchImage(url, mCallback2);
verify(mImageFetcher, times(2)).fetchImage(any(), callbackCaptor.capture());
// Resolve the image. Observe only the second callback receives notification.
callbackCaptor.getValue().onResult(mBitmap2);
verify(mCallback2, times(1)).onResult(mBitmap2);
verifyNoMoreInteractions(mImageFetcher, mCallback1, mCallback2);
}
@Test
public void fetchImage_requestsForNonOverlappingUrlsAreNotAggregated() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
var url1 = JUnitTestGURLs.RED_1;
var url2 = JUnitTestGURLs.RED_2;
ArgumentCaptor<Callback<Bitmap>> captor1 = ArgumentCaptor.forClass(Callback.class);
ArgumentCaptor<Callback<Bitmap>> captor2 = ArgumentCaptor.forClass(Callback.class);
// Issue 2 requests for two different URLs.
mSupplier.fetchImage(url1, mCallback1);
verify(mImageFetcher, times(1)).fetchImage(any(), captor1.capture());
mSupplier.fetchImage(url2, mCallback2);
verify(mImageFetcher, times(2)).fetchImage(any(), captor2.capture());
verifyNoMoreInteractions(mImageFetcher, mCallback1, mCallback2);
// Emit first reply.
captor1.getValue().onResult(mBitmap1);
verify(mCallback1, times(1)).onResult(mBitmap1);
verifyNoMoreInteractions(mImageFetcher, mCallback1, mCallback2);
// Emit second reply.
captor2.getValue().onResult(mBitmap2);
verify(mCallback2, times(1)).onResult(mBitmap2);
verifyNoMoreInteractions(mImageFetcher, mCallback1, mCallback2);
}
@Test
public void fetchImage_resultsAfterResetAreDiscarded() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
var url = JUnitTestGURLs.RED_1;
mSupplier.fetchImage(url, mCallback1);
// Observe only one interaction with ImageFetcher.
ArgumentCaptor<Callback<Bitmap>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
verify(mImageFetcher, times(1)).fetchImage(any(), callbackCaptor.capture());
verifyNoMoreInteractions(mImageFetcher, mCallback1);
// Simulate end of Omnibox interaction.
mSupplier.resetCache();
verify(mImageFetcher, times(1)).clear();
verifyNoMoreInteractions(mImageFetcher, mCallback1);
// Emit late reply. The callback should not be delivered.
callbackCaptor.getValue().onResult(mBitmap1);
verifyNoMoreInteractions(mImageFetcher, mCallback1);
}
@Test
public void fetchImage_resultsAfterProfileSwitchAreDiscarded() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
var url = JUnitTestGURLs.RED_1;
mSupplier.fetchImage(url, mCallback1);
// Observe only one interaction with ImageFetcher.
ArgumentCaptor<Callback<Bitmap>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
verify(mImageFetcher, times(1)).fetchImage(any(), callbackCaptor.capture());
verifyNoMoreInteractions(mImageFetcher, mCallback1);
// Simulate end of Omnibox interaction.
mSupplier.setProfile(mProfile);
verify(mImageFetcher, times(1)).clear();
verify(mImageFetcher, times(1)).destroy();
verifyNoMoreInteractions(mImageFetcher, mCallback1);
// Emit late reply. The callback should not be delivered.
callbackCaptor.getValue().onResult(mBitmap1);
verifyNoMoreInteractions(mImageFetcher, mCallback1);
}
@Test
public void fetchImage_resultsAfterDestroyAreDiscarded() {
mSupplier.setImageFetcherForTesting(mImageFetcher);
var url = JUnitTestGURLs.RED_1;
mSupplier.fetchImage(url, mCallback1);
// Observe only one interaction with ImageFetcher.
ArgumentCaptor<Callback<Bitmap>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
verify(mImageFetcher, times(1)).fetchImage(any(), callbackCaptor.capture());
verifyNoMoreInteractions(mImageFetcher, mCallback1);
mSupplier.destroy();
verify(mImageFetcher, times(1)).destroy();
verifyNoMoreInteractions(mImageFetcher, mCallback1);
// Emit late reply. The callback should not be delivered.
callbackCaptor.getValue().onResult(mBitmap1);
verifyNoMoreInteractions(mImageFetcher, mCallback1);
}
@Test
public void fetchImage_emptyUrlsAreRejected() {
mSupplier.setImageFetcherForTesting(null);
var url = GURL.emptyGURL();
// Issue 2 requests for the same URL.
mSupplier.fetchImage(url, mCallback1);
mSupplier.fetchImage(url, mCallback2);
verifyNoMoreInteractions(mImageFetcher);
// Observe listeners receiving no notification.
verifyNoMoreInteractions(mCallback1, mCallback2);
}
@Test
public void fetchImage_invalidUrlsAreRejected() {
mSupplier.setImageFetcherForTesting(null);
var url = JUnitTestGURLs.INVALID_URL;
// Issue 2 requests for the same URL.
mSupplier.fetchImage(url, mCallback1);
mSupplier.fetchImage(url, mCallback2);
verifyNoMoreInteractions(mImageFetcher);
// Observe listeners receiving no notification.
verifyNoMoreInteractions(mCallback1, mCallback2);
}
}