chromium/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java

// Copyright 2017 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;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;

import org.junit.After;
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.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.fakes.RoboMenu;
import org.robolectric.shadows.ShadowPackageManager;

import org.chromium.base.Callback;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.locale.LocaleManagerDelegate;
import org.chromium.chrome.browser.readaloud.ReadAloudController;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.share.ShareDelegate.ShareOrigin;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content.R;
import org.chromium.content_public.browser.ActionModeCallbackHelper;
import org.chromium.content_public.browser.WebContents;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/** Unit tests for the {@link ChromeActionModeHandler}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ChromeActionModeHandlerUnitTest {
    @Mock private Tab mTab;
    @Mock private ActionModeCallbackHelper mActionModeCallbackHelper;
    @Mock private ActionMode mActionMode;
    @Mock private Menu mMenu;
    @Mock private ShareDelegate mShareDelegate;
    @Mock private ReadAloudController mReadAloudController;

    private class TestChromeActionModeCallback
            extends ChromeActionModeHandler.ChromeActionModeCallback {
        TestChromeActionModeCallback(Tab tab, ActionModeCallbackHelper helper) {
            super(
                    tab,
                    null,
                    urlParams -> {},
                    true,
                    () -> mShareDelegate,
                    () -> mReadAloudController);
        }

        @Override
        public ActionModeCallbackHelper getActionModeCallbackHelper(WebContents webContents) {
            return mActionModeCallbackHelper;
        }
    }

    private TestChromeActionModeCallback mActionModeCallback;

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

        mActionModeCallback =
                Mockito.spy(new TestChromeActionModeCallback(mTab, mActionModeCallbackHelper));
    }

    @After
    public void tearDown() {
        FirstRunStatus.setFirstRunFlowComplete(false);
    }

    @Test
    public void testOptionsBeforeFre() {
        FirstRunStatus.setFirstRunFlowComplete(false);

        mActionModeCallback.onCreateActionMode(mActionMode, mMenu);

        Mockito.verify(mActionModeCallbackHelper)
                .setAllowedMenuItems(
                        ActionModeCallbackHelper.MENU_ITEM_PROCESS_TEXT
                                | ActionModeCallbackHelper.MENU_ITEM_SHARE);
    }

    @Test
    public void testOptionsAfterFre() {
        FirstRunStatus.setFirstRunFlowComplete(true);

        mActionModeCallback.onCreateActionMode(mActionMode, mMenu);

        Mockito.verify(mActionModeCallbackHelper)
                .setAllowedMenuItems(
                        ActionModeCallbackHelper.MENU_ITEM_PROCESS_TEXT
                                | ActionModeCallbackHelper.MENU_ITEM_SHARE
                                | ActionModeCallbackHelper.MENU_ITEM_WEB_SEARCH);
    }

    @Test
    public void testShareTriggersSearchPromo() {
        FirstRunStatus.setFirstRunFlowComplete(true);

        Mockito.when(mActionModeCallbackHelper.isActionModeValid()).thenReturn(true);
        Mockito.when(mActionModeCallbackHelper.getSelectedText()).thenReturn("OhHai");

        LocaleManagerDelegate delegate =
                Mockito.spy(
                        new LocaleManagerDelegate() {
                            @Override
                            public void showSearchEnginePromoIfNeeded(
                                    Activity activity, Callback<Boolean> onSearchEngineFinalized) {
                                onSearchEngineFinalized.onResult(true);
                            }
                        });

        LocaleManager.getInstance().setDelegateForTest(delegate);

        MenuItem shareItem = Mockito.mock(MenuItem.class);
        Mockito.when(shareItem.getItemId()).thenReturn(R.id.select_action_menu_web_search);
        mActionModeCallback.onActionItemClicked(mActionMode, shareItem);

        Mockito.verify(delegate).showSearchEnginePromoIfNeeded(Mockito.any(), Mockito.any());
    }

    @Test
    public void testSelectActionMenuTextProcessingMenus() {
        ShadowPackageManager packageManager =
                Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager());
        List<String> browserPackageNames = new ArrayList<>();
        List<String> launcherPackageNames = new ArrayList<>();
        List<String> otherPackageNames = new ArrayList<>();
        List<ResolveInfo> browsersList = new LinkedList<>();
        List<ResolveInfo> launchersList = new LinkedList<>();
        for (int i = 0; i < 5; i++) {
            browserPackageNames.add("foo " + i);
            browsersList.add(createResolveInfo(browserPackageNames.get(i)));
            launcherPackageNames.add("bar " + i);
            launchersList.add(createResolveInfo(launcherPackageNames.get(i)));
            otherPackageNames.add("baz " + i);
        }

        // Mock intent for querying web browsers.
        packageManager.addResolveInfoForIntent(PackageManagerUtils.BROWSER_INTENT, browsersList);

        // Mock intent for querying home launchers.
        packageManager.addResolveInfoForIntent(
                PackageManagerUtils.getQueryInstalledHomeLaunchersIntent(), launchersList);

        RoboMenu menu = new RoboMenu(RuntimeEnvironment.application);

        List<String> allNames = new LinkedList<>();
        allNames.addAll(browserPackageNames);
        allNames.addAll(launcherPackageNames);
        allNames.addAll(otherPackageNames);
        // Shuffle the list to get it closer to the reality.
        Collections.shuffle(allNames, new Random(42));
        for (int i = 0; i < allNames.size(); i++) {
            addMenuItem(menu, i, allNames.get(i));
        }

        mActionModeCallback.onPrepareActionMode(mActionMode, menu);

        // Verify that some menu items have been made invisible.
        for (int i = 0; i < menu.size(); i++) {
            MenuItem item = menu.getItem(i);
            if (item.getIntent() == null || item.getIntent().getComponent() == null) continue;
            String packageName = item.getIntent().getComponent().getPackageName();
            if (browserPackageNames.contains(packageName)
                    || launcherPackageNames.contains(packageName)) {
                Assert.assertFalse(
                        "Browser or home launcher application should be filtered out",
                        item.isVisible());
            } else {
                Assert.assertTrue(
                        "Actions other than browsers or home launchers should not be filtered out",
                        item.isVisible());
            }
        }
    }

    @Test
    public void testShare() {
        Mockito.when(mActionModeCallbackHelper.isActionModeValid()).thenReturn(true);
        MenuItem shareItem = Mockito.mock(MenuItem.class);
        Mockito.when(shareItem.getItemId()).thenReturn(R.id.select_action_menu_share);
        mActionModeCallback.onActionItemClicked(mActionMode, shareItem);

        Mockito.verify(mShareDelegate).share(any(), any(), eq(ShareOrigin.MOBILE_ACTION_MODE));
        Mockito.verify(mActionModeCallbackHelper, times(0)).onActionItemClicked(any(), any());
    }

    @Test
    public void testShareWithoutShareDelegate() {
        mShareDelegate = null;

        Mockito.when(mActionModeCallbackHelper.isActionModeValid()).thenReturn(true);
        MenuItem shareItem = Mockito.mock(MenuItem.class);
        Mockito.when(shareItem.getItemId()).thenReturn(R.id.select_action_menu_share);
        mActionModeCallback.onActionItemClicked(mActionMode, shareItem);

        Mockito.verify(mActionModeCallbackHelper).onActionItemClicked(any(), eq(shareItem));
    }

    @Test
    public void testMaybePauseReadAloudOnActionItemClicked() {
        Mockito.when(mActionModeCallbackHelper.isActionModeValid()).thenReturn(true);
        MenuItem item = Mockito.mock(MenuItem.class);
        Intent intent = new Intent();
        doReturn(intent).when(item).getIntent();

        mActionModeCallback.onActionItemClicked(mActionMode, item);
        verify(mReadAloudController).maybePauseForOutgoingIntent(eq(intent));
    }

    private ResolveInfo createResolveInfo(String packageName) {
        ResolveInfo resolveInfo = new ResolveInfo();
        ActivityInfo activityInfo = new ActivityInfo();
        activityInfo.packageName = packageName;
        resolveInfo.activityInfo = activityInfo;
        return resolveInfo;
    }

    private void addMenuItem(Menu menu, int order, String packageName) {
        menu.add(R.id.select_action_menu_text_processing_items, Menu.NONE, order, "title")
                .setIntent(
                        new Intent()
                                .setAction(Intent.ACTION_PROCESS_TEXT)
                                .setType("text/plain")
                                .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
                                .setClassName(packageName, "foo"));
    }
}