chromium/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/player/expanded/VoiceMenuUnitTest.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.readaloud.player.expanded;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.view.View;
import android.widget.RadioButton;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

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

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.readaloud.player.InteractionHandler;
import org.chromium.chrome.browser.readaloud.player.PlayerProperties;
import org.chromium.chrome.browser.readaloud.player.R;
import org.chromium.chrome.modules.readaloud.PlaybackArgs.PlaybackVoice;
import org.chromium.chrome.modules.readaloud.PlaybackListener;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.List;

/** Unit tests for {@link VoiceMenuSheetContent}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class VoiceMenuUnitTest {
    @Mock private InteractionHandler mInteractionHandler;
    @Mock private Runnable mHideSelfRunnable;

    @Captor private ArgumentCaptor<PlaybackVoice> mVoiceCaptor;

    private Activity mActivity;
    private PropertyModel mModel;
    private VoiceMenu mContent;
    private Menu mMenu;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mActivity = Robolectric.buildActivity(AppCompatActivity.class).setup().get();
        // Need to set theme before inflating layout.
        mActivity.setTheme(R.style.Theme_BrowserUI_DayNight);
        mModel =
                new PropertyModel.Builder(PlayerProperties.ALL_KEYS)
                        .with(PlayerProperties.INTERACTION_HANDLER, mInteractionHandler)
                        .with(
                                PlayerProperties.VOICES_LIST,
                                List.of(
                                        createVoice("a", "voice a"),
                                        createVoice("b", "voice b"),
                                        createVoice("c", "voice c")))
                        .with(PlayerProperties.SELECTED_VOICE_ID, "a")
                        .build();
        mContent = new VoiceMenu(mActivity, mModel, mHideSelfRunnable, mActivity.getLayoutInflater());
        mMenu = mContent.getMenu();
    }

    @Test
    public void testTitle() {
        assertEquals("Voice", getText(mMenu, R.id.readaloud_menu_title));
    }

    @Test
    public void testSetVoices() {
        // First check the voices from setUp().
        MenuItem item0 = mMenu.getItem(0);
        assertEquals("voice a", getText(item0, R.id.item_label));
        assertNotNull(item0.findViewById(R.id.readaloud_radio_button));

        MenuItem item1 = mMenu.getItem(1);
        assertEquals("voice b", getText(item1, R.id.item_label));
        assertNotNull(item1.findViewById(R.id.readaloud_radio_button));

        MenuItem item2 = mMenu.getItem(2);
        assertEquals("voice c", getText(item2, R.id.item_label));
        assertNotNull(item2.findViewById(R.id.readaloud_radio_button));
        assertNull(mMenu.getItem(3));

        mContent.setVoices(
                List.of(
                        createVoice(
                                "a", "voice d", PlaybackVoice.Pitch.LOW, PlaybackVoice.Tone.SMOOTH),
                        createVoice(
                                "e", "voice e", PlaybackVoice.Pitch.MID, PlaybackVoice.Tone.CALM)));

        assertEquals("voice d", getText(mMenu.getItem(0), R.id.item_label));
        assertEquals("Low-pitch, Smooth", getText(mMenu.getItem(0), R.id.item_sublabel));
        assertEquals("voice e", getText(mMenu.getItem(1), R.id.item_label));
        assertEquals("Mid-pitch, Calm", getText(mMenu.getItem(1), R.id.item_sublabel));
        assertNull(mMenu.getItem(2));
    }

    @Test
    public void testSetVoiceSelection() {
        // Initial setting from setUp()
        assertTrue(getRadioButton(mMenu.getItem(0)).isChecked());

        mContent.setVoiceSelection("b");
        assertFalse(getRadioButton(mMenu.getItem(0)).isChecked());
        assertTrue(getRadioButton(mMenu.getItem(1)).isChecked());
        assertFalse(getRadioButton(mMenu.getItem(2)).isChecked());

        mContent.setVoiceSelection("c");
        assertFalse(getRadioButton(mMenu.getItem(0)).isChecked());
        assertFalse(getRadioButton(mMenu.getItem(1)).isChecked());
        assertTrue(getRadioButton(mMenu.getItem(2)).isChecked());
    }

    @Test
    public void testUserSelectsVoice() {
        mMenu.getItem(1).getChildAt(0).performClick();
        verify(mInteractionHandler, times(1)).onVoiceSelected(mVoiceCaptor.capture());

        PlaybackVoice voice = mVoiceCaptor.getValue();
        assertNotNull(voice);
        assertEquals("en", voice.getLanguage());
        assertEquals("b", voice.getVoiceId());
        assertEquals("voice b", voice.getDisplayName());
    }

    @Test
    public void testMenuItemInitialState() {
        assertEquals(View.GONE, mMenu.getItem(0).findViewById(R.id.spinner).getVisibility());
        assertEquals(View.VISIBLE, mMenu.getItem(0).findViewById(R.id.play_button).getVisibility());
    }

    @Test
    public void testShowPreviewSpinner() {
        mContent.updatePreviewButtons("a", PlaybackListener.State.BUFFERING);
        assertEquals(View.VISIBLE, mMenu.getItem(0).findViewById(R.id.spinner).getVisibility());
        assertEquals(View.GONE, mMenu.getItem(0).findViewById(R.id.play_button).getVisibility());
    }

    @Test
    public void testPlayPreview() {
        mContent.updatePreviewButtons("a", PlaybackListener.State.PLAYING);
        assertEquals(View.GONE, mMenu.getItem(0).findViewById(R.id.spinner).getVisibility());
        assertEquals(View.VISIBLE, mMenu.getItem(0).findViewById(R.id.play_button).getVisibility());
    }

    @Test
    public void testStopPreview() {
        mContent.updatePreviewButtons("a", PlaybackListener.State.STOPPED);
        assertEquals(View.GONE, mMenu.getItem(0).findViewById(R.id.spinner).getVisibility());
        assertEquals(View.VISIBLE, mMenu.getItem(0).findViewById(R.id.play_button).getVisibility());
    }

    @Test
    public void testClickPreviewButton() {
        mMenu.getItem(0).findViewById(R.id.play_button).performClick();
        verify(mInteractionHandler)
                .onPreviewVoiceClick(eq(mModel.get(PlayerProperties.VOICES_LIST).get(0)));
    }

    private static CharSequence getText(View ancestor, int viewId) {
        return ((TextView) ancestor.findViewById(viewId)).getText();
    }

    private static RadioButton getRadioButton(MenuItem item) {
        return (RadioButton) item.findViewById(R.id.readaloud_radio_button);
    }


    @Test
    public void testEmptyVoiceList() {
        mModel =
                new PropertyModel.Builder(PlayerProperties.ALL_KEYS)
                        .with(PlayerProperties.INTERACTION_HANDLER, mInteractionHandler)
                        .with(PlayerProperties.VOICES_LIST, List.of())
                        .with(PlayerProperties.SELECTED_VOICE_ID, "a")
                        .build();
        mContent = new VoiceMenu(mActivity, mModel, mHideSelfRunnable, mActivity.getLayoutInflater());
        mMenu = mContent.getMenu();

        assertNull(mMenu.getItem(0));
        // test there's no crash
        mContent.setVoiceSelection("fr");

        // now set voices and check that they're updated
        mContent.setVoices(
                List.of(
                        createVoice(
                                "d", "voice d", PlaybackVoice.Pitch.LOW, PlaybackVoice.Tone.SMOOTH),
                        createVoice(
                                "e", "voice e", PlaybackVoice.Pitch.MID, PlaybackVoice.Tone.CALM)));

        assertEquals("voice d", getText(mMenu.getItem(0), R.id.item_label));
        assertEquals("Low-pitch, Smooth", getText(mMenu.getItem(0), R.id.item_sublabel));
        assertEquals("voice e", getText(mMenu.getItem(1), R.id.item_label));
        assertEquals("Mid-pitch, Calm", getText(mMenu.getItem(1), R.id.item_sublabel));
    }

    @Test
    public void testLocaleLabel() {
        mContent.setVoices(
                List.of(
                        createVoice(
                                "a", "voice d", PlaybackVoice.Pitch.LOW, PlaybackVoice.Tone.SMOOTH),
                        createVoice(
                                "e", "voice e", PlaybackVoice.Pitch.MID, PlaybackVoice.Tone.CALM),
                        new PlaybackVoice(
                                "en",
                                "IN",
                                "in",
                                "voice in",
                                PlaybackVoice.Pitch.MID,
                                PlaybackVoice.Tone.CALM)));

        assertEquals("English (United States)", getText(mMenu.getItem(0), R.id.item_header));
        assertEquals("", getText(mMenu.getItem(1), R.id.item_header));
        assertEquals("English (India)", getText(mMenu.getItem(2), R.id.item_header));
    }

    private static PlaybackVoice createVoice(String id, String displayName) {
        return createVoice(id, displayName, PlaybackVoice.Pitch.NONE, PlaybackVoice.Tone.NONE);
    }

    private static PlaybackVoice createVoice(
            String id,
            String displayName,
            @PlaybackVoice.Pitch int pitch,
            @PlaybackVoice.Tone int tone) {
        return new PlaybackVoice("en", "US", id, displayName, pitch, tone);
    }
}