// 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;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static org.hamcrest.Matchers.hasItems;
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.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
import android.content.Intent;
import android.view.WindowManager;
import androidx.appcompat.app.AppCompatActivity;
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.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationState;
import org.chromium.base.Promise;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.task.test.ShadowPostTask;
import org.chromium.base.task.test.ShadowPostTask.TestImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.UserActionTester;
import org.chromium.chrome.browser.browser_controls.BottomControlsStacker;
import org.chromium.chrome.browser.device.DeviceConditions;
import org.chromium.chrome.browser.device.ShadowDeviceConditions;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.language.AppLocaleUtils;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.readaloud.ReadAloudMetrics.IneligibilityReason;
import org.chromium.chrome.browser.readaloud.exceptions.ReadAloudUnsupportedException;
import org.chromium.chrome.browser.search_engines.SearchEngineType;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab.TabTestUtils;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.translate.FakeTranslateBridgeJni;
import org.chromium.chrome.browser.translate.TranslateBridgeJni;
import org.chromium.chrome.modules.readaloud.Playback;
import org.chromium.chrome.modules.readaloud.Playback.PlaybackTextPart;
import org.chromium.chrome.modules.readaloud.Playback.PlaybackTextType;
import org.chromium.chrome.modules.readaloud.PlaybackArgs;
import org.chromium.chrome.modules.readaloud.PlaybackArgs.PlaybackVoice;
import org.chromium.chrome.modules.readaloud.PlaybackListener;
import org.chromium.chrome.modules.readaloud.PlaybackListener.PlaybackData;
import org.chromium.chrome.modules.readaloud.Player;
import org.chromium.chrome.modules.readaloud.ReadAloudPlaybackHooks;
import org.chromium.chrome.modules.readaloud.contentjs.Extractor;
import org.chromium.chrome.modules.readaloud.contentjs.Highlighter;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModelSelector;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.components.user_prefs.UserPrefsJni;
import org.chromium.content_public.browser.GlobalRenderFrameHostId;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.SelectionClient;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.net.ConnectionType;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/** Unit tests for {@link ReadAloudController}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
manifest = Config.NONE,
shadows = {ShadowDeviceConditions.class, ShadowPostTask.class})
@EnableFeatures({ChromeFeatureList.READALOUD, ChromeFeatureList.READALOUD_PLAYBACK})
@DisableFeatures({
ChromeFeatureList.READALOUD_IN_MULTI_WINDOW,
ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK,
ChromeFeatureList.READALOUD_TAP_TO_SEEK
})
public class ReadAloudControllerUnitTest {
private static final GURL sTestGURL = JUnitTestGURLs.EXAMPLE_URL;
private static final GURL sTestRedirectGURL = JUnitTestGURLs.URL_1_WITH_PATH;
private static final long KNOWN_READABLE_TRIAL_PTR = 12345678L;
private MockTab mTab;
private ReadAloudController mController;
private ReadAloudController mController2;
private Activity mActivity;
@Rule public JniMocker mJniMocker = new JniMocker();
private FakeTranslateBridgeJni mFakeTranslateBridge;
private ObservableSupplierImpl<Profile> mProfileSupplier;
private ObservableSupplierImpl<LayoutManager> mLayoutManagerSupplier;
@Mock private Profile mMockProfile;
@Mock private Profile mMockIncognitoProfile;
@Mock private ReadAloudReadabilityHooksImpl mHooksImpl;
@Mock private ReadAloudPlaybackHooks mPlaybackHooks;
@Mock private Player mPlayerCoordinator;
@Mock private BottomSheetController mBottomSheetController;
@Mock private Extractor mExtractor;
@Mock private Highlighter mHighlighter;
@Mock private PlaybackListener.PhraseTiming mPhraseTiming;
@Mock private BottomControlsStacker mBottomControlsStacker;
@Mock private LayoutManager mLayoutManager;
@Mock private ReadAloudPrefs.Natives mReadAloudPrefsNatives;
@Mock private ReadAloudFeatures.Natives mReadAloudFeaturesNatives;
@Mock private UserPrefsJni mUserPrefsNatives;
@Mock private PrefService mPrefService;
@Mock private TemplateUrlService mTemplateUrlService;
@Mock private ActivityWindowAndroid mActivityWindowAndroid;
@Mock private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
MockTabModelSelector mTabModelSelector;
@Captor ArgumentCaptor<ReadAloudReadabilityHooks.ReadabilityCallback> mCallbackCaptor;
@Captor ArgumentCaptor<ReadAloudPlaybackHooks.CreatePlaybackCallback> mPlaybackCallbackCaptor;
@Captor ArgumentCaptor<PlaybackArgs> mPlaybackArgsCaptor;
@Captor ArgumentCaptor<PlaybackListener> mPlaybackListenerCaptor;
@Mock private Playback mPlayback;
@Mock private Playback.Metadata mMetadata;
@Mock private WebContents mWebContents;
@Mock private RenderFrameHost mRenderFrameHost;
@Mock private TemplateUrl mSearchEngine;
@Mock private SelectionClient mSelectionClient;
@Mock private SelectionPopupController mSelectionPopupController;
private GlobalRenderFrameHostId mGlobalRenderFrameHostId = new GlobalRenderFrameHostId(1, 1);
public UserActionTester mUserActionTester;
private HistogramWatcher mHighlightingEnabledOnStartupHistogram;
private Promise<Long> mExtractorPromise;
private FakeClock mClock;
/** FakeClock for setting the time. */
static class FakeClock implements ReadAloudController.Clock {
private long mCurrentTimeMillis;
FakeClock() {
mCurrentTimeMillis = 0;
}
@Override
public long currentTimeMillis() {
return mCurrentTimeMillis;
}
void advanceCurrentTimeMillis(long millis) {
mCurrentTimeMillis += millis;
}
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
ShadowPostTask.setTestImpl(
new TestImpl() {
@Override
public void postDelayedTask(
@TaskTraits int taskTraits, Runnable task, long delay) {
task.run();
}
});
mProfileSupplier = new ObservableSupplierImpl<>();
mProfileSupplier.set(mMockProfile);
doReturn(true).when(mMockProfile).isNativeInitialized();
mLayoutManagerSupplier = new ObservableSupplierImpl<>();
mLayoutManagerSupplier.set(mLayoutManager);
mActivity = Robolectric.buildActivity(AppCompatActivity.class).setup().get();
mActivity.setTheme(R.style.Theme_BrowserUI_DayNight);
when(mMockProfile.isOffTheRecord()).thenReturn(false);
when(mMockIncognitoProfile.isOffTheRecord()).thenReturn(true);
UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(true);
PriceTrackingFeatures.setPriceTrackingEnabledForTesting(false);
mFakeTranslateBridge = new FakeTranslateBridgeJni();
mJniMocker.mock(TranslateBridgeJni.TEST_HOOKS, mFakeTranslateBridge);
mJniMocker.mock(ReadAloudPrefsJni.TEST_HOOKS, mReadAloudPrefsNatives);
mJniMocker.mock(ReadAloudFeaturesJni.TEST_HOOKS, mReadAloudFeaturesNatives);
mJniMocker.mock(UserPrefsJni.TEST_HOOKS, mUserPrefsNatives);
doReturn(mPrefService).when(mUserPrefsNatives).get(any());
when(mPrefService.getBoolean(Pref.LISTEN_TO_THIS_PAGE_ENABLED)).thenReturn(true);
mTabModelSelector =
new MockTabModelSelector(
mMockProfile,
mMockIncognitoProfile,
/* tabCount= */ 2,
/* incognitoTabCount= */ 1,
(id, incognito) -> {
Profile profile = incognito ? mMockIncognitoProfile : mMockProfile;
MockTab tab = spy(MockTab.createAndInitialize(id, profile));
return tab;
});
when(mHooksImpl.isEnabled()).thenReturn(true);
when(mHooksImpl.getCompatibleLanguages())
.thenReturn(new HashSet<String>(Arrays.asList("en", "es", "fr", "ja")));
initPlaybackHooks();
ReadAloudController.setReadabilityHooks(mHooksImpl);
ReadAloudController.setPlaybackHooks(mPlaybackHooks);
TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlService);
doReturn(SearchEngineType.SEARCH_ENGINE_GOOGLE)
.when(mTemplateUrlService)
.getSearchEngineTypeFromTemplateUrl(anyString());
doReturn("Google").when(mSearchEngine).getKeyword();
doReturn(mSearchEngine).when(mTemplateUrlService).getDefaultSearchEngineTemplateUrl();
doReturn(KNOWN_READABLE_TRIAL_PTR)
.when(mReadAloudFeaturesNatives)
.initSyntheticTrial(eq(ChromeFeatureList.READALOUD), eq("_KnownReadable"));
mHighlightingEnabledOnStartupHistogram =
HistogramWatcher.newSingleRecordWatcher(
"ReadAloud.HighlightingEnabled.OnStartup", true);
mClock = new FakeClock();
ReadAloudController.setClockForTesting(mClock);
doReturn(false).when(mWebContents).isDestroyed();
mTab = mTabModelSelector.getCurrentTab();
mTab.setGurlOverrideForTesting(sTestGURL);
mTab.setWebContentsOverrideForTesting(mWebContents);
TapToSeekSelectionManager.setSmartSelectionClient(mSelectionClient);
TapToSeekSelectionManager.setSelectionPopupController(mSelectionPopupController);
mController = createController();
when(mMetadata.languageCode()).thenReturn("en");
when(mPlayback.getMetadata()).thenReturn(mMetadata);
when(mWebContents.getMainFrame()).thenReturn(mRenderFrameHost);
when(mRenderFrameHost.getGlobalRenderFrameHostId()).thenReturn(mGlobalRenderFrameHostId);
mController.setHighlighterForTests(mHighlighter);
mUserActionTester = new UserActionTester();
mExtractorPromise = new Promise<Long>();
when(mExtractor.getDateModified(any())).thenReturn(mExtractorPromise);
mExtractorPromise.fulfill(1234567123456L);
}
void initPlaybackHooks() {
when(mPlaybackHooks.createPlayer(any())).thenReturn(mPlayerCoordinator);
when(mPlaybackHooks.createExtractor()).thenReturn(mExtractor);
doReturn(false).when(mPlaybackHooks).voicesInitialized();
doReturn(List.of(new PlaybackVoice("en", "voiceA", "")))
.when(mPlaybackHooks)
.getVoicesFor(anyString());
}
private void resetPlaybackMocks() {
reset(mPlayback);
when(mPlayback.getMetadata()).thenReturn(mMetadata);
reset(mPlaybackHooks);
reset(mPlayerCoordinator);
initPlaybackHooks();
}
private ReadAloudController createController() {
var controller =
new ReadAloudController(
mActivity,
mProfileSupplier,
mTabModelSelector.getModel(false),
mTabModelSelector.getModel(true),
mBottomSheetController,
mBottomControlsStacker,
mLayoutManagerSupplier,
mActivityWindowAndroid,
mActivityLifecycleDispatcher);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
return controller;
}
@After
public void tearDown() {
mUserActionTester.tearDown();
ReadAloudFeatures.shutdown();
mController.destroy();
if (mController2 != null) mController2.destroy();
ReadAloudController.resetReadabilityCacheForTesting();
}
@Test
public void testIsAvailable() {
// test set up: non incognito profile + MSBB Accepted + policy pref returns true
assertTrue(mController.isAvailable());
// test returns false when policy pref is false
when(mPrefService.getBoolean("readaloud.listen_to_this_page_enabled")).thenReturn(false);
assertFalse(mController.isAvailable());
}
@Test
public void testIsAvailable_offTheRecord() {
when(mMockProfile.isOffTheRecord()).thenReturn(true);
assertFalse(mController.isAvailable());
}
@Test
public void testIsAvailable_noMSBB() {
UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(false);
assertFalse(mController.isAvailable());
}
@Test
public void testIsAvailable_inMultiWindow() {
shadowOf(mActivity).setInMultiWindowMode(true);
assertFalse(mController.isAvailable());
shadowOf(mActivity).setInMultiWindowMode(false);
assertTrue(mController.isAvailable());
}
@Test
@EnableFeatures({ChromeFeatureList.READALOUD_IN_MULTI_WINDOW})
public void testIsAvailable_inMultiWindow_flag() {
shadowOf(mActivity).setInMultiWindowMode(true);
assertTrue(mController.isAvailable());
shadowOf(mActivity).setInMultiWindowMode(false);
assertTrue(mController.isAvailable());
}
@Test
public void testOnLoadStarted_differentDocument() {
// start a successful playback
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
resolvePromises();
// Load new url
when(mTab.getUrl()).thenReturn(new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc."));
mController.getTabModelTabObserverforTests().onLoadStarted(mTab, true);
verify(mHighlighter).handleTabReloaded(eq(mTab));
verify(mPlayerCoordinator).dismissPlayers();
}
@Test
public void testOnLoadStarted_sameDocument() {
// start a successful playback
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
resolvePromises();
// Load the same document
mController.getTabModelTabObserverforTests().onLoadStarted(mTab, false);
// nothing should happen
verify(mHighlighter, never()).handleTabReloaded(eq(mTab));
verify(mPlayerCoordinator, never()).dismissPlayers();
}
@Test
public void testReloadingPage() {
// Reload tab before any playback starts - tests null checks
mController.getTabModelTabObserverforTests().onPageLoadStarted(mTab, mTab.getUrl());
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
// now start playing a tab
requestAndStartPlayback();
// reload some other tab, playback should keep going
MockTab newTab = mTabModelSelector.addMockTab();
newTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc."));
mController.getTabModelTabObserverforTests().onUrlUpdated(newTab);
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
// now reload the playing tab, playback should still keep going
mController.getTabModelTabObserverforTests().onUrlUpdated(mTab);
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
}
@Test
public void testOnActivityAttachmentChanged() {
// change tab attachment before any playback starts - tests null checks
mController
.getTabModelTabObserverforTests()
.onActivityAttachmentChanged(mTab, /* window= */ null);
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
// now start playing a tab
requestAndStartPlayback();
// change attachement of some other tab, playback should keep going
MockTab newTab = mTabModelSelector.addMockTab();
newTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc."));
mController
.getTabModelTabObserverforTests()
.onActivityAttachmentChanged(newTab, /* window= */ null);
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
// now detach the playing tab
mController
.getTabModelTabObserverforTests()
.onActivityAttachmentChanged(mTab, /* window= */ null);
verify(mPlayerCoordinator).dismissPlayers();
verify(mPlayback).release();
}
@Test
public void testOnActivityAttachmentChanged_saveAndRestoreState() {
// start playing a tab
requestAndStartPlayback();
// now detach the playing tab
mController
.getTabModelTabObserverforTests()
.onActivityAttachmentChanged(mTab, /* window= */ null);
verify(mPlayerCoordinator).dismissPlayers();
verify(mPlayback).release();
// Load a different tab. Playback shouldn't be restored
// Load the previously playing tab. Saved playback state should be restored.
Tab tab = mTabModelSelector.addMockTab();
TabModelUtils.selectTabById(mTabModelSelector, tab.getId(), TabSelectionType.FROM_NEW);
verify(mPlaybackHooks, times(1)).createPlayback(any(), mPlaybackCallbackCaptor.capture());
// Load the previously playing tab. Saved playback state should be restored.
TabModelUtils.selectTabById(mTabModelSelector, mTab.getId(), TabSelectionType.FROM_NEW);
verify(mPlaybackHooks, times(2)).createPlayback(any(), mPlaybackCallbackCaptor.capture());
// Loading the same tab should not re-trigger playback
TabModelUtils.selectTabById(mTabModelSelector, mTab.getId(), TabSelectionType.FROM_NEW);
verify(mPlaybackHooks, times(2)).createPlayback(any(), mPlaybackCallbackCaptor.capture());
}
@Test
public void testIsRestoringPlayer() {
assertFalse(mController.isRestoringPlayer());
// Start playing a tab, detach, restore
requestAndStartPlayback();
mController
.getTabModelTabObserverforTests()
.onActivityAttachmentChanged(mTab, /* window= */ null);
verify(mPlayerCoordinator).dismissPlayers();
verify(mPlayback).release();
TabModelUtils.selectTabById(mTabModelSelector, mTab.getId(), TabSelectionType.FROM_NEW);
verify(mPlaybackHooks, times(2)).createPlayback(any(), mPlaybackCallbackCaptor.capture());
// Player is now being restored
assertTrue(mController.isRestoringPlayer());
// Mini player finishes showing, done restoring player
mController.onMiniPlayerShown();
assertFalse(mController.isRestoringPlayer());
}
@Test
public void testClosingTab() {
// Close a tab before any playback starts - tests null checks
mController.getTabModelTabObserverforTests().willCloseTab(mTab);
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
// now start playing a tab
requestAndStartPlayback();
// close some other tab, playback should keep going
MockTab newTab = mTabModelSelector.addMockTab();
newTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc."));
mController.getTabModelTabObserverforTests().willCloseTab(newTab);
verify(mPlayerCoordinator, never()).dismissPlayers();
verify(mPlayback, never()).release();
// now close the playing tab
mController.getTabModelTabObserverforTests().willCloseTab(mTab);
verify(mPlayerCoordinator).dismissPlayers();
verify(mPlayback).release();
}
@Test
public void testClosingTab_errorUiDismissed() {
// start a playback with an error
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mPlaybackCallbackCaptor.getValue().onFailure(new Exception("Very bad error"));
resolvePromises();
// Close this tab
mController.getTabModelTabObserverforTests().willCloseTab(mTab);
// No playback but error UI should get dismissed
verify(mPlayerCoordinator).dismissPlayers();
}
// Helper function for checkReadabilityOnPageLoad_URLnotReadAloudSupported() to check
// the provided url is recognized as unreadable
private void checkURLNotReadAloudSupported(GURL url) {
mTab.setGurlOverrideForTesting(url);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, never())
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void checkReadabilityOnPageLoad_URLnotReadAloudSupported() {
reset(mHooksImpl);
checkURLNotReadAloudSupported(new GURL("invalid"));
checkURLNotReadAloudSupported(GURL.emptyGURL());
checkURLNotReadAloudSupported(new GURL("chrome://history/"));
checkURLNotReadAloudSupported(new GURL("about:blank"));
checkURLNotReadAloudSupported(new GURL("https://www.google.com/search?q=weather"));
checkURLNotReadAloudSupported(new GURL("https://myaccount.google.com/"));
checkURLNotReadAloudSupported(new GURL("https://myactivity.google.com/"));
}
@Test
public void checkReadability_TabError() {
TabTestUtils.setIsShowingErrorPage(mTab, true);
assertFalse(mController.isReadable(mTab));
verify(mHooksImpl, never())
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void checkReadability_success() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertTrue(mController.isReadable(mTab));
assertFalse(mController.timepointsSupported(mTab));
// now check that the second time the same url loads we don't resend a request
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void checkReadability_noMSBB() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(false);
assertFalse(mController.isReadable(mTab));
}
@Test
public void checkReadability_onlyOnePendingRequest() {
mController.maybeCheckReadability(mTab);
mController.maybeCheckReadability(mTab);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1)).isPageReadable(Mockito.anyString(), mCallbackCaptor.capture());
}
@Test
public void checkReadability_notReadable_resultExpired() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), false, false);
assertFalse(mController.isReadable(mTab));
// check 1hr1s later for the same url, we should return false and request readability again
mClock.advanceCurrentTimeMillis(1 * 60 * 60 * 1000 + 1000);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(2))
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void checkReadability_readable_resultExpired() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertTrue(mController.isReadable(mTab));
// check 1hr1s later for the same url, we should remove the record, return false and request
// readability again
mClock.advanceCurrentTimeMillis(1 * 60 * 60 * 1000 + 1000);
assertFalse(mController.isReadable(mTab));
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(2))
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void checkReadability_failure() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
mCallbackCaptor
.getValue()
.onFailure(sTestGURL.getSpec(), new Throwable("Something went wrong"));
assertFalse(mController.isReadable(mTab));
assertFalse(mController.timepointsSupported(mTab));
// now check that the second time the same url loads we will resend a request
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(2))
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void checkReadability_emptyURL() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
boolean failed = false;
try {
mCallbackCaptor.getValue().onSuccess("", true, true);
} catch (AssertionError e) {
failed = true;
}
assertTrue(failed);
}
@Test
public void checkReadability_offline() {
DeviceConditions.sForceConnectionTypeForTesting = true;
assertFalse(mController.isReadable(mTab));
}
@Test
public void testNetworkConnectionTypeChangedNotifiesReadabilityChanged() {
Runnable runnable = Mockito.mock(Runnable.class);
mController.addReadabilityUpdateListener(runnable);
mController.onConnectionTypeChanged(0);
verify(runnable, times(1)).run();
}
@Test
public void isReadable_cacheSharedBetweenInstances() {
// Check readability
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
// The page is readable, result should be cached.
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertTrue(mController.isReadable(mTab));
assertFalse(mController.timepointsSupported(mTab));
// A second newly created controller should know that the page is readable.
mController2 = createController();
assertTrue(mController2.isReadable(mTab));
assertFalse(mController2.timepointsSupported(mTab));
// The second controller should not send requests to check the same URL's readability.
mController2.maybeCheckReadability(mTab);
// Still only one call.
verify(mHooksImpl, times(1))
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void isReadable_languageSupported() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertTrue(mController.isReadable(mTab));
// check that URL is supported when the language is set to a supported language
mFakeTranslateBridge.setCurrentLanguage("en");
assertTrue(mController.isReadable(mTab));
}
@Test
public void isReadable_resultExpired() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl).isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertTrue(mController.isReadable(mTab));
// advance by 1hr
mClock.advanceCurrentTimeMillis(1 * 60 * 60 * 1000);
assertTrue(mController.isReadable(mTab));
// advance by 1s - we're past the 1h limit, the record should be deleted
mClock.advanceCurrentTimeMillis(1000);
assertFalse(mController.isReadable(mTab));
// make sure readability isn't called again
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
}
@Test
public void isReadable_languageUnsupported() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertTrue(mController.isReadable(mTab));
// check that URL isn't supported when the language is set to an unsupported language
mFakeTranslateBridge.setCurrentLanguage("he");
assertFalse(mController.isReadable(mTab));
}
@Test
public void testIsReadable_errorCases() {
assertFalse(mController.isReadable(null));
when(mTab.getUrl()).thenReturn(null);
assertFalse(mController.isReadable(mTab));
when(mTab.getUrl()).thenReturn(sTestGURL);
when(mTab.getWebContents()).thenReturn(mWebContents);
doReturn(false).when(mMockProfile).isNativeInitialized();
assertFalse(mController.isReadable(mTab));
}
@Test
public void testReactingtoMSBBChange() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
// Disable MSBB. Sending requests to Google servers no longer allowed but using
// previous results is ok.
UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(false);
mTab.setGurlOverrideForTesting(JUnitTestGURLs.GOOGLE_URL_CAT);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(
Mockito.anyString(),
Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class));
}
@Test
public void testPlayTab() {
requestAndStartPlayback();
verify(mPlayerCoordinator).addObserver(mController);
// test that previous playback is released when another playback is called
MockTab newTab = mTabModelSelector.addMockTab();
newTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc."));
newTab.setWebContentsOverrideForTesting(mWebContents);
mController.playTab(newTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlayback, times(1)).release();
}
@Test
public void testPlayTab_inMultiWindow() {
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
assertEquals(null, mPlaybackArgsCaptor.getValue().getLanguage());
shadowOf(mActivity).setInMultiWindowMode(true);
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator).playbackFailed();
}
@Test
@EnableFeatures({ChromeFeatureList.READALOUD_IN_MULTI_WINDOW})
public void testPlayTab_inMultiWindow_flag() {
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
assertEquals(null, mPlaybackArgsCaptor.getValue().getLanguage());
shadowOf(mActivity).setInMultiWindowMode(true);
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator, times(1))
.playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING));
verify(mPlayerCoordinator).addObserver(mController);
}
@Test
public void testKeepScreenOnFlag() {
// default - don't keep the screen on
int flags = mActivity.getWindow().getAttributes().flags;
assertTrue((flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0);
// play tab
requestAndStartPlayback();
verify(mPlayback).addListener(mPlaybackListenerCaptor.capture());
// update playback data so it isn't null
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
// keep the screen on while something is playing
flags = mActivity.getWindow().getAttributes().flags;
assertTrue((flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0);
doReturn(PlaybackListener.State.BUFFERING).when(data).state();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
// don't keep the screen on if paused/stopped/buffering
flags = mActivity.getWindow().getAttributes().flags;
assertTrue((flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
// playing again - keep the screen on
flags = mActivity.getWindow().getAttributes().flags;
assertTrue((flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0);
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
// playback stopped, clear the flag, don't keep the screen on
flags = mActivity.getWindow().getAttributes().flags;
assertTrue((flags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0);
}
@Test
public void testPlayTab_sendsVoiceList() {
mFakeTranslateBridge.setCurrentLanguage("en");
doReturn(
List.of(
new PlaybackVoice("en", "voiceA"),
new PlaybackVoice("es", "voiceB"),
new PlaybackVoice("fr", "voiceC")))
.when(mPlaybackHooks)
.getPlaybackVoiceList(any());
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1)).initVoices();
verify(mPlaybackHooks, times(1)).createPlayback(mPlaybackArgsCaptor.capture(), any());
List<PlaybackVoice> voices = mPlaybackArgsCaptor.getValue().getVoices();
assertNotNull(voices);
assertEquals(3, voices.size());
assertEquals("en", voices.get(0).getLanguage());
assertEquals("voiceA", voices.get(0).getVoiceId());
assertEquals("es", voices.get(1).getLanguage());
assertEquals("voiceB", voices.get(1).getVoiceId());
assertEquals("fr", voices.get(2).getLanguage());
assertEquals("voiceC", voices.get(2).getVoiceId());
}
@Test
public void testPlayTab_EmptyUrl() {
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL(""));
boolean failed = false;
try {
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
} catch (AssertionError e) {
failed = true;
}
assertTrue(failed);
}
@Test
public void testPlayTranslatedTab_tabLanguageEmpty() {
AppLocaleUtils.setAppLanguagePref("fr-FR");
mFakeTranslateBridge.setIsPageTranslated(true);
mFakeTranslateBridge.setCurrentLanguage("");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(mPlaybackArgsCaptor.capture(), any());
assertEquals("fr", mPlaybackArgsCaptor.getValue().getLanguage());
}
@Test
public void testPlayTranslatedTab_unsupportedLanguage() {
doReturn(List.of()).when(mPlaybackHooks).getVoicesFor(anyString());
mFakeTranslateBridge.setCurrentLanguage("pl-PL");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, never()).createPlayback(mPlaybackArgsCaptor.capture(), any());
verify(mPlayerCoordinator).playbackFailed();
}
@Test
public void testPlayTranslatedTab_tabLanguageUnd() {
AppLocaleUtils.setAppLanguagePref("fr-FR");
mFakeTranslateBridge.setIsPageTranslated(true);
mFakeTranslateBridge.setCurrentLanguage("und");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(mPlaybackArgsCaptor.capture(), any());
assertEquals("fr", mPlaybackArgsCaptor.getValue().getLanguage());
}
@Test
public void testPlayUntranslatedTab() {
AppLocaleUtils.setAppLanguagePref("fr-FR");
mFakeTranslateBridge.setIsPageTranslated(false);
mFakeTranslateBridge.setCurrentLanguage("fr");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(mPlaybackArgsCaptor.capture(), any());
assertEquals(null, mPlaybackArgsCaptor.getValue().getLanguage());
}
@Test
public void testVoicesMatchLanguage_pageTranslated() {
// translated page should use chrome language
var voiceEn = new PlaybackVoice("en", "asdf", "");
var voiceFr = new PlaybackVoice("fr", "asdf", "");
when(mMetadata.languageCode()).thenReturn("en");
doReturn(List.of(voiceEn)).when(mPlaybackHooks).getVoicesFor(eq("en"));
doReturn(List.of(voiceFr)).when(mPlaybackHooks).getVoicesFor(eq("fr"));
doReturn(List.of(voiceEn, voiceFr)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mFakeTranslateBridge.setIsPageTranslated(true);
mFakeTranslateBridge.setCurrentLanguage("fr");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
assertEquals("fr", mPlaybackArgsCaptor.getValue().getLanguage());
onPlaybackSuccess(mPlayback);
// Page is in French, voice options should have voices for "fr"
assertEquals(
"fr", mController.getCurrentLanguageVoicesSupplier().get().get(0).getLanguage());
}
@Test
public void testVoicesMatchLanguage_pageNotTranslated() {
// non translated page should use server detected content language
var voiceEn = new PlaybackVoice("en", "asdf", "");
var voiceFr = new PlaybackVoice("fr", "asdf", "");
doReturn(List.of(voiceEn)).when(mPlaybackHooks).getVoicesFor(eq("en"));
doReturn(List.of(voiceFr)).when(mPlaybackHooks).getVoicesFor(eq("fr"));
doReturn(List.of(voiceEn, voiceFr)).when(mPlaybackHooks).getPlaybackVoiceList(any());
when(mMetadata.languageCode()).thenReturn("en");
mFakeTranslateBridge.setIsPageTranslated(false);
mFakeTranslateBridge.setCurrentLanguage("fr");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
assertEquals(null, mPlaybackArgsCaptor.getValue().getLanguage());
onPlaybackSuccess(mPlayback);
assertEquals(
"en", mController.getCurrentLanguageVoicesSupplier().get().get(0).getLanguage());
}
@Test
public void testFailureIfServerLanguageUnsupported() {
// non translated page should use server detected content language
var voiceEn = new PlaybackVoice("en", "asdf", "");
var voiceFr = new PlaybackVoice("fr", "asdf", "");
doReturn(List.of(voiceEn)).when(mPlaybackHooks).getVoicesFor(eq("en"));
doReturn(List.of(voiceFr)).when(mPlaybackHooks).getVoicesFor(eq("fr"));
doReturn(List.of(voiceEn, voiceFr)).when(mPlaybackHooks).getPlaybackVoiceList(any());
// unsupported
when(mMetadata.languageCode()).thenReturn("pl");
mFakeTranslateBridge.setIsPageTranslated(false);
mFakeTranslateBridge.setCurrentLanguage("fr");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
assertEquals(null, mPlaybackArgsCaptor.getValue().getLanguage());
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator).playbackFailed();
}
@Test
public void testPlayTab_onFailure() {
mFakeTranslateBridge.setCurrentLanguage("en");
GURL gurl = new GURL("https://en.wikipedia.org/wiki/Google");
mTab.setGurlOverrideForTesting(gurl);
mController.maybeCheckReadability(mTab);
// also check that a generic error doesn't invalidate readability result
verify(mHooksImpl).isPageReadable(eq(gurl.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor
.getValue()
.onSuccess(
gurl.getSpec(), /* isReadable= */ true, /* timepointsSupported= */ false);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
assertTrue(mController.isReadable(mTab));
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mPlaybackCallbackCaptor.getValue().onFailure(new Throwable());
resolvePromises();
verify(mPlayerCoordinator, times(1)).playbackFailed();
assertTrue(mController.isReadable(mTab));
}
@Test
public void testPlayTab_onFailure_unsupportedLink() {
mFakeTranslateBridge.setCurrentLanguage("en");
GURL gurl = new GURL("https://en.wikipedia.org/wiki/Google");
mTab.setGurlOverrideForTesting(gurl);
mController.maybeCheckReadability(mTab);
// also check that a readAloudUnsupported error does invalidate a false positive readability
// result
verify(mHooksImpl).isPageReadable(eq(gurl.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor
.getValue()
.onSuccess(
gurl.getSpec(), /* isReadable= */ true, /* timepointsSupported= */ false);
assertTrue(mController.isReadable(mTab));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mPlaybackCallbackCaptor
.getValue()
.onFailure(
new ReadAloudUnsupportedException(
"message",
/* throwable= */ null,
ReadAloudUnsupportedException.RejectionReason
.UNKNOWN_REJECTION_REASON));
resolvePromises();
verify(mPlayerCoordinator, times(1)).playbackFailed();
assertFalse(mController.isReadable(mTab));
}
@Test
public void testStopPlayback() {
// Play tab
requestAndStartPlayback();
// Stop playback
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
verify(mPlayback).release();
resetPlaybackMocks();
// Subsequent playTab() should play without trying to release anything.
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(any(), any());
verify(mPlayback, never()).release();
}
@Test
public void testStopPlaybackWhenBackPressingToNewTab() {
// Play tab
requestAndStartPlayback();
// now simulate back press to a new tab page (doesn't trigger new url load)
when(mTab.getUrl()).thenReturn(new GURL("chrome-native://newtab/"));
mController.getTabModelTabObserverforTests().onUrlUpdated(mTab);
verify(mPlayback).release();
}
@Test
public void testDontStopPlayback() {
// Play tab
requestAndStartPlayback();
// now simulate a situation updateUrl was called with the same url as the one playing -
// nothing should happen
mController.getTabModelTabObserverforTests().onUrlUpdated(mTab);
verify(mPlayback, never()).release();
// now update url from a different, non playing tab. The active playback should be
// unaffected.
MockTab tab = mTabModelSelector.addMockTab();
tab.setWebContentsOverrideForTesting(mWebContents);
tab.setUrl(new GURL(""));
mController.getTabModelTabObserverforTests().onUrlUpdated(tab);
verify(mPlayback, never()).release();
}
@Test
public void highlightsRequested() {
// set up the highlighter
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mHighlighter).initializeJs(eq(mTab), eq(mMetadata), any(Highlighter.Config.class));
// Checks that the pref is read to set up highlighter state
// hasPrefPath is called twice, once during ReadAloudPrefs.isHighlightingEnabled and during
// ReadAloudPrefs.setHighlightingEnabled
verify(mPrefService, times(2)).hasPrefPath(eq(ReadAloudPrefs.HIGHLIGHTING_ENABLED_PATH));
// trigger highlights
mController.onPhraseChanged(mPhraseTiming);
verify(mHighlighter)
.highlightText(eq(mGlobalRenderFrameHostId), eq(mTab), eq(mPhraseTiming));
// now disable highlighting - we should not trigger highlights anymore
mController.getHighlightingEnabledSupplier().set(false);
// Pref is updated.
verify(mPrefService).setBoolean(eq(ReadAloudPrefs.HIGHLIGHTING_ENABLED_PATH), eq(false));
mController.onPhraseChanged(mPhraseTiming);
verify(mHighlighter, times(1))
.highlightText(eq(mGlobalRenderFrameHostId), eq(mTab), eq(mPhraseTiming));
}
@Test
public void reloadingTab_highlightsNotCleared() {
// set up the highlighter
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mHighlighter).initializeJs(eq(mTab), eq(mMetadata), any(Highlighter.Config.class));
// Reload tab to a different url.
mController
.getTabModelTabObserverforTests()
.onPageLoadStarted(mTab, new GURL("http://wikipedia.org"));
verify(mHighlighter, never()).handleTabReloaded(any());
}
@Test
public void stoppingPlaybackClearsHighlighter() {
// set up the highlighter
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mHighlighter).initializeJs(eq(mTab), eq(mMetadata), any(Highlighter.Config.class));
// stopping playback should clear highlighting.
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
verify(mHighlighter).clearHighlights(eq(mGlobalRenderFrameHostId), eq(mTab));
}
@Test
public void testUserDataStrippedFromReadabilityCheck() {
GURL tabUrl = new GURL("http://user:[email protected]");
mTab.setGurlOverrideForTesting(tabUrl);
mController.maybeCheckReadability(mTab);
String sanitized = "http://example.com/";
verify(mHooksImpl, times(1)).isPageReadable(eq(sanitized), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
mCallbackCaptor.getValue().onSuccess(sanitized, true, true);
assertTrue(mController.isReadable(mTab));
assertTrue(mController.timepointsSupported(mTab));
}
@Test
public void testSetHighlighterMode() {
// highlighter can be null if page doesn't support highlighting,
// this just test null checkss
mController.setHighlighterMode(2);
verify(mHighlighter, never()).handleTabReloaded(mTab);
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
mController.setHighlighterMode(2);
verify(mHighlighter, times(1)).handleTabReloaded(mTab);
// only do something if new mode is different
mController.setHighlighterMode(2);
verify(mHighlighter, times(1)).handleTabReloaded(mTab);
mController.setHighlighterMode(1);
verify(mHighlighter, times(2)).handleTabReloaded(mTab);
}
@Test
public void testSetVoiceAndRestartPlayback() {
// Voices setup
var oldVoice = new PlaybackVoice("lang", "OLD VOICE ID");
doReturn(List.of(oldVoice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
// First play tab.
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// Verify the original voice list.
verify(mPlaybackHooks, times(1))
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
List<PlaybackVoice> gotVoices = mPlaybackArgsCaptor.getValue().getVoices();
assertEquals(1, gotVoices.size());
assertEquals("OLD VOICE ID", gotVoices.get(0).getVoiceId());
onPlaybackSuccess(mPlayback);
reset(mPlaybackHooks);
// Set the new voice.
var newVoice = new PlaybackVoice("lang", "NEW VOICE ID");
doReturn(List.of(newVoice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
doReturn(List.of(newVoice)).when(mPlaybackHooks).getVoicesFor(anyString());
var data = Mockito.mock(PlaybackData.class);
doReturn(99).when(data).paragraphIndex();
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mController.onPlaybackDataChanged(data);
mController.setVoiceOverrideAndApplyToPlayback(newVoice);
// Pref is updated.
verify(mReadAloudPrefsNatives).setVoice(eq(mPrefService), eq("lang"), eq("NEW VOICE ID"));
// Playback is stopped.
verify(mPlayback).release();
doReturn(List.of(newVoice)).when(mPlaybackHooks).getVoicesFor(anyString());
// Playback starts again with new voice and original paragraph index.
verify(mPlaybackHooks, times(1))
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
gotVoices = mPlaybackArgsCaptor.getValue().getVoices();
assertEquals(1, gotVoices.size());
assertEquals("NEW VOICE ID", gotVoices.get(0).getVoiceId());
onPlaybackSuccess(mPlayback);
verify(mPlayback, times(2)).play();
verify(mPlayback).seekToParagraph(eq(99), eq(0L));
}
@Test
public void testSetVoiceWhilePaused() {
// Play tab.
requestAndStartPlayback();
verify(mPlayback).addListener(mPlaybackListenerCaptor.capture());
resetPlaybackMocks();
// Pause at paragraph 99.
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PAUSED).when(data).state();
doReturn(99).when(data).paragraphIndex();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
// Change voice setting.
var newVoice = new PlaybackVoice("lang", "NEW VOICE ID", "description");
doReturn(List.of(newVoice)).when(mPlaybackHooks).getVoicesFor(anyString());
doReturn(List.of(newVoice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mController.setVoiceOverrideAndApplyToPlayback(newVoice);
// Tab audio should be loaded with the new voice but it should not be playing.
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
var voices = mPlaybackArgsCaptor.getValue().getVoices();
assertEquals(1, voices.size());
assertEquals("NEW VOICE ID", voices.get(0).getVoiceId());
onPlaybackSuccess(mPlayback);
verify(mPlayback, never()).play();
verify(mPlayback).seekToParagraph(eq(99), eq(0L));
}
@Test
public void testPreviewVoice_whilePlaying_success() {
// Play tab.
requestAndStartPlayback();
reset(mPlaybackHooks);
// Preview a voice.
var voice = new PlaybackVoice("en", "asdf", "");
doReturn(List.of(voice)).when(mPlaybackHooks).getVoicesFor(anyString());
doReturn(List.of(voice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mController.previewVoice(voice);
// Tab playback should stop.
verify(mPlayback).release();
reset(mPlayerCoordinator);
reset(mPlayback);
// Preview playback requested.
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
reset(mPlaybackHooks);
// Check preview playback args.
PlaybackArgs args = mPlaybackArgsCaptor.getValue();
assertNotNull(args);
assertEquals("en", args.getLanguage());
assertNotNull(args.getVoices());
assertEquals(1, args.getVoices().size());
assertEquals("en", args.getVoices().get(0).getLanguage());
assertEquals("asdf", args.getVoices().get(0).getVoiceId());
// Preview playback succeeds.
Playback previewPlayback = Mockito.mock(Playback.class);
onPlaybackSuccess(previewPlayback);
verify(previewPlayback).play();
verify(previewPlayback).addListener(mPlaybackListenerCaptor.capture());
assertNotNull(mPlaybackListenerCaptor.getValue());
// Preview finishes playing.
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.STOPPED).when(data).state();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
verify(previewPlayback).release();
}
@Test
public void testPreviewVoice_whilePlaying_failure() {
// Play tab.
requestAndStartPlayback();
reset(mPlaybackHooks);
// Preview a voice.
var voice = new PlaybackVoice("en", "asdf", "");
doReturn(List.of(voice)).when(mPlaybackHooks).getVoicesFor(anyString());
doReturn(List.of(voice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mController.previewVoice(voice);
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
reset(mPlaybackHooks);
// Preview fails. Nothing to verify here yet.
mPlaybackCallbackCaptor.getValue().onFailure(new Throwable());
resolvePromises();
}
@Test
public void testPreviewVoice_previewDuringPreview() {
// Play tab.
requestAndStartPlayback();
reset(mPlaybackHooks);
// Preview a voice.
var voice = new PlaybackVoice("en", "asdf", "");
doReturn(List.of(voice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
doReturn(List.of(voice)).when(mPlaybackHooks).getVoicesFor(anyString());
mController.previewVoice(voice);
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
Playback previewPlayback = Mockito.mock(Playback.class);
onPlaybackSuccess(previewPlayback);
reset(mPlaybackHooks);
// Start another preview.
doReturn(List.of(voice)).when(mPlaybackHooks).getVoicesFor(anyString());
mController.previewVoice(new PlaybackVoice("en", "abcd", ""));
// Preview playback should be stopped and cleaned up.
verify(previewPlayback).release();
reset(previewPlayback);
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
reset(mPlaybackHooks);
onPlaybackSuccess(previewPlayback);
verify(previewPlayback).addListener(mPlaybackListenerCaptor.capture());
// Preview finishes playing.
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.STOPPED).when(data).state();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
verify(previewPlayback).release();
}
@Test
public void testPreviewVoice_closeVoiceMenu() {
// Set up playback and restorable state.
requestAndStartPlayback();
verify(mPlayback).play();
resetPlaybackMocks();
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.STOPPED).when(data).state();
doReturn(99).when(data).paragraphIndex();
mController.onPlaybackDataChanged(data);
// Preview a voice.
var voice = new PlaybackVoice("en", "asdf", "");
doReturn(List.of(voice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mController.previewVoice(voice);
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
Playback previewPlayback = Mockito.mock(Playback.class);
onPlaybackSuccess(previewPlayback);
resetPlaybackMocks();
// Closing the voice menu should stop the preview.
mController.onVoiceMenuClosed();
verify(previewPlayback).release();
// Tab audio should be loaded and played. Position should be restored.
verify(mPlaybackHooks)
.createPlayback(mPlaybackArgsCaptor.capture(), mPlaybackCallbackCaptor.capture());
assertEquals(1234567123456L, mPlaybackArgsCaptor.getValue().getDateModifiedMsSinceEpoch());
onPlaybackSuccess(mPlayback);
// Don't play, because original state was STOPPED.
verify(mPlayback, never()).play();
verify(mPlayback).seekToParagraph(eq(99), eq(0L));
}
@Test
public void testRestorePlaybackState_whileLoading() {
// Request playback but don't succeed yet.
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
resetPlaybackMocks();
// User changes voices before the first playback is ready.
mController.setVoiceOverrideAndApplyToPlayback(new PlaybackVoice("en", "1234", ""));
// TODO(b/315028038): If changing voice during loading is possible, then we
// should instead cancel the first request and request again.
verify(mPlaybackHooks, never()).createPlayback(any(), any());
}
@Test
public void testRestorePlaybackState_previewThenChangeVoice() {
// When previewing a voice, tab playback should only be restored when closing
// the menu. This test makes sure it doesn't start up early when a voice is
// selected.
// Set up playback and restorable state.
requestAndStartPlayback();
verify(mPlayback).play();
resetPlaybackMocks();
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
doReturn(99).when(data).paragraphIndex();
mController.onPlaybackDataChanged(data);
// Preview voice.
var voice = new PlaybackVoice("en", "asdf", "");
doReturn(List.of(voice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mController.previewVoice(voice);
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
resetPlaybackMocks();
Playback previewPlayback = Mockito.mock(Playback.class);
onPlaybackSuccess(previewPlayback);
// Select a voice. Tab shouldn't start playing.
mController.setVoiceOverrideAndApplyToPlayback(new PlaybackVoice("en", "1234", ""));
verify(mPlaybackHooks, never()).createPlayback(any(), any());
// Close the menu. Now the tab should resume playback.
mController.onVoiceMenuClosed();
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayback).play();
verify(mPlayback).seekToParagraph(eq(99), eq(0L));
}
@Test
public void testTranslationListenerRegistration() {
// Play tab.
requestAndStartPlayback();
// One observer should be registered on the playing tab to stop playback if translated, and
// one is registered regardless of playback for refreshing the entrypoint.
assertEquals(2, mFakeTranslateBridge.getObserverCount());
// stopping playback should unregister the listener that stops playback
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
assertEquals(1, mFakeTranslateBridge.getObserverCount());
}
@Test
public void testTranslationListenerRegistration_nullWebContents() {
assertEquals(1, mFakeTranslateBridge.getObserverCount());
// Play tab.
when(mTab.getWebContents()).thenReturn(null);
requestAndStartPlayback();
assertEquals(1, mFakeTranslateBridge.getObserverCount());
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
assertEquals(1, mFakeTranslateBridge.getObserverCount());
}
@Test
public void testTranslationListenersUnregisteredOnTabDestroyed() {
// Play tab.
requestAndStartPlayback();
// One observer should be registered on the playing tab to stop playback if translated, and
// one is registered regardless of playback for refreshing the entrypoint.
assertEquals(2, mFakeTranslateBridge.getObserverCount());
// Both should be removed if the tab is destroyed.
mController.getTabModelTabObserverforTests().onDestroyed(mTab);
assertEquals(0, mFakeTranslateBridge.getObserverCount());
}
@Test
public void testTranslationListenersUnregisteredBeforeWebContentsSwap() {
// Listener should be registered already because onTabSelected() is called when
// TabModelTabObserver is created.
assertEquals(1, mFakeTranslateBridge.getObserverCount());
mController.getTabModelTabObserverforTests().webContentsWillSwap(mTab);
assertEquals(0, mFakeTranslateBridge.getObserverCount());
}
@Test
public void testTranslationListenerRegisteredOnPageLoad() {
// Listener should be registered already because onTabSelected() is called when
// TabModelTabObserver is created.
assertEquals(1, mFakeTranslateBridge.getObserverCount());
// Destroying tab should remove the observer.
mController.getTabModelTabObserverforTests().onDestroyed(mTab);
assertEquals(0, mFakeTranslateBridge.getObserverCount());
// Do not register in onPageLoadStarted()!
mController.getTabModelTabObserverforTests().onPageLoadStarted(mTab, sTestGURL);
assertEquals(0, mFakeTranslateBridge.getObserverCount());
// Instead register in onContentChanged().
mController.getTabModelTabObserverforTests().onContentChanged(mTab);
assertEquals(1, mFakeTranslateBridge.getObserverCount());
}
@Test
public void testTranslationListenersUnregistered_nullWebContents() {
assertEquals(1, mFakeTranslateBridge.getObserverCount());
// If tab has null web contents, we should still remove the observer from whatever
// WebContents it was added to.
doReturn(null).when(mTab).getWebContents();
mController.getTabModelTabObserverforTests().onDestroyed(mTab);
assertEquals(0, mFakeTranslateBridge.getObserverCount());
}
@Test
public void testTranslationListener_tabWebContentsChanged() {
// An observer is added during ReadAloudController creation through onTabSelected().
assertEquals(1, mFakeTranslateBridge.getObserverCount(mWebContents));
// Simulate WebContents changing.
WebContents otherWebContents = Mockito.mock(WebContents.class);
mTab.setWebContentsOverrideForTesting(otherWebContents);
mController.getTabModelTabObserverforTests().onContentChanged(mTab);
// Observer should have been removed from old WebContents and added to the new one.
assertEquals(0, mFakeTranslateBridge.getObserverCount(mWebContents));
assertEquals(1, mFakeTranslateBridge.getObserverCount(otherWebContents));
}
@Test
public void testTranslationListener_unsupportedURLTabSelected() {
// An observer is added during ReadAloudController creation through onTabSelected().
assertEquals(1, mFakeTranslateBridge.getObserverCount(mWebContents));
// Select a different tab with an invalid URL.
WebContents otherWebContents = Mockito.mock(WebContents.class);
MockTab tab = mTabModelSelector.addMockTab();
tab.setWebContentsOverrideForTesting(otherWebContents);
tab.setUrl(new GURL(""));
mController.getTabModelTabObserverforTests().onTabSelected(tab);
// The observer should have been removed from the original WebContents. No need to observe
// translation on the new tab since it's not readable: the observer will be added on
// onContentChanged() if the user navigates to a readable page.
assertEquals(0, mFakeTranslateBridge.getObserverCount(mWebContents));
assertEquals(0, mFakeTranslateBridge.getObserverCount(otherWebContents));
}
@Test
public void testTranslationListener_playingTabWebContentsChanged() {
// An observer is added during ReadAloudController creation through onTabSelected().
assertEquals(1, mFakeTranslateBridge.getObserverCount(mWebContents));
// Play tab.
requestAndStartPlayback();
assertEquals(2, mFakeTranslateBridge.getObserverCount(mWebContents));
// Switching WebContents of playing tab should remove the "playing tab" translation observer
// and the "current tab" translation observer since mTab was also the currently selected
// tab.
WebContents otherWebContents = Mockito.mock(WebContents.class);
mTab.setWebContentsOverrideForTesting(otherWebContents);
mController.getTabModelTabObserverforTests().onContentChanged(mTab);
assertEquals(0, mFakeTranslateBridge.getObserverCount(mWebContents));
}
@Test
public void testTranslationListener_onTabSelected() {
// An observer is added during ReadAloudController creation through onTabSelected().
assertEquals(1, mFakeTranslateBridge.getObserverCount(mWebContents));
// Select a different tab with a valid URL.
WebContents otherWebContents = Mockito.mock(WebContents.class);
MockTab tab = mTabModelSelector.addMockTab();
tab.setWebContentsOverrideForTesting(otherWebContents);
tab.setUrl(new GURL("https://some.cool.website/"));
mController.getTabModelTabObserverforTests().onTabSelected(tab);
// The observer should have been removed from the original WebContents and the new tab's
// WebContents should be observed.
assertEquals(0, mFakeTranslateBridge.getObserverCount(mWebContents));
assertEquals(1, mFakeTranslateBridge.getObserverCount(otherWebContents));
}
@Test
public void testTranslationListenersRemovedWhenControllerDestroyed() {
// An observer is added during ReadAloudController creation through onTabSelected().
assertEquals(1, mFakeTranslateBridge.getObserverCount(mWebContents));
// Play tab.
requestAndStartPlayback();
assertEquals(2, mFakeTranslateBridge.getObserverCount(mWebContents));
mController.destroy();
assertEquals(0, mFakeTranslateBridge.getObserverCount(mWebContents));
}
@Test
public void testIsPageTranslated_nullWebContent() {
mFakeTranslateBridge.setIsPageTranslated(true);
when(mTab.getWebContents()).thenReturn(null);
assertFalse(mController.isTranslated(mTab));
}
@Test
public void testIsPageTranslated() {
mFakeTranslateBridge.setIsPageTranslated(true);
assertTrue(mController.isTranslated(mTab));
}
@Test
public void testIsTranslatedChangedStopsPlayback() {
// Play tab.
requestAndStartPlayback();
// Trigger isTranslated state changed. Playback should stop.
mController
.getTranslationObserverForTest()
.onIsPageTranslatedChanged(mTab.getWebContents());
verify(mPlayback).release();
}
@Test
public void testSuccessfulTranslationStopsPlayback() {
// Play tab.
requestAndStartPlayback();
// Finish translating (status code 0 means "no error"). Playback should stop.
mController.getTranslationObserverForTest().onPageTranslated("en", "es", 0);
verify(mPlayback).release();
}
@Test
public void testFailedTranslationDoesNotStopPlayback() {
// Play tab.
requestAndStartPlayback();
// Fail to translate (status code 1). Playback should not stop.
mController.getTranslationObserverForTest().onPageTranslated("en", "es", 1);
verify(mPlayback, never()).release();
}
@Test
public void testPageTranslatedNotifiesReadabilityChanged() {
Runnable runnable = Mockito.mock(Runnable.class);
mController.addReadabilityUpdateListener(runnable);
var translationObserver = mController.getCurrentTabTranslationObserverForTest();
translationObserver.onPageTranslated("en", "es", 1);
verify(runnable, times(1)).run();
translationObserver.onIsPageTranslatedChanged(null);
verify(runnable, times(2)).run();
}
@Test
public void testStoppingAnyPlayback() {
// Play tab.
requestAndStartPlayback();
verify(mPlayback).play();
// request to stop any playback
mController.maybeStopPlayback(
null, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
verify(mPlayback).release();
verify(mPlayerCoordinator).dismissPlayers();
}
@Test
public void testIsHighlightingSupported_noPlayback() {
mFakeTranslateBridge.setIsPageTranslated(false);
assertFalse(mController.isHighlightingSupported());
}
@Test
public void testIsHighlightingSupported_pageTranslated() {
mFakeTranslateBridge.setIsPageTranslated(true);
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
assertFalse(mController.isHighlightingSupported());
}
@Test
public void testIsHighlightingSupported_notSupported() {
mFakeTranslateBridge.setIsPageTranslated(false);
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), false);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
assertFalse(mController.isHighlightingSupported());
}
@Test
public void testIsHighlightingSupported_supported() {
mFakeTranslateBridge.setIsPageTranslated(false);
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
assertTrue(mController.isHighlightingSupported());
}
@Test
public void testReadabilitySupplier() {
String testUrl = "https://en.wikipedia.org/wiki/Google";
Runnable runnable = Mockito.mock(Runnable.class);
mController.addReadabilityUpdateListener(runnable);
mTab.setGurlOverrideForTesting(new GURL(testUrl));
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1)).isPageReadable(eq(testUrl), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(testUrl, true, false);
verify(runnable).run();
}
@Test
public void testMetricRecorded_isReadable() {
final String histogramName = ReadAloudMetrics.IS_READABLE;
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
histogram.assertExpected();
histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, false);
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), false, false);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_readabilitySuccessful() {
final String histogramName = ReadAloudMetrics.READABILITY_SUCCESS;
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
histogram.assertExpected();
histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, false);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor
.getValue()
.onFailure(sTestGURL.getSpec(), new Throwable("Something went wrong"));
histogram.assertExpected();
}
@Test
public void testMetricRecorded_serverReadability() {
final String histogramName = ReadAloudMetrics.READABILITY_SERVER_SIDE;
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true);
mController.maybeCheckReadability(mTab);
verify(mHooksImpl).isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor
.getValue()
.onSuccess(
sTestGURL.getSpec(),
/* isReadable= */ true,
/* timepointsSupported= */ false);
histogram.assertExpected();
histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, false);
mCallbackCaptor
.getValue()
.onSuccess(
sTestGURL.getSpec(),
/* isReadable= */ false,
/* timepointsSupported= */ false);
histogram.assertExpected();
// nothing should be emitted on error
histogram = HistogramWatcher.newBuilder().expectNoRecords(histogramName).build();
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor
.getValue()
.onFailure(sTestGURL.getSpec(), new Throwable("Something went wrong"));
histogram.assertExpected();
}
@Test
@DisableFeatures(ChromeFeatureList.READALOUD_PLAYBACK)
public void testReadAloudPlaybackFlagCheckedAfterReadability() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
assertFalse(mController.isReadable(mTab));
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK)
public void testBackgroundPlaybackContinuesWhenActivityPaused() {
// Play tab.
requestAndStartPlayback();
// set progress
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
// App is backgrounded with the screen on. Playback should continue if the flag is on.
setIsScreenOnAndUnlocked(true);
mController.onApplicationStateChange(ApplicationState.HAS_PAUSED_ACTIVITIES);
verify(mPlayback, never()).release();
// also the screen is still on, don;t notify about screen state change
verify(mPlayerCoordinator, never()).onScreenStatusChanged(anyBoolean());
// now turn the screen off.
setIsScreenOnAndUnlocked(false);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
verify(mPlayback, never()).release();
// also the screen is still on, don;t notify about screen state change
verify(mPlayerCoordinator).onScreenStatusChanged(true);
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK)
public void testBackgroundPlayback_doesntCrashWhenNoPlayer() {
try {
// App is backgrounded with the screen of. Playback should continue if the flag is on.
setIsScreenOnAndUnlocked(false);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
} catch (NullPointerException ex) {
Assert.fail();
}
}
@Test
public void testPlaybackStopsAndStateSavedWhenAppBackgrounded_screenOn() {
// Play tab.
requestAndStartPlayback();
// set progress
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
// App is backgrounded with the screen on. Make sure playback stops.
setIsScreenOnAndUnlocked(true);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
verify(mPlayback).release();
reset(mPlayback);
when(mPlayback.getMetadata()).thenReturn(mMetadata);
// Activity goes back in foreground. Restore progress.
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verify(mPlaybackHooks, times(2)).createPlayback(any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayback).seekToParagraph(2, 1000000L);
verify(mPlayback, never()).play();
// once saved state is restored, it's cleared and no further interactions with playback
// should happen.
resetPlaybackMocks();
mController.onApplicationStateChange(ApplicationState.HAS_PAUSED_ACTIVITIES);
mController.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES);
verifyNoInteractions(mPlaybackHooks);
verifyNoInteractions(mPlayback);
}
@Test
public void testPlaybackWhenAppStops_screenOff() {
// Play tab.
requestAndStartPlayback();
// set progress
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
// App is backgrounded when the screen is off. Playback should keep playing.
setIsScreenOnAndUnlocked(false);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
verify(mPlayback, never()).release();
}
@Test
public void testPlaybackWhenAppStops_userHint() {
// Play tab.
requestAndStartPlayback();
// set progress
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
// App is backgrounded. Screen is off but there is user hint present - stop playback
mController.onUserLeaveHint();
setIsScreenOnAndUnlocked(false);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
verify(mPlayback).release();
resetPlaybackMocks();
// App goes back in foreground. Restore progress.
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayback).seekToParagraph(2, 1000000L);
verify(mPlayback, never()).play();
}
private void setIsScreenOnAndUnlocked(boolean isScreenOnAndUnlocked) {
DeviceConditions deviceConditions =
new DeviceConditions(
/* powerConnected= */ false,
/* batteryPercentage= */ 75,
ConnectionType.CONNECTION_UNKNOWN,
/* powerSaveOn= */ false,
/* activeNetworkMetered= */ false,
isScreenOnAndUnlocked);
ShadowDeviceConditions.setCurrentConditions(deviceConditions);
}
@Test
public void testPlaybackResumesWhenActivityResumes() {
// Play tab.
requestAndStartPlayback();
// set progress
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
// App is backgrounded with the screen on. Make sure playback stops.
setIsScreenOnAndUnlocked(true);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
verify(mPlayback).release();
resetPlaybackMocks();
// App returns to foreground, but activity hasn't resumed yet.
mController.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES);
verify(mPlaybackHooks, never()).createPlayback(any(), any());
// Activity goes back in foreground. Restore progress.
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayback).seekToParagraph(2, 1000000L);
verify(mPlayback, never()).play();
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK)
public void testPlaybackResumesWhenActivityResumes_backgroundPlaybackEnabled() {
// Play tab.
requestAndStartPlayback();
// set progress
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
// App is backgrounded with the screen on. Playback should not stop.
setIsScreenOnAndUnlocked(true);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
resetPlaybackMocks();
// Activity goes back in foreground. Nothing should be restored; playback was never stopped
// in the first place.
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verifyNoInteractions(mPlayback);
verifyNoInteractions(mPlaybackHooks);
}
@Test
public void testMetricRecorded_eligibility() {
final String histogramName = ReadAloudMetrics.IS_USER_ELIGIBLE;
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true);
mController.onProfileAvailable(mMockProfile);
histogram.assertExpected();
histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, false);
when(mPrefService.getBoolean("readaloud.listen_to_this_page_enabled")).thenReturn(false);
mController.onProfileAvailable(mMockProfile);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_ineligibilityReason() {
final String histogramName = ReadAloudMetrics.INELIGIBILITY_REASON;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, IneligibilityReason.POLICY_DISABLED);
when(mPrefService.getBoolean("readaloud.listen_to_this_page_enabled")).thenReturn(false);
mController.onProfileAvailable(mMockProfile);
histogram.assertExpected();
when(mPrefService.getBoolean("readaloud.listen_to_this_page_enabled")).thenReturn(true);
histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, IneligibilityReason.DEFAULT_SEARCH_ENGINE_GOOGLE_FALSE);
doReturn(SearchEngineType.SEARCH_ENGINE_OTHER)
.when(mTemplateUrlService)
.getSearchEngineTypeFromTemplateUrl(anyString());
mController.onProfileAvailable(mMockProfile);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_isPlaybackCreationSuccessful_True() {
final String histogramName = ReadAloudMetrics.IS_TAB_PLAYBACK_CREATION_SUCCESSFUL;
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true);
requestAndStartPlayback();
histogram.assertExpected();
}
@Test
public void testMetricRecorded_isPlaybackCreationSuccessful_False() {
final String histogramName = ReadAloudMetrics.IS_TAB_PLAYBACK_CREATION_SUCCESSFUL;
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, false);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks).createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mPlaybackCallbackCaptor.getValue().onFailure(new Exception("Very bad error"));
resolvePromises();
histogram.assertExpected();
}
@Test
public void testMetricRecorded_playbackWithoutReadabilityCheck() {
final String histogramName = ReadAloudMetrics.TAB_PLAYBACK_WITHOUT_READABILITY_CHECK_ERROR;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, ReadAloudController.Entrypoint.OVERFLOW_MENU);
mController.playTab(mTab, ReadAloudController.Entrypoint.OVERFLOW_MENU);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_playbackSuccess() {
final String histogramName = ReadAloudMetrics.TAB_PLAYBACK_CREATION_SUCCESS;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
requestAndStartPlayback();
histogram.assertExpected();
}
@Test
public void testMetricRecorded_playbackFailure() {
final String histogramName = ReadAloudMetrics.TAB_PLAYBACK_CREATION_FAILURE;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, ReadAloudController.Entrypoint.OVERFLOW_MENU);
// Play tab to set up playbackhooks
mController.playTab(mTab, ReadAloudController.Entrypoint.OVERFLOW_MENU);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mPlaybackCallbackCaptor.getValue().onFailure(new Exception("Very bad error"));
resolvePromises();
histogram.assertExpected();
}
@Test
public void testMetricNotRecorded_isPlaybackCreationSuccessful() {
final String histogramName = ReadAloudMetrics.IS_TAB_PLAYBACK_CREATION_SUCCESSFUL;
var histogram = HistogramWatcher.newBuilder().expectNoRecords(histogramName).build();
// Play tab to set up playbackhooks
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// Preview a voice.
var voice = new PlaybackVoice("en", "asdf", "");
doReturn(List.of(voice)).when(mPlaybackHooks).getVoicesFor(anyString());
doReturn(List.of(voice)).when(mPlaybackHooks).getPlaybackVoiceList(any());
mController.previewVoice(voice);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_playbackStarted() {
final String actionName = "ReadAloud.PlaybackStarted";
ReadAloudMetrics.recordPlaybackStarted();
assertThat(mUserActionTester.getActions(), hasItems(actionName));
}
@Test
public void testMetricRecorded_highlightingEnabledOnStartup() {
mHighlightingEnabledOnStartupHistogram.assertExpected();
}
@Test
public void testMetricRecorded_highlightingSupported_true() {
final String histogramName = "ReadAloud.HighlightingSupported";
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mFakeTranslateBridge.setIsPageTranslated(false);
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true);
onPlaybackSuccess(mPlayback);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_highlightingSupported_false() {
final String histogramName = "ReadAloud.HighlightingSupported";
var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, false);
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
mFakeTranslateBridge.setIsPageTranslated(false);
mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), false);
onPlaybackSuccess(mPlayback);
histogram.assertExpected();
}
@Test
public void testNavigateToPlayingTab() {
// Play tab.
requestAndStartPlayback();
MockTab newTab = mTabModelSelector.addMockTab();
mTabModelSelector
.getModel(false)
.setIndex(
mTabModelSelector.getModel(false).indexOf(newTab),
TabSelectionType.FROM_USER);
// check that we switched to new tab
assertEquals(mTabModelSelector.getCurrentTab(), newTab);
// navigate
mController.navigateToPlayingTab();
// should switch back to original one
assertEquals(mTabModelSelector.getCurrentTab(), mTab);
// navigate
mController.navigateToPlayingTab();
// should still be on the playing tab
assertEquals(mTabModelSelector.getCurrentTab(), mTab);
}
@Test
public void testInitClearsStaleSyntheticTrialPrefs() {
verify(mReadAloudFeaturesNatives, times(1)).clearStaleSyntheticTrialPrefs();
}
@Test
public void testKnownReadableTrialInit() {
// ReadAloudController creation should init the trial.
verify(mReadAloudFeaturesNatives, times(1))
.initSyntheticTrial(eq(ChromeFeatureList.READALOUD), eq("_KnownReadable"));
}
@Test
public void testKnownReadableTrialActivate() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
// Page is readable so activate the trial.
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
verify(mReadAloudFeaturesNatives, times(1))
.activateSyntheticTrial(eq(KNOWN_READABLE_TRIAL_PTR));
// Subsequent readability checks may cause activateSyntheticTrial() to be called again
// (though it has no effect after the first call).
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
verify(mReadAloudFeaturesNatives, times(2))
.activateSyntheticTrial(eq(KNOWN_READABLE_TRIAL_PTR));
}
@Test
public void testKnownReadableTrialDoesNotActivateIfNotReadable() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
// Page is not readable so do not activate the trial.
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), false, false);
verify(mReadAloudFeaturesNatives, never()).activateSyntheticTrial(anyLong());
}
@Test
@DisableFeatures(ChromeFeatureList.READALOUD_PLAYBACK)
public void testKnownReadableTrialCanActivateWithoutPlaybackFlag() {
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
// Page is readable so activate the trial.
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
verify(mReadAloudFeaturesNatives, times(1))
.activateSyntheticTrial(eq(KNOWN_READABLE_TRIAL_PTR));
}
@Test
public void testDestroy() {
// Play tab
requestAndStartPlayback();
// Destroy should clean up playback, UI, synthetic trials, and more
mController.destroy();
verify(mPlayback).release();
verify(mPlayerCoordinator).destroy();
verify(mReadAloudFeaturesNatives).destroySyntheticTrial(eq(KNOWN_READABLE_TRIAL_PTR));
}
@Test
public void testMaybeShowPlayer() {
// no playback, request is a no op
mController.maybeShowPlayer();
verify(mPlayerCoordinator, never()).restorePlayers();
requestAndStartPlayback();
mController.maybeShowPlayer();
verify(mPlayerCoordinator).restorePlayers();
}
@Test
public void testMaybeHideMiniPlayer() {
// no playback, request is a no op
mController.maybeHidePlayer();
verify(mPlayerCoordinator, never()).hidePlayers();
requestAndStartPlayback();
mController.maybeHidePlayer();
verify(mPlayerCoordinator).hidePlayers();
}
@Test
public void testPauseAndHideOnIncognitoTabSelected() {
requestAndStartPlayback();
Tab tab = mTabModelSelector.addMockIncognitoTab();
TabModelUtils.selectTabById(mTabModelSelector, tab.getId(), TabSelectionType.FROM_NEW);
verify(mPlayback).pause();
verify(mPlayerCoordinator).hidePlayers();
}
@Test
public void testRestorePlayerOnReturnFromIncognitoTab() {
requestAndStartPlayback();
reset(mPlayback);
Tab tab = mTabModelSelector.addMockIncognitoTab();
TabModelUtils.selectTabById(mTabModelSelector, tab.getId(), TabSelectionType.FROM_NEW);
verify(mPlayback).pause();
verify(mPlayerCoordinator).hidePlayers();
TabModelUtils.selectTabById(mTabModelSelector, mTab.getId(), TabSelectionType.FROM_USER);
verify(mPlayback, never()).play();
verify(mPlayerCoordinator).restorePlayers();
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK)
public void testCrossActivityPlayback_stopBackgroundPlayback() {
// Play in Chrome, then play in CCT. Chrome playback should stop only when CCT plays.
// Create a second instance of ReadAloudController to simulate other app's CCT.
mController2 = createController();
// Play in Chrome. requestAndStartPlayback() verifies playback started.
requestAndStartPlayback();
resetPlaybackMocks();
// Simulate backgrounding the activity, and the entire app. Playback should not stop.
mController.onActivityStateChange(mActivity, ActivityState.STOPPED);
mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES);
verifyNoInteractions(mPlayback);
// Play in CCT.
var histogram =
HistogramWatcher.newSingleRecordWatcher(
ReadAloudMetrics.REASON_FOR_STOPPING_PLAYBACK,
ReadAloudMetrics.ReasonForStoppingPlayback.EXTERNAL_PLAYBACK_REQUEST);
mController2.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// Chrome playback should stop. Reason should be recorded as EXTERNAL_PLAYBACK_REQUEST.
verify(mPlayback).release();
histogram.assertExpected();
// CCT playback should start.
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator, times(1))
.playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING));
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK)
public void testCrossActivityPlayback_canRestoreIfSameTab() {
// Play in Chrome, play in CCT, then request playback for original tab in Chrome. Playback
// should be restored.
// Create a second instance of ReadAloudController to simulate other app's CCT.
mController2 = createController();
// Play in Chrome. requestAndStartPlayback() verifies playback started.
requestAndStartPlayback();
// Simulate some progress.
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
resetPlaybackMocks();
// Play in CCT.
mController2.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// Chrome playback should stop.
verify(mPlayback).release();
// CCT playback should start.
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator, times(1))
.playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING));
resetPlaybackMocks();
// Return to Chrome. CCT playback should not stop.
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verifyNoInteractions(mPlayback);
// Tap an entrypoint on the same tab in Chrome. CCT should stop playing and playback should
// be restored (paused).
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
// Release CCT playback
verify(mPlayback).release();
// Simulate successful playback creation.
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
// Progress should be restored and play should not have been called.
verify(mPlayback).seekToParagraph(2, 1000000L);
verify(mPlayback, never()).play();
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK)
public void testCrossActivityPlayback_doNotRestoreIfDifferentTab() {
// Play in Chrome, play in CCT, then request playback for a different tab in Chrome. A new
// playback should start and the old one should not be restored.
// Create a second instance of ReadAloudController to simulate other app's CCT.
mController2 = createController();
// Play in Chrome. requestAndStartPlayback() verifies playback started.
requestAndStartPlayback();
// Simulate some progress.
var data = Mockito.mock(PlaybackData.class);
doReturn(2).when(data).paragraphIndex();
doReturn(1000000L).when(data).positionInParagraphNanos();
mController.onPlaybackDataChanged(data);
resetPlaybackMocks();
// Play in CCT.
mController2.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// Chrome playback should stop.
verify(mPlayback).release();
// CCT playback should start.
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator, times(1))
.playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING));
resetPlaybackMocks();
// Return to Chrome. CCT playback should not stop.
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verifyNoInteractions(mPlayback);
// Tap an entrypoint on a different tab in Chrome.
MockTab tab = mTabModelSelector.addMockTab();
tab.setGurlOverrideForTesting(sTestGURL);
tab.setWebContentsOverrideForTesting(mWebContents);
mController.playTab(tab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// CCT playback was released
verify(mPlayback).release();
// Simulate successful playback creation and make sure playback starts.
verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayback).play();
// Make sure saved state was not restored and was instead cleared.
verify(mPlayback, never()).seekToParagraph(eq(2), eq(1000000L));
resetPlaybackMocks();
mController.onActivityStateChange(mActivity, ActivityState.RESUMED);
verifyNoInteractions(mPlaybackHooks);
verifyNoInteractions(mPlayback);
}
// TODO(b/322052505): This test won't be necessary if we keep track of profile changes.
@Test
public void testNoRequestsIfProfileDestroyed() {
reset(mHooksImpl);
doReturn(false).when(mMockProfile).isNativeInitialized();
mController = createController();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
// Check readability.
mController.maybeCheckReadability(mTab);
// No readability request should be made.
verify(mHooksImpl, never()).isPageReadable(any(), any());
// Try playing the tab.
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
// No playback request should be made.
verify(mPlaybackHooks, never()).createPlayback(any(), any());
}
@Test
public void testPause_notPlayingTab() {
mController.pause();
// Not currently playing, so nothing should happen.
verify(mPlayback, never()).pause();
}
@Test
public void testPause_alreadyStopped() {
requestAndStartPlayback();
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.STOPPED).when(data).state();
mController.onPlaybackDataChanged(data);
mController.pause();
// Not currently playing, so nothing should happen.
verify(mPlayback, never()).pause();
}
@Test
public void testPause() {
requestAndStartPlayback();
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mController.onPlaybackDataChanged(data);
mController.pause();
verify(mPlayback).pause();
}
@Test
public void testMaybePauseForOutgoingIntent_pause() {
// Play.
requestAndStartPlayback();
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mController.onPlaybackDataChanged(data);
// Simulate select-to-speak context menu click. Playback should pause.
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PROCESS_TEXT);
mController.maybePauseForOutgoingIntent(intent);
verify(mPlayback).pause();
}
@Test
public void testMaybePauseForOutgoingIntent_noPause() {
// Play.
requestAndStartPlayback();
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mController.onPlaybackDataChanged(data);
// Simulate some unimportant context menu click. Playback should not pause.
Intent intent = new Intent();
intent.setAction(Intent.ACTION_DEFINE);
mController.maybePauseForOutgoingIntent(intent);
verify(mPlayback, never()).pause();
}
@Test
public void testPlayTabWithDateExtraction() {
requestAndStartPlayback();
verify(mPlayerCoordinator).addObserver(mController);
verify(mPlaybackHooks, times(1)).createPlayback(mPlaybackArgsCaptor.capture(), any());
assertEquals(1234567123456L, mPlaybackArgsCaptor.getValue().getDateModifiedMsSinceEpoch());
}
@Test
public void testLogDateExtraction_hasDateModified() {
mFakeTranslateBridge.setCurrentLanguage("en");
var histogram = HistogramWatcher.newSingleRecordWatcher("ReadAloud.HasDateModified", true);
requestAndStartPlayback();
histogram.assertExpected();
}
@Test
public void testLogDateExtraction_noDateModified() {
mFakeTranslateBridge.setCurrentLanguage("en");
var failedPromise = new Promise<Long>();
when(mExtractor.getDateModified(any())).thenReturn(failedPromise);
failedPromise.reject(new Exception(""));
var histogram = HistogramWatcher.newSingleRecordWatcher("ReadAloud.HasDateModified", false);
requestAndStartPlayback();
histogram.assertExpected();
}
@Test
public void testIsPlayingCurrentTab() {
// should be false at first since currentlyPlayingTab is null
assertFalse(mController.isPlayingCurrentTab());
// set to playing tab
requestAndStartPlayback();
assertTrue(mController.isPlayingCurrentTab());
// should be false after switching to a non playing tab
MockTab newTab = mTabModelSelector.addMockTab();
mTabModelSelector
.getModel(false)
.setIndex(
mTabModelSelector.getModel(false).indexOf(newTab),
TabSelectionType.FROM_USER);
assertFalse(mController.isPlayingCurrentTab());
// switch back to current tab
mTabModelSelector
.getModel(false)
.setIndex(
mTabModelSelector.getModel(false).indexOf(mTab),
TabSelectionType.FROM_USER);
assertTrue(mController.isPlayingCurrentTab());
// back to null after stopping playback
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.NEW_PLAYBACK_REQUEST);
assertFalse(mController.isPlayingCurrentTab());
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_TAP_TO_SEEK)
public void testTapToSeek() {
// play tab
requestAndStartPlayback();
verify(mPlayback).addListener(mPlaybackListenerCaptor.capture());
// update playback data so it isn't null
var data = Mockito.mock(PlaybackListener.PlaybackData.class);
doReturn(PlaybackListener.State.PLAYING).when(data).state();
mPlaybackListenerCaptor.getValue().onPlaybackDataChanged(data);
var histogram =
HistogramWatcher.newSingleRecordWatcher(ReadAloudMetrics.TAP_TO_SEEK_TIME, 12);
when(mMetadata.fullText())
.thenAnswer(
invocation -> {
mClock.advanceCurrentTimeMillis(12);
return "the quick brown fox jumps over the lazy dog";
});
PlaybackTextPart p =
new PlaybackTextPart() {
@Override
public int getOffset() {
return 0;
}
@Override
public int getType() {
return PlaybackTextType.TEXT_TYPE_UNSPECIFIED;
}
@Override
public int getParagraphIndex() {
return -1;
}
@Override
public int getLength() {
return -1;
}
};
PlaybackTextPart[] paragraphs = new PlaybackTextPart[] {p};
when(mMetadata.paragraphs()).thenReturn(paragraphs);
mController.tapToSeek("the quick brown fox", 4, 9);
verify(mPlayback, times(1)).seekToWord(0, 4);
histogram.assertExpected();
}
@Test
@EnableFeatures(ChromeFeatureList.READALOUD_TAP_TO_SEEK)
public void testTapToSeek_differentTab() {
// play tab
requestAndStartPlayback();
// switch tabs
MockTab newTab = mTabModelSelector.addMockTab();
mTabModelSelector
.getModel(false)
.setIndex(
mTabModelSelector.getModel(false).indexOf(newTab),
TabSelectionType.FROM_USER);
// shouldn't seek
mController.tapToSeek("the quick brown fox", 4, 9);
verify(mPlayback, never()).seekToWord(0, 8);
}
@Test
public void testDidFirstVisuallyNonEmptyPaint() {
GURL gurl = new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc.");
when(mTab.getUrl()).thenReturn(gurl);
mController.getTabModelTabObserverforTests().didFirstVisuallyNonEmptyPaint(mTab);
verify(mHooksImpl).isPageReadable(eq(gurl.getPossiblyInvalidSpec()), any());
}
@Test
public void testOnTabSelected() {
MockTab tab = mTabModelSelector.addMockTab();
// should do nothing on empty url
tab.setUrl(new GURL(""));
mController.getTabModelTabObserverforTests().onTabSelected(tab);
verify(tab, never()).getUserDataHost();
// should get user data for actual urls
tab.setUrl(new GURL("https://en.wikipedia.org/wiki/Alphabet_Inc."));
mController.getTabModelTabObserverforTests().onTabSelected(tab);
verify(tab, times(1)).getUserDataHost();
}
@Test
public void testTimepointsSupported_emptyUrl() {
// if somehow an empty url sneaks into timepoints supported
mController.setTimepointsSupportedForTest("", true);
when(mTab.getUrl()).thenReturn(new GURL(""));
// a tab with an empty url should not be supported
assertFalse(mController.timepointsSupported(mTab));
}
@Test
public void testEmptyUrlReadability() {
// grab the callback
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
// if somehow an empty url sneaks into the readability maps
boolean failed = false;
try {
mCallbackCaptor.getValue().onSuccess("", true, true);
} catch (AssertionError e) {
failed = true;
}
assertTrue(failed);
when(mTab.getUrl()).thenReturn(new GURL(""));
// empty urls should not be returned as readable
assertFalse(mController.isReadable(mTab));
}
@Test
public void testMetricRecorded_EmptyUrlPlayback() {
final String histogramName = ReadAloudMetrics.EMPTY_URL_PLAYBACK;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL(""));
boolean failed = false;
try {
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
} catch (AssertionError e) {
failed = true;
}
assertTrue(failed);
histogram.assertExpected();
}
@Test
public void testMetricRecorded_EmptyUrlPlayback_RestoreState() {
final String histogramName = ReadAloudMetrics.EMPTY_URL_PLAYBACK;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, ReadAloudController.Entrypoint.RESTORED_PLAYBACK);
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
var data = Mockito.mock(PlaybackData.class);
ReadAloudController.RestoreState restoreState =
mController.new RestoreState(mTab, data, true, false, 0L);
mController.setStateToRestoreOnBringingToForegroundForTests(restoreState);
// for some reason the tab url goes null
mTab.setGurlOverrideForTesting(new GURL(""));
boolean failed = false;
try {
restoreState.restore();
} catch (AssertionError e) {
failed = true;
}
assertTrue(failed);
histogram.assertExpected();
}
@Test
public void testNoReadabilityUpdateAfterDestroy() {
Runnable readabilityObserver = Mockito.mock(Runnable.class);
mController.addReadabilityUpdateListener(readabilityObserver);
// Check readability
mController.maybeCheckReadability(mTab);
verify(mHooksImpl, times(1))
.isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture());
assertFalse(mController.isReadable(mTab));
// Simulate response coming back after ReadAloudController being destroyed.
mController.destroy();
mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false);
verify(readabilityObserver, never()).run();
}
@Test
public void testReasonForStoppingPlaybackLogged() {
final String histogramName = ReadAloudMetrics.REASON_FOR_STOPPING_PLAYBACK;
var histogram =
HistogramWatcher.newSingleRecordWatcher(
histogramName, ReadAloudMetrics.ReasonForStoppingPlayback.MANUAL_CLOSE);
requestAndStartPlayback();
mController.maybeStopPlayback(
mTab, ReadAloudMetrics.ReasonForStoppingPlayback.MANUAL_CLOSE);
histogram.assertExpected();
}
private void requestAndStartPlayback() {
mFakeTranslateBridge.setCurrentLanguage("en");
mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google"));
mController.playTab(mTab, ReadAloudController.Entrypoint.MAGIC_TOOLBAR);
resolvePromises();
verify(mPlaybackHooks, times(1))
.createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture());
onPlaybackSuccess(mPlayback);
verify(mPlayerCoordinator, times(1))
.playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING));
}
private void onPlaybackSuccess(Playback playback) {
mPlaybackCallbackCaptor.getValue().onSuccess(playback);
resolvePromises();
}
private static void resolvePromises() {
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
}
}