chromium/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/tile/MostVisitedMediatorUnitTest.java

// 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.suggestions.tile;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.suggestions.tile.MostVisitedTilesProperties.HORIZONTAL_EDGE_PADDINGS;
import static org.chromium.chrome.browser.suggestions.tile.MostVisitedTilesProperties.HORIZONTAL_INTERVAL_PADDINGS;
import static org.chromium.chrome.browser.suggestions.tile.MostVisitedTilesProperties.IS_CONTAINER_VISIBLE;
import static org.chromium.chrome.browser.suggestions.tile.MostVisitedTilesProperties.IS_MVT_LAYOUT_VISIBLE;
import static org.chromium.chrome.browser.suggestions.tile.MostVisitedTilesProperties.PLACEHOLDER_VIEW;

import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewStub;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.native_page.ContextMenuManager;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate;
import org.chromium.chrome.browser.suggestions.mostvisited.MostVisitedSites;
import org.chromium.chrome.test.util.browser.suggestions.mostvisited.FakeMostVisitedSites;
import org.chromium.components.browser_ui.widget.displaystyle.HorizontalDisplayStyle;
import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
import org.chromium.components.browser_ui.widget.displaystyle.UiConfig.DisplayStyle;
import org.chromium.components.browser_ui.widget.displaystyle.VerticalDisplayStyle;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.JUnitTestGURLs;

import java.util.ArrayList;

