| // 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.hamcrest.Matchers.is; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertThat; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.ArgumentMatchers.anyBoolean; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyString; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.atLeast; |
| import static org.mockito.Mockito.doNothing; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import static org.chromium.chrome.browser.ntp.cards.ContentSuggestionsUnitTestUtils.makeUiConfig; |
| import static org.chromium.chrome.test.util.browser.suggestions.ContentSuggestionsTestUtils.createDummySuggestions; |
| import static org.chromium.chrome.test.util.browser.suggestions.ContentSuggestionsTestUtils.registerCategory; |
| |
| import android.accounts.Account; |
| import android.content.res.Resources; |
| import android.support.annotation.Nullable; |
| import android.support.v7.widget.RecyclerView; |
| import android.support.v7.widget.RecyclerView.AdapterDataObserver; |
| import android.view.View; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.shadows.ShadowResources; |
| |
| import org.chromium.base.Callback; |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| import org.chromium.base.test.asynctask.CustomShadowAsyncTask; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter; |
| import org.chromium.chrome.browser.native_page.ContextMenuManager; |
| import org.chromium.chrome.browser.ntp.cards.NewTabPageViewHolder.PartialBindCallback; |
| import org.chromium.chrome.browser.ntp.cards.SignInPromo.SigninObserver; |
| import org.chromium.chrome.browser.ntp.snippets.CategoryInt; |
| import org.chromium.chrome.browser.ntp.snippets.CategoryStatus; |
| import org.chromium.chrome.browser.ntp.snippets.KnownCategories; |
| import org.chromium.chrome.browser.ntp.snippets.SnippetArticle; |
| import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; |
| import org.chromium.chrome.browser.preferences.ChromePreferenceManager; |
| import org.chromium.chrome.browser.preferences.Pref; |
| import org.chromium.chrome.browser.preferences.PrefServiceBridge; |
| import org.chromium.chrome.browser.signin.SigninManager; |
| import org.chromium.chrome.browser.suggestions.ContentSuggestionsAdditionalAction; |
| import org.chromium.chrome.browser.suggestions.DestructionObserver; |
| import org.chromium.chrome.browser.suggestions.SuggestionsEventReporter; |
| import org.chromium.chrome.browser.suggestions.SuggestionsRanker; |
| import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate; |
| import org.chromium.chrome.test.support.DisableHistogramsRule; |
| import org.chromium.chrome.test.util.browser.Features; |
| import org.chromium.chrome.test.util.browser.Features.DisableFeatures; |
| import org.chromium.chrome.test.util.browser.suggestions.ContentSuggestionsTestUtils.CategoryInfoBuilder; |
| import org.chromium.chrome.test.util.browser.suggestions.FakeSuggestionsSource; |
| import org.chromium.components.signin.AccountManagerFacade; |
| import org.chromium.components.signin.test.util.AccountHolder; |
| import org.chromium.components.signin.test.util.FakeAccountManagerDelegate; |
| import org.chromium.net.NetworkChangeNotifier; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * Unit tests for {@link NewTabPageAdapter}. {@link AccountManagerFacade} uses AsyncTasks, thus |
| * the need for {@link CustomShadowAsyncTask}. |
| */ |
| @RunWith(BaseRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE, shadows = {CustomShadowAsyncTask.class}) |
| @DisableFeatures({ChromeFeatureList.CONTENT_SUGGESTIONS_SCROLL_TO_LOAD, |
| ChromeFeatureList.SIMPLIFIED_NTP, ChromeFeatureList.CHROME_DUET, |
| ChromeFeatureList.UNIFIED_CONSENT}) |
| public class NewTabPageAdapterTest { |
| @Rule |
| public DisableHistogramsRule mDisableHistogramsRule = new DisableHistogramsRule(); |
| |
| @Rule |
| public TestRule mFeatureProcessor = new Features.JUnitProcessor(); |
| |
| @CategoryInt |
| private static final int TEST_CATEGORY = 42; |
| |
| private FakeSuggestionsSource mSource; |
| private NewTabPageAdapter mAdapter; |
| @Mock |
| private SigninManager mMockSigninManager; |
| @Mock |
| private OfflinePageBridge mOfflinePageBridge; |
| @Mock |
| private SuggestionsUiDelegate mUiDelegate; |
| @Mock |
| private PrefServiceBridge mPrefServiceBridge; |
| |
| /** |
| * Stores information about a section that should be present in the adapter. |
| */ |
| private static class SectionDescriptor { |
| // TODO(https://crbug.com/754763): Smells. To be cleaned up. |
| public boolean mIsSignInPromo; |
| |
| public boolean mHeader = true; |
| public List<SnippetArticle> mSuggestions; |
| public boolean mStatusCard; |
| public boolean mViewAllButton; |
| public boolean mFetchButton; |
| public boolean mProgressItem; |
| |
| public SectionDescriptor() {} |
| |
| public SectionDescriptor(List<SnippetArticle> suggestions) { |
| mSuggestions = suggestions; |
| } |
| |
| public SectionDescriptor withViewAllButton() { |
| assertFalse(mProgressItem); |
| mViewAllButton = true; |
| return this; |
| } |
| |
| public SectionDescriptor withFetchButton() { |
| assertFalse(mProgressItem); |
| mFetchButton = true; |
| return this; |
| } |
| |
| public SectionDescriptor withProgress() { |
| assertFalse(mViewAllButton); |
| assertFalse(mFetchButton); |
| mProgressItem = true; |
| return this; |
| } |
| |
| public SectionDescriptor isSigninPromo() { |
| mIsSignInPromo = true; |
| return this; |
| } |
| |
| public SectionDescriptor withStatusCard() { |
| mStatusCard = true; |
| return this; |
| } |
| } |
| |
| /** |
| * Checks the list of items from the adapter against a sequence of expectation, which is |
| * expressed as a sequence of calls to the {@code expect...()} methods. |
| */ |
| private static class ItemsMatcher { // TODO(pke): Find better name. |
| private final List<String> mExpectedDescriptions = new ArrayList<>(); |
| private final List<String> mActualDescriptions = new ArrayList<>(); |
| |
| public ItemsMatcher(RecyclerViewAdapter.Delegate root) { |
| for (int i = 0; i < root.getItemCount(); i++) { |
| mActualDescriptions.add(root.describeItemForTesting(i)); |
| } |
| } |
| |
| private void expectDescription(String description) { |
| mExpectedDescriptions.add(description); |
| } |
| |
| public void expectSection(SectionDescriptor descriptor) { |
| if (descriptor.mIsSignInPromo) { |
| expectDescription("SIGN_IN_PROMO"); |
| return; |
| } |
| |
| if (descriptor.mHeader) { |
| expectDescription("HEADER"); |
| } |
| |
| for (SnippetArticle suggestion : descriptor.mSuggestions) { |
| expectDescription( |
| String.format(Locale.US, "SUGGESTION(%1.42s)", suggestion.mTitle)); |
| } |
| |
| if (descriptor.mStatusCard) { |
| expectDescription("NO_SUGGESTIONS"); |
| } |
| |
| if (descriptor.mViewAllButton) { |
| expectDescription(String.format( |
| Locale.US, "ACTION(%d)", ContentSuggestionsAdditionalAction.VIEW_ALL)); |
| } |
| |
| if (descriptor.mFetchButton) { |
| expectDescription(String.format( |
| Locale.US, "ACTION(%d)", ContentSuggestionsAdditionalAction.FETCH)); |
| } |
| |
| if (descriptor.mProgressItem) { |
| expectDescription("PROGRESS"); |
| } |
| } |
| |
| public void expectAboveTheFoldItem() { |
| expectDescription("ABOVE_THE_FOLD"); |
| } |
| |
| public void expectAllDismissedItem() { |
| expectDescription("ALL_DISMISSED"); |
| } |
| |
| public void expectFooter() { |
| expectDescription("FOOTER"); |
| } |
| |
| public void finish() { |
| assertThat(mActualDescriptions, is(mExpectedDescriptions)); |
| } |
| } |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| |
| // Ensure that NetworkChangeNotifier is initialized. |
| if (!NetworkChangeNotifier.isInitialized()) { |
| NetworkChangeNotifier.init(); |
| } |
| NetworkChangeNotifier.forceConnectivityState(true); |
| |
| // Set empty variation params for the test. |
| CardsVariationParameters.setTestVariationParams(new HashMap<>()); |
| |
| // Initialise AccountManagerFacade and add one dummy account. |
| FakeAccountManagerDelegate fakeAccountManager = new FakeAccountManagerDelegate( |
| FakeAccountManagerDelegate.ENABLE_PROFILE_DATA_SOURCE); |
| AccountManagerFacade.overrideAccountManagerFacadeForTests(fakeAccountManager); |
| Account account = AccountManagerFacade.createAccountFromName("test@gmail.com"); |
| fakeAccountManager.addAccountHolderExplicitly(new AccountHolder.Builder(account).build()); |
| assertFalse(AccountManagerFacade.get().isUpdatePending()); |
| |
| // Initialise the sign in state. We will be signed in by default in the tests. |
| assertFalse(ChromePreferenceManager.getInstance().readBoolean( |
| ChromePreferenceManager.NTP_SIGNIN_PROMO_DISMISSED, false)); |
| SigninManager.setInstanceForTesting(mMockSigninManager); |
| when(mMockSigninManager.isSignedInOnNative()).thenReturn(true); |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| |
| mSource = new FakeSuggestionsSource(); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.INITIALIZING); |
| mSource.setInfoForCategory( |
| TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).showIfEmpty().build()); |
| |
| // Initialize a test instance for PrefServiceBridge. |
| when(mPrefServiceBridge.getBoolean(anyInt())).thenReturn(false); |
| doNothing().when(mPrefServiceBridge).setBoolean(anyInt(), anyBoolean()); |
| PrefServiceBridge.setInstanceForTesting(mPrefServiceBridge); |
| |
| resetUiDelegate(); |
| reloadNtp(); |
| } |
| |
| @After |
| public void tearDown() { |
| CardsVariationParameters.setTestVariationParams(null); |
| SigninManager.setInstanceForTesting(null); |
| ChromePreferenceManager.getInstance().writeBoolean( |
| ChromePreferenceManager.NTP_SIGNIN_PROMO_DISMISSED, false); |
| ChromePreferenceManager.getInstance().clearNewTabPageSigninPromoSuppressionPeriodStart(); |
| PrefServiceBridge.setInstanceForTesting(null); |
| } |
| |
| /** |
| * Tests the content of the adapter under standard conditions: on start and after a suggestions |
| * fetch. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSuggestionLoading() { |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| final int numSuggestions = 3; |
| List<SnippetArticle> suggestions = createDummySuggestions(numSuggestions, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| |
| assertItemsFor(section(suggestions)); |
| } |
| |
| /** |
| * Tests that the adapter keeps listening for suggestion updates. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSuggestionLoadingInitiallyEmpty() { |
| // If we don't get anything, we should be in the same situation as the initial one. |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, new ArrayList<>()); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // We should load new suggestions when we get notified about them. |
| final int numSuggestions = 5; |
| |
| List<SnippetArticle> suggestions = createDummySuggestions(numSuggestions, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| |
| assertItemsFor(section(suggestions)); |
| } |
| |
| /** |
| * Tests that the adapter clears the suggestions when asked to. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSuggestionClearing() { |
| List<SnippetArticle> suggestions = createDummySuggestions(4, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // If we get told that the category is enabled, we just leave the current suggestions do not |
| // clear them. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| assertItemsFor(section(suggestions)); |
| |
| // When the category is disabled, the section should go away completely. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| assertItemsFor(); |
| |
| // Now we're in the "all dismissed" state. No suggestions should be accepted. |
| suggestions = createDummySuggestions(6, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(); |
| |
| // After a full refresh, the adapter should accept suggestions again. |
| mSource.fireFullRefreshRequired(); |
| assertItemsFor(section(suggestions)); |
| } |
| |
| /** |
| * Tests that the adapter loads suggestions only when the status is favorable. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSuggestionLoadingBlock() { |
| List<SnippetArticle> suggestions = createDummySuggestions(3, TEST_CATEGORY); |
| |
| // By default, status is INITIALIZING, so we can load suggestions. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // Add another suggestion. |
| suggestions.add(new SnippetArticle(TEST_CATEGORY, "https://site.com/url3", "title3", "pub3", |
| "https://site.com/url3", 0, 0, 0, false, /* thumbnailDominantColor = */ null)); |
| |
| // When the provider is removed, we should not be able to load suggestions. The UI should |
| // stay the same though. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.NOT_PROVIDED); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions.subList(0, 3))); |
| |
| // INITIALIZING lets us load suggestions still. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.INITIALIZING); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // The adapter should now be waiting for new suggestions and the fourth one should appear. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // When the category gets disabled, the section should go away and not load any suggestions. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(); |
| } |
| |
| /** |
| * Tests how the loading indicator reacts to status changes. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testProgressIndicatorDisplay() { |
| SuggestionsSection section = mAdapter.getSectionListForTesting().getSection(TEST_CATEGORY); |
| ActionItem item = section.getActionItemForTesting(); |
| |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.INITIALIZING); |
| assertEquals(ActionItem.State.LOADING, item.getState()); |
| |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| assertEquals(ActionItem.State.HIDDEN, item.getState()); |
| |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE_LOADING); |
| assertEquals(ActionItem.State.LOADING, item.getState()); |
| |
| // After the section gets disabled, it should gone completely, so checking the progress |
| // indicator doesn't make sense anymore. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| assertEquals(mAdapter.getSectionListForTesting().getSection(TEST_CATEGORY), null); |
| } |
| |
| /** |
| * Tests that the entire section disappears if its status switches to LOADING_ERROR or |
| * CATEGORY_EXPLICITLY_DISABLED. Also tests that they are not shown when the NTP reloads. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSectionClearingWhenUnavailable() { |
| List<SnippetArticle> suggestions = createDummySuggestions(5, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // When the category goes away with a hard error, the section is cleared from the UI. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.LOADING_ERROR); |
| assertItemsFor(); |
| |
| // Same when loading a new NTP. |
| reloadNtp(); |
| assertItemsFor(); |
| |
| // Same for CATEGORY_EXPLICITLY_DISABLED. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| reloadNtp(); |
| assertItemsFor(section(suggestions)); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| assertItemsFor(); |
| |
| reloadNtp(); |
| assertItemsFor(); |
| } |
| |
| /** |
| * Tests that the UI remains untouched if a category switches to NOT_PROVIDED. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testUIUntouchedWhenNotProvided() { |
| List<SnippetArticle> suggestions = createDummySuggestions(4, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // When the category switches to NOT_PROVIDED, UI stays the same. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.NOT_PROVIDED); |
| mSource.silentlyRemoveCategory(TEST_CATEGORY); |
| assertItemsFor(section(suggestions)); |
| |
| reloadNtp(); |
| assertItemsFor(); |
| } |
| |
| /** |
| * Tests that the UI updates on updated suggestions. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testUIUpdatesOnNewSuggestionsWhenOtherSectionSeen() { |
| List<SnippetArticle> suggestions = createDummySuggestions(4, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| |
| @CategoryInt |
| final int otherCategory = TEST_CATEGORY + 1; |
| List<SnippetArticle> otherSuggestions = createDummySuggestions(2, otherCategory); |
| mSource.setStatusForCategory(otherCategory, CategoryStatus.AVAILABLE); |
| mSource.setInfoForCategory( |
| otherCategory, new CategoryInfoBuilder(otherCategory).showIfEmpty().build()); |
| mSource.setSuggestionsForCategory(otherCategory, otherSuggestions); |
| |
| reloadNtp(); |
| assertItemsFor(section(suggestions), section(otherSuggestions)); |
| |
| // Indicate that the whole section is being viewed. |
| for (SnippetArticle article : otherSuggestions) article.mExposed = true; |
| |
| List<SnippetArticle> newSuggestions = createDummySuggestions(3, TEST_CATEGORY, "new"); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, newSuggestions); |
| assertItemsFor(section(newSuggestions), section(otherSuggestions)); |
| |
| reloadNtp(); |
| assertItemsFor(section(newSuggestions), section(otherSuggestions)); |
| } |
| |
| /** Tests whether a section stays visible if empty, if required. */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSectionVisibleIfEmpty() { |
| // Part 1: VisibleIfEmpty = true |
| FakeSuggestionsSource suggestionsSource = new FakeSuggestionsSource(); |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.INITIALIZING); |
| suggestionsSource.setInfoForCategory( |
| TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).showIfEmpty().build()); |
| |
| // 1.1 - Initial state |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| reloadNtp(); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // 1.2 - With suggestions |
| List<SnippetArticle> suggestions = |
| Collections.unmodifiableList(createDummySuggestions(3, TEST_CATEGORY)); |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| suggestionsSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // 1.3 - When all suggestions are dismissed |
| SuggestionsSection section = mAdapter.getSectionListForTesting().getSection(TEST_CATEGORY); |
| assertSectionMatches(section(suggestions), section); |
| section.removeSuggestionById(suggestions.get(0).mIdWithinCategory); |
| section.removeSuggestionById(suggestions.get(1).mIdWithinCategory); |
| section.removeSuggestionById(suggestions.get(2).mIdWithinCategory); |
| assertItemsFor(sectionWithStatusCard()); |
| |
| // Part 2: VisibleIfEmpty = false |
| suggestionsSource = new FakeSuggestionsSource(); |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| suggestionsSource.setInfoForCategory( |
| TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).build()); |
| |
| // 2.1 - Initial state |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| reloadNtp(); |
| assertItemsFor(); |
| |
| // 2.2 - With suggestions |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| suggestionsSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(); |
| |
| // 2.3 - When all suggestions are dismissed - N/A, suggestions don't get added. |
| } |
| |
| /** |
| * Tests that the more button is shown for sections that declare it. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testViewAllButton() { |
| // Part 1: With "View All" action |
| FakeSuggestionsSource suggestionsSource = new FakeSuggestionsSource(); |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.INITIALIZING); |
| suggestionsSource.setInfoForCategory(TEST_CATEGORY, |
| new CategoryInfoBuilder(TEST_CATEGORY) |
| .withAction(ContentSuggestionsAdditionalAction.VIEW_ALL) |
| .showIfEmpty() |
| .build()); |
| |
| // 1.1 - Initial state. |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| reloadNtp(); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // 1.2 - With suggestions. |
| List<SnippetArticle> suggestions = |
| Collections.unmodifiableList(createDummySuggestions(3, TEST_CATEGORY)); |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| suggestionsSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions).withViewAllButton()); |
| |
| // 1.3 - When all suggestions are dismissed. |
| SuggestionsSection section = mAdapter.getSectionListForTesting().getSection(TEST_CATEGORY); |
| assertSectionMatches(section(suggestions).withViewAllButton(), section); |
| section.removeSuggestionById(suggestions.get(0).mIdWithinCategory); |
| section.removeSuggestionById(suggestions.get(1).mIdWithinCategory); |
| section.removeSuggestionById(suggestions.get(2).mIdWithinCategory); |
| assertItemsFor(sectionWithStatusCard().withViewAllButton()); |
| |
| // Part 1: Without "View All" action |
| suggestionsSource = new FakeSuggestionsSource(); |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.INITIALIZING); |
| suggestionsSource.setInfoForCategory( |
| TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).showIfEmpty().build()); |
| |
| // 2.1 - Initial state. |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| reloadNtp(); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // 2.2 - With suggestions. |
| suggestionsSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| suggestionsSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| // 2.3 - When all suggestions are dismissed. |
| section = mAdapter.getSectionListForTesting().getSection(TEST_CATEGORY); |
| assertSectionMatches(section(suggestions), section); |
| section.removeSuggestionById(suggestions.get(0).mIdWithinCategory); |
| section.removeSuggestionById(suggestions.get(1).mIdWithinCategory); |
| section.removeSuggestionById(suggestions.get(2).mIdWithinCategory); |
| assertItemsFor(sectionWithStatusCard()); |
| } |
| |
| /** |
| * Tests that the more button is shown for sections that declare it. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testFetchButton() { |
| @CategoryInt |
| final int category = TEST_CATEGORY; |
| |
| // Part 1: With "Fetch more" action |
| FakeSuggestionsSource suggestionsSource = new FakeSuggestionsSource(); |
| suggestionsSource.setStatusForCategory(category, CategoryStatus.INITIALIZING); |
| suggestionsSource.setInfoForCategory(category, |
| new CategoryInfoBuilder(category) |
| .withAction(ContentSuggestionsAdditionalAction.FETCH) |
| .showIfEmpty() |
| .build()); |
| |
| // 1.1 - Initial state. |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| reloadNtp(); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // 1.2 - With suggestions. |
| List<SnippetArticle> articles = |
| Collections.unmodifiableList(createDummySuggestions(3, category)); |
| suggestionsSource.setStatusForCategory(category, CategoryStatus.AVAILABLE); |
| suggestionsSource.setSuggestionsForCategory(category, articles); |
| assertItemsFor(section(articles).withFetchButton()); |
| |
| // 1.3 - When all suggestions are dismissed. |
| SuggestionsSection section = mAdapter.getSectionListForTesting().getSection(category); |
| assertSectionMatches(section(articles).withFetchButton(), section); |
| section.removeSuggestionById(articles.get(0).mIdWithinCategory); |
| section.removeSuggestionById(articles.get(1).mIdWithinCategory); |
| section.removeSuggestionById(articles.get(2).mIdWithinCategory); |
| assertItemsFor(sectionWithStatusCard().withFetchButton()); |
| |
| // Part 1: Without "Fetch more" action |
| suggestionsSource = new FakeSuggestionsSource(); |
| suggestionsSource.setStatusForCategory(category, CategoryStatus.INITIALIZING); |
| suggestionsSource.setInfoForCategory( |
| category, new CategoryInfoBuilder(category).showIfEmpty().build()); |
| |
| // 2.1 - Initial state. |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| reloadNtp(); |
| assertItemsFor(sectionWithStatusCard().withProgress()); |
| |
| // 2.2 - With suggestions. |
| suggestionsSource.setStatusForCategory(category, CategoryStatus.AVAILABLE); |
| suggestionsSource.setSuggestionsForCategory(category, articles); |
| assertItemsFor(section(articles)); |
| |
| // 2.3 - When all suggestions are dismissed. |
| section = mAdapter.getSectionListForTesting().getSection(category); |
| assertSectionMatches(section(articles), section); |
| section.removeSuggestionById(articles.get(0).mIdWithinCategory); |
| section.removeSuggestionById(articles.get(1).mIdWithinCategory); |
| section.removeSuggestionById(articles.get(2).mIdWithinCategory); |
| assertItemsFor(sectionWithStatusCard()); |
| } |
| |
| /** |
| * Tests that invalidated suggestions are immediately removed. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testSuggestionInvalidated() { |
| List<SnippetArticle> suggestions = createDummySuggestions(3, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| SnippetArticle removed = suggestions.remove(1); |
| mSource.fireSuggestionInvalidated(TEST_CATEGORY, removed.mIdWithinCategory); |
| assertItemsFor(section(suggestions)); |
| } |
| |
| /** |
| * Tests that the UI handles dynamically added (server-side) categories correctly. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testDynamicCategories() { |
| List<SnippetArticle> suggestions = createDummySuggestions(3, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| assertItemsFor(section(suggestions)); |
| |
| int dynamicCategory1 = 1010; |
| List<SnippetArticle> dynamics1 = createDummySuggestions(5, dynamicCategory1); |
| mSource.setInfoForCategory(dynamicCategory1, |
| new CategoryInfoBuilder(dynamicCategory1) |
| .withAction(ContentSuggestionsAdditionalAction.VIEW_ALL) |
| .build()); |
| mSource.setStatusForCategory(dynamicCategory1, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(dynamicCategory1, dynamics1); |
| reloadNtp(); |
| |
| assertItemsFor(section(suggestions), section(dynamics1).withViewAllButton()); |
| |
| int dynamicCategory2 = 1011; |
| List<SnippetArticle> dynamics2 = createDummySuggestions(11, dynamicCategory2); |
| mSource.setInfoForCategory(dynamicCategory2, |
| new CategoryInfoBuilder(dynamicCategory1).build()); |
| mSource.setStatusForCategory(dynamicCategory2, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(dynamicCategory2, dynamics2); |
| reloadNtp(); |
| assertItemsFor( |
| section(suggestions), section(dynamics1).withViewAllButton(), section(dynamics2)); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| public void testArticlesForYouSection() { |
| when(mPrefServiceBridge.getBoolean(eq(Pref.NTP_ARTICLES_LIST_VISIBLE))).thenReturn(true); |
| // Show one section of suggestions from the test category, and one section with Articles for |
| // You. |
| List<SnippetArticle> suggestions = createDummySuggestions(3, TEST_CATEGORY); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, suggestions); |
| |
| mSource.setInfoForCategory(KnownCategories.ARTICLES, |
| new CategoryInfoBuilder(KnownCategories.ARTICLES).build()); |
| mSource.setStatusForCategory(KnownCategories.ARTICLES, CategoryStatus.AVAILABLE); |
| List<SnippetArticle> articles = createDummySuggestions(3, KnownCategories.ARTICLES); |
| mSource.setSuggestionsForCategory(KnownCategories.ARTICLES, articles); |
| |
| reloadNtp(); |
| assertItemsFor(section(suggestions), section(articles)); |
| |
| // Remove the test category section. The remaining lone Articles for You section should |
| // have a header. |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.NOT_PROVIDED); |
| reloadNtp(); |
| assertItemsFor(section(articles)); |
| } |
| |
| /** |
| * Tests that the order of the categories is kept. |
| */ |
| @Test |
| @Feature({"Ntp"}) |
| public void testCategoryOrder() { |
| int[] categories = {TEST_CATEGORY, TEST_CATEGORY + 2, TEST_CATEGORY + 3, TEST_CATEGORY + 4}; |
| FakeSuggestionsSource suggestionsSource = new FakeSuggestionsSource(); |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| registerCategory(suggestionsSource, categories[0], 0); |
| registerCategory(suggestionsSource, categories[1], 0); |
| registerCategory(suggestionsSource, categories[2], 0); |
| registerCategory(suggestionsSource, categories[3], 0); |
| reloadNtp(); |
| |
| List<RecyclerViewAdapter.Delegate<NewTabPageViewHolder, PartialBindCallback>> children = |
| mAdapter.getSectionListForTesting().getChildren(); |
| assertEquals(4, children.size()); |
| assertEquals(SuggestionsSection.class, children.get(0).getClass()); |
| assertEquals(categories[0], getCategory(children.get(0))); |
| assertEquals(SuggestionsSection.class, children.get(1).getClass()); |
| assertEquals(categories[1], getCategory(children.get(1))); |
| assertEquals(SuggestionsSection.class, children.get(2).getClass()); |
| assertEquals(categories[2], getCategory(children.get(2))); |
| assertEquals(SuggestionsSection.class, children.get(3).getClass()); |
| assertEquals(categories[3], getCategory(children.get(3))); |
| |
| // With a different order. |
| suggestionsSource = new FakeSuggestionsSource(); |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| registerCategory(suggestionsSource, categories[0], 0); |
| registerCategory(suggestionsSource, categories[2], 0); |
| registerCategory(suggestionsSource, categories[3], 0); |
| registerCategory(suggestionsSource, categories[1], 0); |
| reloadNtp(); |
| |
| children = mAdapter.getSectionListForTesting().getChildren(); |
| assertEquals(4, children.size()); |
| assertEquals(SuggestionsSection.class, children.get(0).getClass()); |
| assertEquals(categories[0], getCategory(children.get(0))); |
| assertEquals(SuggestionsSection.class, children.get(1).getClass()); |
| assertEquals(categories[2], getCategory(children.get(1))); |
| assertEquals(SuggestionsSection.class, children.get(2).getClass()); |
| assertEquals(categories[3], getCategory(children.get(2))); |
| assertEquals(SuggestionsSection.class, children.get(3).getClass()); |
| assertEquals(categories[1], getCategory(children.get(3))); |
| |
| // With unknown categories. |
| suggestionsSource = new FakeSuggestionsSource(); |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| registerCategory(suggestionsSource, categories[0], 0); |
| registerCategory(suggestionsSource, categories[2], 0); |
| registerCategory(suggestionsSource, categories[3], 0); |
| reloadNtp(); |
| |
| // The adapter is already initialised, it will not accept new categories anymore. |
| registerCategory(suggestionsSource, TEST_CATEGORY + 5, 1); |
| registerCategory(suggestionsSource, categories[1], 1); |
| |
| children = mAdapter.getSectionListForTesting().getChildren(); |
| assertEquals(3, children.size()); |
| assertEquals(SuggestionsSection.class, children.get(0).getClass()); |
| assertEquals(categories[0], getCategory(children.get(0))); |
| assertEquals(SuggestionsSection.class, children.get(1).getClass()); |
| assertEquals(categories[2], getCategory(children.get(1))); |
| assertEquals(SuggestionsSection.class, children.get(2).getClass()); |
| assertEquals(categories[3], getCategory(children.get(2))); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| public void testChangeNotifications() { |
| FakeSuggestionsSource suggestionsSource = spy(new FakeSuggestionsSource()); |
| registerCategory(suggestionsSource, TEST_CATEGORY, 3); |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(suggestionsSource); |
| |
| @SuppressWarnings("unchecked") |
| Callback<String> itemDismissedCallback = mock(Callback.class); |
| |
| reloadNtp(); |
| AdapterDataObserver dataObserver = mock(AdapterDataObserver.class); |
| mAdapter.registerAdapterDataObserver(dataObserver); |
| |
| // Adapter content: |
| // Idx | Item |
| // ----|---------------- |
| // 0 | Above-the-fold |
| // 1 | Header |
| // 2-4 | Sugg*3 |
| // 5 | Action |
| // 6 | Footer |
| |
| // Dismiss the second suggestion of the second section. |
| mAdapter.dismissItem(3, itemDismissedCallback); |
| verify(itemDismissedCallback).onResult(anyString()); |
| verify(dataObserver).onItemRangeRemoved(3, 1); |
| |
| // Make sure the call with the updated position works properly. |
| mAdapter.dismissItem(3, itemDismissedCallback); |
| verify(itemDismissedCallback, times(2)).onResult(anyString()); |
| verify(dataObserver, times(2)).onItemRangeRemoved(3, 1); |
| |
| // Dismiss the last suggestion in the section. We should now show the status card. |
| reset(dataObserver); |
| mAdapter.dismissItem(2, itemDismissedCallback); |
| verify(itemDismissedCallback, times(3)).onResult(anyString()); |
| verify(dataObserver).onItemRangeRemoved(2, 1); // Suggestion removed |
| verify(dataObserver).onItemRangeInserted(2, 1); // Status card added |
| |
| // Adapter content: |
| // Idx | Item |
| // ----|---------------- |
| // 0 | Above-the-fold |
| // 1 | Header |
| // 2 | Status |
| // 3 | Action |
| // 4 | Progress Indicator |
| // 5 | Footer |
| |
| final int newSuggestionCount = 7; |
| reset(dataObserver); |
| suggestionsSource.setSuggestionsForCategory( |
| TEST_CATEGORY, createDummySuggestions(newSuggestionCount, TEST_CATEGORY)); |
| verify(dataObserver).onItemRangeInserted(2, newSuggestionCount); |
| verify(dataObserver).onItemRangeRemoved(2 + newSuggestionCount, 1); |
| |
| // Adapter content: |
| // Idx | Item |
| // ----|---------------- |
| // 0 | Above-the-fold |
| // 1 | Header |
| // 2-8 | Sugg*7 |
| // 9 | Action |
| // 10 | Footer |
| |
| reset(dataObserver); |
| suggestionsSource.setSuggestionsForCategory( |
| TEST_CATEGORY, createDummySuggestions(0, TEST_CATEGORY)); |
| mAdapter.getSectionListForTesting().onCategoryStatusChanged( |
| TEST_CATEGORY, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| // All suggestions as well as the header and the action should be gone. |
| verify(dataObserver).onItemRangeRemoved(1, newSuggestionCount + 2); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| public void testSigninPromo() { |
| @CategoryInt |
| final int remoteCategory = KnownCategories.REMOTE_CATEGORIES_OFFSET + TEST_CATEGORY; |
| |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| when(mMockSigninManager.isSignedInOnNative()).thenReturn(false); |
| resetUiDelegate(); |
| reloadNtp(); |
| |
| assertItemsFor(sectionWithStatusCard().withProgress(), signinPromo()); |
| assertTrue(isSignInPromoVisible()); |
| |
| List<DestructionObserver> observers = getDestructionObserver(mUiDelegate); |
| SuggestionsSource.Observer suggestionsObserver = |
| findFirstInstanceOf(observers, SuggestionsSource.Observer.class); |
| assertNotNull(suggestionsObserver); |
| |
| SignInPromo signInPromo = mAdapter.getSignInPromoForTesting(); |
| assertNotNull(signInPromo); |
| SigninObserver signinObserver = signInPromo.getSigninObserverForTesting(); |
| assertNotNull(signinObserver); |
| |
| signinObserver.onSignedIn(); |
| assertFalse(isSignInPromoVisible()); |
| |
| signinObserver.onSignedOut(); |
| assertTrue(isSignInPromoVisible()); |
| |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(false); |
| signinObserver.onSignInAllowedChanged(); |
| assertFalse(isSignInPromoVisible()); |
| |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| signinObserver.onSignInAllowedChanged(); |
| assertTrue(isSignInPromoVisible()); |
| |
| mSource.setRemoteSuggestionsEnabled(false); |
| suggestionsObserver.onCategoryStatusChanged( |
| remoteCategory, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| assertFalse(isSignInPromoVisible()); |
| |
| mSource.setRemoteSuggestionsEnabled(true); |
| suggestionsObserver.onCategoryStatusChanged(remoteCategory, CategoryStatus.AVAILABLE); |
| assertTrue(isSignInPromoVisible()); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| public void testSigninPromoSuppressionActive() { |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| when(mMockSigninManager.isSignedInOnNative()).thenReturn(false); |
| |
| // Suppress promo. |
| ChromePreferenceManager.getInstance().setNewTabPageSigninPromoSuppressionPeriodStart( |
| System.currentTimeMillis()); |
| |
| resetUiDelegate(); |
| reloadNtp(); |
| assertFalse(isSignInPromoVisible()); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| public void testSigninPromoSuppressionExpired() { |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| when(mMockSigninManager.isSignedInOnNative()).thenReturn(false); |
| |
| // Suppress promo. |
| ChromePreferenceManager preferenceManager = ChromePreferenceManager.getInstance(); |
| preferenceManager.setNewTabPageSigninPromoSuppressionPeriodStart( |
| System.currentTimeMillis() - SignInPromo.SUPPRESSION_PERIOD_MS); |
| |
| resetUiDelegate(); |
| reloadNtp(); |
| assertTrue(isSignInPromoVisible()); |
| |
| // SignInPromo should clear shared preference when suppression period ends. |
| assertEquals(0, preferenceManager.getNewTabPageSigninPromoSuppressionPeriodStart()); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| @Config(shadows = MyShadowResources.class) |
| public void testSigninPromoDismissal() { |
| final String signInPromoText = "sign in"; |
| when(MyShadowResources.sResources.getText( |
| R.string.signin_promo_description_ntp_content_suggestions_legacy)) |
| .thenReturn(signInPromoText); |
| |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| when(mMockSigninManager.isSignedInOnNative()).thenReturn(false); |
| ChromePreferenceManager.getInstance().writeBoolean( |
| ChromePreferenceManager.NTP_SIGNIN_PROMO_DISMISSED, false); |
| reloadNtp(); |
| |
| final int signInPromoPosition = mAdapter.getFirstPositionForType(ItemViewType.PROMO); |
| assertNotEquals(RecyclerView.NO_POSITION, signInPromoPosition); |
| @SuppressWarnings("unchecked") |
| Callback<String> itemDismissedCallback = mock(Callback.class); |
| mAdapter.dismissItem(signInPromoPosition, itemDismissedCallback); |
| |
| verify(itemDismissedCallback).onResult(anyString()); |
| assertFalse(isSignInPromoVisible()); |
| assertTrue(ChromePreferenceManager.getInstance().readBoolean( |
| ChromePreferenceManager.NTP_SIGNIN_PROMO_DISMISSED, false)); |
| reloadNtp(); |
| assertFalse(isSignInPromoVisible()); |
| } |
| |
| @Test |
| @Feature({"Ntp"}) |
| public void testAllDismissedVisibility() { |
| SignInPromo signInPromo = mAdapter.getSignInPromoForTesting(); |
| assertNotNull(signInPromo); |
| SigninObserver signinObserver = signInPromo.getSigninObserverForTesting(); |
| assertNotNull(signinObserver); |
| |
| @SuppressWarnings("unchecked") |
| Callback<String> itemDismissedCallback = mock(Callback.class); |
| |
| // By default, there is no All Dismissed item. |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| // 1 | Header |
| // 2 | Status |
| // 3 | Progress Indicator |
| // 4 | Footer |
| assertEquals(4, mAdapter.getFirstPositionForType(ItemViewType.FOOTER)); |
| assertEquals(RecyclerView.NO_POSITION, |
| mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| |
| // When we remove the section, the All Dismissed item should be there. |
| mAdapter.dismissItem(2, itemDismissedCallback); |
| |
| verify(itemDismissedCallback).onResult(anyString()); |
| |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| // 1 | All Dismissed |
| assertEquals( |
| RecyclerView.NO_POSITION, mAdapter.getFirstPositionForType(ItemViewType.FOOTER)); |
| assertEquals(1, mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| |
| // On Sign out, the sign in promo should come and the All Dismissed item be removed. |
| when(mMockSigninManager.isSignedInOnNative()).thenReturn(false); |
| signinObserver.onSignedOut(); |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| // 1 | Sign In Promo |
| // 2 | Footer |
| assertEquals(2, mAdapter.getFirstPositionForType(ItemViewType.FOOTER)); |
| assertEquals(RecyclerView.NO_POSITION, |
| mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| |
| // When sign in is disabled, the promo is removed and the All Dismissed item can come back. |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(false); |
| signinObserver.onSignInAllowedChanged(); |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| // 1 | All Dismissed |
| assertEquals( |
| RecyclerView.NO_POSITION, mAdapter.getFirstPositionForType(ItemViewType.FOOTER)); |
| assertEquals(1, mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| |
| // Re-enabling sign in should only bring the promo back, thus removing the AllDismissed item |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| signinObserver.onSignInAllowedChanged(); |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| // 1 | Sign In Promo |
| // 2 | Footer |
| assertEquals(ItemViewType.FOOTER, mAdapter.getItemViewType(2)); |
| assertEquals(RecyclerView.NO_POSITION, |
| mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| |
| // Disabling remote suggestions should remove both the promo and the AllDismissed item |
| mSource.setRemoteSuggestionsEnabled(false); |
| mAdapter.getSuggestionsSourceObserverForTesting().onCategoryStatusChanged( |
| KnownCategories.REMOTE_CATEGORIES_OFFSET + TEST_CATEGORY, |
| CategoryStatus.CATEGORY_EXPLICITLY_DISABLED); |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| assertEquals( |
| RecyclerView.NO_POSITION, mAdapter.getFirstPositionForType(ItemViewType.FOOTER)); |
| assertEquals(RecyclerView.NO_POSITION, |
| mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| assertEquals( |
| RecyclerView.NO_POSITION, mAdapter.getFirstPositionForType(ItemViewType.PROMO)); |
| assertEquals(1, mAdapter.getItemCount()); |
| |
| // Prepare some suggestions. They should not load because the category is dismissed on |
| // the current NTP. |
| mSource.setRemoteSuggestionsEnabled(true); |
| mAdapter.getSuggestionsSourceObserverForTesting().onCategoryStatusChanged( |
| KnownCategories.REMOTE_CATEGORIES_OFFSET + TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); |
| mSource.setSuggestionsForCategory(TEST_CATEGORY, createDummySuggestions(1, TEST_CATEGORY)); |
| mSource.setInfoForCategory(TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).build()); |
| assertEquals(3, mAdapter.getItemCount()); // TODO(dgn): rewrite with section descriptors. |
| |
| // On Sign in, we should reset the sections, bring back suggestions instead of the All |
| // Dismissed item. |
| mAdapter.getSectionListForTesting().refreshSuggestions(); |
| when(mMockSigninManager.isSignInAllowed()).thenReturn(true); |
| signinObserver.onSignedIn(); |
| // Adapter content: |
| // Idx | Item |
| // ----|-------------------- |
| // 0 | Above-the-fold |
| // 1 | Header |
| // 2 | Suggestion |
| // 4 | Footer |
| assertEquals(3, mAdapter.getFirstPositionForType(ItemViewType.FOOTER)); |
| assertEquals(RecyclerView.NO_POSITION, |
| mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); |
| } |
| |
| /** |
| * Robolectric shadow to mock out calls to {@link Resources#getString}. |
| */ |
| @Implements(Resources.class) |
| public static class MyShadowResources extends ShadowResources { |
| public static final Resources sResources = mock(Resources.class); |
| |
| @Implementation |
| public CharSequence getText(int id) { |
| return sResources.getText(id); |
| } |
| } |
| |
| /** |
| * Asserts that the given {@link TreeNode} is a {@link SuggestionsSection} that matches the |
| * given {@link SectionDescriptor}. |
| * @param descriptor The section descriptor to match against. |
| * @param section The section from the adapter. |
| */ |
| private void assertSectionMatches(SectionDescriptor descriptor, SuggestionsSection section) { |
| ItemsMatcher matcher = new ItemsMatcher(section); |
| matcher.expectSection(descriptor); |
| matcher.finish(); |
| } |
| |
| /** |
| * Asserts that {@link #mAdapter}.{@link NewTabPageAdapter#getItemCount()} corresponds to an NTP |
| * with the given sections in it. |
| * |
| * @param descriptors A list of descriptors, each describing a section that should be present on |
| * the UI. |
| */ |
| private void assertItemsFor(SectionDescriptor... descriptors) { |
| ItemsMatcher matcher = new ItemsMatcher(mAdapter.getRootForTesting()); |
| matcher.expectAboveTheFoldItem(); |
| for (SectionDescriptor descriptor : descriptors) matcher.expectSection(descriptor); |
| if (descriptors.length == 0) { |
| matcher.expectAllDismissedItem(); |
| } else { |
| matcher.expectFooter(); |
| } |
| matcher.finish(); |
| } |
| |
| /** |
| * To be used with {@link #assertItemsFor(SectionDescriptor...)}, for a section with |
| * {@code numSuggestions} cards in it. |
| * @param suggestions The list of suggestions in the section. If the list is empty, use either |
| * no section at all (if it is not displayed) or {@link #sectionWithStatusCard()}. |
| * @return A descriptor for the section. |
| */ |
| private SectionDescriptor section(List<SnippetArticle> suggestions) { |
| assert !suggestions.isEmpty(); |
| return new SectionDescriptor(suggestions); |
| } |
| |
| private SectionDescriptor signinPromo() { |
| return new SectionDescriptor().isSigninPromo(); |
| } |
| |
| /** |
| * To be used with {@link #assertItemsFor(SectionDescriptor...)}, for a section that has no |
| * suggestions, but a status card to be displayed. |
| * @return A descriptor for the section. |
| */ |
| private SectionDescriptor sectionWithStatusCard() { |
| return new SectionDescriptor(Collections.emptyList()).withStatusCard(); |
| } |
| |
| /** |
| * To be used with {@link #assertItemsFor(SectionDescriptor...)}, for a section that has no |
| * suggestions. Should only be used with the modern layout; use {@link #sectionWithStatusCard()} |
| * otherwise. |
| * @return A descriptor for the section. |
| */ |
| private SectionDescriptor emptySection() { |
| assertTrue(false); |
| return new SectionDescriptor(Collections.emptyList()); |
| } |
| |
| private void resetUiDelegate() { |
| reset(mUiDelegate); |
| when(mUiDelegate.getSuggestionsSource()).thenReturn(mSource); |
| when(mUiDelegate.getEventReporter()).thenReturn(mock(SuggestionsEventReporter.class)); |
| when(mUiDelegate.getSuggestionsRanker()).thenReturn(mock(SuggestionsRanker.class)); |
| } |
| |
| private void reloadNtp() { |
| mSource.removeObservers(); |
| mAdapter = new NewTabPageAdapter(mUiDelegate, mock(View.class), /* logoView = */ |
| makeUiConfig(), mOfflinePageBridge, mock(ContextMenuManager.class) |
| /* tileGroupDelegate = */); |
| mAdapter.refreshSuggestions(); |
| } |
| |
| private boolean isSignInPromoVisible() { |
| return mAdapter.getFirstPositionForType(ItemViewType.PROMO) != RecyclerView.NO_POSITION; |
| } |
| |
| private int getCategory(RecyclerViewAdapter.Delegate item) { |
| return ((SuggestionsSection) item).getCategory(); |
| } |
| |
| /** |
| * Note: Currently the observers need to be re-registered to be returned again if this method |
| * has been called, as it relies on argument captors that don't repeatedly capture individual |
| * calls. |
| * @return The currently registered destruction observers. |
| */ |
| private List<DestructionObserver> getDestructionObserver(SuggestionsUiDelegate delegate) { |
| ArgumentCaptor<DestructionObserver> observers = |
| ArgumentCaptor.forClass(DestructionObserver.class); |
| verify(delegate, atLeast(0)).addDestructionObserver(observers.capture()); |
| return observers.getAllValues(); |
| } |
| |
| @Nullable |
| @SuppressWarnings("unchecked") |
| private static <T> T findFirstInstanceOf(Collection<?> collection, Class<T> clazz) { |
| for (Object item : collection) { |
| if (clazz.isAssignableFrom(item.getClass())) return (T) item; |
| } |
| return null; |
| } |
| } |