blob: 4f0d17994e0a4172e6c689f9e2064b06faed6309 [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.snippets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.v7.content.res.AppCompatResources;
import android.text.format.DateUtils;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.chromium.base.Callback;
import org.chromium.base.DiscardableReferencePool;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.favicon.IconType;
import org.chromium.chrome.browser.favicon.LargeIconBridge;
import org.chromium.chrome.browser.native_page.ContextMenuManager;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.ntp.cards.PersonalizedPromoViewHolder;
import org.chromium.chrome.browser.ntp.cards.SuggestionsCategoryInfo;
import org.chromium.chrome.browser.signin.DisplayableProfileData;
import org.chromium.chrome.browser.signin.SigninAccessPoint;
import org.chromium.chrome.browser.signin.SigninPromoController;
import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.suggestions.ContentSuggestionsAdditionalAction;
import org.chromium.chrome.browser.suggestions.DestructionObserver;
import org.chromium.chrome.browser.suggestions.ImageFetcher;
import org.chromium.chrome.browser.suggestions.SuggestionsEventReporter;
import org.chromium.chrome.browser.suggestions.SuggestionsNavigationDelegate;
import org.chromium.chrome.browser.suggestions.SuggestionsRanker;
import org.chromium.chrome.browser.suggestions.SuggestionsRecyclerView;
import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate;
import org.chromium.chrome.browser.suggestions.ThumbnailGradient;
import org.chromium.chrome.browser.widget.ThumbnailProvider;
import org.chromium.chrome.browser.widget.ThumbnailProvider.ThumbnailRequest;
import org.chromium.chrome.browser.widget.displaystyle.HorizontalDisplayStyle;
import org.chromium.chrome.browser.widget.displaystyle.UiConfig;
import org.chromium.chrome.browser.widget.displaystyle.VerticalDisplayStyle;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.RenderTestRule;
import org.chromium.chrome.test.util.browser.compositor.layouts.DisableChromeAnimations;
import org.chromium.chrome.test.util.browser.suggestions.DummySuggestionsEventReporter;
import org.chromium.chrome.test.util.browser.suggestions.FakeSuggestionsSource;
import org.chromium.chrome.test.util.browser.suggestions.SuggestionsDependenciesRule;
import org.chromium.net.NetworkChangeNotifier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Tests for the appearance of Article Snippets.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class ArticleSnippetsTest {
@Rule
public SuggestionsDependenciesRule mSuggestionsDeps = new SuggestionsDependenciesRule();
@Rule
public ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeActivity.class);
@Rule
public RenderTestRule mRenderTestRule = new RenderTestRule();
@Rule
public TestRule mDisableChromeAnimations = new DisableChromeAnimations();
private SuggestionsUiDelegate mUiDelegate;
private FakeSuggestionsSource mSnippetsSource;
private MockThumbnailProvider mThumbnailProvider;
private SuggestionsRecyclerView mRecyclerView;
private ContextMenuManager mContextMenuManager;
private FrameLayout mContentView;
private SnippetArticleViewHolder mSuggestion;
private PersonalizedPromoViewHolder mSigninPromo;
private UiConfig mUiConfig;
private static final int FULL_CATEGORY = 0;
private static final int MINIMAL_CATEGORY = 1;
private long mTimestamp;
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
mThumbnailProvider = new MockThumbnailProvider();
mSnippetsSource = new FakeSuggestionsSource();
mSuggestionsDeps.getFactory().thumbnailProvider = mThumbnailProvider;
mSuggestionsDeps.getFactory().suggestionsSource = mSnippetsSource;
mUiDelegate = new MockUiDelegate();
mSnippetsSource.setDefaultFavicon(getBitmap(R.drawable.star_green));
mTimestamp = System.currentTimeMillis() - 5 * DateUtils.MINUTE_IN_MILLIS;
ThreadUtils.runOnUiThreadBlocking(() -> {
if (!NetworkChangeNotifier.isInitialized()) {
NetworkChangeNotifier.init();
}
NetworkChangeNotifier.forceConnectivityState(true);
});
ThreadUtils.runOnUiThreadBlocking(() -> {
ChromeActivity activity = mActivityTestRule.getActivity();
mContentView = new FrameLayout(activity);
mUiConfig = new UiConfig(mContentView);
activity.setContentView(mContentView);
mRecyclerView = new SuggestionsRecyclerView(activity);
mContextMenuManager = new ContextMenuManager(mUiDelegate.getNavigationDelegate(),
mRecyclerView::setTouchEnabled, activity::closeContextMenu,
NewTabPage.CONTEXT_MENU_USER_ACTION_PREFIX);
mRecyclerView.init(mUiConfig, mContextMenuManager);
mSuggestion = new SnippetArticleViewHolder(mRecyclerView, mContextMenuManager,
mUiDelegate, mUiConfig, /* offlinePageBridge = */ null);
});
}
@After
public void tearDown() {
if (mSigninPromo != null) mSigninPromo.setSigninPromoControllerForTests(null);
}
@Test
@MediumTest
@Feature({"ArticleSnippets", "RenderTest"})
public void testSnippetAppearance() throws IOException {
SuggestionsCategoryInfo fullCategoryInfo = new SuggestionsCategoryInfo(FULL_CATEGORY,
"Section Title", ContentSuggestionsCardLayout.FULL_CARD,
ContentSuggestionsAdditionalAction.NONE,
/* show_if_empty = */ true, "No suggestions");
SnippetArticle shortSnippet = new SnippetArticle(FULL_CATEGORY, "id1", "Snippet",
"Publisher", "www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
false, // Is video suggestion
null); // Thumbnail dominant color
Bitmap watch = BitmapFactory.decodeFile(
UrlUtils.getIsolatedTestFilePath("chrome/test/data/android/watch.jpg"));
Drawable drawable = ThumbnailGradient.createDrawableWithGradientIfNeeded(
watch, mActivityTestRule.getActivity().getResources());
shortSnippet.setThumbnail(mUiDelegate.getReferencePool().put(drawable));
renderSuggestion(shortSnippet, fullCategoryInfo, "short_snippet");
SnippetArticle longSnippet = new SnippetArticle(FULL_CATEGORY, "id2",
new String(new char[20]).replace("\0", "Snippet "),
new String(new char[20]).replace("\0", "Publisher "),
"www.google.com",
mTimestamp, // Publish timestamp
20f, // Score
mTimestamp, // Fetch timestamp
false, // Is video suggestion
Color.GREEN); // Thumbnail dominant color
renderSuggestion(longSnippet, fullCategoryInfo, "long_snippet");
SuggestionsCategoryInfo minimalCategory = new SuggestionsCategoryInfo(MINIMAL_CATEGORY,
"Section Title", ContentSuggestionsCardLayout.MINIMAL_CARD,
ContentSuggestionsAdditionalAction.NONE,
/* show_if_empty = */ true, "No suggestions");
SnippetArticle minimalSnippet = new SnippetArticle(MINIMAL_CATEGORY, "id3",
new String(new char[20]).replace("\0", "Bookmark "), "Publisher",
"www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
false, // Is video suggestion
null); // Thumbnail dominant color
renderSuggestion(minimalSnippet, minimalCategory, "minimal_snippet");
SnippetArticle minimalSnippet2 = new SnippetArticle(MINIMAL_CATEGORY, "id4", "Bookmark",
"Publisher", "www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
false, // Is video suggestion
null); // Thumbnail dominant color
// See how everything looks in narrow layout.
ThreadUtils.runOnUiThreadBlocking(() -> {
// Since we inform the UiConfig manually about the desired display style, the only
// reason we actually change the LayoutParams is for the rendered Views to look right.
ViewGroup.LayoutParams params = mContentView.getLayoutParams();
params.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350,
mRecyclerView.getResources().getDisplayMetrics());
mContentView.setLayoutParams(params);
mUiConfig.setDisplayStyleForTesting(new UiConfig.DisplayStyle(
HorizontalDisplayStyle.NARROW, VerticalDisplayStyle.REGULAR));
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
renderSuggestion(shortSnippet, fullCategoryInfo, "short_snippet_narrow");
renderSuggestion(longSnippet, fullCategoryInfo, "long_snippet_narrow");
renderSuggestion(minimalSnippet, minimalCategory, "long_minimal_snippet_narrow");
renderSuggestion(minimalSnippet2, minimalCategory, "short_minimal_snippet_narrow");
}
// TODO(bauerb): Test top, middle, and bottom card backgrounds.
@Test
@MediumTest
@Feature({"ArticleSnippets", "RenderTest"})
public void testDownloadSuggestion() throws IOException {
String downloadFilePath =
UrlUtils.getIsolatedTestFilePath("chrome/test/data/android/capybara.jpg");
ThreadUtils.runOnUiThreadBlocking(() -> {
SnippetArticle downloadSuggestion = new SnippetArticle(KnownCategories.DOWNLOADS, "id1",
"test_image.jpg", "example.com", "http://example.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
false, // Is video suggestion
null); // Thumbnail dominant color
downloadSuggestion.setAssetDownloadData("asdf", downloadFilePath, "image/jpeg");
SuggestionsCategoryInfo downloadsCategory = new SuggestionsCategoryInfo(
KnownCategories.DOWNLOADS, "Downloads", ContentSuggestionsCardLayout.FULL_CARD,
ContentSuggestionsAdditionalAction.NONE,
/* show_if_empty = */ true, "No suggestions");
mSuggestion.onBindViewHolder(downloadSuggestion, downloadsCategory);
mContentView.addView(mSuggestion.itemView);
});
mRenderTestRule.render(mSuggestion.itemView, "download_snippet_placeholder");
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
List<ThumbnailRequest> requests = mThumbnailProvider.getRequests();
Assert.assertEquals(1, requests.size());
ThumbnailRequest request = requests.get(0);
Assert.assertEquals(downloadFilePath, request.getFilePath());
Bitmap thumbnail = BitmapFactory.decodeFile(downloadFilePath);
ThreadUtils.runOnUiThreadBlocking(() -> {
mThumbnailProvider.fulfillRequest(request, thumbnail);
});
mRenderTestRule.render(mSuggestion.itemView, "download_snippet_thumbnail");
}
@Test
@MediumTest
@Feature({"ArticleSnippets", "RenderTest"})
public void testVideoSuggestion() throws IOException {
SuggestionsCategoryInfo categoryInfo = new SuggestionsCategoryInfo(FULL_CATEGORY,
"Section Title", ContentSuggestionsCardLayout.FULL_CARD,
ContentSuggestionsAdditionalAction.NONE,
/* show_if_empty = */ true, "No suggestions");
SnippetArticle suggestionWithLightDominantColor =
new SnippetArticle(FULL_CATEGORY, "id1", "Snippet", "Publisher", "www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
true, // Is video suggestion
0xFFEEEEEE); // Thumbnail dominant color
renderSuggestion(suggestionWithLightDominantColor, categoryInfo,
"video_suggestion_with_light_dominant_color");
SnippetArticle suggestionWithLightThumbnail =
new SnippetArticle(FULL_CATEGORY, "id1", "Snippet", "Publisher", "www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
true, // Is video suggestion
0xFFEEEEEE); // Thumbnail dominant color
setThumbnail(suggestionWithLightThumbnail, "chrome/test/data/android/watch.jpg");
renderSuggestion(suggestionWithLightThumbnail, categoryInfo,
"video_suggestion_with_light_thumbnail");
SnippetArticle suggestionWithDarkDominantColor =
new SnippetArticle(FULL_CATEGORY, "id1", "Snippet", "Publisher", "www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
true, // Is video suggestion
0xFF8E5C39); // Thumbnail dominant color
renderSuggestion(suggestionWithDarkDominantColor, categoryInfo,
"video_suggestion_with_dark_dominant_color");
SnippetArticle suggestionWithDarkThumbnail =
new SnippetArticle(FULL_CATEGORY, "id1", "Snippet", "Publisher", "www.google.com",
mTimestamp, // Publish timestamp
10f, // Score
mTimestamp, // Fetch timestamp
true, // Is video suggestion
0xFF8E5C39); // Thumbnail dominant color
setThumbnail(suggestionWithDarkThumbnail, "chrome/test/data/android/capybara.jpg");
renderSuggestion(
suggestionWithDarkThumbnail, categoryInfo, "video_suggestion_with_dark_thumbnail");
}
@Test
@MediumTest
@Feature({"ArticleSnippets", "RenderTest"})
public void testPersonalizedSigninPromosNoAccounts() throws IOException {
ThreadUtils.runOnUiThreadBlocking(() -> {
createPersonalizedSigninPromo(null);
mContentView.addView(mSigninPromo.itemView);
});
mRenderTestRule.render(mSigninPromo.itemView, "cold_state_personalized_signin_promo");
}
@Test
@MediumTest
@Feature({"ArticleSnippets", "RenderTest"})
public void testPersonalizedSigninPromosWithAccount() throws IOException {
ThreadUtils.runOnUiThreadBlocking(() -> {
createPersonalizedSigninPromo(getTestProfileData());
mContentView.addView(mSigninPromo.itemView);
});
mRenderTestRule.render(mSigninPromo.itemView, "hot_state_personalized_signin_promo");
}
private void createPersonalizedSigninPromo(@Nullable DisplayableProfileData profileData) {
SigninPromoController signinPromoController =
new SigninPromoController(SigninAccessPoint.NTP_CONTENT_SUGGESTIONS);
mSigninPromo = new PersonalizedPromoViewHolder(mRecyclerView, null, mUiConfig);
mSigninPromo.setSigninPromoControllerForTests(signinPromoController);
mSigninPromo.bindAndConfigureViewForTests(profileData);
}
private DisplayableProfileData getTestProfileData() {
String accountId = "test@gmail.com";
Drawable image = AppCompatResources.getDrawable(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
R.drawable.logo_avatar_anonymous);
String fullName = "Test Account";
String givenName = "Test";
return new DisplayableProfileData(accountId, image, fullName, givenName);
}
private void renderSuggestion(SnippetArticle suggestion, SuggestionsCategoryInfo categoryInfo,
String renderId) throws IOException {
ThreadUtils.runOnUiThreadBlocking(() -> {
mSuggestion.onBindViewHolder(suggestion, categoryInfo);
mContentView.addView(mSuggestion.itemView);
});
mRenderTestRule.render(mSuggestion.itemView, renderId);
ThreadUtils.runOnUiThreadBlocking(() -> {
mContentView.removeView(mSuggestion.itemView);
mSuggestion.recycle();
});
}
private void setThumbnail(SnippetArticle suggestion, String thumbnailPath) {
Bitmap watch = BitmapFactory.decodeFile(UrlUtils.getIsolatedTestFilePath(thumbnailPath));
Drawable drawable = ThumbnailGradient.createDrawableWithGradientIfNeeded(
watch, mActivityTestRule.getActivity().getResources());
suggestion.setThumbnail(mUiDelegate.getReferencePool().put(drawable));
}
private Bitmap getBitmap(@DrawableRes int resId) {
return BitmapFactory.decodeResource(
InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(),
resId);
}
/**
* Simple mock ThumbnailProvider that allows delaying requests and fulfilling them at a later
* point.
*/
private static class MockThumbnailProvider implements ThumbnailProvider {
private final List<ThumbnailRequest> mRequests = new ArrayList<>();
public List<ThumbnailRequest> getRequests() {
return mRequests;
}
public void fulfillRequest(ThumbnailRequest request, Bitmap bitmap) {
cancelRetrieval(request);
request.onThumbnailRetrieved(request.getFilePath(), bitmap);
}
@Override
public void destroy() {}
@Override
public void getThumbnail(ThumbnailRequest request) {
mRequests.add(request);
}
@Override
public void removeThumbnailsFromDisk(String contentId) {}
@Override
public void cancelRetrieval(ThumbnailRequest request) {
boolean removed = mRequests.remove(request);
Assert.assertTrue(
String.format(Locale.US, "Request for '%s' not found", request.getFilePath()),
removed);
}
}
/**
* A SuggestionsUiDelegate to initialize our Adapter.
*/
private class MockUiDelegate implements SuggestionsUiDelegate {
private final SuggestionsEventReporter mSuggestionsEventReporter =
new DummySuggestionsEventReporter();
private final SuggestionsRanker mSuggestionsRanker = new SuggestionsRanker();
private final DiscardableReferencePool mReferencePool = new DiscardableReferencePool();
private final ImageFetcher mImageFetcher =
new MockImageFetcher(mSnippetsSource, mReferencePool);
@Override
public SuggestionsSource getSuggestionsSource() {
return mSnippetsSource;
}
@Override
public SuggestionsRanker getSuggestionsRanker() {
return mSuggestionsRanker;
}
@Override
public DiscardableReferencePool getReferencePool() {
return mReferencePool;
}
@Override
public void addDestructionObserver(DestructionObserver destructionObserver) {}
@Override
public boolean isVisible() {
return true;
}
@Override
public SuggestionsEventReporter getEventReporter() {
return mSuggestionsEventReporter;
}
@Override
public SuggestionsNavigationDelegate getNavigationDelegate() {
return null;
}
@Override
public ImageFetcher getImageFetcher() {
return mImageFetcher;
}
@Override
public SnackbarManager getSnackbarManager() {
return mActivityTestRule.getActivity().getSnackbarManager();
}
}
private class MockImageFetcher extends ImageFetcher {
public MockImageFetcher(
SuggestionsSource suggestionsSource, DiscardableReferencePool referencePool) {
super(suggestionsSource, null, referencePool, null);
}
@Override
public void makeFaviconRequest(SnippetArticle suggestion, final int faviconSizePx,
final Callback<Bitmap> faviconCallback) {
// Run the callback asynchronously in case the caller made that assumption.
ThreadUtils.postOnUiThread(() -> {
// Return an arbitrary drawable.
faviconCallback.onResult(getBitmap(R.drawable.star_green));
});
}
@Override
public void makeLargeIconRequest(final String url, final int largeIconSizePx,
final LargeIconBridge.LargeIconCallback callback) {
// Run the callback asynchronously in case the caller made that assumption.
ThreadUtils.postOnUiThread(() -> {
// Return an arbitrary drawable.
callback.onLargeIconAvailable(
getBitmap(R.drawable.star_green), largeIconSizePx, true, IconType.INVALID);
});
}
}
}