Chromium Code Reviews| Index: chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java |
| diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java |
| index fc193a783bc4aff78d0d5a57c500662d7fffbe7b..ad98a2a6e4ed99594cb4ec9f814883f2dc1e62c1 100644 |
| --- a/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java |
| +++ b/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java |
| @@ -4,43 +4,69 @@ |
| package org.chromium.chrome.browser.suggestions; |
| -import static junit.framework.TestCase.assertNotNull; |
| - |
| -import static org.mockito.Mockito.inOrder; |
| +import static org.hamcrest.CoreMatchers.is; |
| +import static org.junit.Assert.assertFalse; |
| +import static org.junit.Assert.assertNotNull; |
| +import static org.junit.Assert.assertThat; |
| +import static org.junit.Assert.fail; |
| +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.mock; |
| +import static org.mockito.Mockito.never; |
| +import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.verify; |
| +import static org.mockito.Mockito.verifyNoMoreInteractions; |
| +import static org.mockito.Mockito.when; |
| +import android.content.Context; |
| import android.content.res.Resources; |
| +import android.graphics.Bitmap; |
| +import android.graphics.Color; |
| import android.support.annotation.ColorRes; |
| import android.support.annotation.DimenRes; |
| +import android.support.annotation.LayoutRes; |
| +import android.view.LayoutInflater; |
| +import android.view.View; |
| +import android.view.ViewGroup; |
| +import org.hamcrest.CoreMatchers; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| -import org.mockito.InOrder; |
| +import org.mockito.ArgumentCaptor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| +import org.mockito.invocation.InvocationOnMock; |
| +import org.mockito.stubbing.Answer; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Config; |
| +import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.annotation.RealObject; |
| import org.robolectric.shadows.ShadowResources; |
| import org.chromium.chrome.R; |
| -import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.EnableFeatures; |
| +import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback; |
| import org.chromium.chrome.browser.ntp.ContextMenuManager; |
| -import org.chromium.chrome.browser.ntp.cards.NodeParent; |
| -import org.chromium.chrome.browser.ntp.cards.SuggestionsSection; |
| +import org.chromium.chrome.browser.ntp.NTPTileSource; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; |
| import org.chromium.testing.local.LocalRobolectricTestRunner; |
| +import java.util.ArrayList; |
| + |
| /** |
| * Unit tests for {@link TileGroup}. |
| + * TODO(dgn): Split some of the tests out to TileTest.java |
| + * TODO(dgn): renderTileView tests don't work. Need to mock out TileView creation. |
| */ |
| @RunWith(LocalRobolectricTestRunner.class) |
| -@Config(manifest = Config.NONE, shadows = {TileGroupTest.TileShadowResources.class}) |
| +@Config(manifest = Config.NONE, |
| + shadows = {TileGroupTest.TileShadowResources.class, |
| + TileGroupTest.ShadowLayoutInflater.class}) |
| public class TileGroupTest { |
| private static final int MAX_TILES_TO_FETCH = 4; |
| private static final int TILE_TITLE_LINES = 1; |
| @@ -50,15 +76,7 @@ |
| public EnableFeatures.Processor mEnableFeatureProcessor = new EnableFeatures.Processor(); |
| @Mock |
| - private SuggestionsSection.Delegate mDelegate; |
| - @Mock |
| - private NodeParent mParent; |
| - @Mock |
| - private SuggestionsUiDelegate mUiDelegate; |
| - @Mock |
| private TileGroup.Observer mTileGroupObserver; |
| - @Mock |
| - private OfflinePageBridge mOfflinePageBridge; |
| private FakeTileGroupDelegate mTileGroupDelegate; |
| @@ -70,36 +88,246 @@ public void setUp() { |
| } |
| @Test |
| - @EnableFeatures({ChromeFeatureList.NTP_OFFLINE_PAGES_FEATURE_NAME}) |
| public void testInitialiseWithEmptyTileList() { |
| - TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, mUiDelegate, |
| - mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| - mOfflinePageBridge, TILE_TITLE_LINES); |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| notifyTileUrlsAvailable(); |
| + // The TileGroup.Observer methods should be called even though no tiles are added, which is |
| + // an initialisation but not real state change. |
| verify(mTileGroupObserver).onTileCountChanged(); |
| verify(mTileGroupObserver).onLoadTaskCompleted(); |
| verify(mTileGroupObserver).onTileDataChanged(); |
| } |
| @Test |
| - @EnableFeatures({ChromeFeatureList.NTP_OFFLINE_PAGES_FEATURE_NAME}) |
| public void testInitialiseWithTileList() { |
| - TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, mUiDelegate, |
| - mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| - mOfflinePageBridge, TILE_TITLE_LINES); |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + |
| + notifyTileUrlsAvailable(URLS); |
| + |
| + verify(mTileGroupObserver).onTileCountChanged(); |
| + verify(mTileGroupObserver).onLoadTaskCompleted(); |
| + verify(mTileGroupObserver).onTileDataChanged(); |
| + } |
| + |
| + @Test |
| + public void testReceiveNewTilesWithoutChanges() { |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + |
| + // First initialisation |
| + notifyTileUrlsAvailable(URLS); |
| + reset(mTileGroupObserver); |
| + |
| + // Notify the same thing. No changes so|mTileGroupObserver| should not be notified. |
| + notifyTileUrlsAvailable(URLS); |
| + verifyNoMoreInteractions(mTileGroupObserver); |
| + } |
| + |
| + @Test |
| + public void testReceiveNewTilesWithDataChanges() { |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + // First initialisation |
| notifyTileUrlsAvailable(URLS); |
| + reset(mTileGroupObserver); |
| - InOrder inOrder = inOrder(mTileGroupObserver); |
| - inOrder.verify(mTileGroupObserver).onTileCountChanged(); |
| - inOrder.verify(mTileGroupObserver).onLoadTaskCompleted(); |
| - inOrder.verify(mTileGroupObserver).onTileDataChanged(); |
| - inOrder.verifyNoMoreInteractions(); |
| + // Notify the about different URLs, but the same number. #onTileCountChanged() should not be |
| + // called. |
| + notifyTileUrlsAvailable("foo", "bar"); |
| + verify(mTileGroupObserver, never()).onTileCountChanged(); // Tile count is still 2 |
| + verify(mTileGroupObserver, never()).onLoadTaskCompleted(); // No load task the second time. |
| + verify(mTileGroupObserver).onTileDataChanged(); // Data DID change. |
| } |
| + @Test |
| + public void testReceiveNewTilesWithCountChanges() { |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + |
| + // First initialisation |
| + notifyTileUrlsAvailable(URLS); |
| + reset(mTileGroupObserver); |
| + |
| + notifyTileUrlsAvailable(URLS[0]); |
| + verify(mTileGroupObserver).onTileCountChanged(); // Tile count DID change. |
| + verify(mTileGroupObserver, never()).onLoadTaskCompleted(); // No load task the second time. |
| + verify(mTileGroupObserver).onTileDataChanged(); // Data DID change. |
| + } |
| + |
| + @Test |
| + public void testRenderTileView() { |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + TileGridLayoutWrapper layoutWrapper = new TileGridLayoutWrapper(); |
| + |
| + // Initialise the internal list of tiles |
| + notifyTileUrlsAvailable(URLS); |
| + |
| + // Render them to the layout. |
| + tileGroup.renderTileViews(layoutWrapper.getLayout(), false, false); |
| + assertThat(layoutWrapper.size(), is(2)); |
| + assertThat(((TileView) layoutWrapper.get(0)).getUrl(), is(URLS[0])); |
| + assertThat(((TileView) layoutWrapper.get(1)).getUrl(), is(URLS[1])); |
| + } |
| + |
| + @Test |
| + public void testRenderTileViewReplacing() { |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + notifyTileUrlsAvailable(URLS); |
| + |
| + // Initialise the layout with views whose URLs don't match the ones of the new tiles. |
| + TileGridLayoutWrapper layoutWrapper = new TileGridLayoutWrapper(); |
| + TileView view1 = mock(TileView.class); |
| + layoutWrapper.add(view1); |
| + |
| + TileView view2 = mock(TileView.class); |
| + layoutWrapper.add(view2); |
| + |
| + // The tiles should be updated, the old ones removed. |
| + tileGroup.renderTileViews(layoutWrapper.getLayout(), false, false); |
| + assertThat(layoutWrapper.size(), is(2)); |
| + assertFalse(layoutWrapper.contains(view1)); |
| + assertFalse(layoutWrapper.contains(view2)); |
| + verify(view1, never()).updateIfDataChanged(tileGroup.getTiles()[0]); |
| + verify(view2, never()).updateIfDataChanged(tileGroup.getTiles()[1]); |
| + } |
| + |
| + @Test |
| + public void testRenderTileViewRecycling() { |
| + TileGroup tileGroup = |
| + new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class), |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + notifyTileUrlsAvailable(URLS); |
| + |
| + // Initialise the layout with views whose URLs match the ones of the new tiles. |
| + TileGridLayoutWrapper layoutWrapper = new TileGridLayoutWrapper(); |
| + TileView view1 = mock(TileView.class); |
| + when(view1.getUrl()).thenReturn(URLS[0]); |
| + layoutWrapper.add(view1); |
| + |
| + TileView view2 = mock(TileView.class); |
| + when(view2.getUrl()).thenReturn(URLS[1]); |
| + layoutWrapper.add(view2); |
| + |
| + // The tiles should be updated, the old ones reused. |
| + tileGroup.renderTileViews(layoutWrapper.getLayout(), false, false); |
| + assertThat(layoutWrapper.size(), is(2)); |
| + assertThat(layoutWrapper.get(0), CoreMatchers.<View>is(view1)); |
| + assertThat(layoutWrapper.get(1), CoreMatchers.<View>is(view2)); |
| + verify(view1).updateIfDataChanged(tileGroup.getTiles()[0]); |
| + verify(view2).updateIfDataChanged(tileGroup.getTiles()[1]); |
| + } |
| + |
| + @Test |
| + public void testIconLoading() { |
| + SuggestionsUiDelegate uiDelegate = mock(SuggestionsUiDelegate.class); |
| + TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate, |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + notifyTileUrlsAvailable(URLS); // Initialise the internal state to include the test tile. |
| + reset(mTileGroupObserver); |
| + Tile tile = tileGroup.getTiles()[0]; |
| + |
| + TileGridLayoutWrapper layoutWrapper = new TileGridLayoutWrapper(); |
| + tileGroup.buildTileView(tile, layoutWrapper.getLayout(), /* trackLoadTask = */ true, |
| + /* condensed = */ false); |
| + |
| + verify(mTileGroupObserver).onLoadTaskAdded(); |
| + |
| + ArgumentCaptor<LargeIconCallback> captor = ArgumentCaptor.forClass(LargeIconCallback.class); |
| + verify(uiDelegate).getLargeIconForUrl(any(String.class), anyInt(), captor.capture()); |
| + for (LargeIconCallback cb : captor.getAllValues()) { |
| + cb.onLargeIconAvailable(mock(Bitmap.class), Color.BLACK, /* isColorDefault = */ false); |
| + } |
| + |
| + verify(mTileGroupObserver).onLoadTaskCompleted(); |
| + verify(mTileGroupObserver).onTileIconChanged(tile); |
| + } |
| + |
| + @Test |
| + public void testIconLoadingNoTask() { |
| + SuggestionsUiDelegate uiDelegate = mock(SuggestionsUiDelegate.class); |
| + TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate, |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + notifyTileUrlsAvailable(URLS); // Initialise the internal state to include the test tile. |
| + reset(mTileGroupObserver); |
| + Tile tile = tileGroup.getTiles()[0]; |
| + |
| + TileGridLayoutWrapper layoutWrapper = new TileGridLayoutWrapper(); |
| + tileGroup.buildTileView(tile, layoutWrapper.getLayout(), /* trackLoadTask = */ false, |
| + /* condensed = */ false); |
| + |
| + verify(mTileGroupObserver, never()).onLoadTaskAdded(); |
| + |
| + ArgumentCaptor<LargeIconCallback> captor = ArgumentCaptor.forClass(LargeIconCallback.class); |
| + verify(uiDelegate).getLargeIconForUrl(any(String.class), anyInt(), captor.capture()); |
| + for (LargeIconCallback cb : captor.getAllValues()) { |
| + cb.onLargeIconAvailable(mock(Bitmap.class), Color.BLACK, /* isColorDefault = */ false); |
| + } |
| + |
| + verify(mTileGroupObserver, never()).onLoadTaskCompleted(); |
| + verify(mTileGroupObserver).onTileIconChanged(tile); |
| + } |
| + |
| + @Test |
| + public void testIconLoadingWhenTileNotRegistered() { |
| + SuggestionsUiDelegate uiDelegate = mock(SuggestionsUiDelegate.class); |
| + TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate, |
| + mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver, |
| + mock(OfflinePageBridge.class), TILE_TITLE_LINES); |
| + tileGroup.startObserving(MAX_TILES_TO_FETCH); |
| + reset(mTileGroupObserver); |
| + Tile tile = new Tile("title", URLS[0], "", 0, NTPTileSource.POPULAR); |
| + |
| + TileGridLayoutWrapper layoutWrapper = new TileGridLayoutWrapper(); |
| + tileGroup.buildTileView(tile, layoutWrapper.getLayout(), /* trackLoadTask = */ true, |
| + /* condensed = */ false); |
| + verify(mTileGroupObserver).onLoadTaskAdded(); |
| + |
| + ArgumentCaptor<LargeIconCallback> captor = ArgumentCaptor.forClass(LargeIconCallback.class); |
| + verify(uiDelegate).getLargeIconForUrl(any(String.class), anyInt(), captor.capture()); |
| + captor.getValue().onLargeIconAvailable(mock(Bitmap.class), Color.BLACK, false); |
| + |
| + verify(mTileGroupObserver).onLoadTaskCompleted(); |
| + verify(mTileGroupObserver, never()).onTileIconChanged(tile); |
| + } |
| + |
| + /** |
| + * Notifies the tile group of new tiles created from the provided URLs. Requires |
| + * {@link TileGroup#startObserving(int)} to have been called on the tile group under test. |
| + * @see TileGroup#onMostVisitedURLsAvailable(String[], String[], String[], int[]) |
| + */ |
| private void notifyTileUrlsAvailable(String... urls) { |
| assertNotNull("MostVisitedObserver has not been registered.", mTileGroupDelegate.mObserver); |
| @@ -111,6 +339,25 @@ private void notifyTileUrlsAvailable(String... urls) { |
| titles, urls, whitelistIconPaths, sources); |
| } |
| + /** |
| + * Creates a mocked {@link TileView} that will return the URL of the tile it has been |
| + * initialised with. |
| + */ |
| + private static TileView createMockTileView() { |
| + final TileView tileView = mock(TileView.class); |
| + doAnswer(new Answer<Void>() { |
| + @Override |
| + public Void answer(InvocationOnMock invocation) throws Throwable { |
| + Tile tile = invocation.getArgument(0); |
| + when(tileView.getUrl()).thenReturn(tile.getUrl()); |
| + return null; |
| + } |
| + }) |
| + .when(tileView) |
| + .initialize(any(Tile.class), anyInt(), anyBoolean()); |
| + return tileView; |
| + } |
| + |
| private class FakeTileGroupDelegate implements TileGroup.Delegate { |
| public MostVisitedSites.Observer mObserver; |
| @@ -153,4 +400,68 @@ public int getColor(@ColorRes int id) { |
| return mRealResources.getColor(id); |
| } |
| } |
| + |
| + /** Intercepts calls to inflate views to replace them with mocks. */ |
| + @Implements(LayoutInflater.class) |
| + public static class ShadowLayoutInflater { |
| + @Implementation |
| + public static LayoutInflater from(Context context) { |
| + LayoutInflater layoutInflater = mock(LayoutInflater.class); |
| + when(layoutInflater.inflate(anyInt(), any(ViewGroup.class), anyBoolean())) |
| + .thenAnswer(new Answer<View>() { |
| + @Override |
| + public View answer(InvocationOnMock invocation) throws Throwable { |
| + @LayoutRes |
| + int layoutId = invocation.getArgument(0); |
| + if (layoutId != R.layout.tile_view) fail("Unexpected resource id."); |
| + return createMockTileView(); |
| + } |
| + }); |
| + return layoutInflater; |
| + } |
| + } |
| + |
| + /** Allows substituting the layout object with a mock. */ |
| + private static class TileGridLayoutWrapper extends ArrayList<View> { |
| + private TileGridLayout mLayout; |
| + |
| + public TileGridLayoutWrapper() { |
| + mLayout = mock(TileGridLayout.class); |
|
Bernhard Bauer
2017/03/03 11:54:38
Can you use a custom subclass instead?
dgn
2017/03/03 13:48:30
Used FrameLayout instead.
|
| + when(mLayout.getChildAt(anyInt())).thenAnswer(new Answer<View>() { |
| + @Override |
| + public View answer(InvocationOnMock invocation) throws Throwable { |
| + int childIndex = invocation.getArgument(0); |
| + return get(childIndex); |
| + } |
| + }); |
| + when(mLayout.getChildCount()).thenAnswer(new Answer<Integer>() { |
| + @Override |
| + public Integer answer(InvocationOnMock invocation) throws Throwable { |
| + return size(); |
| + } |
| + }); |
| + doAnswer(new Answer<Void>() { |
| + @Override |
| + public Void answer(InvocationOnMock invocation) throws Throwable { |
| + clear(); |
| + return null; |
| + } |
| + }) |
| + .when(mLayout) |
| + .removeAllViews(); |
| + doAnswer(new Answer<Void>() { |
| + @Override |
| + public Void answer(InvocationOnMock invocation) throws Throwable { |
| + add(invocation.<View>getArgument(0)); |
| + return null; |
| + } |
| + }) |
| + .when(mLayout) |
| + .addView(any(View.class)); |
| + } |
| + |
| + public TileGridLayout getLayout() { |
| + return mLayout; |
| + } |
| + } |
| } |