chromium/chrome/android/junit/src/org/chromium/chrome/browser/usage_stats/PageViewObserverTest.java

// Copyright 2018 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.usage_stats;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.Promise;
import org.chromium.base.UserDataHost;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab.TabViewManager;
import org.chromium.chrome.browser.tab.TabViewProvider;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.lang.ref.WeakReference;

/** Unit tests for PageViewObserver. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public final class PageViewObserverTest {
    private static final GURL STARTING_URL = JUnitTestGURLs.URL_1;
    private static final GURL STARTING_URL_WITH_PATH = JUnitTestGURLs.URL_1_WITH_PATH;
    private static final GURL DIFFERENT_URL = JUnitTestGURLs.URL_2;
    private static final String STARTING_FQDN = "www.one.com";
    private static final String DIFFERENT_FQDN = "www.two.com";

    @Mock private Activity mActivity;
    @Mock private ObservableSupplier<Tab> mTabSupplier;
    @Mock private Tab mTab;
    @Mock private Tab mTab2;
    @Mock private EventTracker mEventTracker;
    @Mock private TokenTracker mTokenTracker;
    @Mock private SuspensionTracker mSuspensionTracker;
    @Mock private WindowAndroid mWindowAndroid;
    @Mock private ChromeActivity mChromeActivity;
    @Mock private Supplier<TabContentManager> mTabContentManagerSupplier;
    @Captor private ArgumentCaptor<Callback<Tab>> mTabSupplierCaptor;

    private UserDataHost mUserDataHost;
    private UserDataHost mUserDataHostTab2;
    private UserDataHost mDestroyedUserDataHost;
    private WeakReference<Activity> mActivityRef;

    private class MockTabViewManager implements TabViewManager {
        private TabViewProvider mTabViewProvider;

        @Override
        public boolean isShowing(TabViewProvider tabViewProvider) {
            return mTabViewProvider != null && mTabViewProvider == tabViewProvider;
        }

        @Override
        public void addTabViewProvider(TabViewProvider tabViewProvider) {
            mTabViewProvider = tabViewProvider;
        }

        @Override
        public void removeTabViewProvider(TabViewProvider tabViewProvider) {
            if (mTabViewProvider == tabViewProvider) mTabViewProvider = null;
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mUserDataHost = new UserDataHost();
        mUserDataHostTab2 = new UserDataHost();
        mDestroyedUserDataHost = new UserDataHost();
        mDestroyedUserDataHost.destroy();

        Activity activity = Robolectric.buildActivity(TestActivity.class).get();

        doReturn(false).when(mTab).isIncognito();
        doReturn(null).when(mTab).getUrl();
        doReturn(activity).when(mTab).getContext();
        doReturn(activity).when(mTab2).getContext();
        doReturn(new MockTabViewManager()).when(mTab).getTabViewManager();
        doReturn(new MockTabViewManager()).when(mTab2).getTabViewManager();
        doReturn(true).when(mTab).isInitialized();
        doReturn(true).when(mTab2).isInitialized();
        doReturn(mTab).when(mTabSupplier).get();
        doReturn(mUserDataHost).when(mTab).getUserDataHost();
        doReturn(mUserDataHostTab2).when(mTab2).getUserDataHost();
        doReturn(Promise.fulfilled("1")).when(mTokenTracker).getTokenForFqdn(anyString());

        mActivityRef = new WeakReference<>(mChromeActivity);
        when(mTab.getWindowAndroid()).thenReturn(mWindowAndroid);
        when(mTab2.getWindowAndroid()).thenReturn(mWindowAndroid);
        when(mWindowAndroid.getActivity()).thenReturn(mActivityRef);
    }

    @Test
    public void onUpdateUrl_currentlyNull_startReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void updateUrl_nullUrl() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, null, observer);
        onHidden(mTab, TabHidingType.ACTIVITY_HIDDEN, observer);
        verify(mEventTracker, times(0)).addWebsiteEvent(any());
    }

    @Test
    public void updateUrl_startStopReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
        reset(mEventTracker);
        updateUrl(mTab, DIFFERENT_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(DIFFERENT_FQDN)));
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStopEvent(STARTING_FQDN)));
    }

    @Test
    public void updateUrl_sameDomain_startStopNotReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));

        updateUrl(mTab, STARTING_URL_WITH_PATH, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void updateUrl_noPaint_doesNotReportStart() {
        PageViewObserver observer = createPageViewObserver();
        updateUrlNoPaint(mTab, STARTING_URL, observer);
        verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
        reportPaint(mTab, STARTING_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void switchTabs_startStopReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        reset(mEventTracker);

        doReturn(DIFFERENT_URL).when(mTab2).getUrl();
        doReturn(mTab2).when(mTabSupplier).get();
        doReturn(false).when(mTab2).isHidden();
        changeTab(mTab2);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(DIFFERENT_FQDN)));
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStopEvent(STARTING_FQDN)));
    }

    @Test
    public void switchTabs_sameDomain_startStopNotReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));

        doReturn(STARTING_URL).when(mTab2).getUrl();
        changeTab(mTab2);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void switchToHiddenTab_startNotReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        reset(mEventTracker);

        doReturn(DIFFERENT_URL).when(mTab2).getUrl();
        doReturn(true).when(mTab2).isHidden();
        changeTab(mTab2);
        verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStartEvent(DIFFERENT_FQDN)));
    }

    @Test
    public void switchToSuspendedTab_startNotReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(STARTING_URL).when(mTab).getUrl();
        doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(STARTING_FQDN);
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);
        assertTrue(SuspendedTab.from(mTab, mTabContentManagerSupplier).isShowing());
        reset(mEventTracker);

        onHidden(mTab, TabHidingType.ACTIVITY_HIDDEN, observer);
        onShown(mTab, TabSelectionType.FROM_USER, observer);
        verify(mEventTracker, never()).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void tabHidden_stopReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        onHidden(mTab, TabHidingType.ACTIVITY_HIDDEN, observer);

        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStopEvent(STARTING_FQDN)));
    }

    @Test
    public void tabShown_startReported() {
        PageViewObserver observer = createPageViewObserver();
        doReturn(STARTING_URL).when(mTab).getUrl();
        onShown(mTab, TabSelectionType.FROM_USER, observer);

        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void tabClosed_switchToNew_startStopReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);
        onHidden(mTab, TabHidingType.ACTIVITY_HIDDEN, observer);

        doReturn(DIFFERENT_URL).when(mTab2).getUrl();
        onShown(mTab2, TabSelectionType.FROM_CLOSE, observer);

        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStopEvent(STARTING_FQDN)));
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(DIFFERENT_FQDN)));
    }

    @Test
    public void tabAdded_startReported() {
        PageViewObserver observer = createPageViewObserver();
        doReturn(STARTING_URL).when(mTab2).getUrl();
        doReturn(mTab2).when(mTabSupplier).get();
        changeTab(mTab2);

        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void tabAdded_notSelected_startNotReported() {
        PageViewObserver observer = createPageViewObserver();
        doReturn(STARTING_URL).when(mTab).getUrl();
        doReturn(null).when(mTabSupplier).get();
        changeTab(mTab);

        verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void tabAdded_suspendedDomain() {
        PageViewObserver observer = createPageViewObserver();
        doReturn(STARTING_URL).when(mTab2).getUrl();
        doReturn(mTab2).when(mTabSupplier).get();
        doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(STARTING_FQDN);
        changeTab(mTab2);

        assertEquals(SuspendedTab.from(mTab2, mTabContentManagerSupplier).getFqdn(), STARTING_FQDN);
    }

    // TODO(pnoland): add test for platform reporting once the System API is available in Q.

    @Test
    public void tabIncognito_eventsNotReported() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(true).when(mTab2).isIncognito();
        doReturn(DIFFERENT_URL).when(mTab2).getUrl();
        changeTab(mTab2);
        verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStartEvent(DIFFERENT_FQDN)));
        verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStopEvent(DIFFERENT_FQDN)));
    }

    @Test
    public void navigationToSuspendedDomain_suspendedTabShown() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(DIFFERENT_URL).when(mTab).getUrl();
        doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(DIFFERENT_FQDN);
        updateUrl(mTab, DIFFERENT_URL, observer);

        SuspendedTab suspendedTab = SuspendedTab.from(mTab, mTabContentManagerSupplier);
        assertEquals(suspendedTab.getFqdn(), DIFFERENT_FQDN);
    }

    @Test
    public void navigationToUnsuspendedDomain_suspendedTabRemoved() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(DIFFERENT_URL).when(mTab).getUrl();
        doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(DIFFERENT_FQDN);
        updateUrl(mTab, DIFFERENT_URL, observer);

        SuspendedTab suspendedTab = SuspendedTab.from(mTab, mTabContentManagerSupplier);
        assertTrue(suspendedTab.isShowing());

        updateUrl(mTab, STARTING_URL, observer);
        assertFalse(suspendedTab.isShowing());
    }

    @Test
    public void eagerSuspension() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(STARTING_URL).when(mTab).getUrl();
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);
        assertTrue(SuspendedTab.from(mTab, mTabContentManagerSupplier).isShowing());

        // Trying to suspend the site again shouldn't have an effect.
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);
        assertTrue(SuspendedTab.from(mTab, mTabContentManagerSupplier).isShowing());
    }

    @Test
    public void eagerSuspension_navigateToDifferentSuspended() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(STARTING_URL).when(mTab).getUrl();
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);

        SuspendedTab suspendedTab = SuspendedTab.from(mTab, mTabContentManagerSupplier);
        assertEquals(STARTING_FQDN, suspendedTab.getFqdn());

        doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(DIFFERENT_FQDN);
        updateUrl(mTab, DIFFERENT_URL, observer);

        assertEquals(DIFFERENT_FQDN, suspendedTab.getFqdn());
    }

    @Test
    public void eagerSuspension_reshowSameDomain_nowUnsuspended() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(STARTING_URL).when(mTab).getUrl();
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);

        SuspendedTab suspendedTab = SuspendedTab.from(mTab, mTabContentManagerSupplier);
        assertTrue(suspendedTab.isShowing());

        doReturn(false).when(mSuspensionTracker).isWebsiteSuspended(STARTING_FQDN);
        onShown(mTab, TabSelectionType.FROM_USER, observer);
        assertFalse(suspendedTab.isShowing());
    }

    @Test
    public void eagerUnsuspension() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(STARTING_URL).when(mTab).getUrl();
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);

        SuspendedTab suspendedTab = SuspendedTab.from(mTab, mTabContentManagerSupplier);
        assertTrue(suspendedTab.isShowing());

        observer.notifySiteSuspensionChanged(STARTING_FQDN, false);
        assertFalse(suspendedTab.isShowing());

        // Trying to un-suspend again should have no effect.
        observer.notifySiteSuspensionChanged(STARTING_FQDN, false);
        assertFalse(suspendedTab.isShowing());
    }

    @Test
    public void eagerUnsuspension_otherDomainActiveAndSuspended() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(STARTING_URL).when(mTab).getUrl();
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);

        SuspendedTab suspendedTab = SuspendedTab.from(mTab, mTabContentManagerSupplier);
        assertTrue(suspendedTab.isShowing());

        doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(DIFFERENT_FQDN);
        updateUrl(mTab, DIFFERENT_URL, observer);

        // Notifying that STARTING_FQDN is no longer suspended shouldn't remove the active
        // SuspendedTab for DIFFERENT_FQDN.
        observer.notifySiteSuspensionChanged(STARTING_FQDN, false);
        assertTrue(suspendedTab.isShowing());
    }

    @Test
    public void eagerUnsuspension_notAlreadySuspended() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        observer.notifySiteSuspensionChanged(STARTING_FQDN, false);
        assertFalse(SuspendedTab.from(mTab, mTabContentManagerSupplier).isShowing());
    }

    @Test
    public void alreadySuspendedDomain_doesNotReportStopEventAgain() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStopEvent(STARTING_FQDN)));

        updateUrl(mTab, DIFFERENT_URL, observer);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStopEvent(STARTING_FQDN)));
    }

    @Test
    public void customTab_startReportedUponConstruction() {
        doReturn(STARTING_URL).when(mTab).getUrl();
        doReturn(false).when(mTab).isHidden();
        PageViewObserver observer = createPageViewObserver();
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));

        doReturn(DIFFERENT_URL).when(mTab2).getUrl();
        doReturn(true).when(mTab2).isHidden();
        changeTab(mTab2);
        verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStartEvent(DIFFERENT_FQDN)));
    }

    @Test
    public void construction_nullInitialTab() {
        doReturn(null).when(mTabSupplier).get();
        PageViewObserver observer = createPageViewObserver();

        doReturn(mTab).when(mTabSupplier).get();
        doReturn(STARTING_URL).when(mTab).getUrl();
        changeTab(mTab);
        verify(mEventTracker, times(1)).addWebsiteEvent(argThat(isStartEvent(STARTING_FQDN)));
    }

    @Test
    public void eagerSuspension_destroyedTab() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        doReturn(mDestroyedUserDataHost).when(mTab).getUserDataHost();
        doReturn(false).when(mTab).isInitialized();
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);
    }

    @Test
    public void eagerSuspension_nullTab() {
        PageViewObserver observer = createPageViewObserver();
        updateUrl(mTab, STARTING_URL, observer);

        changeTab(null);
        observer.notifySiteSuspensionChanged(STARTING_FQDN, true);
    }

    private PageViewObserver createPageViewObserver() {
        PageViewObserver observer =
                new PageViewObserver(
                        mActivity,
                        mTabSupplier,
                        mEventTracker,
                        mTokenTracker,
                        mSuspensionTracker,
                        mTabContentManagerSupplier);
        verify(mTabSupplier, times(1)).addObserver(mTabSupplierCaptor.capture());
        Tab tab = mTabSupplier.get();
        mTabSupplierCaptor.getValue().onResult(tab);
        if (tab != null) {
            verify(tab, times(1)).addObserver(observer);
        }

        return observer;
    }

    private void updateUrl(Tab tab, GURL url, TabObserver tabObserver) {
        updateUrlNoPaint(tab, url, tabObserver);
        reportPaint(tab, url, tabObserver);
    }

    private void updateUrlNoPaint(Tab tab, GURL url, TabObserver tabObserver) {
        tabObserver.onUpdateUrl(tab, url);
    }

    private void reportPaint(Tab tab, GURL url, TabObserver tabObserver) {
        doReturn(url).when(tab).getUrl();
        tabObserver.didFirstVisuallyNonEmptyPaint(tab);
    }

    private void onHidden(Tab tab, @TabHidingType int hidingType, TabObserver tabObserver) {
        tabObserver.onHidden(tab, hidingType);
    }

    private void onShown(Tab tab, @TabSelectionType int selectionType, TabObserver tabObserver) {
        tabObserver.onShown(tab, selectionType);
    }

    private void changeTab(Tab newTab) {
        mTabSupplierCaptor.getValue().onResult(newTab);
    }

    private ArgumentMatcher<WebsiteEvent> isStartEvent(String fqdn) {
        return new ArgumentMatcher<WebsiteEvent>() {
            @Override
            public boolean matches(WebsiteEvent event) {
                return event.getType() == WebsiteEvent.EventType.START
                        && event.getFqdn().equals(fqdn);
            }

            @Override
            public String toString() {
                return "Start event with fqdn: " + fqdn;
            }
        };
    }

    private ArgumentMatcher<WebsiteEvent> isStopEvent(String fqdn) {
        return new ArgumentMatcher<WebsiteEvent>() {
            @Override
            public boolean matches(WebsiteEvent event) {
                return event.getType() == WebsiteEvent.EventType.STOP
                        && event.getFqdn().equals(fqdn);
            }

            @Override
            public String toString() {
                return "Stop event with fqdn: " + fqdn;
            }
        };
    }
}