blob: f0bd654e3a4577e2a408f585d7abb298ff1eb54f [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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.ntp.cards;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.res.Resources;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.FlakyTest;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.native_page.ContextMenuManager;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.ntp.NewTabPageView;
import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsCardLayout;
import org.chromium.chrome.browser.ntp.snippets.KnownCategories;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
import org.chromium.chrome.browser.suggestions.ContentSuggestionsAdditionalAction;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.NewTabPageTestUtils;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.chrome.test.util.browser.RecyclerViewTestUtils;
import org.chromium.chrome.test.util.browser.suggestions.FakeMostVisitedSites;
import org.chromium.chrome.test.util.browser.suggestions.FakeSuggestionsSource;
import org.chromium.chrome.test.util.browser.suggestions.SuggestionsDependenciesRule;
import org.chromium.content_public.browser.test.util.TestTouchUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.test.util.UiRestriction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/**
* Instrumentation tests for {@link NewTabPageRecyclerView}.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
@Features.DisableFeatures("NetworkPrediction")
@RetryOnFailure
public class NewTabPageRecyclerViewTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
@Rule
public SuggestionsDependenciesRule mSuggestionsDeps = new SuggestionsDependenciesRule();
private static final String TEST_PAGE = "/chrome/test/data/android/navigate/simple.html";
private static final long FAKE_PUBLISH_TIMESTAMP = 1466614774;
private static final long FAKE_FETCH_TIMESTAMP = 1466634774;
private static final float FAKE_SNIPPET_SCORE = 10f;
// TODO(dgn): Properly bypass the native code when testing with a fake suggestions source.
// We currently mix the fake and the snippets bridge, resulting in crashes with unregistered
// categories.
@CategoryInt
private static final int TEST_CATEGORY = KnownCategories.ARTICLES;
private Tab mTab;
private NewTabPage mNtp;
private EmbeddedTestServer mTestServer;
private FakeSuggestionsSource mSource;
@Before
public void setUp() throws Exception {
mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
FakeMostVisitedSites mostVisitedSites = new FakeMostVisitedSites();
mostVisitedSites.setTileSuggestions(mTestServer.getURL(TEST_PAGE));
mSuggestionsDeps.getFactory().mostVisitedSites = mostVisitedSites;
mSource = new FakeSuggestionsSource();
mSource.setInfoForCategory(TEST_CATEGORY,
new SuggestionsCategoryInfo(TEST_CATEGORY, "Suggestions test title",
ContentSuggestionsCardLayout.FULL_CARD,
ContentSuggestionsAdditionalAction.FETCH, /*showIfEmpty=*/true,
"noSuggestionsMessage"));
// Set the status as AVAILABLE so no spinner is shown. Showing the spinner during
// initialization can cause the test to hang because the message queue never becomes idle.
mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE);
mSuggestionsDeps.getFactory().suggestionsSource = mSource;
mActivityTestRule.startMainActivityWithURL(UrlConstants.NTP_URL);
mTab = mActivityTestRule.getActivity().getActivityTab();
NewTabPageTestUtils.waitForNtpLoaded(mTab);
assertTrue(mTab.getNativePage() instanceof NewTabPage);
mNtp = (NewTabPage) mTab.getNativePage();
// When scrolling to a View, we wait until the View is no longer updating - when it is no
// longer dirty. If scroll to load is triggered, the animated progress spinner will keep
// the RecyclerView dirty as it is constantly updating.
//
// We do not want to disable the Scroll to Load feature entirely because its presence
// effects other elements of the UI - it moves the Learn More link into the Context Menu.
// Removing the ScrollToLoad listener from the RecyclerView allows us to prevent scroll to
// load triggering while maintaining the UI otherwise.
ThreadUtils.runOnUiThreadBlocking(
() -> mNtp.getNewTabPageView().getRecyclerView().clearScrollToLoadListener());
}
@After
public void tearDown() throws Exception {
mTestServer.stopAndDestroyServer();
}
@Test
@MediumTest
@Feature({"NewTabPage"})
@FlakyTest(message = "crbug.com/875544")
public void testClickSuggestion() throws InterruptedException {
setSuggestionsAndWaitForUpdate(10);
List<SnippetArticle> suggestions = mSource.getSuggestionsForCategory(TEST_CATEGORY);
// Scroll the last suggestion into view and click it.
SnippetArticle suggestion = suggestions.get(suggestions.size() - 1);
int suggestionPosition = getLastCardPosition();
final View suggestionView = getViewHolderAtPosition(suggestionPosition).itemView;
ChromeTabUtils.waitForTabPageLoaded(mTab, new Runnable() {
@Override
public void run() {
TouchCommon.singleClickView(suggestionView);
}
});
assertEquals(suggestion.mUrl, mTab.getUrl());
}
@Test
//@MediumTest
//@Feature({"NewTabPage"})
@DisabledTest(message = "crbug.com/793054")
public void testAllDismissed() throws InterruptedException, TimeoutException {
setSuggestionsAndWaitForUpdate(3);
assertEquals(3, mSource.getSuggestionsForCategory(TEST_CATEGORY).size());
assertEquals(RecyclerView.NO_POSITION,
getAdapter().getFirstPositionForType(ItemViewType.ALL_DISMISSED));
assertEquals(1, mSource.getCategories().length);
assertEquals(TEST_CATEGORY, mSource.getCategories()[0]);
// Dismiss the sign in promo.
int signinPromoPosition = getAdapter().getFirstPositionForType(ItemViewType.PROMO);
dismissItemAtPosition(signinPromoPosition);
// Dismiss all the cards, including status cards, which dismisses the associated category.
while (true) {
int cardPosition = getAdapter().getFirstCardPosition();
if (cardPosition == RecyclerView.NO_POSITION) break;
dismissItemAtPosition(cardPosition);
}
assertEquals(0, mSource.getCategories().length);
// Click the refresh button on the all dismissed item.
int allDismissedPosition = getAdapter().getFirstPositionForType(ItemViewType.ALL_DISMISSED);
assertTrue(allDismissedPosition != RecyclerView.NO_POSITION);
View allDismissedView = getViewHolderAtPosition(allDismissedPosition).itemView;
TouchCommon.singleClickView(allDismissedView.findViewById(R.id.action_button));
RecyclerViewTestUtils.waitForViewToDetach(getRecyclerView(), allDismissedView);
assertEquals(1, mSource.getCategories().length);
assertEquals(TEST_CATEGORY, mSource.getCategories()[0]);
}
@Test
@MediumTest
@Feature({"NewTabPage"})
public void testDismissArticleWithContextMenu() throws Exception {
setSuggestionsAndWaitForUpdate(10);
List<SnippetArticle> suggestions = mSource.getSuggestionsForCategory(TEST_CATEGORY);
assertEquals(10, suggestions.size());
// Scroll a suggestion into view.
int suggestionPosition = getLastCardPosition();
View suggestionView = getViewHolderAtPosition(suggestionPosition).itemView;
// Dismiss the suggestion using the context menu.
invokeContextMenu(suggestionView, ContextMenuManager.ContextMenuItemId.REMOVE);
RecyclerViewTestUtils.waitForViewToDetach(getRecyclerView(), suggestionView);
suggestions = mSource.getSuggestionsForCategory(TEST_CATEGORY);
assertEquals(9, suggestions.size());
}
@Test
@MediumTest
@Feature({"NewTabPage"})
public void testDismissStatusCardWithContextMenu() throws Exception {
setSuggestionsAndWaitForUpdate(0);
assertArrayEquals(new int[] {TEST_CATEGORY}, mSource.getCategories());
// Scroll the status card into view.
int cardPosition = getAdapter().getFirstPositionForType(ItemViewType.STATUS);
assertEquals(ItemViewType.STATUS, getAdapter().getItemViewType(cardPosition));
View statusCardView = getViewHolderAtPosition(cardPosition).itemView;
// Dismiss the status card using the context menu.
invokeContextMenu(statusCardView, ContextMenuManager.ContextMenuItemId.REMOVE);
RecyclerViewTestUtils.waitForViewToDetach(getRecyclerView(), statusCardView);
assertArrayEquals(new int[0], mSource.getCategories());
}
@Test
@MediumTest
@Feature({"NewTabPage"})
public void testDismissActionItemWithContextMenu() throws Exception {
setSuggestionsAndWaitForUpdate(0);
assertArrayEquals(new int[] {TEST_CATEGORY}, mSource.getCategories());
// Scroll the action item into view.
int actionItemPosition = getAdapter().getFirstPositionForType(ItemViewType.ACTION);
assertEquals(ItemViewType.ACTION, getAdapter().getItemViewType(actionItemPosition));
View actionItemView = getViewHolderAtPosition(actionItemPosition).itemView;
// Dismiss the action item using the context menu.
invokeContextMenu(actionItemView, ContextMenuManager.ContextMenuItemId.REMOVE);
RecyclerViewTestUtils.waitForViewToDetach(getRecyclerView(), actionItemView);
assertArrayEquals(new int[0], mSource.getCategories());
}
@Test
@MediumTest
@Feature({"NewTabPage"})
@Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
public void testSnapScroll() {
setSuggestionsAndWaitForUpdate(0);
Resources resources = InstrumentationRegistry.getTargetContext().getResources();
int toolbarHeight = resources.getDimensionPixelSize(R.dimen.toolbar_height_no_shadow)
+ resources.getDimensionPixelSize(R.dimen.toolbar_progress_bar_height);
View searchBox = getNtpView().findViewById(R.id.search_box);
int searchBoxTop = searchBox.getTop() + searchBox.getPaddingTop();
int searchBoxTransitionLength =
resources.getDimensionPixelSize(R.dimen.ntp_search_box_transition_length);
// Two different snapping regions: snapping back up to the watershed point in the middle,
// snapping forward after that.
assertEquals(0, getSnapPosition(0));
assertEquals(0, getSnapPosition(toolbarHeight / 2 - 1));
assertEquals(toolbarHeight, getSnapPosition(toolbarHeight / 2));
assertEquals(toolbarHeight, getSnapPosition(toolbarHeight));
assertEquals(toolbarHeight + 1, getSnapPosition(toolbarHeight + 1));
assertEquals(searchBoxTop - searchBoxTransitionLength - 1,
getSnapPosition(searchBoxTop - searchBoxTransitionLength - 1));
assertEquals(searchBoxTop - searchBoxTransitionLength,
getSnapPosition(searchBoxTop - searchBoxTransitionLength));
assertEquals(searchBoxTop - searchBoxTransitionLength,
getSnapPosition(searchBoxTop - searchBoxTransitionLength / 2 - 1));
assertEquals(searchBoxTop, getSnapPosition(searchBoxTop - searchBoxTransitionLength / 2));
assertEquals(searchBoxTop, getSnapPosition(searchBoxTop));
assertEquals(searchBoxTop + 1, getSnapPosition(searchBoxTop + 1));
}
@Test
@MediumTest
@Feature({"NewTabPage"})
@Restriction({UiRestriction.RESTRICTION_TYPE_TABLET})
public void testSnapScroll_tablet() {
setSuggestionsAndWaitForUpdate(0);
Resources res = InstrumentationRegistry.getTargetContext().getResources();
int toolbarHeight = res.getDimensionPixelSize(R.dimen.toolbar_height_no_shadow)
+ res.getDimensionPixelSize(R.dimen.toolbar_progress_bar_height);
View searchBox = getNtpView().findViewById(R.id.search_box);
int searchBoxTop = searchBox.getTop() + searchBox.getPaddingTop();
int searchBoxTransitionLength =
res.getDimensionPixelSize(R.dimen.ntp_search_box_transition_length);
// No snapping on tablets.
// Note: This ignores snapping for the peeking cards, which is currently disabled
// by default.
assertEquals(0, getSnapPosition(0));
assertEquals(toolbarHeight / 2 - 1, getSnapPosition(toolbarHeight / 2 - 1));
assertEquals(toolbarHeight / 2, getSnapPosition(toolbarHeight / 2));
assertEquals(toolbarHeight, getSnapPosition(toolbarHeight));
assertEquals(toolbarHeight + 1, getSnapPosition(toolbarHeight + 1));
assertEquals(searchBoxTop - searchBoxTransitionLength - 1,
getSnapPosition(searchBoxTop - searchBoxTransitionLength - 1));
assertEquals(searchBoxTop - searchBoxTransitionLength,
getSnapPosition(searchBoxTop - searchBoxTransitionLength));
assertEquals(searchBoxTop - searchBoxTransitionLength / 2 - 1,
getSnapPosition(searchBoxTop - searchBoxTransitionLength / 2 - 1));
assertEquals(searchBoxTop - searchBoxTransitionLength / 2,
getSnapPosition(searchBoxTop - searchBoxTransitionLength / 2));
assertEquals(searchBoxTop, getSnapPosition(searchBoxTop));
assertEquals(searchBoxTop + 1, getSnapPosition(searchBoxTop + 1));
}
private int getSnapPosition(int scrollPosition) {
NewTabPageView ntpView = getNtpView();
return ntpView.getSnapScrollHelper().calculateSnapPosition(scrollPosition);
}
private NewTabPageView getNtpView() {
return mNtp.getNewTabPageView();
}
private NewTabPageRecyclerView getRecyclerView() {
return getNtpView().getRecyclerView();
}
private NewTabPageAdapter getAdapter() {
return getRecyclerView().getNewTabPageAdapter();
}
private int getLastCardPosition() {
int count = getAdapter().getItemCount();
for (int i = count - 1; i >= 0; i--) {
if (getAdapter().getItemViewType(i) == ItemViewType.SNIPPET) return i;
}
return RecyclerView.NO_POSITION;
}
/**
* Scroll the {@link View} at the given adapter position into view and returns
* its {@link ViewHolder}.
* @param position the adapter position for which to return the {@link ViewHolder}.
* @return the ViewHolder for the given {@code position}.
*/
private ViewHolder getViewHolderAtPosition(final int position) {
final NewTabPageRecyclerView recyclerView = getRecyclerView();
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
recyclerView.getLinearLayoutManager().scrollToPositionWithOffset(position,
mActivityTestRule.getActivity().getResources().getDimensionPixelSize(
R.dimen.tab_strip_height));
}
});
return RecyclerViewTestUtils.waitForView(getRecyclerView(), position);
}
/**
* Dismiss the item at the given {@code position} and wait until it has been removed from the
* {@link RecyclerView}.
* @param position the adapter position to remove.
* @throws InterruptedException
* @throws TimeoutException
*/
private void dismissItemAtPosition(int position) throws InterruptedException, TimeoutException {
final ViewHolder viewHolder = getViewHolderAtPosition(position);
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
getRecyclerView().dismissItemWithAnimation(viewHolder);
}
});
RecyclerViewTestUtils.waitForViewToDetach(getRecyclerView(), (viewHolder.itemView));
}
private void setSuggestionsAndWaitForUpdate(final int suggestionsCount) {
final FakeSuggestionsSource source = mSource;
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
source.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE);
source.setSuggestionsForCategory(TEST_CATEGORY, buildSuggestions(suggestionsCount));
}
});
RecyclerViewTestUtils.waitForStableRecyclerView(getRecyclerView());
}
private List<SnippetArticle> buildSuggestions(int suggestionsCount) {
List<SnippetArticle> suggestions = new ArrayList<>();
for (int i = 0; i < suggestionsCount; i++) {
String url = mTestServer.getURL(TEST_PAGE) + "#" + i;
suggestions.add(new SnippetArticle(TEST_CATEGORY, "id" + i, "title" + i,
"publisher" + i, url, FAKE_PUBLISH_TIMESTAMP + i, FAKE_SNIPPET_SCORE,
FAKE_FETCH_TIMESTAMP, false, /* thumbnailDominantColor = */ null));
}
return suggestions;
}
private void invokeContextMenu(View view, int contextMenuItemId) throws ExecutionException {
TestTouchUtils.performLongClickOnMainSync(
InstrumentationRegistry.getInstrumentation(), view);
assertTrue(InstrumentationRegistry.getInstrumentation().invokeContextMenuAction(
mActivityTestRule.getActivity(), contextMenuItemId, 0));
}
private static void assertArrayEquals(int[] expected, int[] actual) {
assertEquals(Arrays.toString(expected), Arrays.toString(actual));
}
}