blob: e03e7d826c1c6428a013058dcfc0520bf8138ea9 [file] [log] [blame]
// Copyright 2018 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.contextual_suggestions;
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.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.pm.ActivityInfo;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
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.ThreadUtils;
import org.chromium.base.test.params.ParameterAnnotations;
import org.chromium.base.test.params.ParameterProvider;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.CallbackHelper;
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.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.contextual_suggestions.ContextualSuggestionsModel.PropertyKey;
import org.chromium.chrome.browser.dependency_injection.ChromeAppModule;
import org.chromium.chrome.browser.dependency_injection.ModuleFactoryOverrides;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.fullscreen.FullscreenManagerTestUtils;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.ListObservable.ListObserver;
import org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.native_page.ContextMenuManager;
import org.chromium.chrome.browser.ntp.cards.NewTabPageViewHolder.PartialBindCallback;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType;
import org.chromium.chrome.browser.test.ScreenShooter;
import org.chromium.chrome.browser.toolbar.ToolbarPhone;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.chrome.test.BottomSheetTestRule;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.RenderTestRule;
import org.chromium.chrome.test.util.ViewUtils;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
import org.chromium.chrome.test.util.browser.RecyclerViewTestUtils;
import org.chromium.chrome.test.util.browser.compositor.layouts.DisableChromeAnimations;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.GestureStateListener;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.RenderCoordinates;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestWebContentsObserver;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.test.util.UiRestriction;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/**
* Tests related to displaying contextual suggestions in a bottom sheet.
*/
@RunWith(ParameterizedRunner.class)
@ParameterAnnotations.UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
@EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BOTTOM_SHEET)
public class ContextualSuggestionsTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
@Rule
public ScreenShooter mScreenShooter = new ScreenShooter();
@Rule
public TestRule mDisableChromeAnimations = new DisableChromeAnimations();
@Rule
public RenderTestRule mRenderTestRule = new RenderTestRule();
/** Parameter provider for the Slim Peek UI */
public static class SlimPeekUIParams implements ParameterProvider {
@Override
public Iterable<ParameterSet> getParameters() {
return Arrays.asList(new ParameterSet().value(false).name("DisableSlimPeekUI"),
new ParameterSet().value(true).name("EnableSlimPeekUI"));
}
}
private static final String TEST_PAGE =
"/chrome/test/data/android/contextual_suggestions/contextual_suggestions_test.html";
private final FakeContextualSuggestionsSource mFakeSource =
new FakeContextualSuggestionsSource();
private EmbeddedTestServer mTestServer;
private ContextualSuggestionsCoordinator mCoordinator;
private ContextualSuggestionsMediator mMediator;
private ContextualSuggestionsModel mModel;
private BottomSheet mBottomSheet;
// Used in multi-instance test.
private ContextualSuggestionsCoordinator mCoordinator2;
private ContextualSuggestionsMediator mMediator2;
private ContextualSuggestionsModel mModel2;
private BottomSheet mBottomSheet2;
private int mNumberOfSourcesCreated;
@ParameterAnnotations.UseMethodParameterBefore(SlimPeekUIParams.class)
public void setupSlimPeekUI(boolean slimPeekUIEnabled) {
if (slimPeekUIEnabled) {
mRenderTestRule.setVariantPrefix("slim-peek");
Features.getInstance().enable(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_SLIM_PEEK_UI);
} else {
Features.getInstance().disable(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_SLIM_PEEK_UI);
}
}
private class ContextualSuggestionsModuleForTest extends ContextualSuggestionsModule {
@Override
public ContextualSuggestionsSource provideContextualSuggestionsSource(Profile profile) {
mNumberOfSourcesCreated++;
return mFakeSource;
}
}
private class ChromeAppModuleForTest extends ChromeAppModule {
@Override
public EnabledStateMonitor provideEnabledStateMonitor() {
return new EmptyEnabledStateMonitor() {
@Override
public boolean getSettingsEnabled() {
return true;
}
@Override
public boolean getEnabledState() {
return true;
}
};
}
}
@Before
public void setUp() throws Exception {
ModuleFactoryOverrides.setOverride(ContextualSuggestionsModuleForTest.Factory.class,
ContextualSuggestionsModuleForTest::new);
ModuleFactoryOverrides.setOverride(
ChromeAppModule.Factory.class, ChromeAppModuleForTest::new);
FetchHelper.setDisableDelayForTesting(true);
ContextualSuggestionsMediator.setOverrideBrowserControlsHiddenForTesting(true);
mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
mActivityTestRule.startMainActivityWithURL(mTestServer.getURL(TEST_PAGE));
final CallbackHelper waitForSuggestionsHelper = new CallbackHelper();
ThreadUtils.runOnUiThreadBlocking(() -> {
mCoordinator = mActivityTestRule.getActivity()
.getComponent()
.getContextualSuggestionsCoordinator();
mMediator = mCoordinator.getMediatorForTesting();
mModel = mCoordinator.getModelForTesting();
if (mModel.getTitle() != null) {
waitForSuggestionsHelper.notifyCalled();
} else {
mModel.addObserver((source, propertyKey) -> {
if (propertyKey == PropertyKey.TITLE && mModel.getTitle() != null) {
waitForSuggestionsHelper.notifyCalled();
}
});
}
});
waitForSuggestionsHelper.waitForCallback(0);
mBottomSheet = mActivityTestRule.getActivity().getBottomSheet();
}
@After
public void tearDown() {
mTestServer.stopAndDestroyServer();
FetchHelper.setDisableDelayForTesting(false);
ContextualSuggestionsMediator.setOverrideBrowserControlsHiddenForTesting(false);
ModuleFactoryOverrides.clearOverrides();
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testOpenContextualSuggestionsBottomSheet() {
assertEquals("Sheet should still be hidden.", BottomSheet.SheetState.HIDDEN,
mBottomSheet.getSheetState());
ThreadUtils.runOnUiThreadBlocking(() -> {
mMediator.showContentInSheetForTesting(true, true);
mBottomSheet.endAnimations();
});
assertEquals("Title text should be set.",
FakeContextualSuggestionsSource.TEST_TOOLBAR_TITLE, mModel.getTitle());
assertEquals("Cluster list should be set.",
(int) FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT,
mModel.getClusterList().getItemCount());
ContextualSuggestionsBottomSheetContent content =
(ContextualSuggestionsBottomSheetContent) mBottomSheet.getCurrentSheetContent();
RecyclerView recyclerView = (RecyclerView) content.getContentView();
assertEquals("RecyclerView should be empty.", 0, recyclerView.getChildCount());
assertEquals("Sheet should be peeked.", BottomSheet.SheetState.PEEK,
mBottomSheet.getSheetState());
assertNull("RecyclerView should still be empty.", recyclerView.getAdapter());
openSheet();
assertEquals("RecyclerView should have content.",
(int) FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT,
recyclerView.getAdapter().getItemCount());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
@DisabledTest(message = "Flaky - crbug.com/859292")
public void testScrollPageToTrigger() throws InterruptedException, TimeoutException {
ContextualSuggestionsMediator.setOverrideBrowserControlsHiddenForTesting(false);
mMediator.setTargetScrollPercentageForTesting(0f);
assertEquals("Sheet should be hidden.", BottomSheet.SheetState.HIDDEN,
mBottomSheet.getSheetState());
CallbackHelper fullyPeekedCallback = new CallbackHelper();
mBottomSheet.addObserver(new EmptyBottomSheetObserver() {
@Override
public void onSheetFullyPeeked() {
fullyPeekedCallback.notifyCalled();
}
});
// Scroll the base page, hiding then reshowing the browser controls.
FullscreenManagerTestUtils.disableBrowserOverrides();
FullscreenManagerTestUtils.waitForBrowserControlsToBeMoveable(
mActivityTestRule, mActivityTestRule.getActivity().getActivityTab());
// Assert that the sheet is now visible.
fullyPeekedCallback.waitForCallback(0);
assertEquals("Sheet should be peeked.", BottomSheet.SheetState.PEEK,
mBottomSheet.getSheetState());
assertTrue("Bottom sheet should contain suggestions content.",
mBottomSheet.getCurrentSheetContent()
instanceof ContextualSuggestionsBottomSheetContent);
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testCloseFromPeek() throws InterruptedException, TimeoutException {
forceShowSuggestions();
simulateClickOnCloseButton();
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testCloseFromOpen() throws InterruptedException, TimeoutException {
forceShowSuggestions();
openSheet();
simulateClickOnCloseButton();
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testTriggerMultipleTimes() throws InterruptedException, TimeoutException {
// Show one time and close.
forceShowSuggestions();
openSheet();
simulateClickOnCloseButton();
// Show a second time.
ThreadUtils.runOnUiThreadBlocking(
() -> mMediator.requestSuggestions("http://www.testurl.com"));
forceShowSuggestions();
openSheet();
}
@Test
@FlakyTest(message = "https://crbug.com/846619")
@MediumTest
@Feature({"ContextualSuggestions"})
public void testTriggerAfterClose() throws InterruptedException, TimeoutException {
int firstTabId = mActivityTestRule.getActivity().getActivityTab().getId();
mActivityTestRule.loadUrlInNewTab(mTestServer.getURL(TEST_PAGE));
int secondTabId = mActivityTestRule.getActivity().getActivityTab().getId();
// Show suggestions on the second tab and simulate clicking close button.
forceShowSuggestions();
openSheet();
simulateClickOnCloseButton();
// Switch to the first tab and verify that the suggestions can be shown.
ChromeTabUtils.switchTabInCurrentTabModel(mActivityTestRule.getActivity(), firstTabId);
forceShowSuggestions();
// Switch to the second tab.
ChromeTabUtils.switchTabInCurrentTabModel(mActivityTestRule.getActivity(), secondTabId);
ThreadUtils.runOnUiThreadBlocking(() -> mBottomSheet.endAnimations());
// Simulate reverse scroll.
FullscreenManagerTestUtils.disableBrowserOverrides();
FullscreenManagerTestUtils.waitForBrowserControlsToBeMoveable(
mActivityTestRule, mActivityTestRule.getActivity().getActivityTab());
// Verify that the model is empty.
assertEquals("Model should be empty.", 0, mModel.getClusterList().getItemCount());
assertEquals("Bottom sheet should be hidden.", BottomSheet.SheetState.HIDDEN,
mBottomSheet.getSheetState());
// Switch to the first tab and verify that the suggestions can still be shown.
ChromeTabUtils.switchTabInCurrentTabModel(mActivityTestRule.getActivity(), firstTabId);
forceShowSuggestions();
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testOpenSuggestion() throws InterruptedException, TimeoutException {
forceShowSuggestions();
openSheet();
testOpenFirstSuggestion();
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testOpenArticleInNewTab() throws Exception {
forceShowSuggestions();
openSheet();
SnippetArticleViewHolder holder = getFirstSuggestionViewHolder();
String expectedUrl = holder.getUrl();
ChromeTabUtils.invokeContextMenuAndOpenInANewTab(mActivityTestRule, holder.itemView,
ContextMenuManager.ContextMenuItemId.OPEN_IN_NEW_TAB, false, expectedUrl);
assertEquals("Sheet should still be opened.", BottomSheet.SheetState.FULL,
mBottomSheet.getSheetState());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testOpenSuggestionInNewTabIncognito() throws Exception {
forceShowSuggestions();
openSheet();
SnippetArticleViewHolder holder = getFirstSuggestionViewHolder();
String expectedUrl = holder.getUrl();
ChromeTabUtils.invokeContextMenuAndOpenInANewTab(mActivityTestRule, holder.itemView,
ContextMenuManager.ContextMenuItemId.OPEN_IN_INCOGNITO_TAB, true, expectedUrl);
ThreadUtils.runOnUiThreadBlocking(() -> mBottomSheet.endAnimations());
assertFalse("Sheet should be closed.", mBottomSheet.isSheetOpen());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testMenuButton() throws Exception {
forceShowSuggestions();
View title =
mBottomSheet.getCurrentSheetContent().getToolbarView().findViewById(R.id.title);
View menuButton =
mBottomSheet.getCurrentSheetContent().getToolbarView().findViewById(R.id.more);
CallbackHelper layoutHelper = new CallbackHelper();
title.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
layoutHelper.notifyCalled();
}
});
layoutHelper.waitForCallback(0);
int titleWidth = title.getWidth();
// Menu button is not visible on peek state.
assertEquals(View.GONE, menuButton.getVisibility());
assertEquals(0, menuButton.getWidth());
// Menu button is visible after sheet is opened.
openSheet();
assertEquals(View.VISIBLE, menuButton.getVisibility());
// Title view should be resized.
ViewUtils.waitForStableView(menuButton);
assertNotEquals(0, menuButton.getWidth());
assertEquals(titleWidth, title.getWidth() + menuButton.getWidth());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testShadowVisibleOnScroll() throws InterruptedException, TimeoutException {
forceShowSuggestions();
openSheet();
assertFalse("Shadow shouldn't be visible.", mModel.getToolbarShadowVisibility());
CallbackHelper shadowVisibilityCallback = new CallbackHelper();
mModel.addObserver((source, propertyKey) -> {
if (propertyKey == PropertyKey.TOOLBAR_SHADOW_VISIBILITY
&& mModel.getToolbarShadowVisibility()) {
shadowVisibilityCallback.notifyCalled();
}
});
ThreadUtils.runOnUiThreadBlocking(() -> {
RecyclerView view =
(RecyclerView) mBottomSheet.getCurrentSheetContent().getContentView();
view.smoothScrollToPosition(5);
});
shadowVisibilityCallback.waitForCallback("Shadow should be visible");
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testInProductHelp() throws InterruptedException, TimeoutException {
FakeTracker tracker = new FakeTracker(FeatureConstants.CONTEXTUAL_SUGGESTIONS_FEATURE);
TrackerFactory.setTrackerForTests(tracker);
forceShowSuggestions();
assertTrue(
"Help bubble should be showing.", mMediator.getHelpBubbleForTesting().isShowing());
ThreadUtils.runOnUiThreadBlocking(() -> mMediator.getHelpBubbleForTesting().dismiss());
Assert.assertEquals("Help bubble should be dimissed.", 1,
tracker.mDimissedCallbackHelper.getCallCount());
}
@Test
@LargeTest
@Feature({"ContextualSuggestions"})
public void testMultiInstanceMode() throws Exception {
ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
forceShowSuggestions();
openSheet();
MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
ChromeTabbedActivity activity2 = MultiWindowTestHelper.createSecondChromeTabbedActivity(
activity1, new LoadUrlParams(mTestServer.getURL(TEST_PAGE)));
ChromeActivityTestRule.waitForActivityNativeInitializationComplete(activity2);
CallbackHelper allItemsInsertedCallback = new CallbackHelper();
ThreadUtils.runOnUiThreadBlocking(() -> {
mCoordinator2 = activity2.getComponent().getContextualSuggestionsCoordinator();
mMediator2 = mCoordinator2.getMediatorForTesting();
mModel2 = mCoordinator2.getModelForTesting();
mBottomSheet2 = activity2.getBottomSheet();
mModel2.getClusterList().addObserver(new ListObserver<PartialBindCallback>() {
@Override
public void onItemRangeInserted(ListObservable source, int index, int count) {
// There will be two calls to this method, one for each cluster that is added
// to the list. Wait for the expected number of items to ensure the model
// is finished updating.
if (mModel2.getClusterList().getItemCount()
== FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT) {
allItemsInsertedCallback.notifyCalled();
}
}
});
});
assertNotEquals("There should be two coordinators.", mCoordinator, mCoordinator2);
assertNotEquals("There should be two mediators.", mMediator, mMediator2);
assertNotEquals("There should be two models.", mModel, mModel2);
assertEquals("There should have been two requests to create a ContextualSuggestionsSource",
2, mNumberOfSourcesCreated);
allItemsInsertedCallback.waitForCallback(0);
int itemCount = ThreadUtils.runOnUiThreadBlocking(
() -> { return mModel.getClusterList().getItemCount(); });
assertEquals("Second model has incorrect number of items.",
(int) FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT, itemCount);
ThreadUtils.runOnUiThreadBlocking(() -> {
mMediator2.showContentInSheetForTesting(true, true);
mBottomSheet2.endAnimations();
ContextualSuggestionsBottomSheetContent content1 =
(ContextualSuggestionsBottomSheetContent) mBottomSheet.getCurrentSheetContent();
ContextualSuggestionsBottomSheetContent content2 =
(ContextualSuggestionsBottomSheetContent)
mBottomSheet2.getCurrentSheetContent();
assertNotEquals("There should be two bottom sheet contents", content1, content2);
});
assertEquals("Sheet in the second activity should be peeked.", BottomSheet.SheetState.PEEK,
mBottomSheet2.getSheetState());
assertEquals("Sheet in the first activity should be open.", BottomSheet.SheetState.FULL,
mBottomSheet.getSheetState());
ThreadUtils.runOnUiThreadBlocking(
() -> mBottomSheet2.setSheetState(BottomSheet.SheetState.FULL, false));
SnippetArticleViewHolder holder = getFirstSuggestionViewHolder(mBottomSheet2);
String expectedUrl = holder.getUrl();
ChromeTabUtils.invokeContextMenuAndOpenInOtherWindow(activity2, activity1, holder.itemView,
ContextMenuManager.ContextMenuItemId.OPEN_IN_NEW_WINDOW, false, expectedUrl);
ThreadUtils.runOnUiThreadBlocking(() -> {
mBottomSheet.endAnimations();
mBottomSheet2.endAnimations();
});
assertTrue("Sheet in second activity should be opened.", mBottomSheet2.isSheetOpen());
assertFalse("Sheet in first activity should be closed.", mBottomSheet.isSheetOpen());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions", "UiCatalogue"})
@DisableFeatures(FeatureConstants.CONTEXTUAL_SUGGESTIONS_FEATURE)
@ParameterAnnotations.UseMethodParameter(SlimPeekUIParams.class)
public void testCaptureContextualSuggestionsBottomSheet(boolean slimPeekUIEnabled)
throws InterruptedException, TimeoutException {
String postfix = slimPeekUIEnabled ? " (slim)" : "";
forceShowSuggestions();
BottomSheetTestRule.waitForWindowUpdates();
mScreenShooter.shoot("Contextual suggestions: peeking" + postfix);
ThreadUtils.runOnUiThreadBlocking(
() -> mBottomSheet.setSheetState(BottomSheet.SheetState.HALF, false));
BottomSheetTestRule.waitForWindowUpdates();
mScreenShooter.shoot("Contextual suggestions: half height, images loading" + postfix);
ThreadUtils.runOnUiThreadBlocking(() -> mFakeSource.runImageFetchCallbacks());
BottomSheetTestRule.waitForWindowUpdates();
mScreenShooter.shoot("Contextual suggestions: half height, images loaded" + postfix);
ThreadUtils.runOnUiThreadBlocking(
() -> mBottomSheet.setSheetState(BottomSheet.SheetState.FULL, false));
BottomSheetTestRule.waitForWindowUpdates();
mScreenShooter.shoot("Contextual suggestions: full height" + postfix);
ThreadUtils.runOnUiThreadBlocking(() -> {
RecyclerView view =
(RecyclerView) mBottomSheet.getCurrentSheetContent().getContentView();
view.scrollToPosition(5);
});
BottomSheetTestRule.waitForWindowUpdates();
mScreenShooter.shoot("Contextual suggestions: scrolled" + postfix);
}
@Test
@MediumTest
@Feature({"ContextualSuggestions", "RenderTest"})
@DisableFeatures(FeatureConstants.CONTEXTUAL_SUGGESTIONS_FEATURE)
@ParameterAnnotations.UseMethodParameter(SlimPeekUIParams.class)
public void testRender(boolean slimPeekUIEnabled) throws Exception {
// Force suggestions to populate in the bottom sheet, then render the peeking bar.
forceShowSuggestions();
BottomSheetTestRule.waitForWindowUpdates();
mRenderTestRule.render(
mBottomSheet.getCurrentSheetContent().getToolbarView(), "peeking_bar");
// Open the sheet to cause the suggestions to be bound in the RecyclerView, then capture
// a suggestion with its thumbnail loading.
ThreadUtils.runOnUiThreadBlocking(
() -> mBottomSheet.setSheetState(BottomSheet.SheetState.FULL, false));
BottomSheetTestRule.waitForWindowUpdates();
mRenderTestRule.render(getFirstSuggestionViewHolder().itemView, "suggestion_image_loading");
// Run the image fetch callback so images load, then capture a suggestion with its
// thumbnail loaded.
ThreadUtils.runOnUiThreadBlocking(() -> mFakeSource.runImageFetchCallbacks());
BottomSheetTestRule.waitForWindowUpdates();
mRenderTestRule.render(getFirstSuggestionViewHolder().itemView, "suggestion_image_loaded");
// Render a thumbnail with an offline badge.
ThreadUtils.runOnUiThreadBlocking(
() -> getSuggestionViewHolder(2).setOfflineBadgeVisibilityForTesting(true));
mRenderTestRule.render(getSuggestionViewHolder(2).itemView, "suggestion_offline");
// Render the full suggestions sheet.
mRenderTestRule.render(mBottomSheet, "full_height");
// Scroll the suggestions and render the full suggestions sheet.
ThreadUtils.runOnUiThreadBlocking(() -> {
RecyclerView view =
(RecyclerView) mBottomSheet.getCurrentSheetContent().getContentView();
view.scrollToPosition(5);
});
BottomSheetTestRule.waitForWindowUpdates();
mRenderTestRule.render(mBottomSheet, "full_height_scrolled");
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
@DisabledTest(message = "Needs updates for SurfaceSynchronization - crbug.com/876943")
public void testPeekWithPageScrollPercentage() throws Exception {
// Set the screen orientation to portrait since we scroll the web contents in absolute
// pixels in the test.
mActivityTestRule.getActivity().setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
CallbackHelper scrollChangedCallback = new CallbackHelper();
GestureStateListener gestureStateListener = new GestureStateListener() {
@Override
public void onScrollOffsetOrExtentChanged(int scrollOffsetY, int scrollExtentY) {
scrollChangedCallback.notifyCalled();
}
};
WebContents webContents = mActivityTestRule.getWebContents();
GestureListenerManager.fromWebContents(webContents).addListener(gestureStateListener);
View view = webContents.getViewAndroidDelegate().getContainerView();
int maxScrollOffset =
RenderCoordinates.fromWebContents(webContents).getMaxVerticalScrollPixInt();
// Verify that suggestions are not shown before scroll.
ThreadUtils.runOnUiThreadBlocking(
() -> mMediator.showContentInSheetForTesting(false, true));
assertEquals("Bottom sheet should be hidden before scroll.", BottomSheet.SheetState.HIDDEN,
mBottomSheet.getSheetState());
// Scroll the page to 30% and verify that the suggestions are not shown.
int callCount = scrollChangedCallback.getCallCount();
ThreadUtils.runOnUiThreadBlocking(
() -> view.scrollBy(0, Math.round(maxScrollOffset * 0.3f)));
scrollChangedCallback.waitForCallback(callCount);
// Simulate call to show content without browser controls being hidden.
ThreadUtils.runOnUiThreadBlocking(
() -> mMediator.showContentInSheetForTesting(false, true));
assertEquals("Bottom sheet should be hidden on 30% scroll percentage.",
BottomSheet.SheetState.HIDDEN, mBottomSheet.getSheetState());
// Scroll the page 20% more for a total of 50% and verify that the suggestions are shown.
callCount = scrollChangedCallback.getCallCount();
ThreadUtils.runOnUiThreadBlocking(
() -> view.scrollBy(0, Math.round(maxScrollOffset * 0.2f)));
scrollChangedCallback.waitForCallback(callCount);
// Simulate call to show content without browser controls being hidden.
ThreadUtils.runOnUiThreadBlocking(
() -> mMediator.showContentInSheetForTesting(false, true));
assertEquals("Bottom sheet should be shown on >=50% scroll percentage.",
BottomSheet.SheetState.PEEK, mBottomSheet.getSheetState());
GestureListenerManager.fromWebContents(webContents).removeListener(gestureStateListener);
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testPeekDelay() throws Exception {
// Close the suggestions from setUp().
ThreadUtils.runOnUiThreadBlocking(() -> {
mMediator.clearState();
mBottomSheet.endAnimations();
});
// Request suggestions with fetch time baseline set for testing.
long startTime = SystemClock.uptimeMillis();
FetchHelper.setFetchTimeBaselineMillisForTesting(startTime);
ThreadUtils.runOnUiThreadBlocking(
() -> mMediator.requestSuggestions("http://www.testurl.com"));
assertEquals("Bottom sheet should be hidden before delay.", BottomSheet.SheetState.HIDDEN,
mBottomSheet.getSheetState());
// Simulate user scroll by calling showContentInSheet until the sheet is peeked.
CriteriaHelper.pollUiThread(() -> {
mMediator.showContentInSheetForTesting(true, false);
mBottomSheet.endAnimations();
return mBottomSheet.getSheetState() == BottomSheet.SheetState.PEEK;
});
// Verify that suggestions is shown after the expected delay.
long duration = SystemClock.uptimeMillis() - startTime;
long expected = FakeContextualSuggestionsSource.TEST_PEEK_DELAY_SECONDS * 1000;
assertTrue(String.format(Locale.US,
"The peek delay should be greater than %d ms, but was %d ms.",
expected, duration),
duration >= expected);
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
public void testPeekCount() throws Exception {
forceShowSuggestions();
// Opening the sheet and setting it back to peek state shouldn't affect the peek count.
setSheetOffsetForState(BottomSheet.SheetState.FULL);
setSheetOffsetForState(BottomSheet.SheetState.PEEK);
// Hide and peek the bottom sheet for (TEST_PEEK_COUNT - 1) number of times, since
// #forceShowSuggestions() has already peeked the bottom sheet once.
for (int i = 1; i < FakeContextualSuggestionsSource.TEST_PEEK_COUNT; ++i) {
setSheetOffsetForState(BottomSheet.SheetState.HIDDEN);
// Verify that the suggestions are not cleared.
assertEquals("Model has incorrect number of items.",
(int) FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT,
mModel.getClusterList().getItemCount());
assertNotNull("Bottom sheet contents should not be null.",
mBottomSheet.getCurrentSheetContent());
setSheetOffsetForState(BottomSheet.SheetState.PEEK);
}
// Hide the sheet and verify that the suggestions are cleared.
setSheetOffsetForState(BottomSheet.SheetState.HIDDEN);
assertEquals("Model should be empty.", 0, mModel.getClusterList().getItemCount());
assertNull("Bottom sheet contents should be null.", mBottomSheet.getCurrentSheetContent());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
@EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
@DisableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BOTTOM_SHEET)
public void testToolbarButton() throws Exception {
View toolbarButton = getToolbarButton();
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
clickToolbarButton();
simulateClickOnCloseButton();
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
clickToolbarButton();
testOpenFirstSuggestion();
assertEquals("Toolbar button should be visible", View.GONE, toolbarButton.getVisibility());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
@EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
@DisableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BOTTOM_SHEET)
public void testToolbarButton_ToggleTabSwitcher() throws Exception {
View toolbarButton = getToolbarButton();
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
ThreadUtils.runOnUiThreadBlocking(
() -> { mActivityTestRule.getActivity().getLayoutManager().showOverview(false); });
assertEquals("Toolbar button should be invisible", View.INVISIBLE,
toolbarButton.getVisibility());
ThreadUtils.runOnUiThreadBlocking(
() -> { mActivityTestRule.getActivity().getLayoutManager().hideOverview(false); });
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
@EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
@DisableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BOTTOM_SHEET)
public void testToolbarButton_SwitchTabs() throws Exception {
View toolbarButton = getToolbarButton();
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
final TabModel currentModel =
mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
int currentIndex = currentModel.index();
ChromeTabUtils.newTabFromMenu(
InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
assertEquals("Toolbar button should be gone", View.GONE, toolbarButton.getVisibility());
ThreadUtils.runOnUiThreadBlocking(
() -> currentModel.setIndex(currentIndex, TabSelectionType.FROM_USER));
CriteriaHelper.pollUiThread(
() -> { return toolbarButton.getVisibility() == View.VISIBLE; });
}
@Test
@MediumTest
@Feature({"ContextualSuggestions"})
@EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
@DisableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BOTTOM_SHEET)
public void testToolbarButton_ResponseInTabSwitcher() throws Exception {
View toolbarButton = getToolbarButton();
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
// Simulate suggestions being cleared.
ThreadUtils.runOnUiThreadBlocking(() -> mMediator.clearState());
assertEquals("Toolbar button should be gone", View.GONE, toolbarButton.getVisibility());
assertEquals("Suggestions should be cleared", 0, mModel.getClusterList().getItemCount());
// Enter tab switcher.
ThreadUtils.runOnUiThreadBlocking(
() -> { mActivityTestRule.getActivity().getLayoutManager().showOverview(false); });
// Simulate a new suggestions request.
ThreadUtils.runOnUiThreadBlocking(
() -> mMediator.requestSuggestions("https://www.google.com"));
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mModel.getClusterList().getItemCount()
== FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT;
}
});
assertEquals("Toolbar button should be invisible", View.INVISIBLE,
toolbarButton.getVisibility());
ThreadUtils.runOnUiThreadBlocking(
() -> { mActivityTestRule.getActivity().getLayoutManager().hideOverview(false); });
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
}
private void forceShowSuggestions() throws InterruptedException, TimeoutException {
assertEquals("Model has incorrect number of items.",
(int) FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT,
mModel.getClusterList().getItemCount());
ThreadUtils.runOnUiThreadBlocking(() -> {
mMediator.showContentInSheetForTesting(true, true);
mBottomSheet.endAnimations();
assertEquals("Sheet should be peeked.", BottomSheet.SheetState.PEEK,
mBottomSheet.getSheetState());
assertTrue("Bottom sheet should contain suggestions content.",
mBottomSheet.getCurrentSheetContent()
instanceof ContextualSuggestionsBottomSheetContent);
});
}
private void simulateClickOnCloseButton() {
ThreadUtils.runOnUiThreadBlocking(() -> {
mBottomSheet.getCurrentSheetContent()
.getToolbarView()
.findViewById(R.id.close_button)
.performClick();
mBottomSheet.endAnimations();
});
assertEquals("Sheet should be hidden.", BottomSheet.SheetState.HIDDEN,
mBottomSheet.getSheetState());
assertNull("Bottom sheet contents should be null.", mBottomSheet.getCurrentSheetContent());
}
private void openSheet() {
ThreadUtils.runOnUiThreadBlocking(
() -> mBottomSheet.setSheetState(BottomSheet.SheetState.FULL, false));
}
private void setSheetOffsetForState(@BottomSheet.SheetState int state) {
ThreadUtils.runOnUiThreadBlocking(() -> {
mBottomSheet.setSheetOffsetFromBottomForTesting(
mBottomSheet.getSheetHeightForState(state));
mBottomSheet.endAnimations();
});
}
private SnippetArticleViewHolder getFirstSuggestionViewHolder() {
return getFirstSuggestionViewHolder(mBottomSheet);
}
private SnippetArticleViewHolder getFirstSuggestionViewHolder(BottomSheet bottomSheet) {
return getSuggestionViewHolder(bottomSheet, 0);
}
private SnippetArticleViewHolder getSuggestionViewHolder(int index) {
return getSuggestionViewHolder(mBottomSheet, index);
}
private SnippetArticleViewHolder getSuggestionViewHolder(BottomSheet bottomSheet, int index) {
ContextualSuggestionsBottomSheetContent content =
(ContextualSuggestionsBottomSheetContent) bottomSheet.getCurrentSheetContent();
RecyclerView recyclerView = (RecyclerView) content.getContentView();
RecyclerViewTestUtils.waitForStableRecyclerView(recyclerView);
return (SnippetArticleViewHolder) recyclerView.findViewHolderForAdapterPosition(index);
}
private View getToolbarButton() throws ExecutionException {
return ThreadUtils.runOnUiThreadBlocking(() -> {
return ((ToolbarPhone) mActivityTestRule.getActivity()
.getToolbarManager()
.getToolbarLayout())
.getExperimentalButtonForTesting();
});
}
private void clickToolbarButton() throws ExecutionException {
View toolbarButton = getToolbarButton();
assertEquals(
"Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
ThreadUtils.runOnUiThreadBlocking(() -> {
toolbarButton.performClick();
mBottomSheet.endAnimations();
});
assertTrue("Sheet should be open.", mBottomSheet.isSheetOpen());
}
private void testOpenFirstSuggestion() throws InterruptedException, TimeoutException {
SnippetArticleViewHolder holder = getFirstSuggestionViewHolder();
String expectedUrl = holder.getUrl();
TestWebContentsObserver webContentsObserver = new TestWebContentsObserver(
mActivityTestRule.getActivity().getActivityTab().getWebContents());
int callCount = webContentsObserver.getOnPageStartedHelper().getCallCount();
ThreadUtils.runOnUiThreadBlocking(() -> { holder.itemView.performClick(); });
webContentsObserver.getOnPageStartedHelper().waitForCallback(callCount);
ThreadUtils.runOnUiThreadBlocking(() -> mBottomSheet.endAnimations());
assertFalse("Sheet should be closed.", mBottomSheet.isSheetOpen());
// URL may not have been updated yet when WebContentsObserver#didStartLoading is called.
CriteriaHelper.pollUiThread(() -> {
return mActivityTestRule.getActivity().getActivityTab().getUrl().equals(expectedUrl);
});
}
}