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; |
+ } |
+ } |
} |