// Copyright 2022 The Chromium Authors
// 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.customtabs.content;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.chromium.cc.mojom.RootScrollOffsetUpdateFrequency.ON_SCROLL_END;
import static org.chromium.chrome.browser.customtabs.content.RealtimeEngagementSignalObserver.DEFAULT_AFTER_SCROLL_END_THRESHOLD_MS;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.browser.customtabs.EngagementSignalsCallback;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowSystemClock;
import org.chromium.base.FeatureList;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.cc.mojom.RootScrollOffsetUpdateFrequency;
import org.chromium.chrome.browser.customtabs.content.RealtimeEngagementSignalObserver.ScrollState;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar.CustomTabTabObserver;
import org.chromium.chrome.browser.customtabs.features.TabInteractionRecorder;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.content.browser.GestureListenerManagerImpl;
import org.chromium.content.browser.RenderCoordinatesImpl;
import org.chromium.content_public.browser.GestureStateListener;
import org.chromium.content_public.browser.LoadCommittedDetails;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.url.JUnitTestGURLs;
import java.util.List;
/** Unit test for {@link RealtimeEngagementSignalObserver}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(shadows = {ShadowSystemClock.class})
public class RealtimeEngagementSignalObserverUnitTest {
@Rule
public final CustomTabActivityContentTestEnvironment env =
new CustomTabActivityContentTestEnvironment();
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final int SCROLL_EXTENT = 100;
private static final long CURRENT_TIME_MS = 9000000L;
RealtimeEngagementSignalObserver mEngagementSignalObserver;
@Mock private GestureListenerManagerImpl mGestureListenerManagerImpl;
@Mock private RenderCoordinatesImpl mRenderCoordinatesImpl;
@Mock private PrivacyPreferencesManagerImpl mPrivacyPreferencesManagerImpl;
@Mock private TabInteractionRecorder mTabInteractionRecorder;
@Mock private EngagementSignalsCallback mEngagementSignalsCallback;
@Before
public void setUp() {
when(mRenderCoordinatesImpl.getMaxVerticalScrollPixInt()).thenReturn(SCROLL_EXTENT);
GestureListenerManagerImpl.setInstanceForTesting(mGestureListenerManagerImpl);
RenderCoordinatesImpl.setInstanceForTesting(mRenderCoordinatesImpl);
PrivacyPreferencesManagerImpl.setInstanceForTesting(mPrivacyPreferencesManagerImpl);
TabInteractionRecorder.setInstanceForTesting(mTabInteractionRecorder);
RealtimeEngagementSignalObserver.ScrollState.setInstanceForTesting(new ScrollState());
doReturn(true).when(mPrivacyPreferencesManagerImpl).isUsageAndCrashReportingPermitted();
SystemClock.setCurrentTimeMillis(CURRENT_TIME_MS);
}
@After
public void tearDown() {
RealtimeEngagementSignalObserver.ScrollState.setInstanceForTesting(null);
FeatureList.setTestValues(null);
}
@Test
public void doesNotAddListenersForSignalsIfUmaUploadIsDisabled() {
doReturn(false).when(mPrivacyPreferencesManagerImpl).isUsageAndCrashReportingPermitted();
initializeTabForTest();
verify(mGestureListenerManagerImpl, never()).addListener(any(GestureStateListener.class));
}
@Test
public void addsListenersForSignalsIfFeatureIsEnabled_alternativeImpl() {
initializeTabForTest();
verify(mGestureListenerManagerImpl)
.addListener(any(GestureStateListener.class), eq(ON_SCROLL_END));
}
@Test
public void removesGestureStateListenerWhenWebContentsWillSwap() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
List<TabObserver> tabObservers = captureTabObservers();
for (TabObserver observer : tabObservers) {
observer.webContentsWillSwap(env.tabProvider.getTab());
}
verify(mGestureListenerManagerImpl).removeListener(listener);
}
@Test
public void removesGestureStateListenerWhenTabDetached() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
WebContentsObserver webContentsObserver = captureWebContentsObserver();
List<TabObserver> tabObservers = captureTabObservers();
for (TabObserver observer : tabObservers) {
observer.onActivityAttachmentChanged(env.tabProvider.getTab(), null);
}
verify(env.tabProvider.getTab().getWebContents()).removeObserver(webContentsObserver);
verify(mGestureListenerManagerImpl).removeListener(listener);
}
@Test
public void reAttachGestureStateListenerWhenTabClosed() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
WebContentsObserver webContentsObserver = captureWebContentsObserver();
List<TabObserver> tabObservers = captureTabObservers();
for (TabObserver observer : tabObservers) {
observer.onClosingStateChanged(env.tabProvider.getTab(), /* isClosing= */ true);
}
verify(env.tabProvider.getTab().getWebContents()).removeObserver(webContentsObserver);
verify(mGestureListenerManagerImpl).removeListener(listener);
}
@Test
public void reAttachGestureStateListenerWhenTabChanged() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
List<TabObserver> tabObservers = captureTabObservers();
boolean hasCustomTabTabObserver = false;
Tab anotherTab = env.prepareTab();
for (TabObserver observer : tabObservers) {
if (observer instanceof CustomTabTabObserver) {
hasCustomTabTabObserver = true;
((CustomTabTabObserver) observer).onObservingDifferentTab(anotherTab);
}
}
verify(mGestureListenerManagerImpl).removeListener(listener);
assertTrue(
"At least one CustomTabTabObserver should be captured.", hasCustomTabTabObserver);
// Now, verify a new listener is attached.
GestureStateListener listener2 = captureGestureStateListener();
Assert.assertNotEquals(
"A new listener should be created once tab swapped.", listener, listener2);
verifyNoMemoryLeakForGestureStateListener(listener2);
}
@Test
public void doesNotSendUserInteractionWhenIncognito() {
env.isOffTheRecord = true;
initializeTabForTest();
List<TabObserver> tabObservers = captureTabObservers();
for (TabObserver observer : tabObservers) {
observer.onDestroyed(env.tabProvider.getTab());
}
verify(mEngagementSignalsCallback, never()).onSessionEnded(anyBoolean(), any(Bundle.class));
}
@Test
public void doesNotSendUserInteractionWhenUmaUploadDisabled() {
doReturn(false).when(mPrivacyPreferencesManagerImpl).isUsageAndCrashReportingPermitted();
initializeTabForTest();
List<TabObserver> tabObservers = captureTabObservers();
for (TabObserver observer : tabObservers) {
observer.onDestroyed(env.tabProvider.getTab());
}
verify(mEngagementSignalsCallback, never()).onSessionEnded(anyBoolean(), any(Bundle.class));
}
@Test
public void sendsSignalsForScrollStartThenEnd() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
// Start scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
verify(mEngagementSignalsCallback).onVerticalScrollEvent(eq(false), any(Bundle.class));
// End scrolling at 50%.
listener.onScrollEnded(50, SCROLL_EXTENT);
// We shouldn't make any more calls.
verify(mEngagementSignalsCallback, times(1))
.onVerticalScrollEvent(anyBoolean(), any(Bundle.class));
}
@Test
public void sendsSignalsForScrollStartDirectionChangeThenEnd() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
verify(mEngagementSignalsCallback).onVerticalScrollEvent(eq(false), any(Bundle.class));
// Change direction to up at 10%.
listener.onVerticalScrollDirectionChanged(true, .1f);
verify(mEngagementSignalsCallback).onVerticalScrollEvent(eq(true), any(Bundle.class));
// Change direction to down at 5%.
listener.onVerticalScrollDirectionChanged(false, .05f);
verify(mEngagementSignalsCallback, times(2))
.onVerticalScrollEvent(eq(false), any(Bundle.class));
// End scrolling at 50%.
listener.onScrollEnded(50, SCROLL_EXTENT);
// We shouldn't make any more calls.
verify(mEngagementSignalsCallback, times(3))
.onVerticalScrollEvent(anyBoolean(), any(Bundle.class));
}
@Test
public void doesNotSendMaxScrollSignalForZeroPercent() {
initializeTabForTest();
// We shouldn't make any calls.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void onlySendsMaxScrollSignalAfterScrollEnd() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
// Scroll down to 55%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(55);
listener.onScrollOffsetOrExtentChanged(55, SCROLL_EXTENT);
// Scroll up to 30%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(30);
listener.onScrollOffsetOrExtentChanged(30, SCROLL_EXTENT);
// We shouldn't make any calls at this point.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
// End scrolling.
listener.onScrollEnded(30, SCROLL_EXTENT);
// Now we should make the call.
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(eq(55), any(Bundle.class));
}
@Test
public void onlySendsMaxScrollSignalForFivesMultiples() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
// Scroll down to 3%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(3);
listener.onScrollOffsetOrExtentChanged(3, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(3, SCROLL_EXTENT);
// We shouldn't make any calls at this point.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
// Start scrolling down again.
listener.onScrollStarted(3, SCROLL_EXTENT, false);
// Scroll down to 8%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(8);
listener.onScrollOffsetOrExtentChanged(8, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(8, SCROLL_EXTENT);
// We should make a call for 5%.
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(eq(5), any(Bundle.class));
// Start scrolling down again.
listener.onScrollStarted(8, SCROLL_EXTENT, false);
// Scroll down to 94%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(94);
listener.onScrollOffsetOrExtentChanged(94, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(94, SCROLL_EXTENT);
// We should make a call for 90%.
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(eq(90), any(Bundle.class));
}
@Test
public void doesNotSendSignalForLowerPercentage() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
// Scroll down to 63%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(63);
listener.onScrollOffsetOrExtentChanged(63, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(63, SCROLL_EXTENT);
// We should make a call for 60%.
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(eq(60), any(Bundle.class));
clearInvocations(mEngagementSignalsCallback);
// Now scroll back up.
listener.onScrollStarted(63, SCROLL_EXTENT, true);
// Scroll up to 30%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(30);
listener.onScrollOffsetOrExtentChanged(30, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(30, SCROLL_EXTENT);
// We shouldn't make any more calls since the max didn't change.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void doesNotSendSignalEqualToPreviousMax() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener();
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
// Scroll down to 50%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(50);
listener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(50, SCROLL_EXTENT);
// Now scroll up, then back down to 50%.
listener.onScrollStarted(50, SCROLL_EXTENT, true);
// Scroll up to 30%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(30);
listener.onScrollOffsetOrExtentChanged(30, SCROLL_EXTENT);
// Back down to 50%.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(50);
listener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(50, SCROLL_EXTENT);
// There should be only one call.
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(eq(50), any(Bundle.class));
}
@Test
public void resetsMaxOnNavigation_MainFrame_NewDocument() {
initializeTabForTest();
GestureStateListener gestureStateListener = captureGestureStateListener();
WebContentsObserver webContentsObserver = captureWebContentsObserver();
// Scroll down to 50%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(50);
gestureStateListener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(50, SCROLL_EXTENT);
// Verify 50% is reported.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(50), any(Bundle.class));
clearInvocations(mEngagementSignalsCallback);
LoadCommittedDetails details =
new LoadCommittedDetails(
0,
JUnitTestGURLs.URL_1,
false,
/* isSameDocument= */ false,
/* isMainFrame= */ true,
200);
webContentsObserver.navigationEntryCommitted(details);
// Scroll down to 10%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(10);
gestureStateListener.onScrollOffsetOrExtentChanged(10, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(10, SCROLL_EXTENT);
// Verify 10% is reported.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(10), any(Bundle.class));
}
@Test
public void doesNotResetMaxOnNavigation_MainFrame_SameDocument() {
initializeTabForTest();
GestureStateListener gestureStateListener = captureGestureStateListener();
WebContentsObserver webContentsObserver = captureWebContentsObserver();
// Scroll down to 30%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(30);
gestureStateListener.onScrollOffsetOrExtentChanged(30, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(30, SCROLL_EXTENT);
// Verify 30% is reported.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(30), any(Bundle.class));
clearInvocations(mEngagementSignalsCallback);
LoadCommittedDetails details =
new LoadCommittedDetails(
0,
JUnitTestGURLs.URL_1,
false,
/* isSameDocument= */ true,
/* isMainFrame= */ true,
200);
webContentsObserver.navigationEntryCommitted(details);
// Scroll down to 10%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(10);
gestureStateListener.onScrollOffsetOrExtentChanged(10, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(10, SCROLL_EXTENT);
// Verify % isn't reported.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void doesNotResetMaxOnNavigation_SubFrame_NewDocument() {
initializeTabForTest();
GestureStateListener gestureStateListener = captureGestureStateListener();
WebContentsObserver webContentsObserver = captureWebContentsObserver();
// Scroll down to 90%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(90);
gestureStateListener.onScrollOffsetOrExtentChanged(90, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(90, SCROLL_EXTENT);
// Verify 90% is reported.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(90), any(Bundle.class));
clearInvocations(mEngagementSignalsCallback);
LoadCommittedDetails details =
new LoadCommittedDetails(
0,
JUnitTestGURLs.URL_1,
false,
/* isSameDocument= */ false,
/* isMainFrame= */ false,
200);
webContentsObserver.navigationEntryCommitted(details);
// Scroll down to 50%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(50);
gestureStateListener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(50, SCROLL_EXTENT);
// Verify % isn't reported.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void resetsMaxOnTabChange() {
initializeTabForTest();
GestureStateListener gestureStateListener = captureGestureStateListener();
// Scroll down to 50%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(50);
gestureStateListener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(50, SCROLL_EXTENT);
// Verify 50% is reported.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(50), any(Bundle.class));
clearInvocations(mEngagementSignalsCallback);
// Change tabs.
mEngagementSignalObserver.onHidden(env.tabProvider.getTab(), TabHidingType.CHANGED_TABS);
// Scroll down to 10%.
gestureStateListener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(10);
gestureStateListener.onScrollOffsetOrExtentChanged(10, SCROLL_EXTENT);
gestureStateListener.onScrollEnded(10, SCROLL_EXTENT);
// Verify 10% is reported.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(10), any(Bundle.class));
}
@Test
public void sendsSignalWithAlternativeImpl_updateBeforeEnd() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
// Scroll down to 24%
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(24);
listener.onScrollOffsetOrExtentChanged(24, SCROLL_EXTENT);
// End scrolling.
listener.onScrollEnded(24, SCROLL_EXTENT);
// We should make a call with 20.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(20), any(Bundle.class));
}
@Test
public void sendsSignalWithAlternativeImpl_updateAfterEnd() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Start by scrolling down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
// End scrolling.
listener.onScrollEnded(0, SCROLL_EXTENT);
// Send the signal for 24% 10ms after the scroll ended.
advanceTime(10);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(24);
listener.onScrollOffsetOrExtentChanged(24, SCROLL_EXTENT);
// We should make a call with 20.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(20), any(Bundle.class));
// Any update after this will be ignored.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(25);
listener.onScrollOffsetOrExtentChanged(25, SCROLL_EXTENT);
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(eq(25), any(Bundle.class));
}
@Test
public void doesNotSendLowerPercentWithAlternativeImpl() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Start by scrolling down.
listener.onScrollStarted(24, SCROLL_EXTENT, false);
// End scrolling.
listener.onScrollEnded(55, SCROLL_EXTENT);
// Send the signal for 55% 10ms after the scroll ended.
advanceTime(15);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(55);
listener.onScrollOffsetOrExtentChanged(55, SCROLL_EXTENT);
// We should make a call with 55.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(55), any(Bundle.class));
// Scroll back up to 20%.
listener.onScrollStarted(55, SCROLL_EXTENT, true);
listener.onScrollEnded(20, SCROLL_EXTENT);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(20);
listener.onScrollOffsetOrExtentChanged(20, SCROLL_EXTENT);
// We shouldn't make any other calls (after the one from above).
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void doNotSendSignalWithAlternativeImplAfterThreshold() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Start by scrolling down.
listener.onScrollStarted(59, SCROLL_EXTENT, false);
// End scrolling.
listener.onScrollEnded(59, SCROLL_EXTENT);
// Send the signal for 59% 18ms outside the threshold.
advanceTime(DEFAULT_AFTER_SCROLL_END_THRESHOLD_MS + 18);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(59);
listener.onScrollOffsetOrExtentChanged(59, SCROLL_EXTENT);
// We shouldn't make a call since the call was outside the threshold.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void doNotSendSignalWithAlternativeImplIfScrollStartReceived() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Start by scrolling down.
listener.onScrollStarted(30, SCROLL_EXTENT, false);
// End scrolling.
listener.onScrollEnded(30, SCROLL_EXTENT);
// Start scrolling again after 10ms
advanceTime(10);
listener.onScrollStarted(30, SCROLL_EXTENT, false);
// Send update after 5ms
advanceTime(5);
listener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
// We shouldn't make a call since the call came after a new scroll started.
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void sendOnSessionEnded_HadInteraction() {
initializeTabForTest();
doReturn(false).when(mTabInteractionRecorder).didGetUserInteraction();
Tab tab = mock(Tab.class);
doReturn(mock(WebContents.class)).when(tab).getWebContents();
doReturn(false).when(tab).isIncognito();
mEngagementSignalObserver.onObservingDifferentTab(tab);
doReturn(true).when(mTabInteractionRecorder).didGetUserInteraction();
mEngagementSignalObserver.webContentsWillSwap(tab);
// Close all tabs.
mEngagementSignalObserver.onClosingStateChanged(tab, true);
doReturn(false).when(mTabInteractionRecorder).didGetUserInteraction();
mEngagementSignalObserver.onClosingStateChanged(env.tabProvider.getTab(), true);
mEngagementSignalObserver.onAllTabsClosed();
verify(mEngagementSignalsCallback, times(1)).onSessionEnded(eq(true), any(Bundle.class));
}
@Test
public void sendOnSessionEnded_HadNoInteraction() {
initializeTabForTest();
doReturn(false).when(mTabInteractionRecorder).didGetUserInteraction();
Tab tab = mock(Tab.class);
doReturn(mock(WebContents.class)).when(tab).getWebContents();
doReturn(false).when(tab).isIncognito();
mEngagementSignalObserver.onObservingDifferentTab(tab);
mEngagementSignalObserver.webContentsWillSwap(tab);
// Close all tabs.
mEngagementSignalObserver.onClosingStateChanged(tab, true);
mEngagementSignalObserver.onClosingStateChanged(env.tabProvider.getTab(), true);
mEngagementSignalObserver.onAllTabsClosed();
verify(mEngagementSignalsCallback, times(1)).onSessionEnded(eq(false), any(Bundle.class));
}
@Test
public void doNotSendOnSessionEndedWhenSuspended() {
initializeTabForTest();
mEngagementSignalObserver.suppressNextSessionEndedCall();
doReturn(false).when(mTabInteractionRecorder).didGetUserInteraction();
Tab tab = mock(Tab.class);
doReturn(mock(WebContents.class)).when(tab).getWebContents();
doReturn(false).when(tab).isIncognito();
mEngagementSignalObserver.onObservingDifferentTab(tab);
mEngagementSignalObserver.webContentsWillSwap(tab);
// Close all tabs.
mEngagementSignalObserver.onClosingStateChanged(tab, true);
mEngagementSignalObserver.onClosingStateChanged(env.tabProvider.getTab(), true);
mEngagementSignalObserver.onAllTabsClosed();
verify(mEngagementSignalsCallback, never()).onSessionEnded(eq(false), any(Bundle.class));
// We should only suspend for one call.
assertFalse(mEngagementSignalObserver.getSuspendSessionEndedForTesting());
}
@Test
public void pauseAndUnpauseSignalsOnPageWithTextFragment() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
WebContentsObserver webContentsObserver = captureWebContentsObserver();
// Navigate to a URL with text fragment.
var navigationHandle =
NavigationHandle.createForTesting(
JUnitTestGURLs.TEXT_FRAGMENT_URL, false, 0, false);
webContentsObserver.didStartNavigationInPrimaryMainFrame(navigationHandle);
// Do a scroll.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(24);
listener.onScrollOffsetOrExtentChanged(24, SCROLL_EXTENT);
listener.onScrollEnded(24, SCROLL_EXTENT);
// We shouldn't get scroll signals.
verify(mEngagementSignalsCallback, never())
.onVerticalScrollEvent(anyBoolean(), any(Bundle.class));
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
// Navigate back to a URL with no text fragment.
var navigationHandle2 =
NavigationHandle.createForTesting(JUnitTestGURLs.HTTP_URL, false, 0, false);
webContentsObserver.didStartNavigationInPrimaryMainFrame(navigationHandle2);
// Do a scroll.
listener.onScrollStarted(24, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(50);
listener.onScrollOffsetOrExtentChanged(50, SCROLL_EXTENT);
listener.onScrollEnded(50, SCROLL_EXTENT);
// We should normally get signals.
verify(mEngagementSignalsCallback).onVerticalScrollEvent(eq(false), any(Bundle.class));
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(50), any(Bundle.class));
}
@Test
public void doesNotSendSignalsBeforeDownScroll() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Assume we started further down on the page and scroll up.
listener.onScrollStarted(50, SCROLL_EXTENT, true);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(30);
listener.onScrollOffsetOrExtentChanged(30, SCROLL_EXTENT);
listener.onScrollEnded(30, SCROLL_EXTENT);
// We shouldn't get any signals.
verify(mEngagementSignalsCallback, never())
.onVerticalScrollEvent(anyBoolean(), any(Bundle.class));
verify(mEngagementSignalsCallback, never())
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
// Now scroll down from here.
listener.onScrollStarted(30, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(45);
listener.onScrollOffsetOrExtentChanged(45, SCROLL_EXTENT);
listener.onScrollEnded(45, SCROLL_EXTENT);
// We should get signals as if we've only scrolled down to this %.
verify(mEngagementSignalsCallback).onVerticalScrollEvent(eq(false), any(Bundle.class));
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(45), any(Bundle.class));
}
@Test
public void doesNotSendSignalsBeforeDownScroll_AfterNavigation() {
initializeTabForTest();
GestureStateListener listener = captureGestureStateListener(ON_SCROLL_END);
// Scroll down.
listener.onScrollStarted(0, SCROLL_EXTENT, false);
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(25);
listener.onScrollOffsetOrExtentChanged(25, SCROLL_EXTENT);
listener.onScrollEnded(25, SCROLL_EXTENT);
// We should get signals as usual.
verify(mEngagementSignalsCallback).onVerticalScrollEvent(eq(false), any(Bundle.class));
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(25), any(Bundle.class));
// Now, navigate to another page.
WebContentsObserver webContentsObserver = captureWebContentsObserver();
LoadCommittedDetails details =
new LoadCommittedDetails(
0,
JUnitTestGURLs.URL_1,
false,
/* isSameDocument= */ false,
/* isMainFrame= */ true,
200);
webContentsObserver.navigationEntryCommitted(details);
// Scroll up from some point in the page, e.g. back navigation or anchor fragment on page.
// We shouldn't get any (more) signals.
verify(mEngagementSignalsCallback, times(1))
.onVerticalScrollEvent(anyBoolean(), any(Bundle.class));
verify(mEngagementSignalsCallback, times(1))
.onGreatestScrollPercentageIncreased(anyInt(), any(Bundle.class));
}
@Test
public void sendInitialOffsetUpdate_AltImplEnabled() {
initializeTabForTest(/* hadScrollDown= */ true);
// When the alternative impl flag is enabled, the listener should be added with
// `ON_SCROLL_END`.
var listener = captureGestureStateListener(ON_SCROLL_END);
// Simulate renderer sending the offset update.
when(mRenderCoordinatesImpl.getScrollYPixInt()).thenReturn(35);
listener.onScrollOffsetOrExtentChanged(35, SCROLL_EXTENT);
// We should get a notification since we initialized the observer class with true for
// hadScrollDown.
verify(mEngagementSignalsCallback)
.onGreatestScrollPercentageIncreased(eq(35), any(Bundle.class));
}
private void advanceTime(long millis) {
SystemClock.setCurrentTimeMillis(CURRENT_TIME_MS + millis);
}
private void initializeTabForTest(boolean hadScrollDown) {
Tab initialTab = env.prepareTab();
doAnswer(
invocation -> {
CustomTabTabObserver observer = invocation.getArgument(0);
initialTab.addObserver(observer);
observer.onAttachedToInitialTab(initialTab);
return null;
})
.when(env.tabObserverRegistrar)
.registerActivityTabObserver(any());
mEngagementSignalObserver =
new RealtimeEngagementSignalObserver(
env.tabObserverRegistrar,
env.connection,
env.session,
mEngagementSignalsCallback,
hadScrollDown);
verify(env.tabObserverRegistrar).registerActivityTabObserver(mEngagementSignalObserver);
env.tabProvider.setInitialTab(initialTab, TabCreationMode.DEFAULT);
}
private void initializeTabForTest() {
initializeTabForTest(false);
}
private GestureStateListener captureGestureStateListener() {
return captureGestureStateListener(ON_SCROLL_END);
}
private GestureStateListener captureGestureStateListener(
@RootScrollOffsetUpdateFrequency.EnumType int frequency) {
ArgumentCaptor<GestureStateListener> gestureStateListenerArgumentCaptor =
ArgumentCaptor.forClass(GestureStateListener.class);
verify(mGestureListenerManagerImpl, atLeastOnce())
.addListener(gestureStateListenerArgumentCaptor.capture(), eq(frequency));
return gestureStateListenerArgumentCaptor.getValue();
}
private WebContentsObserver captureWebContentsObserver() {
ArgumentCaptor<WebContentsObserver> webContentsObserverArgumentCaptor =
ArgumentCaptor.forClass(WebContentsObserver.class);
WebContents webContents = env.tabProvider.getTab().getWebContents();
verify(webContents).addObserver(webContentsObserverArgumentCaptor.capture());
return webContentsObserverArgumentCaptor.getValue();
}
private List<TabObserver> captureTabObservers() {
ArgumentCaptor<TabObserver> tabObserverArgumentCaptor =
ArgumentCaptor.forClass(TabObserver.class);
verify(env.tabProvider.getTab(), atLeastOnce())
.addObserver(tabObserverArgumentCaptor.capture());
return tabObserverArgumentCaptor.getAllValues();
}
private void verifyNoMemoryLeakForGestureStateListener(GestureStateListener listener) {
listener.onScrollStarted(0, 1, false);
listener.onVerticalScrollDirectionChanged(false, 0.1f);
listener.onScrollEnded(1, 0);
}
}