chromium/chrome/android/junit/src/org/chromium/chrome/browser/dragdrop/toolbar/ToolbarDragDropCoordinatorUnitTest.java

// Copyright 2023 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.dragdrop.toolbar;

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.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.dragdrop.toolbar.ToolbarDragDropCoordinator.isValidMimeType;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipData.Item;
import android.content.Intent;
import android.net.Uri;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;

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.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.dragdrop.toolbar.ToolbarDragDropCoordinator.DropType;
import org.chromium.chrome.browser.omnibox.OmniboxFocusReason;
import org.chromium.chrome.browser.omnibox.OmniboxStub;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteDelegate;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.dragdrop.DropDataAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.JUnitTestGURLs;

/** Basic test for creating, using drag and drop to omnibox with {@link ToolbarDragDropCoordinator}. */
@RunWith(BaseRobolectricTestRunner.class)
public class ToolbarDragDropCoordinatorUnitTest {
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock private AutocompleteDelegate mAutocompleteDelegate;
    @Mock private OmniboxStub mOmniboxStub;
    @Mock private TemplateUrlService mTemplateUrlService;

    @Captor private ArgumentCaptor<OmniboxLoadUrlParams> mOmniboxLoadUrlParamsCaptor;

    private Activity mActivity;

    private ToolbarDragDropCoordinator mToolbarDragDropCoordinator;
    private TargetViewDragListener mTargetViewDragListener;
    private FrameLayout mTargetView;
    private ClipData mValidData =
            new ClipData(null, new String[] {"text/plain"}, new Item("Mock Text"));
    private ClipData mInvalidData =
            new ClipData(null, new String[] {"image/jpeg", "text/html"}, new Item("Mock Text"));
    private String[] mMockPostData = {"Mock Post Content Type", "Mock Post Data"};

    @Before
    public void setup() {
        mActivity = Robolectric.buildActivity(Activity.class).setup().get();
        mTargetView =
                (FrameLayout)
                        LayoutInflater.from(mActivity)
                                .inflate(R.layout.drag_drop_target_view, null);
        mAutocompleteDelegate = Mockito.mock(AutocompleteDelegate.class);
        mOmniboxStub = Mockito.mock(OmniboxStub.class);
        mTemplateUrlService = Mockito.mock(TemplateUrlService.class);
        when(mTemplateUrlService.isSearchByImageAvailable()).thenReturn(true);
        when(mTemplateUrlService.getImageUrlAndPostContent()).thenReturn(mMockPostData);
        mToolbarDragDropCoordinator =
                new ToolbarDragDropCoordinator(
                        mTargetView,
                        mAutocompleteDelegate,
                        mOmniboxStub,
                        () -> mTemplateUrlService);
        PropertyModel model =
                new PropertyModel.Builder(TargetViewProperties.ALL_KEYS)
                        .with(TargetViewProperties.TARGET_VIEW_VISIBLE, View.GONE)
                        .build();
        // mTargetViewDragListener is stateless in order to test TargetViewDragListener
        mTargetViewDragListener =
                new TargetViewDragListener(model, mToolbarDragDropCoordinator::parseDragEvent);
    }

    @After
    public void tearDown() {
        mToolbarDragDropCoordinator.destroy();
        mTargetViewDragListener = null;
        assertEquals(
                "target view should be gone on destroy", mTargetView.getVisibility(), View.GONE);
        mActivity.finish();
    }

    @Test
    public void testOnDragStart_MimeTypeNotSupported() {
        DragEvent event = mockDragEvent(DragEvent.ACTION_DRAG_STARTED, mInvalidData);
        boolean result = mToolbarDragDropCoordinator.onDrag(mTargetView, event);
        Assert.assertFalse("Drag event should not be consumed.", result);
        assertEquals("MimeType should not be valid", isValidMimeType(event), false);
        assertEquals(
                "The target view should not be visible.", mTargetView.getVisibility(), View.GONE);
    }

    @Test
    public void testOnDragStart_MimeTypeSupported() {
        DragEvent event = mockDragEvent(DragEvent.ACTION_DRAG_STARTED, mValidData);
        boolean result = mToolbarDragDropCoordinator.onDrag(mTargetView, event);
        assertTrue("Drag event should be consumed.", result);
        assertEquals("MimeType should be valid", isValidMimeType(event), true);
        assertEquals(
                "The target view should be visible.", mTargetView.getVisibility(), View.VISIBLE);
    }

