| // 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.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.when; |
| import static org.robolectric.Shadows.shadowOf; |
| |
| import android.app.Activity; |
| import android.content.Intent; |
| |
| import androidx.appcompat.app.AppCompatActivity; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TestRule; |
| 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.ApplicationState; |
| import org.chromium.base.Promise; |
| import org.chromium.base.supplier.ObservableSupplierImpl; |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| import org.chromium.base.test.util.Features; |
| 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.BrowserControlsSizer; |
| 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.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.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.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.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}) |
| @EnableFeatures({ |
| ChromeFeatureList.READALOUD, |
| ChromeFeatureList.READALOUD_PLAYBACK, |
| ChromeFeatureList.READALOUD_TAP_TO_SEEK |
| }) |
| @DisableFeatures({ChromeFeatureList.READALOUD_IN_MULTI_WINDOW}) |
| public class ReadAloudControllerUnitTest { |
| private static final GURL sTestGURL = JUnitTestGURLs.EXAMPLE_URL; |
| private static final long KNOWN_READABLE_TRIAL_PTR = 12345678L; |
| |
| private MockTab mTab; |
| private ReadAloudController mController; |
| private Activity mActivity; |
| |
| @Rule public JniMocker mJniMocker = new JniMocker(); |
| @Rule public TestRule mProcessor = new Features.JUnitProcessor(); |
| |
| 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 BrowserControlsSizer mBrowserControlsSizer; |
| @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; |
| private GlobalRenderFrameHostId mGlobalRenderFrameHostId = new GlobalRenderFrameHostId(1, 1); |
| public UserActionTester mUserActionTester; |
| private HistogramWatcher mHighlightingEnabledOnStartupHistogram; |
| private Promise<Long> mExtractorPromise; |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| 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"))); |
| when(mPlaybackHooks.createPlayer(any())).thenReturn(mPlayerCoordinator); |
| 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); |
| |
| mController = |
| new ReadAloudController( |
| mActivity, |
| mProfileSupplier, |
| mTabModelSelector.getModel(false), |
| mTabModelSelector.getModel(true), |
| mBottomSheetController, |
| mBrowserControlsSizer, |
| mLayoutManagerSupplier, |
| mActivityWindowAndroid, |
| mActivityLifecycleDispatcher); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| |
| mTab = mTabModelSelector.getCurrentTab(); |
| mTab.setGurlOverrideForTesting(sTestGURL); |
| mTab.setWebContentsOverrideForTesting(mWebContents); |
| |
| when(mMetadata.languageCode()).thenReturn("en"); |
| when(mPlayback.getMetadata()).thenReturn(mMetadata); |
| when(mWebContents.getMainFrame()).thenReturn(mRenderFrameHost); |
| when(mRenderFrameHost.getGlobalRenderFrameHostId()).thenReturn(mGlobalRenderFrameHostId); |
| mController.setHighlighterForTests(mHighlighter); |
| when(mPlaybackHooks.createExtractor()).thenReturn(mExtractor); |
| |
| doReturn(false).when(mPlaybackHooks).voicesInitialized(); |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| mUserActionTester = new UserActionTester(); |
| mExtractorPromise = new Promise<Long>(); |
| when(mExtractor.getDateModified(any())).thenReturn(mExtractorPromise); |
| mExtractorPromise.fulfill(1234567123456L); |
| } |
| |
| @After |
| public void tearDown() { |
| mUserActionTester.tearDown(); |
| ReadAloudFeatures.shutdown(); |
| } |
| |
| @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 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 |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| |
| // 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().onPageLoadStarted(newTab, newTab.getUrl()); |
| |
| verify(mPlayerCoordinator, never()).dismissPlayers(); |
| verify(mPlayback, never()).release(); |
| |
| // now reload the playing tab |
| mController.getTabModelTabObserverforTests().onPageLoadStarted(mTab, mTab.getUrl()); |
| |
| verify(mPlayerCoordinator).dismissPlayers(); |
| verify(mPlayback).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 |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| |
| // 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 |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| |
| // 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, |
| /* skipLoadingTab= */ true); |
| |
| 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, |
| /* skipLoadingTab= */ true); |
| 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, |
| /* skipLoadingTab= */ true); |
| verify(mPlaybackHooks, times(2)).createPlayback(any(), mPlaybackCallbackCaptor.capture()); |
| } |
| |
| @Test |
| public void testReloadPage_errorUiDismissed() { |
| // start a playback with an error |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| mPlaybackCallbackCaptor.getValue().onFailure(new Exception("Very bad error")); |
| resolvePromises(); |
| |
| // Reload this url |
| mController.getTabModelTabObserverforTests().onPageLoadStarted(mTab, mTab.getUrl()); |
| |
| // No playback but error UI should get dismissed |
| verify(mPlayerCoordinator).dismissPlayers(); |
| } |
| |
| @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 |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| |
| // 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); |
| 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.getUrl()); |
| |
| verify(mHooksImpl, never()) |
| .isPageReadable( |
| Mockito.anyString(), |
| Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class)); |
| } |
| |
| @Test |
| public void checkReadabilityOnPageLoad_URLnotReadAloudSupported() { |
| 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_success() { |
| mController.maybeCheckReadability(sTestGURL); |
| |
| 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(sTestGURL); |
| |
| verify(mHooksImpl, times(1)) |
| .isPageReadable( |
| Mockito.anyString(), |
| Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class)); |
| } |
| |
| @Test |
| public void checkReadability_noMSBB() { |
| mController.maybeCheckReadability(sTestGURL); |
| |
| 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(sTestGURL); |
| mController.maybeCheckReadability(sTestGURL); |
| mController.maybeCheckReadability(sTestGURL); |
| |
| verify(mHooksImpl, times(1)).isPageReadable(Mockito.anyString(), mCallbackCaptor.capture()); |
| } |
| |
| @Test |
| public void checkReadability_failure() { |
| mController.maybeCheckReadability(sTestGURL); |
| |
| 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(sTestGURL); |
| |
| verify(mHooksImpl, times(2)) |
| .isPageReadable( |
| Mockito.anyString(), |
| Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class)); |
| } |
| |
| @Test |
| public void isReadable_languageSupported() { |
| mController.maybeCheckReadability(sTestGURL); |
| |
| 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_languageUnsupported() { |
| mController.maybeCheckReadability(sTestGURL); |
| |
| 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 testReactingtoMSBBChange() { |
| mController.maybeCheckReadability(sTestGURL); |
| |
| 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); |
| mController.maybeCheckReadability(JUnitTestGURLs.GOOGLE_URL_CAT); |
| |
| verify(mHooksImpl, times(1)) |
| .isPageReadable( |
| Mockito.anyString(), |
| Mockito.any(ReadAloudReadabilityHooks.ReadabilityCallback.class)); |
| } |
| |
| @Test |
| public void testPlayTab() { |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| |
| onPlaybackSuccess(mPlayback); |
| verify(mPlayerCoordinator, times(1)) |
| .playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING)); |
| 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); |
| 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); |
| 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); |
| 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 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); |
| 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 testPlayTranslatedTab_tabLanguageEmpty() { |
| AppLocaleUtils.setAppLanguagePref("fr-FR"); |
| |
| mFakeTranslateBridge.setIsPageTranslated(true); |
| mFakeTranslateBridge.setCurrentLanguage(""); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| |
| mController.playTab(mTab); |
| 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); |
| 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); |
| 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); |
| 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); |
| 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); |
| 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); |
| 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"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| |
| mPlaybackCallbackCaptor.getValue().onFailure(new Throwable()); |
| resolvePromises(); |
| verify(mPlayerCoordinator, times(1)).playbackFailed(); |
| } |
| |
| @Test |
| public void testStopPlayback() { |
| // Play tab |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| |
| onPlaybackSuccess(mPlayback); |
| verify(mPlayerCoordinator, times(1)) |
| .playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING)); |
| |
| // Stop playback |
| mController.maybeStopPlayback(mTab); |
| verify(mPlayback).release(); |
| |
| reset(mPlayerCoordinator); |
| reset(mPlayback); |
| reset(mPlaybackHooks); |
| |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| // Subsequent playTab() should play without trying to release anything. |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks).createPlayback(any(), any()); |
| verify(mPlayback, never()).release(); |
| } |
| |
| @Test |
| public void highlightsRequested() { |
| // set up the highlighter |
| mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true); |
| mController.playTab(mTab); |
| 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_highlightsCleared() { |
| // set up the highlighter |
| mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true); |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| verify(mHighlighter).initializeJs(eq(mTab), eq(mMetadata), any(Highlighter.Config.class)); |
| |
| // Reload this url |
| mController.getTabModelTabObserverforTests().onPageLoadStarted(mTab, mTab.getUrl()); |
| |
| verify(mHighlighter).handleTabReloaded(eq(mTab)); |
| } |
| |
| @Test |
| public void reloadingTab_highlightsNotCleared() { |
| // set up the highlighter |
| mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true); |
| mController.playTab(mTab); |
| 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); |
| 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); |
| |
| verify(mHighlighter).clearHighlights(eq(mGlobalRenderFrameHostId), eq(mTab)); |
| } |
| |
| @Test |
| public void testUserDataStrippedFromReadabilityCheck() { |
| GURL tabUrl = new GURL("http://user:pass@example.com"); |
| mTab.setGurlOverrideForTesting(tabUrl); |
| |
| mController.maybeCheckReadability(tabUrl); |
| |
| 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); |
| 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); |
| 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. |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| verify(mPlayback).addListener(mPlaybackListenerCaptor.capture()); |
| reset(mPlaybackHooks); |
| reset(mPlayback); |
| |
| // 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()); |
| |
| doReturn(mMetadata).when(mPlayback).getMetadata(); |
| 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(); |
| reset(mPlaybackHooks); |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| |
| 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); |
| reset(mPlaybackHooks); |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| |
| // 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, times(1)).play(); |
| verify(mPlayback).seekToParagraph(eq(99), eq(0L)); |
| } |
| |
| @Test |
| public void testPreviewVoice_metric() { |
| final String histogramName = ReadAloudMetrics.VOICE_PREVIEWED; |
| |
| var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName + "abc", true); |
| |
| // Play tab. |
| requestAndStartPlayback(); |
| |
| reset(mPlaybackHooks); |
| // Preview a voice. |
| var voice = new PlaybackVoice("en", "abc", ""); |
| doReturn(List.of(voice)).when(mPlaybackHooks).getVoicesFor(anyString()); |
| 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); |
| |
| histogram.assertExpected(); |
| } |
| |
| @Test |
| public void testRestorePlaybackState_whileLoading() { |
| // Request playback but don't succeed yet. |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture()); |
| reset(mPlaybackHooks); |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| |
| // 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(); |
| reset(mPlaybackHooks); |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| var data = Mockito.mock(PlaybackListener.PlaybackData.class); |
| doReturn(PlaybackListener.State.PLAYING).when(data).state(); |
| doReturn(99).when(data).paragraphIndex(); |
| mController.onPlaybackDataChanged(data); |
| |
| verify(mPlayback).play(); |
| |
| // 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()); |
| reset(mPlaybackHooks); |
| doReturn(List.of(new PlaybackVoice("en", "voiceA", ""))) |
| .when(mPlaybackHooks) |
| .getVoicesFor(anyString()); |
| 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, times(2)).play(); |
| verify(mPlayback).seekToParagraph(eq(99), eq(0L)); |
| } |
| |
| @Test |
| public void testTranslationListenerRegistration() { |
| // Play tab. |
| requestAndStartPlayback(); |
| |
| assertEquals(1, mFakeTranslateBridge.getObserverCount()); |
| |
| // stopping playback should unregister a listener |
| mController.maybeStopPlayback(mTab); |
| assertEquals(0, mFakeTranslateBridge.getObserverCount()); |
| } |
| |
| @Test |
| public void testTranslationListenerRegistration_nullWebContents() { |
| // Play tab. |
| when(mTab.getWebContents()).thenReturn(null); |
| requestAndStartPlayback(); |
| |
| assertEquals(0, mFakeTranslateBridge.getObserverCount()); |
| |
| // stopping playback should not a listener |
| mController.maybeStopPlayback(mTab); |
| assertEquals(0, mFakeTranslateBridge.getObserverCount()); |
| } |
| |
| @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 testStoppingAnyPlayback() { |
| // Play tab. |
| requestAndStartPlayback(); |
| verify(mPlayback).play(); |
| |
| // request to stop any playback |
| mController.maybeStopPlayback(null); |
| 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); |
| resolvePromises(); |
| |
| assertFalse(mController.isHighlightingSupported()); |
| } |
| |
| @Test |
| public void testIsHighlightingSupported_notSupported() { |
| mFakeTranslateBridge.setIsPageTranslated(false); |
| mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), false); |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| assertFalse(mController.isHighlightingSupported()); |
| } |
| |
| @Test |
| public void testIsHighlightingSupported_supported() { |
| mFakeTranslateBridge.setIsPageTranslated(false); |
| mController.setTimepointsSupportedForTest(mTab.getUrl().getSpec(), true); |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| assertTrue(mController.isHighlightingSupported()); |
| } |
| |
| @Test |
| public void testReadabilitySupplier() { |
| String testUrl = "https://en.wikipedia.org/wiki/Google"; |
| |
| mController.maybeCheckReadability(new GURL(testUrl)); |
| |
| verify(mHooksImpl, times(1)).isPageReadable(eq(testUrl), mCallbackCaptor.capture()); |
| |
| mCallbackCaptor.getValue().onSuccess(testUrl, true, false); |
| |
| assertEquals(mController.getReadabilitySupplier().get(), testUrl); |
| } |
| |
| @Test |
| public void testMetricRecorded_isReadable() { |
| final String histogramName = ReadAloudMetrics.IS_READABLE; |
| |
| var histogram = HistogramWatcher.newSingleRecordWatcher(histogramName, true); |
| mController.maybeCheckReadability(sTestGURL); |
| 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(sTestGURL); |
| 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(sTestGURL); |
| 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(sTestGURL); |
| 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(sTestGURL); |
| 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(sTestGURL); |
| verify(mHooksImpl, times(1)) |
| .isPageReadable(eq(sTestGURL.getSpec()), mCallbackCaptor.capture()); |
| mCallbackCaptor.getValue().onSuccess(sTestGURL.getSpec(), true, false); |
| |
| assertFalse(mController.isReadable(mTab)); |
| } |
| |
| @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. Make sure playback stops. |
| setIsScreenOnAndUnlocked(true); |
| mController.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); |
| verify(mPlayback).release(); |
| reset(mPlayback); |
| when(mPlayback.getMetadata()).thenReturn(mMetadata); |
| |
| // App goes back in foreground. Restore progress. |
| mController.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); |
| 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. |
| reset(mPlayback); |
| reset(mPlaybackHooks); |
| when(mPlayback.getMetadata()).thenReturn(mMetadata); |
| |
| mController.onApplicationStateChange(ApplicationState.HAS_PAUSED_ACTIVITIES); |
| mController.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); |
| verify(mPlaybackHooks, never()).createPlayback(any(), mPlaybackCallbackCaptor.capture()); |
| verify(mPlayback, never()).release(); |
| } |
| |
| @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(); |
| reset(mPlayback); |
| when(mPlayback.getMetadata()).thenReturn(mMetadata); |
| |
| // App goes back in foreground. Restore progress. |
| mController.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); |
| verify(mPlaybackHooks, times(2)).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 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); |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| 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); |
| 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); |
| 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); |
| 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); |
| 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. |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| verify(mPlayback, times(1)).play(); |
| |
| MockTab newTab = mTabModelSelector.addMockTab(); |
| mTabModelSelector |
| .getModel(false) |
| .setIndex( |
| mTabModelSelector.getModel(false).indexOf(newTab), |
| TabSelectionType.FROM_USER, |
| false); |
| // 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(sTestGURL); |
| 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(sTestGURL); |
| 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(sTestGURL); |
| 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 |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks).createPlayback(any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| |
| // 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, |
| /* skipLoadingTab= */ true); |
| |
| 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, |
| /* skipLoadingTab= */ true); |
| |
| verify(mPlayback).pause(); |
| verify(mPlayerCoordinator).hidePlayers(); |
| |
| TabModelUtils.selectTabById( |
| mTabModelSelector, |
| mTab.getId(), |
| TabSelectionType.FROM_USER, |
| /* skipLoadingTab= */ true); |
| verify(mPlayback, never()).play(); |
| verify(mPlayerCoordinator).restorePlayers(); |
| } |
| |
| // TODO(b/322052505): This test won't be necessary if we keep track of profile changes. |
| @Test |
| public void testNoRequestsIfProfileDestroyed() { |
| doReturn(false).when(mMockProfile).isNativeInitialized(); |
| mController = |
| new ReadAloudController( |
| mActivity, |
| mProfileSupplier, |
| mTabModelSelector.getModel(false), |
| mTabModelSelector.getModel(true), |
| mBottomSheetController, |
| mBrowserControlsSizer, |
| mLayoutManagerSupplier, |
| mActivityWindowAndroid, |
| mActivityLifecycleDispatcher); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| |
| // Check readability. |
| mController.maybeCheckReadability(sTestGURL); |
| // 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); |
| 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() { |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| |
| resolvePromises(); |
| |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| |
| onPlaybackSuccess(mPlayback); |
| verify(mPlayerCoordinator, times(1)) |
| .playbackReady(eq(mPlayback), eq(PlaybackListener.State.PLAYING)); |
| 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); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| |
| 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); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| |
| resolvePromises(); |
| histogram.assertExpected(); |
| } |
| |
| @Test |
| public void testTapToSeek_Success() { |
| // play tab |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| resolvePromises(); |
| verify(mPlaybackHooks, times(1)) |
| .createPlayback(Mockito.any(), mPlaybackCallbackCaptor.capture()); |
| onPlaybackSuccess(mPlayback); |
| } |
| |
| private void requestAndStartPlayback() { |
| mFakeTranslateBridge.setCurrentLanguage("en"); |
| mTab.setGurlOverrideForTesting(new GURL("https://en.wikipedia.org/wiki/Google")); |
| mController.playTab(mTab); |
| 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(); |
| } |
| } |