chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/RecyclerViewSelectionControllerUnitTest.java

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

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.view.View;

import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;

import org.junit.Assert;
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.robolectric.annotation.Config;

import org.chromium.base.test.BaseRobolectricTestRunner;

/** Tests for {@link RecyclerViewSelectionController}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class RecyclerViewSelectionControllerUnitTest {
    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();

    private @Mock RecyclerView mRecyclerView;
    private @Mock LayoutManager mLayoutManager;
    private @Mock View mChildView1;
    private @Mock View mChildView2;
    private @Mock View mChildView3;
    private @Mock View mChildView4;
    private @Mock View mChildView5;
    RecyclerViewSelectionController mSelectionController;

    @Before
    public void setUp() {
        when(mLayoutManager.getItemCount()).thenReturn(5);
        when(mLayoutManager.findViewByPosition(0)).thenReturn(mChildView1);
        when(mLayoutManager.findViewByPosition(1)).thenReturn(mChildView2);
        when(mLayoutManager.findViewByPosition(2)).thenReturn(mChildView3);
        when(mLayoutManager.findViewByPosition(3)).thenReturn(mChildView4);
        when(mLayoutManager.findViewByPosition(4)).thenReturn(mChildView5);

        doReturn(true).when(mChildView1).isFocusable();
        doReturn(true).when(mChildView2).isFocusable();
        doReturn(true).when(mChildView3).isFocusable();
        doReturn(true).when(mChildView4).isFocusable();
        doReturn(true).when(mChildView5).isFocusable();

        mSelectionController = new RecyclerViewSelectionController(mLayoutManager);
    }

    @Test
    public void selectNextItem_fromNone() {
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        mSelectionController.selectNextItem();
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());
    }

    @Test
    public void selectNextItem_fromPrevious() {
        mSelectionController.setSelectedItem(1);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView2, mSelectionController.getSelectedView());
        verify(mLayoutManager).scrollToPosition(3);
        mSelectionController.selectNextItem();
        Assert.assertEquals(2, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView3, mSelectionController.getSelectedView());
        verify(mLayoutManager).scrollToPosition(4);
    }

    @Test
    public void selectNextItem_fromLast() {
        mSelectionController.setSelectedItem(4);
        verify(mLayoutManager).scrollToPosition(4);
        Assert.assertEquals(4, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView5, mSelectionController.getSelectedView());

        // Selecting next item should result in item being highlighted.
        Assert.assertTrue(mSelectionController.selectNextItem());
        Assert.assertEquals(4, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView5, mSelectionController.getSelectedView());

        // Permit cycling through to no selection.
        mSelectionController.setCycleThroughNoSelection(true);

        // Cycling should result in no item being selected.
        Assert.assertFalse(mSelectionController.selectNextItem());
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());
    }

    @Test
    public void selectPreviousItem_fromNone() {
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        mSelectionController.selectPreviousItem();
        // Jump to the last element on the list.
        Assert.assertEquals(4, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView5, mSelectionController.getSelectedView());
        verify(mLayoutManager).scrollToPosition(4);
    }

    @Test
    public void selectPreviousItem_fromPrevious() {
        mSelectionController.setSelectedItem(1);
        verify(mLayoutManager).scrollToPosition(3);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView2, mSelectionController.getSelectedView());
        mSelectionController.selectPreviousItem();
        verify(mLayoutManager).scrollToPosition(0);
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());
    }

    @Test
    public void selectPreviousItem_fromFirst() {
        mSelectionController.setSelectedItem(0);
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());

        // Selecting previous item should result in item being highlighted.
        Assert.assertTrue(mSelectionController.selectPreviousItem());
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());

        // Permit cycling through no selection.
        mSelectionController.setCycleThroughNoSelection(true);
        // Cycling should result in no item being selected.
        Assert.assertFalse(mSelectionController.selectPreviousItem());
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());
    }

    @Test
    public void selectPreviousItem_skipNonFocusableItems_noCycling() {
        mSelectionController.setSelectedItem(2);
        Assert.assertEquals(2, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView3, mSelectionController.getSelectedView());

        // View at position 1 is not focusable:
        doReturn(false).when(mChildView2).isFocusable();

        // Focus skips position 1.
        mSelectionController.selectPreviousItem();
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());
    }

    @Test
    public void selectPreviousItem_ignoreHeadNonFocusableViews() {
        mSelectionController.setSelectedItem(2);
        Assert.assertEquals(2, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView3, mSelectionController.getSelectedView());

        // Views at position 0 and 1 are not focusable:
        doReturn(false).when(mChildView1).isFocusable();
        doReturn(false).when(mChildView2).isFocusable();

        // Focus must not move.
        mSelectionController.selectPreviousItem();
        Assert.assertEquals(2, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView3, mSelectionController.getSelectedView());
    }

    @Test
    public void selectPreviousItem_skipNonFocusableItems_withCycling() {
        mSelectionController.setCycleThroughNoSelection(true);
        mSelectionController.setSelectedItem(1);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView2, mSelectionController.getSelectedView());

        // View at position 0 is not focusable:
        doReturn(false).when(mChildView1).isFocusable();

        // We wrap around ignoring view at position 2.
        mSelectionController.selectPreviousItem();
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());
    }

    @Test
    public void selectPreviousItem_noFocusableViews() {
        doReturn(false).when(mChildView1).isFocusable();
        doReturn(false).when(mChildView2).isFocusable();
        doReturn(false).when(mChildView3).isFocusable();
        doReturn(false).when(mChildView4).isFocusable();
        doReturn(false).when(mChildView5).isFocusable();

        mSelectionController.selectPreviousItem();
        verify(mLayoutManager).scrollToPosition(0);
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());

        // Permit cycling through.
        mSelectionController.setCycleThroughNoSelection(true);
        mSelectionController.selectPreviousItem();
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());
    }

    @Test
    public void selectNextItem_skipNonFocusableItems_noCycling() {
        mSelectionController.setSelectedItem(0);
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());

        // View at position 1 is not focusable:
        doReturn(false).when(mChildView2).isFocusable();

        // Focus skips position 1.
        mSelectionController.selectNextItem();
        Assert.assertEquals(2, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView3, mSelectionController.getSelectedView());
    }

    @Test
    public void selectNextItem_ignoreTailNonFocusableViews() {
        mSelectionController.setSelectedItem(0);
        verify(mLayoutManager).scrollToPosition(2);
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());

        // Views at positions 1-4 are not focusable:
        doReturn(false).when(mChildView2).isFocusable();
        doReturn(false).when(mChildView3).isFocusable();
        doReturn(false).when(mChildView4).isFocusable();
        doReturn(false).when(mChildView5).isFocusable();

        // Focus must not move.
        mSelectionController.selectNextItem();
        Assert.assertEquals(0, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView1, mSelectionController.getSelectedView());
    }

    @Test
    public void selectNextItem_skipNonFocusableItems_withCycling() {
        mSelectionController.setCycleThroughNoSelection(true);
        mSelectionController.setSelectedItem(1);
        verify(mLayoutManager).scrollToPosition(3);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(mChildView2, mSelectionController.getSelectedView());

        // Views at positions 2-4 are not focusable:
        doReturn(false).when(mChildView3).isFocusable();
        doReturn(false).when(mChildView4).isFocusable();
        doReturn(false).when(mChildView5).isFocusable();

        // We wrap around ignoring view at position 2.
        mSelectionController.selectNextItem();
        verify(mLayoutManager).scrollToPosition(0);
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());
    }

    @Test
    public void selectNextItem_noFocusableViews() {
        doReturn(false).when(mChildView1).isFocusable();
        doReturn(false).when(mChildView2).isFocusable();
        doReturn(false).when(mChildView3).isFocusable();
        doReturn(false).when(mChildView4).isFocusable();
        doReturn(false).when(mChildView5).isFocusable();

        mSelectionController.selectNextItem();
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());

        // Permit cycling through.
        mSelectionController.setCycleThroughNoSelection(true);
        mSelectionController.selectNextItem();
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        Assert.assertEquals(null, mSelectionController.getSelectedView());
    }

    @Test
    public void setSelectedItem_moveSelectionFromNone() {
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        mSelectionController.setSelectedItem(1);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());

        verify(mChildView2, times(1)).setSelected(true);
        verify(mChildView2, times(1)).setSelected(anyBoolean());
        verifyNoMoreInteractions(mChildView1, mChildView2, mChildView3);

        // Reset selection back to none.

        mSelectionController.resetSelection();
        verify(mChildView2, times(1)).setSelected(false);
        verify(mChildView2, times(2)).setSelected(anyBoolean());
        verifyNoMoreInteractions(mChildView1, mChildView2, mChildView3);

        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
    }

    @Test
    public void setSelectedItem_moveSelectionFromAnotherItem() {
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        mSelectionController.setSelectedItem(1);
        reset(mChildView2);

        mSelectionController.setSelectedItem(2);
        Assert.assertEquals(2, mSelectionController.getSelectedItemForTest());

        verify(mChildView1, times(0)).setSelected(anyBoolean());
        verify(mChildView2, times(0)).setSelected(true);
        verify(mChildView2, times(1)).setSelected(false);
        verify(mChildView3, times(1)).setSelected(true);
        verify(mChildView3, times(0)).setSelected(false);
    }

    @Test
    public void setSelectedItem_moveSelectionToNone() {
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());
        mSelectionController.setSelectedItem(1);
        reset(mChildView2);

        mSelectionController.setSelectedItem(RecyclerView.NO_POSITION);
        Assert.assertEquals(
                RecyclerView.NO_POSITION, mSelectionController.getSelectedItemForTest());

        verify(mChildView1, times(0)).setSelected(anyBoolean());
        verify(mChildView3, times(0)).setSelected(anyBoolean());
        verify(mChildView2, times(0)).setSelected(true);
        verify(mChildView2, times(1)).setSelected(false);
    }

    @Test
    public void setSelectedItem_indexNegative() {
        mSelectionController.setSelectedItem(1);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
        // This call should be rejected, leaving selected item as it was.
        // Note that (-1) is reserved value: RecyclerView.NO_POSITION.
        mSelectionController.setSelectedItem(-2);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
    }

    @Test
    public void setSelectedItem_indexTooLarge() {
        mSelectionController.setSelectedItem(1);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
        // This call should be rejected, leaving selected item as it was.
        mSelectionController.setSelectedItem(30);
        Assert.assertEquals(1, mSelectionController.getSelectedItemForTest());
    }

    @Test
    public void onChildViewAttached_viewIsReused() {
        // Simulates the case where View at position 0 is used as a View at position 3.
        when(mLayoutManager.getItemCount()).thenReturn(4);

        // Select View at position 0.
        mSelectionController.setSelectedItem(0);
        verify(mChildView1, times(1)).setSelected(true);
        verifyNoMoreInteractions(mChildView1);
        verifyNoMoreInteractions(mChildView2);
        verifyNoMoreInteractions(mChildView3);
        reset(mChildView1);

        // Pretend that the view is out of screen.
        // This should not result in view selection being cleared.
        when(mLayoutManager.findViewByPosition(0)).thenReturn(null);
        mSelectionController.onChildViewDetachedFromWindow(mChildView1);
        verify(mChildView1, times(1)).setSelected(false);
        verifyNoMoreInteractions(mChildView1);
        verifyNoMoreInteractions(mChildView2);
        verifyNoMoreInteractions(mChildView3);
        reset(mChildView1);

        // Pretend that the View 0 is now re-used as View 3.
        // We should see that the Selected state is cleared.
        when(mLayoutManager.findViewByPosition(3)).thenReturn(mChildView1);
        mSelectionController.onChildViewAttachedToWindow(mChildView1);
        verifyNoMoreInteractions(mChildView1);
        verifyNoMoreInteractions(mChildView2);
        verifyNoMoreInteractions(mChildView3);

        // Finally, pretend that the view 0 is back on screen.
        // This happens in 2 steps:
        // - 1. the view is removed from last position
        when(mLayoutManager.findViewByPosition(3)).thenReturn(null);
        mSelectionController.onChildViewDetachedFromWindow(mChildView1);
        // - 2. the view is inserted at first position.
        when(mLayoutManager.findViewByPosition(0)).thenReturn(mChildView1);
        mSelectionController.onChildViewAttachedToWindow(mChildView1);
        // This will result in the setSelected(false) being called 2 times:
        // - once, when we signal the view is detached, and
        // - once, when we force re-set the selected view state.
        verify(mChildView1, times(2)).setSelected(false);
        verify(mChildView1, times(1)).setSelected(true);
        verifyNoMoreInteractions(mChildView1);
        verifyNoMoreInteractions(mChildView2);
        verifyNoMoreInteractions(mChildView3);
    }
}