    @Test
    public void testOnDragEnd_ViewGone() {
        DragEvent event = mockDragEvent(DragEvent.ACTION_DRAG_ENDED, mValidData);
        boolean result = mToolbarDragDropCoordinator.onDrag(mTargetView, event);
        assertFalse("Drag event should not be consumed.", result);
        assertEquals(
                "The target view should not be visible.", mTargetView.getVisibility(), View.GONE);
    }

    @Test
    public void testToolbarDragEvents_EventsNotConsumed() {
        DragEvent eventDragEntered = mockDragEvent(DragEvent.ACTION_DRAG_ENTERED, mValidData);
        boolean resultDragEntered =
                mToolbarDragDropCoordinator.onDrag(mTargetView, eventDragEntered);
        assertFalse("Drag event should not be consumed by toolbar.", resultDragEntered);

        DragEvent eventDragExited = mockDragEvent(DragEvent.ACTION_DRAG_EXITED, mValidData);
        boolean resultDragExited = mToolbarDragDropCoordinator.onDrag(mTargetView, eventDragExited);
        assertFalse("Drag event should not be consumed by toolbar.", resultDragExited);

        DragEvent eventDragLocation = mockDragEvent(DragEvent.ACTION_DRAG_LOCATION, mValidData);
        boolean resultDragLocation =
                mToolbarDragDropCoordinator.onDrag(mTargetView, eventDragLocation);
        assertFalse("Drag event should not be consumed by toolbar.", resultDragLocation);

        DragEvent eventDrop = mockDragEvent(DragEvent.ACTION_DROP, mValidData);
        boolean resultDrop = mToolbarDragDropCoordinator.onDrag(mTargetView, eventDrop);
        assertFalse("Drag event should not be consumed by toolbar.", resultDrop);
    }

    @Test
    public void testTargetViewDragListener_DragEntered() {
        DragEvent event = mockDragEvent(DragEvent.ACTION_DRAG_ENTERED, mValidData);
        boolean result = mTargetViewDragListener.onDrag(mTargetView, event);
        assertFalse("Drag event should not be consumed by target view.", result);
    }

    @Test
    public void testTargetViewDragListener_DragExited() {
        DragEvent event = mockDragEvent(DragEvent.ACTION_DRAG_EXITED, mValidData);
        boolean result = mTargetViewDragListener.onDrag(mTargetView, event);
        assertFalse("Drag event should not be consumed by target view.", result);
    }

