chromium/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/webfeed/WebFeedMainMenuItemTest.java

// Copyright 2021 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.feed.webfeed;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.feed.test.R;
import org.chromium.chrome.browser.feed.webfeed.WebFeedSnackbarController.FeedLauncher;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.browser_ui.widget.chips.ChipView;
import org.chromium.components.embedder_support.util.ShadowUrlUtilities;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.ui.base.TestActivity;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.widget.LoadingView;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;

import java.util.ArrayList;

/** Tests {@link WebFeedMainMenuItem}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {ShadowUrlUtilities.class})
@LooperMode(LooperMode.Mode.LEGACY)
@EnableFeatures({ChromeFeatureList.CORMORANT})
@SmallTest
public final class WebFeedMainMenuItemTest {
    private static final GURL TEST_URL = JUnitTestGURLs.EXAMPLE_URL;

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    @Captor ArgumentCaptor<Intent> mIntentCaptor;

    @Mock private Context mContext;
    @Mock private FeedLauncher mFeedLauncher;
    @Mock private AppMenuHandler mAppMenuHandler;
    @Mock private ModalDialogManager mDialogManager;
    @Mock private SnackbarManager mSnackBarManager;
    @Mock private Tab mTab;
    @Mock public WebFeedBridge.Natives mWebFeedBridgeJniMock;

    private Activity mActivity;
    private Class<?> mCreatorActivityClass;
    private WebFeedMainMenuItem mWebFeedMainMenuItem;
    private TestWebFeedFaviconFetcher mFaviconFetcher = new TestWebFeedFaviconFetcher();
    private ArrayList<Callback<WebFeedBridge.WebFeedMetadata>> mWaitingMetadataCallbacks =
            new ArrayList();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        // Print logs to stdout.
        ShadowLog.stream = System.out;
        mJniMocker.mock(WebFeedBridge.getTestHooksForTesting(), mWebFeedBridgeJniMock);

        when(mWebFeedBridgeJniMock.isCormorantEnabledForLocale()).thenReturn(true);

        doReturn(GURL.emptyGURL()).when(mTab).getOriginalUrl();
        doReturn(false).when(mTab).isShowingErrorPage();

        // Required for resolving an attribute used in AppMenuItemText.
        mActivityScenarioRule.getScenario().onActivity(activity -> mActivity = activity);

        // Add requests for web feed information to mWaitingMetadataCallbacks.
        doAnswer(
                        invocation -> {
                            assertEquals(
                                    "Incorrect WebFeedPageInformationRequestReason was used.",
                                    WebFeedPageInformationRequestReason.MENU_ITEM_PRESENTATION,
                                    invocation.<Integer>getArgument(1).intValue());
                            mWaitingMetadataCallbacks.add(
                                    invocation.<Callback<WebFeedBridge.WebFeedMetadata>>getArgument(
                                            2));
                            return null;
                        })
                .when(mWebFeedBridgeJniMock)
                .findWebFeedInfoForPage(
                        any(WebFeedBridge.WebFeedPageInformation.class),
                        anyInt(),
                        any(Callback.class));

        // Initialize an empty class for mCreatorActivityClass
        class CreatorActivityClassTest {}
        mCreatorActivityClass = CreatorActivityClassTest.class;

        mWebFeedMainMenuItem =
                (WebFeedMainMenuItem)
                        (LayoutInflater.from(mActivity)
                                .inflate(R.layout.web_feed_main_menu_item, null));

        LoadingView.setDisableAnimationForTest(true);
    }

    @Test
    public void initialize_hasFavicon_displaysFavicon() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(null);
        mFaviconFetcher.answerWithBitmap();

        ImageView imageView = mWebFeedMainMenuItem.findViewById(R.id.icon);
        Bitmap actualIcon = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
        assertEquals("Icon should be favicon.", mFaviconFetcher.getTestBitmap(), actualIcon);
        assertEquals("Icon should be visible.", View.VISIBLE, imageView.getVisibility());
    }

    @Test
    public void initialize_emptyUrl_removesIcon() {
        doReturn(GURL.emptyGURL()).when(mTab).getOriginalUrl();
        mWebFeedMainMenuItem.initialize(
                mTab,
                mAppMenuHandler,
                mFaviconFetcher,
                mFeedLauncher,
                mDialogManager,
                mSnackBarManager,
                mCreatorActivityClass);
        respondWithFeedMetadata(null);
        mFaviconFetcher.answerWithNull();

        ImageView imageView = mWebFeedMainMenuItem.findViewById(R.id.icon);
        assertEquals("Icon should be gone.", View.GONE, imageView.getVisibility());
    }

    @Test
    public void initialize_displaysCorrectTitle() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(null);

        TextView textView = mWebFeedMainMenuItem.findViewById(R.id.menu_item_text);
        assertEquals(
                "Title should be shortened URL.",
                UrlFormatter.formatUrlForDisplayOmitSchemePathAndTrivialSubdomains(TEST_URL),
                textView.getText());
    }

    @Test
    public void initialize_launchCreatorActivity() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.SUBSCRIBED, GURL.emptyGURL()));

        mIntentCaptor = ArgumentCaptor.forClass(Intent.class);
        TextView textView = mWebFeedMainMenuItem.findViewById(R.id.menu_item_text);
        mWebFeedMainMenuItem.setContextForTest(mContext);
        textView.performClick();

        verify(mContext).startActivity(mIntentCaptor.capture());
        Intent intent = mIntentCaptor.getValue();
        assertNotNull(intent);
        assertEquals(4, intent.getExtras().size());
        assertTrue(intent.hasExtra(CreatorIntentConstants.CREATOR_URL));
        assertNotNull(intent.getExtras().getString(CreatorIntentConstants.CREATOR_URL));
        assertTrue(intent.hasExtra(CreatorIntentConstants.CREATOR_ENTRY_POINT));
        assertNotNull(intent.getExtras().getInt(CreatorIntentConstants.CREATOR_ENTRY_POINT));
        assertTrue(intent.hasExtra(CreatorIntentConstants.CREATOR_FOLLOWING));
        assertNotNull(
                intent.getExtras().getBoolean(CreatorIntentConstants.CREATOR_FOLLOWING, false));
        assertTrue(intent.hasExtra(CreatorIntentConstants.CREATOR_TAB_ID));
        assertNotNull(intent.getExtras().getInt(CreatorIntentConstants.CREATOR_TAB_ID));
    }

    @Test
    public void initialize_noMetadata_displaysFollowChip() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(null);

        assertEquals("follow", getChipState());
    }

    @Test
    public void initialize_notFollowed_displaysFollowChip() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.NOT_SUBSCRIBED, GURL.emptyGURL()));

        assertEquals("follow", getChipState());
    }

    @Test
    public void initialize_errorPage_displaysDisabledFollowChip() {
        doReturn(true).when(mTab).isShowingErrorPage();
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.NOT_SUBSCRIBED, GURL.emptyGURL()));

        assertEquals("disabled follow", getChipState());
    }

    @Test
    public void initialize_unknownFollowStatus_displaysFollowChip() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.UNKNOWN, GURL.emptyGURL()));

        assertEquals("follow", getChipState());
    }

    @Test
    public void initialize_followed_displaysFollowingChip() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.SUBSCRIBED, GURL.emptyGURL()));

        assertEquals("following", getChipState());
    }

    @Test
    public void initialize_unfollowInProgress() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(
                        WebFeedSubscriptionStatus.UNSUBSCRIBE_IN_PROGRESS, GURL.emptyGURL()));

        // ChipView imposes a delay.
        assertEquals("invisible", getChipState());
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals("disabled following", getChipState());
    }

    @Test
    public void initialize_unfollowInProgress_succeeds() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(
                        WebFeedSubscriptionStatus.UNSUBSCRIBE_IN_PROGRESS, GURL.emptyGURL()));
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.NOT_SUBSCRIBED, GURL.emptyGURL()));
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        assertEquals("follow", getChipState());
    }

    @Test
    public void initialize_unfollowInProgress_fails() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(
                        WebFeedSubscriptionStatus.UNSUBSCRIBE_IN_PROGRESS, GURL.emptyGURL()));
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.SUBSCRIBED, GURL.emptyGURL()));
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        assertEquals("following", getChipState());
    }

    @Test
    public void initialize_followInProgress_succeeds() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(
                        WebFeedSubscriptionStatus.SUBSCRIBE_IN_PROGRESS, GURL.emptyGURL()));

        // ChipView imposes a delay.
        assertEquals("invisible", getChipState());
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals("disabled follow", getChipState());

        // Now the web feed is subscribed.
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.SUBSCRIBED, GURL.emptyGURL()));
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals("following", getChipState());
    }

    @Test
    public void initialize_followInProgress_fails() {
        initializeWebFeedMainMenuItem();
        respondWithFeedMetadata(
                createWebFeedMetadata(
                        WebFeedSubscriptionStatus.SUBSCRIBE_IN_PROGRESS, GURL.emptyGURL()));

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        respondWithFeedMetadata(
                createWebFeedMetadata(WebFeedSubscriptionStatus.NOT_SUBSCRIBED, GURL.emptyGURL()));
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        assertEquals("follow", getChipState());
    }

    /** Helper method to return the visual state of the chip. */
    private String getChipState() {
        // Note there are some invariants checked here:
        // * There are two ChipViews, ensure that only one is non-GONE.
        // * For the visible chip, if any, verify it has the right text.
        ChipView followingChip = getFollowingChipView();
        ChipView followChip = getFollowChipView();

        if (followingChip.getVisibility() != View.GONE) {
            assertEquals(View.GONE, followChip.getVisibility());
            if (followingChip.getVisibility() == View.VISIBLE) {
                assertEquals(
                        mActivity.getResources().getString(R.string.menu_following),
                        followingChip.getPrimaryTextView().getText());
                return (followingChip.isEnabled() ? "" : "disabled ") + "following";
            }
            return "invisible";
        } else if (followChip.getVisibility() != View.GONE) {
            if (followChip.getVisibility() == View.VISIBLE) {
                assertEquals(
                        mActivity.getResources().getString(R.string.menu_follow),
                        followChip.getPrimaryTextView().getText());
                return (followChip.isEnabled() ? "" : "disabled ") + "follow";
            }
            return "invisible";
        } else {
            return "none";
        }
    }

    /** Helper method to initialize {@code mWebFeedMainMenuItem} with standard parameters. */
    private void initializeWebFeedMainMenuItem() {
        doReturn(TEST_URL).when(mTab).getOriginalUrl();
        mWebFeedMainMenuItem.initialize(
                mTab,
                mAppMenuHandler,
                mFaviconFetcher,
                mFeedLauncher,
                mDialogManager,
                mSnackBarManager,
                mCreatorActivityClass);
    }

    /**
     * Helper method to create a {@link WebFeedBridge.WebFeedMetadata} with standard parameters.
     *
     * @param subscriptionStatus {@link WebFeedSubscriptionStatus} for the metadata.
     */
    private WebFeedBridge.WebFeedMetadata createWebFeedMetadata(
            @WebFeedSubscriptionStatus int subscriptionStatus, GURL faviconUrl) {
        return new WebFeedBridge.WebFeedMetadata(
                "id".getBytes(),
                "title",
                TEST_URL,
                subscriptionStatus,
                WebFeedAvailabilityStatus.INACTIVE,
                /* isRecommended= */ false,
                faviconUrl);
    }

    ChipView getFollowChipView() {
        return mWebFeedMainMenuItem.findViewById(R.id.follow_chip_view);
    }

    ChipView getFollowingChipView() {
        return mWebFeedMainMenuItem.findViewById(R.id.following_chip_view);
    }

    private void respondWithFeedMetadata(WebFeedBridge.WebFeedMetadata metadata) {
        assertFalse(mWaitingMetadataCallbacks.isEmpty());
        Callback<WebFeedBridge.WebFeedMetadata> callback = mWaitingMetadataCallbacks.get(0);
        mWaitingMetadataCallbacks.remove(0);
        callback.onResult(metadata);
    }
}