chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/base/BaseSuggestionViewUnitTest.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.omnibox.suggestions.base;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import android.content.Context;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.View;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.omnibox.suggestions.RecyclerViewSelectionController;
import org.chromium.chrome.browser.omnibox.test.R;

/** Tests for {@link BaseSuggestionView}. */
@RunWith(BaseRobolectricTestRunner.class)
public class BaseSuggestionViewUnitTest {
    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();

    private @Mock View.OnClickListener mOnClickListener;
    private @Mock View.OnLongClickListener mOnLongClickListener;

    private Context mContext;
    private View mInnerView;
    private BaseSuggestionView<View> mView;

    @Before
    public void setUp() {
        mContext =
                new ContextThemeWrapper(
                        ContextUtils.getApplicationContext(), R.style.Theme_BrowserUI_DayNight);
        mInnerView = new View(mContext);
        mView = spy(new BaseSuggestionView<>(mInnerView));
        mView.setOnClickListener(mOnClickListener);
        mView.setOnLongClickListener(mOnLongClickListener);
    }

    private boolean sendKey(int keyCode) {
        var event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
        return mView.onKeyDown(keyCode, event);
    }

    @Test
    public void onKeyDown_enterKeyActivatesSuggestion() {
        // View internally installs a mechanism to detect enter long-presses.
        // We currently don't pass onKeyUp event, which incorrectly triggers a long-press event.
        // This test evaluates that <Enter> key triggers the navigation event until we plumb both
        // keyDown and keyUp events.
        assertTrue(sendKey(KeyEvent.KEYCODE_ENTER));
        verify(mOnClickListener).onClick(any());
        verifyNoMoreInteractions(mOnClickListener, mOnLongClickListener);
        verify(mView, never()).super_onKeyDown(anyInt(), any());
    }

    @Test
    public void onKeyDown_actionButtonKeysAreConsumedIfActionsArePresent() {
        var controller = mock(RecyclerViewSelectionController.class);
        mView.actionChipsView.setSelectionControllerForTesting(controller);

        // Simulate Actions consuming key stroke.
        doReturn(true).when(controller).selectNextItem();
        assertTrue(sendKey(KeyEvent.KEYCODE_TAB));
        verify(mView, never()).super_onKeyDown(anyInt(), any());

        // Simulate Actions rejecting key stroke.
        doReturn(false).when(controller).selectNextItem();
        assertFalse(sendKey(KeyEvent.KEYCODE_TAB));
        verify(mView).super_onKeyDown(anyInt(), any());
    }

    @Test
    public void onKeyDown_unrecognizedKeysPassedToSuper() {
        assertFalse(sendKey(KeyEvent.KEYCODE_A));
        verifyNoMoreInteractions(mOnClickListener, mOnLongClickListener);
        verify(mView).super_onKeyDown(eq(KeyEvent.KEYCODE_A), any());
    }

    @Test
    public void setSelected_noFocusListener() {
        // No side effects if the listener is not installed.
        mView.setOnFocusViaSelectionListener(null);
        mView.setSelected(false);
        mView.setSelected(true);
        mView.setSelected(false);
    }

    @Test
    public void setSelected_withFocusListener() {
        Runnable callback = mock(Runnable.class);
        mView.setOnFocusViaSelectionListener(callback);

        mView.setSelected(false);
        verifyNoMoreInteractions(callback);

        mView.setSelected(true);
        verify(callback).run();
        clearInvocations(callback);

        mView.setSelected(false);
        verifyNoMoreInteractions(callback);
    }

    @Test
    public void setActionButtonsCount_addButtons() {
        // Verify that we don't unnecessarily create new buttons / retain already created ones.
        assertEquals(0, mView.getActionButtons().size());

        mView.setActionButtonsCount(1);
        assertEquals(1, mView.getActionButtons().size());
        var btn0 = mView.getActionButtons().get(0);
        assertNotNull(btn0);

        mView.setActionButtonsCount(2);
        assertEquals(2, mView.getActionButtons().size());
        assertEquals(btn0, mView.getActionButtons().get(0));
        var btn1 = mView.getActionButtons().get(1);
        assertNotEquals(btn0, btn1);

        // Retain buttons / no change.
        mView.setActionButtonsCount(2);
        assertEquals(2, mView.getActionButtons().size());
        assertEquals(btn0, mView.getActionButtons().get(0));
        assertEquals(btn1, mView.getActionButtons().get(1));
    }

    @Test
    public void setActionButtonsCount_removeButtons() {
        // Verify that we don't unnecessarily create new buttons / retain already created ones.
        assertEquals(0, mView.getActionButtons().size());

        mView.setActionButtonsCount(2);
        assertEquals(2, mView.getActionButtons().size());
        var btn0 = mView.getActionButtons().get(0);
        assertNotNull(btn0);

        mView.setActionButtonsCount(1);
        assertEquals(1, mView.getActionButtons().size());
        assertEquals(btn0, mView.getActionButtons().get(0));
    }

    @Test
    public void isFocused_replicatesSuperFocus() {
        mView.setSelected(false);

        doReturn(false).when(mView).super_isFocused();
        assertFalse(mView.isFocused());

        doReturn(true).when(mView).super_isFocused();
        assertTrue(mView.isFocused());
    }

    @Test
    public void isFocused_selectionHintsFocusExceptTouchMode() {
        doReturn(false).when(mView).super_isFocused();

        mView.setSelected(true);
        assertTrue(mView.isFocused());

        mView.setSelected(false);
        assertFalse(mView.isFocused());
    }
}