/** Tests for {@link MostVisitedTilesMediator}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class MostVisitedMediatorUnitTest {
    @Mock Resources mResources;
    @Mock Configuration mConfiguration;
    @Mock UiConfig mUiConfig;
    @Mock DisplayMetrics mDisplayMetrics;
    @Mock MostVisitedTilesLayout mMvTilesLayout;
    @Mock ViewStub mNoMvPlaceholderStub;
    @Mock View mNoMvPlaceholder;
    @Mock Tile mTile;
    @Mock SuggestionsTileView mTileView;
    @Mock SiteSuggestion mData;
    @Mock TileRenderer mTileRenderer;
    @Mock SuggestionsUiDelegate mSuggestionsUiDelegate;
    @Mock ContextMenuManager mContextMenuManager;
    @Mock TileGroup.Delegate mTileGroupDelegate;
    @Mock OfflinePageBridge mOfflinePageBridge;
    @Mock private Profile mProfile;
    @Mock private TemplateUrlService mTemplateUrlService;
    @Mock private Runnable mSnapshotTileGridChangedRunnable;
    @Mock private Runnable mTileCountChangedRunnable;

    private FakeMostVisitedSites mMostVisitedSites;
    private PropertyModel mModel;
    private MostVisitedTilesMediator mMediator;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mModel = new PropertyModel(MostVisitedTilesProperties.ALL_KEYS);
        when(mResources.getConfiguration()).thenReturn(mConfiguration);
        mDisplayMetrics.widthPixels = 1000;
        when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
        when(mResources.getDimensionPixelSize(R.dimen.tile_view_padding_edge_portrait))
                .thenReturn(12);
        when(mResources.getDimensionPixelSize(R.dimen.tile_view_padding_landscape)).thenReturn(16);
        when(mResources.getDimensionPixelOffset(R.dimen.tile_view_width)).thenReturn(80);

        when(mUiConfig.getCurrentDisplayStyle())
                .thenReturn(
                        new DisplayStyle(
                                HorizontalDisplayStyle.REGULAR, VerticalDisplayStyle.REGULAR));

        when(mTileView.getData()).thenReturn(mData);
        when(mTile.getData()).thenReturn(mData);

        mMostVisitedSites = new FakeMostVisitedSites();
        doAnswer(
                        invocation -> {
                            mMostVisitedSites.setObserver(
                                    invocation.getArgument(0), invocation.<Integer>getArgument(1));
                            return null;
                        })
                .when(mTileGroupDelegate)
                .setMostVisitedSitesObserver(any(MostVisitedSites.Observer.class), anyInt());

        TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlService);
        when(mTemplateUrlService.doesDefaultSearchEngineHaveLogo()).thenReturn(true);
    }

    @Test
    public void testOnTileDataChanged() {
        createMediator();
        mMediator.onTileDataChanged();

        verify(mTileRenderer, atLeastOnce())
                .renderTileSection(anyList(), eq(mMvTilesLayout), any());
        verify(mMvTilesLayout).addView(any());
        verify(mSnapshotTileGridChangedRunnable, atLeastOnce()).run();
    }

    @Test
    public void testOnTileCountChanged() {
        when(mTemplateUrlService.doesDefaultSearchEngineHaveLogo()).thenReturn(false);
        ArrayList<SiteSuggestion> array = new ArrayList<>();
        array.add(mData);
        mMostVisitedSites.setTileSuggestions(array);

        createMediator();

        Assert.assertFalse(mMediator.isMVTilesCleanedUp());
        Assert.assertTrue(mModel.get(IS_MVT_LAYOUT_VISIBLE));
        Assert.assertNull(mModel.get(PLACEHOLDER_VIEW));

        // When there's no mv tile and the default search engine doesn't have logo, the placeholder
        // should be shown and the mv tiles layout should be hidden.
        mMostVisitedSites.setTileSuggestions(new ArrayList<>());

        mMediator.onTileCountChanged();

        Assert.assertFalse(mModel.get(IS_MVT_LAYOUT_VISIBLE));
        Assert.assertNotNull(mModel.get(PLACEHOLDER_VIEW));
        verify(mTileCountChangedRunnable, atLeastOnce()).run();

        // When there is mv tile and the default search engine doesn't have logo, the placeholder
        // should be hidden and the mv tiles layout should be shown.
        mMostVisitedSites.setTileSuggestions(JUnitTestGURLs.HTTP_URL.getSpec());

        mMediator.onTileCountChanged();

        Assert.assertTrue(mModel.get(IS_MVT_LAYOUT_VISIBLE));
        Assert.assertNotNull(mModel.get(PLACEHOLDER_VIEW));
    }

    @Test
    public void testMvtContainerOnTileCountChanged() {
        ArrayList<SiteSuggestion> array = new ArrayList<>();
        array.add(mData);
        mMostVisitedSites.setTileSuggestions(array);
        createMediator();

        Assert.assertTrue(mModel.get(IS_CONTAINER_VISIBLE));

        // When there's no mv tile, the mv tiles container should be hidden.
        mMostVisitedSites.setTileSuggestions(new ArrayList<>());
        mMediator.onTileCountChanged();
        Assert.assertFalse(mModel.get(IS_CONTAINER_VISIBLE));

        // When there is mv tile, the mv tiles container should be shown.
        mMostVisitedSites.setTileSuggestions(JUnitTestGURLs.HTTP_URL.getSpec());
        mMediator.onTileCountChanged();
        Assert.assertTrue(mModel.get(IS_CONTAINER_VISIBLE));
    }

    @Test
    public void testOnTileIconChanged() {
        createMediator();
        mMediator.onTileIconChanged(mTile);

        verify(mTileView).renderIcon(mTile);
        verify(mSnapshotTileGridChangedRunnable, atLeastOnce()).run();
    }

    @Test
    public void testOnTileOfflineBadgeVisibilityChanged() {
        createMediator();
        mMediator.onTileOfflineBadgeVisibilityChanged(mTile);

        verify(mTileView).renderOfflineBadge(mTile);
        verify(mSnapshotTileGridChangedRunnable, atLeastOnce()).run();
    }

    @Test
    public void testOnTemplateURLServiceChanged() {
        mMostVisitedSites.setTileSuggestions(new ArrayList<>());
        createMediator();

        Assert.assertTrue(mModel.get(IS_MVT_LAYOUT_VISIBLE));
        Assert.assertNull(mModel.get(PLACEHOLDER_VIEW));

        // When the default search engine has logo and there's no mv tile, the placeholder
        // should be hidden and the mv tiles layout should be shown.
        when(mTemplateUrlService.doesDefaultSearchEngineHaveLogo()).thenReturn(false);

        mMediator.onTemplateURLServiceChanged();

        Assert.assertFalse(mModel.get(IS_MVT_LAYOUT_VISIBLE));
        Assert.assertNotNull(mModel.get(PLACEHOLDER_VIEW));

        // When the default search engine doesn't have logo and there's no mv tile, the placeholder
        // should be shown and the mv tiles layout should be hidden.
        when(mTemplateUrlService.doesDefaultSearchEngineHaveLogo()).thenReturn(true);

        mMediator.onTemplateURLServiceChanged();

        Assert.assertTrue(mModel.get(IS_MVT_LAYOUT_VISIBLE));
        Assert.assertNotNull(mModel.get(PLACEHOLDER_VIEW));
    }

    @Test
    public void testSetPortraitPaddings_NotSmallDevice() {
        mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT;
        createMediator();
        mMediator.onTileDataChanged();

        Assert.assertEquals(
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_edge_portrait),
                (int) (mModel.get(HORIZONTAL_EDGE_PADDINGS)));
        int tileViewWidth = mResources.getDimensionPixelOffset(R.dimen.tile_view_width);
        Assert.assertEquals(
                (int)
                        ((mDisplayMetrics.widthPixels
                                        - mModel.get(HORIZONTAL_EDGE_PADDINGS)
                                        - tileViewWidth * 4.5)
                                / 4),
                (int) (mModel.get(HORIZONTAL_INTERVAL_PADDINGS)));
    }

    @Test
    public void testSetPortraitPaddings_SmallDevice() {
        mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT;
        when(mUiConfig.getCurrentDisplayStyle())
                .thenReturn(
                        new DisplayStyle(HorizontalDisplayStyle.NARROW, VerticalDisplayStyle.FLAT));
        createMediator();
        mMediator.onTileDataChanged();

        Assert.assertEquals(
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_edge_portrait),
                (int) (mModel.get(HORIZONTAL_EDGE_PADDINGS)));
        int tileViewWidth = mResources.getDimensionPixelOffset(R.dimen.tile_view_width_condensed);
        Assert.assertEquals(
                Integer.max(
                        0,
                        (int)
                                ((mDisplayMetrics.widthPixels
                                                - mModel.get(HORIZONTAL_EDGE_PADDINGS)
                                                - tileViewWidth * 4.5)
                                        / 4)),
                (int) (mModel.get(HORIZONTAL_INTERVAL_PADDINGS)));
    }

    @Test
    public void testSetLandscapePaddings() {
        mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
        createMediator();
        mMediator.onTileDataChanged();

        Assert.assertEquals(
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_landscape),
                (int) (mModel.get(HORIZONTAL_EDGE_PADDINGS)));
        Assert.assertEquals(
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_landscape),
                (int) (mModel.get(HORIZONTAL_INTERVAL_PADDINGS)));
    }

    @Test
    public void testDestroy() {
        createMediator();

        mMediator.destroy();

        verify(mMvTilesLayout).destroy();
        verify(mTemplateUrlService).removeObserver(mMediator);
    }

    @Test
    public void testUpdateTilesView_Tablet() {
        int expectedTileViewEdgePadding =
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_edge_tablet);
        int expectedTileViewIntervalPadding =
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_interval_tablet);
        mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT;
        createMediator(/* isTablet= */ true);
        mMediator.onTileDataChanged();
        Assert.assertEquals(
                "The horizontal edge padding passed to the model is wrong",
                expectedTileViewEdgePadding,
                (int) mModel.get(HORIZONTAL_EDGE_PADDINGS));
        Assert.assertEquals(
                "The horizontal interval padding passed to the model is wrong",
                expectedTileViewIntervalPadding,
                (int) mModel.get(HORIZONTAL_INTERVAL_PADDINGS));

        mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
        createMediator(/* isTablet= */ true);
        mMediator.onTileDataChanged();
        Assert.assertEquals(
                "The horizontal edge padding passed to the model is wrong",
                expectedTileViewEdgePadding,
                (int) mModel.get(HORIZONTAL_EDGE_PADDINGS));
        Assert.assertEquals(
                "The horizontal interval padding passed to the model is wrong",
                expectedTileViewIntervalPadding,
                (int) mModel.get(HORIZONTAL_INTERVAL_PADDINGS));
    }

    @Test
    public void testUpdateTilesView_Phone() {
        mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT;
        createMediator(/* isTablet= */ false);
        mMediator.onTileDataChanged();
        // tile_view_padding_edge_portrait
        Assert.assertEquals(
                "The horizontal edge padding passed to the model is wrong",
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_edge_portrait),
                (int) mModel.get(HORIZONTAL_EDGE_PADDINGS));

        mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
        createMediator(/* isTablet= */ false);
        mMediator.onTileDataChanged();
        Assert.assertEquals(
                "The horizontal edge padding passed to the model is wrong",
                mResources.getDimensionPixelSize(R.dimen.tile_view_padding_landscape),
                (int) mModel.get(HORIZONTAL_EDGE_PADDINGS));
    }

    private void createMediator() {
        createMediator(/* isTablet= */ false);
    }

    private void createMediator(boolean isTablet) {
        mMvTilesLayout = Mockito.mock(MostVisitedTilesLayout.class);

        mMvTilesLayout.addView(mTileView);
        when(mMvTilesLayout.getChildCount()).thenReturn(1);
        when(mMvTilesLayout.getChildAt(0)).thenReturn(mTileView);
        when(mNoMvPlaceholderStub.inflate()).thenReturn(mNoMvPlaceholder);

        mMediator =
                new MostVisitedTilesMediator(
                        mResources,
                        mUiConfig,
                        mMvTilesLayout,
                        mNoMvPlaceholderStub,
                        mTileRenderer,
                        mModel,
                        isTablet,
                        mSnapshotTileGridChangedRunnable,
                        mTileCountChangedRunnable);
        mMediator.initWithNative(
                mProfile,
                mSuggestionsUiDelegate,
                mContextMenuManager,
                mTileGroupDelegate,
                mOfflinePageBridge,
                mTileRenderer);
    }
}