    @Test
    public void testOnDrop_ChromeText() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DragDrop.ToOmnibox.DropType", DropType.CHROME_TEXT);
        ClipData chromeText =
                new ClipData(
                        null, new String[] {"text/plain", "chrome/text"}, new Item("Mock Text"));
        DragEvent eventDragTextFromChrome = mockDragEvent(DragEvent.ACTION_DROP, chromeText);
        boolean resultDragTextFromChrome =
                mTargetViewDragListener.onDrag(mTargetView, eventDragTextFromChrome);
        assertTrue(
                "Drag eventTextFromChrome should be consumed by target view",
                resultDragTextFromChrome);
        verify(mOmniboxStub)
                .setUrlBarFocus(true, "Mock Text", OmniboxFocusReason.DRAG_DROP_TO_OMNIBOX);
        histogramExpectation.assertExpected();
    }

    @Test
    public void testOnDrop_NonChromeText() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DragDrop.ToOmnibox.DropType", DropType.TEXT);
        ClipData nonChromeText =
                new ClipData(null, new String[] {"text/plain"}, new Item("Mock Text 2"));
        DragEvent eventDragText = mockDragEvent(DragEvent.ACTION_DROP, nonChromeText);
        boolean resultDragText = mTargetViewDragListener.onDrag(mTargetView, eventDragText);
        assertTrue("Drag eventTextFromChrome should be consumed by target view", resultDragText);
        verify(mOmniboxStub)
                .setUrlBarFocus(true, "Mock Text 2", OmniboxFocusReason.DRAG_DROP_TO_OMNIBOX);
        histogramExpectation.assertExpected();
    }

    @Test
    public void testOnDrop_MultipleMimeTypes() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DragDrop.ToOmnibox.DropType", DropType.TEXT);
        ClipData multipleMimeTypes =
                new ClipData(
                        null,
                        new String[] {"audio/ac3", "text/plain", "audio/alac"},
                        new Item("Mock Text 3"));
        DragEvent eventDragMultipleMimeTypes =
                mockDragEvent(DragEvent.ACTION_DROP, multipleMimeTypes);
        boolean resultDragMultipleMimeTypes =
                mTargetViewDragListener.onDrag(mTargetView, eventDragMultipleMimeTypes);
        assertTrue(
                "Drag eventDragMultipleMimeTypes should be consumed by target view",
                resultDragMultipleMimeTypes);
        verify(mOmniboxStub)
                .setUrlBarFocus(true, "Mock Text 3", OmniboxFocusReason.DRAG_DROP_TO_OMNIBOX);
        histogramExpectation.assertExpected();
    }

    @Test
    public void testTargetViewDragEvents_ChromeLink() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DragDrop.ToOmnibox.DropType", DropType.CHROME_LINK);
        Intent intent = new Intent().setData(Uri.parse(JUnitTestGURLs.EXAMPLE_URL.getSpec()));
        ClipData chromeLink =
                new ClipData(null, new String[] {"text/plain", "chrome/link"}, new Item(intent));
        DragEvent eventDragChromeLink = mockDragEvent(DragEvent.ACTION_DROP, chromeLink);
        boolean resultDragMultipleMimeTypes =
                mTargetViewDragListener.onDrag(mTargetView, eventDragChromeLink);
        assertTrue(
                "DragEvent eventDragMultipleMimeTypes should be consumed by target view",
                resultDragMultipleMimeTypes);

        verify(mAutocompleteDelegate).loadUrl(mOmniboxLoadUrlParamsCaptor.capture());
        assertEquals(
                mOmniboxLoadUrlParamsCaptor.getValue().url, JUnitTestGURLs.EXAMPLE_URL.getSpec());
        assertEquals(mOmniboxLoadUrlParamsCaptor.getValue().transitionType, PageTransition.TYPED);
        assertFalse(mOmniboxLoadUrlParamsCaptor.getValue().openInNewTab);

        histogramExpectation.assertExpected();
    }

    @Test
    public void testTargetViewDragEvents_ValidImage() {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.DragDrop.ToOmnibox.DropType", DropType.CHROME_IMAGE);
        Intent intent = new Intent().setData(Uri.parse(JUnitTestGURLs.EXAMPLE_URL.getSpec()));
        ClipData imageMimeType =
                new ClipData(null, new String[] {"image/jpeg", "text/plain"}, new Item(intent));
        DragEvent eventDragImage = mockDragEvent(DragEvent.ACTION_DROP, imageMimeType);
        DropDataAndroid dropDataAndroid = mock(DropDataAndroid.class);
        doReturn(dropDataAndroid).when(eventDragImage).getLocalState();
        boolean resultDragImage = mTargetViewDragListener.onDrag(mTargetView, eventDragImage);
        assertTrue("DragEvent eventDragImage should be consumed by target view", resultDragImage);

        verify(mAutocompleteDelegate).loadUrl(mOmniboxLoadUrlParamsCaptor.capture());
        assertEquals(mOmniboxLoadUrlParamsCaptor.getValue().url, mMockPostData[0]);
        assertEquals(mOmniboxLoadUrlParamsCaptor.getValue().postDataType, mMockPostData[1]);
        assertNotNull(mOmniboxLoadUrlParamsCaptor.getValue().postData);
        histogramExpectation.assertExpected();
    }

    @Test
    public void testTargetViewDragEvents_EventsNotConsumed() {
        DragEvent eventDragEnded = mockDragEvent(DragEvent.ACTION_DRAG_ENDED, mValidData);
        boolean resultDragEnded = mTargetViewDragListener.onDrag(mTargetView, eventDragEnded);
        assertFalse("Drag event should not be consumed by target view.", resultDragEnded);

        DragEvent eventDragLocation = mockDragEvent(DragEvent.ACTION_DRAG_LOCATION, mValidData);
        boolean resultDragLocation = mTargetViewDragListener.onDrag(mTargetView, eventDragLocation);
        assertFalse("Drag event should not be consumed by target view.", resultDragLocation);
    }

    private DragEvent mockDragEvent(int action, ClipData data) {
        DragEvent event = Mockito.mock(DragEvent.class);
        doReturn(action).when(event).getAction();
        doReturn(data).when(event).getClipData();
        doReturn(data.getDescription()).when(event).getClipDescription();
        return event;
    }
}