| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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.autofill.keyboard_accessory; |
| |
| import static android.content.res.Configuration.HARDKEYBOARDHIDDEN_UNDEFINED; |
| |
| import static org.hamcrest.CoreMatchers.equalTo; |
| import static org.hamcrest.CoreMatchers.is; |
| import static org.hamcrest.CoreMatchers.not; |
| import static org.hamcrest.CoreMatchers.notNullValue; |
| import static org.hamcrest.CoreMatchers.nullValue; |
| import static org.junit.Assert.assertThat; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessoryAction.GENERATE_PASSWORD_AUTOMATIC; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KEYBOARD_EXTENSION_STATE; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.EXTENDING_KEYBOARD; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.FLOATING_BAR; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.FLOATING_SHEET; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.HIDDEN; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.REPLACING_KEYBOARD; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.KeyboardExtensionState.WAITING_TO_REPLACE; |
| import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingProperties.SHOW_WHEN_VISIBLE; |
| import static org.chromium.chrome.browser.tab.Tab.INVALID_TAB_ID; |
| import static org.chromium.chrome.browser.tabmodel.TabLaunchType.FROM_BROWSER_ACTIONS; |
| import static org.chromium.chrome.browser.tabmodel.TabSelectionType.FROM_NEW; |
| import static org.chromium.chrome.browser.tabmodel.TabSelectionType.FROM_USER; |
| |
| import android.content.res.Configuration; |
| import android.graphics.drawable.Drawable; |
| import android.support.annotation.Nullable; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Config; |
| |
| import org.chromium.base.UserDataHost; |
| import org.chromium.base.metrics.test.ShadowRecordHistogram; |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| import org.chromium.chrome.browser.ActivityTabProvider; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.ChromeKeyboardVisibilityDelegate; |
| import org.chromium.chrome.browser.ChromeWindow; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.bar_component.KeyboardAccessoryCoordinator; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.data.KeyboardAccessoryData; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.data.KeyboardAccessoryData.AccessorySheetData; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.data.KeyboardAccessoryData.Action; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.data.KeyboardAccessoryData.UserInfo; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.data.PropertyProvider; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.sheet_component.AccessorySheetCoordinator; |
| import org.chromium.chrome.browser.autofill.keyboard_accessory.sheet_tabs.PasswordAccessorySheetCoordinator; |
| import org.chromium.chrome.browser.compositor.CompositorViewHolder; |
| import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tab.Tab.TabHidingType; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelector; |
| import org.chromium.chrome.test.util.browser.Features; |
| import org.chromium.chrome.test.util.browser.Features.EnableFeatures; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.ui.KeyboardVisibilityDelegate; |
| import org.chromium.ui.display.DisplayAndroid; |
| import org.chromium.ui.modelutil.PropertyModel; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** |
| * Controller tests for the root controller for interactions with the manual filling UI. |
| */ |
| @RunWith(BaseRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE, shadows = {ShadowRecordHistogram.class}) |
| @EnableFeatures({ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY, |
| ChromeFeatureList.AUTOFILL_KEYBOARD_ACCESSORY, |
| ChromeFeatureList.AUTOFILL_MANUAL_FALLBACK_ANDROID}) |
| public class ManualFillingControllerTest { |
| @Mock |
| private ChromeWindow mMockWindow; |
| @Mock |
| private ChromeActivity mMockActivity; |
| private WebContents mLastMockWebContents; |
| @Mock |
| private ViewGroup mMockContentView; |
| @Mock |
| private TabModelSelector mMockTabModelSelector; |
| @Mock |
| private Drawable mMockIcon; |
| @Mock |
| private android.content.res.Resources mMockResources; |
| @Mock |
| private ChromeKeyboardVisibilityDelegate mMockKeyboard; |
| @Mock |
| private KeyboardAccessoryCoordinator mMockKeyboardAccessory; |
| @Mock |
| private AccessorySheetCoordinator mMockAccessorySheet; |
| @Mock |
| private CompositorViewHolder mMockCompositorViewHolder; |
| |
| @Rule |
| public Features.JUnitProcessor mFeaturesProcessor = new Features.JUnitProcessor(); |
| |
| private final ManualFillingCoordinator mController = new ManualFillingCoordinator(); |
| private final ManualFillingMediator mMediator = mController.getMediatorForTesting(); |
| private final ManualFillingStateCache mCache = mMediator.getStateCacheForTesting(); |
| private final PropertyModel mModel = mMediator.getModelForTesting(); |
| private final UserDataHost mUserDataHost = new UserDataHost(); |
| |
| /** |
| * Helper class that provides shortcuts to providing and observing AccessorySheetData and |
| * Actions. |
| */ |
| private static class SheetProviderHelper { |
| private final PropertyProvider<Action[]> mActionListProvider = |
| new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC); |
| private final PropertyProvider<AccessorySheetData> mAccessorySheetDataProvider = |
| new PropertyProvider<>(); |
| |
| private final ArrayList<Action> mRecordedActions = new ArrayList<>(); |
| private int mRecordedActionNotifications; |
| private final AtomicReference<AccessorySheetData> mRecordedSheetData = |
| new AtomicReference<>(); |
| |
| /** |
| * Can be used to capture data from an observer. Retrieve the last captured data with |
| * {@link #getRecordedActions()} and {@link #getFirstRecordedAction()}. |
| * @param unusedTypeId Unused but necessary to enable use as method reference. |
| * @param item The {@link Action[]} provided by a {@link PropertyProvider<Action[]>}. |
| */ |
| void record(int unusedTypeId, Action[] item) { |
| mRecordedActionNotifications++; |
| mRecordedActions.clear(); |
| mRecordedActions.addAll(Arrays.asList(item)); |
| } |
| |
| /** |
| * Can be used to capture data from an observer. Retrieve the last captured data with |
| * {@link #getRecordedSheetData()} and {@link #getFirstRecordedPassword()}. |
| * @param unusedTypeId Unused but necessary to enable use as method reference. |
| * @param data The {@link AccessorySheetData} provided by a {@link PropertyProvider}. |
| */ |
| void record(int unusedTypeId, AccessorySheetData data) { |
| mRecordedSheetData.set(data); |
| } |
| |
| /** |
| * Uses the provider as returned by {@link #getActionListProvider()} to provide an Action. |
| * @param actionCaption The caption for the provided generation action. |
| */ |
| void provideAction(String actionCaption) { |
| provideActions(new Action[] { |
| new Action(actionCaption, GENERATE_PASSWORD_AUTOMATIC, action -> {})}); |
| } |
| |
| /** |
| * Uses the provider as returned by {@link #getActionListProvider()} to provide Actions. |
| * @param actions The {@link Action}s to provide. |
| */ |
| void provideActions(Action[] actions) { |
| mActionListProvider.notifyObservers(actions); |
| } |
| |
| /** |
| * Uses the provider as returned by {@link #getSheetDataProvider()} to provide an simple |
| * password sheet with one credential pair. |
| * @param passwordString The only provided password in the new sheet. |
| */ |
| void providePasswordSheet(String passwordString) { |
| AccessorySheetData sheetData = |
| new AccessorySheetData(FallbackSheetType.PASSWORD, "Passwords"); |
| UserInfo userInfo = new UserInfo(null); |
| userInfo.addField(new UserInfo.Field("(No username)", "No username", false, null)); |
| userInfo.addField(new UserInfo.Field(passwordString, "Password", true, null)); |
| sheetData.getUserInfoList().add(userInfo); |
| mAccessorySheetDataProvider.notifyObservers(sheetData); |
| } |
| |
| /** |
| * @return The {@link Action} last captured with {@link #record(int, Action[])}. |
| */ |
| Action getFirstRecordedAction() { |
| int firstNonTabLayoutAction = 1; |
| if (ChromeFeatureList.isEnabled(ChromeFeatureList.AUTOFILL_KEYBOARD_ACCESSORY)) { |
| firstNonTabLayoutAction = 0; |
| } |
| assert mRecordedActions.size() >= firstNonTabLayoutAction; |
| return mRecordedActions.get(firstNonTabLayoutAction); |
| } |
| |
| /** |
| * @return First password in a sheet captured by {@link #record(int, AccessorySheetData)}. |
| */ |
| String getFirstRecordedPassword() { |
| assert getRecordedSheetData() != null; |
| assert getRecordedSheetData().getUserInfoList() != null; |
| UserInfo info = getRecordedSheetData().getUserInfoList().get(0); |
| assert info != null; |
| assert info.getFields() != null; |
| assert info.getFields().size() > 1; |
| return info.getFields().get(1).getDisplayText(); |
| } |
| |
| /** |
| * @return True if {@link #record(int, Action[])} was notified. |
| */ |
| boolean hasRecordedActions() { |
| return mRecordedActionNotifications > 0; |
| } |
| |
| /** |
| * @return The {@link Action}s last captured with {@link #record(int, Action[])}. |
| */ |
| ArrayList<Action> getRecordedActions() { |
| return mRecordedActions; |
| } |
| |
| /** |
| * @return {@link AccessorySheetData} captured by {@link #record(int, AccessorySheetData)}. |
| */ |
| AccessorySheetData getRecordedSheetData() { |
| return mRecordedSheetData.get(); |
| } |
| |
| /** |
| * The returned provider is the same used by {@link #provideActions(Action[])}. |
| * @return A {@link PropertyProvider}. |
| */ |
| PropertyProvider<Action[]> getActionListProvider() { |
| return mActionListProvider; |
| } |
| |
| /** |
| * The returned provider is the same used by {@link #providePasswordSheet(String)}. |
| * @return A {@link PropertyProvider}. |
| */ |
| PropertyProvider<AccessorySheetData> getSheetDataProvider() { |
| return mAccessorySheetDataProvider; |
| } |
| } |
| |
| @Before |
| public void setUp() { |
| ShadowRecordHistogram.reset(); |
| MockitoAnnotations.initMocks(this); |
| KeyboardVisibilityDelegate.setInstance(mMockKeyboard); |
| when(mMockWindow.getKeyboardDelegate()).thenReturn(mMockKeyboard); |
| when(mMockWindow.getActivity()).thenReturn(new WeakReference<>(mMockActivity)); |
| when(mMockKeyboard.calculateKeyboardHeight(any())).thenReturn(0); |
| when(mMockActivity.getTabModelSelector()).thenReturn(mMockTabModelSelector); |
| when(mMockActivity.getActivityTabProvider()).thenReturn(mock(ActivityTabProvider.class)); |
| ChromeFullscreenManager fullscreenManager = new ChromeFullscreenManager(mMockActivity, 0); |
| when(mMockActivity.getFullscreenManager()).thenReturn(fullscreenManager); |
| when(mMockActivity.getCompositorViewHolder()).thenReturn(mMockCompositorViewHolder); |
| when(mMockActivity.getResources()).thenReturn(mMockResources); |
| when(mMockActivity.getPackageManager()) |
| .thenReturn(RuntimeEnvironment.application.getPackageManager()); |
| when(mMockActivity.findViewById(android.R.id.content)).thenReturn(mMockContentView); |
| when(mMockContentView.getRootView()).thenReturn(mock(View.class)); |
| mLastMockWebContents = mock(WebContents.class); |
| when(mMockActivity.getCurrentWebContents()).then(i -> mLastMockWebContents); |
| setContentAreaDimensions(2.f, 80, 300); |
| Configuration config = new Configuration(); |
| config.hardKeyboardHidden = HARDKEYBOARDHIDDEN_UNDEFINED; |
| when(mMockResources.getConfiguration()).thenReturn(config); |
| PasswordAccessorySheetCoordinator.IconProvider.getInstance().setIconForTesting(mMockIcon); |
| mController.initialize(mMockWindow, mMockKeyboardAccessory, mMockAccessorySheet); |
| } |
| |
| @Test |
| public void testCreatesValidSubComponents() { |
| assertThat(mController, is(notNullValue())); |
| assertThat(mMediator, is(notNullValue())); |
| assertThat(mCache, is(notNullValue())); |
| } |
| |
| @Test |
| public void testAddingNewTabIsAddedToAccessoryAndSheet() { |
| // Clear any calls that happened during initialization: |
| reset(mMockKeyboardAccessory); |
| reset(mMockAccessorySheet); |
| |
| // Create a new tab with a passwords tab: |
| addBrowserTab(mMediator, 1111, null); |
| |
| // Registering a provider creates a new passwords tab: |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| |
| // Now check the how many tabs were sent to the sub components: |
| ArgumentCaptor<KeyboardAccessoryData.Tab[]> barTabCaptor = |
| ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class); |
| ArgumentCaptor<KeyboardAccessoryData.Tab[]> sheetTabCaptor = |
| ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class); |
| verify(mMockKeyboardAccessory, times(2)).setTabs(barTabCaptor.capture()); |
| verify(mMockAccessorySheet, times(2)).setTabs(sheetTabCaptor.capture()); |
| |
| // Initial empty state: |
| assertThat(barTabCaptor.getAllValues().get(0).length, is(0)); |
| assertThat(sheetTabCaptor.getAllValues().get(0).length, is(0)); |
| |
| // When creating the password sheet: |
| assertThat(barTabCaptor.getAllValues().get(1).length, is(1)); |
| assertThat(sheetTabCaptor.getAllValues().get(1).length, is(1)); |
| } |
| |
| @Test |
| public void testAddingBrowserTabsCreatesValidAccessoryState() { |
| // Emulate adding a browser tab. Expect the model to have another entry. |
| Tab firstTab = addBrowserTab(mMediator, 1111, null); |
| ManualFillingState firstState = mCache.getStateFor(firstTab); |
| assertThat(firstState, notNullValue()); |
| |
| // Emulate adding a second browser tab. Expect the model to have another entry. |
| Tab secondTab = addBrowserTab(mMediator, 2222, firstTab); |
| ManualFillingState secondState = mCache.getStateFor(secondTab); |
| assertThat(secondState, notNullValue()); |
| |
| assertThat(firstState, not(equalTo(secondState))); |
| } |
| |
| @Test |
| public void testPasswordItemsPersistAfterSwitchingBrowserTabs() { |
| SheetProviderHelper firstTabHelper = new SheetProviderHelper(); |
| SheetProviderHelper secondTabHelper = new SheetProviderHelper(); |
| |
| // Simulate opening a new tab which automatically triggers the registration: |
| Tab firstTab = addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(firstTabHelper.getSheetDataProvider()); |
| getStateForBrowserTab().getPasswordSheetDataProvider().addObserver(firstTabHelper::record); |
| firstTabHelper.providePasswordSheet("FirstPassword"); |
| assertThat(firstTabHelper.getFirstRecordedPassword(), is("FirstPassword")); |
| |
| // Simulate creating a second tab: |
| Tab secondTab = addBrowserTab(mMediator, 2222, firstTab); |
| mController.registerPasswordProvider(secondTabHelper.getSheetDataProvider()); |
| getStateForBrowserTab().getPasswordSheetDataProvider().addObserver(secondTabHelper::record); |
| secondTabHelper.providePasswordSheet("SecondPassword"); |
| assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword")); |
| |
| // Simulate switching back to the first tab: |
| switchBrowserTab(mMediator, /*from=*/secondTab, /*to=*/firstTab); |
| assertThat(firstTabHelper.getFirstRecordedPassword(), is("FirstPassword")); |
| |
| // And back to the second: |
| switchBrowserTab(mMediator, /*from=*/firstTab, /*to=*/secondTab); |
| assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword")); |
| } |
| |
| @Test |
| public void testKeyboardAccessoryActionsPersistAfterSwitchingBrowserTabs() { |
| SheetProviderHelper firstTabHelper = new SheetProviderHelper(); |
| SheetProviderHelper secondTabHelper = new SheetProviderHelper(); |
| |
| // Simulate opening a new tab which automatically triggers the registration: |
| Tab firstTab = addBrowserTab(mMediator, 1111, null); |
| mController.registerActionProvider(firstTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getActionsProvider().addObserver(firstTabHelper::record); |
| firstTabHelper.provideAction("Generate Password"); |
| assertThat(firstTabHelper.getFirstRecordedAction().getCaption(), is("Generate Password")); |
| |
| // Simulate creating a second tab: |
| Tab secondTab = addBrowserTab(mMediator, 2222, firstTab); |
| mController.registerActionProvider(secondTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record); |
| secondTabHelper.provideActions(new Action[0]); |
| assertThat(secondTabHelper.getRecordedActions().size(), is(0)); |
| |
| // Simulate switching back to the first tab: |
| switchBrowserTab(mMediator, /*from=*/secondTab, /*to=*/firstTab); |
| assertThat(firstTabHelper.getFirstRecordedAction().getCaption(), is("Generate Password")); |
| |
| // And back to the second: |
| switchBrowserTab(mMediator, /*from=*/firstTab, /*to=*/secondTab); |
| assertThat(secondTabHelper.getRecordedActions().size(), is(0)); |
| } |
| |
| @Test |
| public void testPasswordTabRestoredWhenSwitchingBrowserTabs() { |
| // Clear any calls that happened during initialization: |
| reset(mMockKeyboardAccessory); |
| reset(mMockAccessorySheet); |
| |
| // Create a new tab: |
| Tab firstTab = addBrowserTab(mMediator, 1111, null); |
| |
| // Create a new passwords tab: |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| |
| // Simulate creating a second tab without any tabs: |
| Tab secondTab = addBrowserTab(mMediator, 2222, firstTab); |
| |
| // Simulate switching back to the first tab: |
| switchBrowserTab(mMediator, /*from=*/secondTab, /*to=*/firstTab); |
| |
| // And back to the second: |
| switchBrowserTab(mMediator, /*from=*/firstTab, /*to=*/secondTab); |
| |
| ArgumentCaptor<KeyboardAccessoryData.Tab[]> barTabCaptor = |
| ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class); |
| ArgumentCaptor<KeyboardAccessoryData.Tab[]> sheetTabCaptor = |
| ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class); |
| verify(mMockKeyboardAccessory, times(5)).setTabs(barTabCaptor.capture()); |
| verify(mMockAccessorySheet, times(5)).setTabs(sheetTabCaptor.capture()); |
| |
| // Initial empty state: |
| assertThat(barTabCaptor.getAllValues().get(0).length, is(0)); |
| assertThat(sheetTabCaptor.getAllValues().get(0).length, is(0)); |
| |
| // When creating the password sheet in 1st tab: |
| assertThat(barTabCaptor.getAllValues().get(1).length, is(1)); |
| assertThat(sheetTabCaptor.getAllValues().get(1).length, is(1)); |
| |
| // When switching to empty 2nd tab: |
| assertThat(barTabCaptor.getAllValues().get(2).length, is(0)); |
| assertThat(sheetTabCaptor.getAllValues().get(2).length, is(0)); |
| |
| // When switching back to 1st tab with password sheet: |
| assertThat(barTabCaptor.getAllValues().get(3).length, is(1)); |
| assertThat(sheetTabCaptor.getAllValues().get(3).length, is(1)); |
| |
| // When switching back to empty 2nd tab: |
| assertThat(barTabCaptor.getAllValues().get(4).length, is(0)); |
| assertThat(sheetTabCaptor.getAllValues().get(4).length, is(0)); |
| } |
| |
| @Test |
| public void testPasswordTabRestoredWhenClosingTabIsUndone() { |
| // Clear any calls that happened during initialization: |
| reset(mMockKeyboardAccessory); |
| reset(mMockAccessorySheet); |
| |
| // Create a new tab with a passwords tab: |
| Tab tab = addBrowserTab(mMediator, 1111, null); |
| |
| // Create a new passwords tab: |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| |
| // Simulate closing the tab (uncommitted): |
| mMediator.getTabModelObserverForTesting().willCloseTab(tab, false); |
| mMediator.getTabObserverForTesting().onHidden(tab, TabHidingType.CHANGED_TABS); |
| getStateForBrowserTab().getWebContentsObserverForTesting().wasHidden(); |
| // The state should be kept if the closure wasn't committed. |
| assertThat(getStateForBrowserTab().getPasswordAccessorySheet(), is(not(nullValue()))); |
| mLastMockWebContents = null; |
| |
| // Simulate undo closing the tab and selecting it: |
| mMediator.getTabModelObserverForTesting().tabClosureUndone(tab); |
| switchBrowserTab(mMediator, null, tab); |
| |
| // Simulate closing the tab and committing to it (i.e. wait out undo message): |
| WebContents oldWebContents = mLastMockWebContents; |
| closeBrowserTab(mMediator, tab); |
| // The state should be cleaned up, now that it was committed. |
| assertThat(mCache.getStateFor(oldWebContents).getPasswordAccessorySheet(), is(nullValue())); |
| |
| ArgumentCaptor<KeyboardAccessoryData.Tab[]> barTabCaptor = |
| ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class); |
| ArgumentCaptor<KeyboardAccessoryData.Tab[]> sheetTabCaptor = |
| ArgumentCaptor.forClass(KeyboardAccessoryData.Tab[].class); |
| verify(mMockKeyboardAccessory, times(4)).setTabs(barTabCaptor.capture()); |
| verify(mMockAccessorySheet, times(4)).setTabs(sheetTabCaptor.capture()); |
| |
| // Initial empty state: |
| assertThat(barTabCaptor.getAllValues().get(0).length, is(0)); |
| assertThat(sheetTabCaptor.getAllValues().get(0).length, is(0)); |
| |
| // When creating the password sheet: |
| assertThat(barTabCaptor.getAllValues().get(1).length, is(1)); |
| assertThat(sheetTabCaptor.getAllValues().get(1).length, is(1)); |
| |
| // When restoring the tab: |
| assertThat(barTabCaptor.getAllValues().get(2).length, is(1)); |
| assertThat(sheetTabCaptor.getAllValues().get(2).length, is(1)); |
| |
| // When committing to close the tab: |
| assertThat(barTabCaptor.getAllValues().get(3).length, is(0)); |
| assertThat(sheetTabCaptor.getAllValues().get(3).length, is(0)); |
| } |
| |
| @Test |
| public void testTreatNeverProvidedActionsAsEmptyActionList() { |
| SheetProviderHelper firstTabHelper = new SheetProviderHelper(); |
| SheetProviderHelper secondTabHelper = new SheetProviderHelper(); |
| |
| // Open a tab. |
| Tab tab = addBrowserTab(mMediator, 1111, null); |
| // Add an action provider that never provides any actions. |
| mController.registerActionProvider(new PropertyProvider<>(GENERATE_PASSWORD_AUTOMATIC)); |
| getStateForBrowserTab().getActionsProvider().addObserver(firstTabHelper::record); |
| |
| // Create a new tab with an action: |
| Tab secondTab = addBrowserTab(mMediator, 1111, tab); |
| mController.registerActionProvider(secondTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record); |
| secondTabHelper.provideAction("Test Action"); |
| assertThat(secondTabHelper.getFirstRecordedAction().getCaption(), is("Test Action")); |
| |
| // Switching back should notify the accessory about the still empty state of the accessory. |
| switchBrowserTab(mMediator, secondTab, tab); |
| assertThat(firstTabHelper.hasRecordedActions(), is(true)); |
| assertThat(firstTabHelper.getRecordedActions().size(), is(0)); |
| } |
| |
| @Test |
| public void testUpdatesInactiveAccessory() { |
| SheetProviderHelper delayedTabHelper = new SheetProviderHelper(); |
| SheetProviderHelper secondTabHelper = new SheetProviderHelper(); |
| |
| // Open a tab. |
| Tab delayedTab = addBrowserTab(mMediator, 1111, null); |
| // Add an action provider that hasn't provided actions yet. |
| mController.registerActionProvider(delayedTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getActionsProvider().addObserver(delayedTabHelper::record); |
| assertThat(delayedTabHelper.hasRecordedActions(), is(false)); |
| |
| // Create and switch to a new tab: |
| Tab secondTab = addBrowserTab(mMediator, 2222, delayedTab); |
| mController.registerActionProvider(secondTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record); |
| |
| // And provide data to the active browser tab. |
| secondTabHelper.provideAction("Test Action"); |
| // Now, have the delayed provider provide data for the backgrounded browser tab. |
| delayedTabHelper.provideAction("Delayed"); |
| |
| // The current tab should not be influenced by the delayed provider. |
| assertThat(secondTabHelper.getRecordedActions().size(), is(1)); |
| assertThat(secondTabHelper.getFirstRecordedAction().getCaption(), is("Test Action")); |
| |
| // Switching tabs back should only show the action that was received in the background. |
| switchBrowserTab(mMediator, secondTab, delayedTab); |
| assertThat(delayedTabHelper.getRecordedActions().size(), is(1)); |
| assertThat(delayedTabHelper.getFirstRecordedAction().getCaption(), is("Delayed")); |
| } |
| |
| @Test |
| public void testDestroyingTabCleansModelForThisTab() { |
| // Clear any calls that happened during initialization: |
| reset(mMockKeyboardAccessory); |
| reset(mMockAccessorySheet); |
| SheetProviderHelper firstTabHelper = new SheetProviderHelper(); |
| SheetProviderHelper secondTabHelper = new SheetProviderHelper(); |
| |
| // Simulate opening a new tab: |
| Tab firstTab = addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(firstTabHelper.getSheetDataProvider()); |
| mController.registerActionProvider(firstTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getPasswordSheetDataProvider().addObserver(firstTabHelper::record); |
| getStateForBrowserTab().getActionsProvider().addObserver(firstTabHelper::record); |
| firstTabHelper.providePasswordSheet("FirstPassword"); |
| firstTabHelper.provideAction("2BDestroyed"); |
| |
| // Create and switch to a new tab: (because destruction shouldn't rely on tab to be active) |
| Tab secondTab = addBrowserTab(mMediator, 2222, firstTab); |
| mController.registerPasswordProvider(secondTabHelper.getSheetDataProvider()); |
| mController.registerActionProvider(secondTabHelper.getActionListProvider()); |
| getStateForBrowserTab().getPasswordSheetDataProvider().addObserver(secondTabHelper::record); |
| getStateForBrowserTab().getActionsProvider().addObserver(secondTabHelper::record); |
| secondTabHelper.providePasswordSheet("SecondPassword"); |
| secondTabHelper.provideAction("2BKept"); |
| |
| // The newly created tab should be valid. |
| assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword")); |
| assertThat(secondTabHelper.getFirstRecordedAction().getCaption(), is("2BKept")); |
| |
| // Request destruction of the first Tab: |
| mMediator.getTabObserverForTesting().onDestroyed(firstTab); |
| |
| // The current tab should not be influenced by the destruction... |
| assertThat(secondTabHelper.getFirstRecordedPassword(), is("SecondPassword")); |
| assertThat(secondTabHelper.getFirstRecordedAction().getCaption(), is("2BKept")); |
| assertThat(getStateForBrowserTab(), is(mCache.getStateFor(secondTab))); |
| // ... but the other tab's data should be gone. |
| assertThat(mCache.getStateFor(firstTab).getActionsProvider(), nullValue()); |
| assertThat(mCache.getStateFor(firstTab).getPasswordAccessorySheet(), nullValue()); |
| } |
| |
| @Test |
| public void testDisplaysAccessoryOnlyWhenSpaceIsSufficient() { |
| reset(mMockKeyboardAccessory); |
| |
| addBrowserTab(mMediator, 1234, null); |
| SheetProviderHelper tabHelper = new SheetProviderHelper(); |
| mController.registerPasswordProvider(tabHelper.getSheetDataProvider()); |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| |
| // Show the accessory bar for the default dimensions (300x80@2.f). |
| mController.showWhenKeyboardIsVisible(); |
| verify(mMockKeyboardAccessory).show(); |
| |
| // The accessory is shown and the content area plus bar size don't exceed the threshold. |
| simulateLayoutSizeChange(3.f, 180, 80); |
| |
| verify(mMockKeyboardAccessory, never()).dismiss(); |
| } |
| |
| @Test |
| public void testHidesAccessoryAfterRotation() { |
| reset(mMockKeyboardAccessory); |
| setContentAreaDimensions(2.f, 180, 320); |
| addBrowserTab(mMediator, 1234, null); |
| SheetProviderHelper tabHelper = new SheetProviderHelper(); |
| mController.registerPasswordProvider(tabHelper.getSheetDataProvider()); |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| |
| mController.showWhenKeyboardIsVisible(); |
| setContentAreaDimensions(2.f, 180, 220); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 540, 360, 0, 0, 640, 360); |
| verify(mMockKeyboardAccessory).show(); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN))); |
| |
| // Rotating the screen causes a relayout: |
| setContentAreaDimensions(2.f, 320, 80, Surface.ROTATION_90); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 160, 640, 0, 0, 540, 360); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN)); |
| } |
| |
| @Test |
| public void testDisplaysAccessoryOnlyWhenVerticalSpaceIsSufficient() { |
| reset(mMockKeyboardAccessory); |
| addBrowserTab(mMediator, 1234, null); |
| SheetProviderHelper tabHelper = new SheetProviderHelper(); |
| mController.registerPasswordProvider(tabHelper.getSheetDataProvider()); |
| when(mMockKeyboard.isSoftKeyboardShowing(eq(mMockActivity), any())).thenReturn(true); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| |
| // Show the accessory bar for the dimensions exactly at the threshold: 300x80@2.f. |
| simulateLayoutSizeChange(2.0f, 300, 80); |
| mController.showWhenKeyboardIsVisible(); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN))); |
| verify(mMockKeyboardAccessory).show(); |
| |
| // The height is now reduced by the 48dp high accessory -- it should remain visible. |
| simulateLayoutSizeChange(2.0f, 300, 32); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN))); |
| |
| // Use a height that is too small but with a valid width (e.g. resized multi-window window). |
| simulateLayoutSizeChange(2.0f, 300, 31); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN)); |
| } |
| |
| @Test |
| public void testDisplaysAccessoryOnlyWhenHorizontalSpaceIsSufficient() { |
| reset(mMockKeyboardAccessory); |
| |
| addBrowserTab(mMediator, 1234, null); |
| SheetProviderHelper tabHelper = new SheetProviderHelper(); |
| mController.registerPasswordProvider(tabHelper.getSheetDataProvider()); |
| when(mMockKeyboard.isSoftKeyboardShowing(eq(mMockActivity), any())).thenReturn(true); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| |
| // Show the accessory bar for the dimensions exactly at the threshold: 180x100@2.f. |
| simulateLayoutSizeChange(2.0f, 180, 100); |
| mController.showWhenKeyboardIsVisible(); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), not(is(HIDDEN))); |
| |
| // Use a width that is too small but with a valid height (e.g. resized multi-window window). |
| simulateLayoutSizeChange(2.0f, 179, 100); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN)); |
| } |
| |
| @Test |
| public void testRestrictsSheetSizeIfVerticalSpaceChanges() { |
| addBrowserTab(mMediator, 1234, null); |
| // Resize the screen from 300x80@2.f to 300x200@2.f. |
| setContentAreaDimensions(2.f, 200, 300); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 400, 600, 0, 0, 160, 600); |
| |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| when(mMockAccessorySheet.getHeight()).thenReturn(200); // Return height of a large keyboard. |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| reset(mMockKeyboardAccessory, mMockAccessorySheet, mMockKeyboard); |
| |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| when(mMockAccessorySheet.isShown()).thenReturn(true); |
| when(mMockAccessorySheet.getHeight()).thenReturn(200); // Return height of a large keyboard. |
| |
| // Set layout as if it was rotated: 200x300@2f minus the 100dp+48dp high filling ui. |
| setContentAreaDimensions(2.f, 300, 52); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 600, 102, 0, 0, 320, 600); |
| verify(mMockAccessorySheet).setHeight(144); // == 2f * (200dp - (80dp - 48dp)) |
| } |
| |
| @Test |
| public void testClosingTabDoesntAffectUnitializedComponents() { |
| // A leftover tab is closed before the filling component could pick up the active tab. |
| closeBrowserTab(mMediator, mock(Tab.class)); |
| |
| // Without any tab, there should be no state that would allow creating a sheet. |
| assertThat(mMediator.getOrCreatePasswordSheet(), is(nullValue())); |
| } |
| |
| @Test |
| public void testIsFillingViewShownReturnsTargetValueAheadOfComponentUpdate() { |
| // After initialization with one tab, the accessory sheet is closed. |
| addBrowserTab(mMediator, 1234, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false); |
| assertThat(mController.isFillingViewShown(null), is(false)); |
| |
| // As soon as active tab and keyboard change, |isFillingViewShown| returns the expected |
| // state - even if the sheet component wasn't updated yet. |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| when(mMockKeyboard.isSoftKeyboardShowing(eq(mMockActivity), any())).thenReturn(false); |
| assertThat(mController.isFillingViewShown(null), is(true)); |
| |
| // The layout change impacts the component, but not the coordinator method. |
| mMediator.onLayoutChange(null, 0, 0, 0, 0, 0, 0, 0, 0); |
| assertThat(mController.isFillingViewShown(null), is(true)); |
| } |
| |
| @Test |
| public void testTransitionToHiddenHidesEverything() { |
| addBrowserTab(mMediator, 1111, null); |
| // Make sure the model is in a non-HIDDEN state first. |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| |
| // Set the model HIDDEN. This should update keyboard and subcomponents. |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(HIDDEN)); |
| |
| verify(mMockAccessorySheet).hide(); |
| verify(mMockKeyboardAccessory).closeActiveTab(); |
| verify(mMockKeyboardAccessory).dismiss(); |
| verify(mMockCompositorViewHolder).requestLayout(); // Triggered as if it was a keyboard. |
| } |
| |
| @Test |
| public void testTransitionToExtendingShowsBarAndHidesSheet() { |
| addBrowserTab(mMediator, 1111, null); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| // Make sure the model is in a non-EXTENDING_KEYBOARD state first. |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| |
| // Set the model EXTENDING_KEYBOARD. This should update keyboard and subcomponents. |
| mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD)); |
| |
| verify(mMockAccessorySheet).hide(); |
| verify(mMockKeyboardAccessory).closeActiveTab(); |
| verify(mMockKeyboardAccessory).show(); |
| } |
| |
| @Test |
| public void testTransitionToFloatingBarShowsBarAndHidesSheet() { |
| addBrowserTab(mMediator, 1111, null); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| // Make sure the model is in a non-FLOATING_BAR state first. |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| |
| // Set the model FLOATING_BAR. This should update keyboard and subcomponents. |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR)); |
| |
| verify(mMockKeyboard).showKeyboard(any()); |
| verify(mMockAccessorySheet).hide(); |
| verify(mMockKeyboardAccessory).closeActiveTab(); |
| verify(mMockCompositorViewHolder).requestLayout(); // Triggered as if it was a keyboard. |
| verify(mMockKeyboardAccessory).show(); |
| } |
| |
| @Test |
| public void testTransitionToFloatingSheetShowsBarAndSheet() { |
| addBrowserTab(mMediator, 1111, null); |
| // Make sure the model is in a non-FLOATING_SHEET state first. |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| |
| // Set the model FLOATING_SHEET. This should update keyboard and subcomponents. |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_SHEET)); |
| |
| verify(mMockKeyboard).showKeyboard(any()); |
| verify(mMockAccessorySheet).show(); |
| verify(mMockKeyboardAccessory).show(); |
| } |
| |
| @Test |
| public void testTransitionToReplacingShowsBarAndSheet() { |
| addBrowserTab(mMediator, 1111, null); |
| // Make sure the model is in a non-REPLACING_KEYBOARD state first. |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| |
| // Set the model REPLACING_KEYBOARD. This should update keyboard and subcomponents. |
| mModel.set(KEYBOARD_EXTENSION_STATE, REPLACING_KEYBOARD); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(REPLACING_KEYBOARD)); |
| |
| verify(mMockAccessorySheet).show(); |
| verify(mMockKeyboardAccessory).show(); |
| } |
| |
| @Test |
| public void testTransitionToWaitingHidesKeyboardAndShowsBarAndSheet() { |
| addBrowserTab(mMediator, 1111, null); |
| // Make sure the model is in a non-REPLACING_KEYBOARD state first. |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| |
| // Set the model REPLACING_KEYBOARD. This should update keyboard and subcomponents. |
| mModel.set(KEYBOARD_EXTENSION_STATE, WAITING_TO_REPLACE); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(WAITING_TO_REPLACE)); |
| |
| verify(mMockKeyboard).hideSoftKeyboardOnly(any()); |
| verify(mMockAccessorySheet, never()).hide(); |
| verify(mMockKeyboardAccessory, never()).closeActiveTab(); |
| verify(mMockKeyboardAccessory).show(); |
| } |
| |
| @Test |
| public void testTransitionFromHiddenToExtendingByKeyboard() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| |
| // Showing the keyboard should now trigger a transition into EXTENDING state. |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180); |
| |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD)); |
| } |
| |
| @Test |
| public void testTransitionFromHiddenToExtendingByAvailableData() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| |
| // Showing the keyboard should now trigger a transition into EXTENDING state. |
| mController.showWhenKeyboardIsVisible(); |
| |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD)); |
| } |
| |
| @Test |
| public void testTransitionFromHiddenToFloatingBarByAvailableData() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(KEYBOARD_EXTENSION_STATE, HIDDEN); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| |
| // Showing the keyboard should now trigger a transition into EXTENDING state. |
| mController.showWhenKeyboardIsVisible(); |
| |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR)); |
| } |
| |
| @Test |
| public void testTransitionFromFloatingBarToExtendingByKeyboard() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| |
| // Simulate opening a keyboard: |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180); |
| |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD)); |
| } |
| |
| @Test |
| public void testTransitionFromFloatingBarToFloatingSheetByActivatingTab() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_BAR); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| |
| // Simulate selecting a bottom sheet: |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| mMediator.onChangeAccessorySheet(0); |
| |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_SHEET)); |
| } |
| |
| @Test |
| public void testTransitionFromFloatingSheetToFloatingBarByClosingSheet() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, FLOATING_SHEET); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| |
| // Simulate closing the bottom sheet: |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false); |
| mMediator.onCloseAccessorySheet(); |
| |
| // This will cause a temporary floating sheet state which allows a nicer animation: |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_BAR)); |
| } |
| |
| @Test |
| public void testTransitionFromExtendingToReplacingKeyboardByActivatingSheet() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, EXTENDING_KEYBOARD); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| |
| // Simulate selecting a bottom sheet: |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| mMediator.onChangeAccessorySheet(0); |
| |
| // Now the filling component waits for the keyboard to disappear before changing the stat: |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(WAITING_TO_REPLACE)); |
| // Layout changes but the keyboard is still there, so nothing happens: |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 90); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(WAITING_TO_REPLACE)); |
| |
| // The keyboard finally hides completely and the state changes to REPLACING. |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(false); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180); |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(REPLACING_KEYBOARD)); |
| } |
| |
| @Test |
| public void testTransitionFromReplacingKeyboardToExtendingByClosingSheet() { |
| // Prepare a tab and register a new tab, so there is a reason to display the bar. |
| addBrowserTab(mMediator, 1111, null); |
| mController.registerPasswordProvider(new PropertyProvider<>()); |
| mModel.set(SHOW_WHEN_VISIBLE, true); |
| mModel.set(KEYBOARD_EXTENSION_STATE, REPLACING_KEYBOARD); |
| reset(mMockKeyboard, mMockKeyboardAccessory, mMockAccessorySheet); |
| when(mMockKeyboardAccessory.empty()).thenReturn(false); |
| when(mMockKeyboardAccessory.isShown()).thenReturn(true); |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(true); |
| |
| // Simulate closing the bottom sheet: |
| when(mMockKeyboardAccessory.hasActiveTab()).thenReturn(false); |
| mMediator.onCloseAccessorySheet(); |
| |
| // This will cause a temporary floating sheet state which allows a nicer animation: |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(FLOATING_SHEET)); |
| // This must trigger the keyboard to open, so the transition into EXTENDING can proceed. |
| verify(mMockKeyboard).showKeyboard(any()); |
| |
| // Simulate the keyboard opening: |
| when(mMockKeyboard.isSoftKeyboardShowing(any(), any())).thenReturn(true); |
| mMediator.onLayoutChange(mMockContentView, 0, 0, 320, 90, 0, 0, 320, 180); |
| |
| assertThat(mModel.get(KEYBOARD_EXTENSION_STATE), is(EXTENDING_KEYBOARD)); |
| } |
| |
| /** |
| * Creates a tab and calls the observer events as if it was just created and switched to. |
| * @param mediator The {@link ManualFillingMediator} whose observers should be triggered. |
| * @param id The id of the new browser tab. |
| * @param lastTab A previous mocked {@link Tab} to be hidden. Needs |getId()|. May be null. |
| * @return Returns a mock of the newly added {@link Tab}. Provides |getId()|. |
| */ |
| private Tab addBrowserTab(ManualFillingMediator mediator, int id, @Nullable Tab lastTab) { |
| int lastId = INVALID_TAB_ID; |
| if (lastTab != null) { |
| lastId = lastTab.getId(); |
| mediator.getTabObserverForTesting().onHidden(lastTab, TabHidingType.CHANGED_TABS); |
| mCache.getStateFor(mLastMockWebContents).getWebContentsObserverForTesting().wasHidden(); |
| } |
| Tab tab = mock(Tab.class); |
| when(tab.getId()).thenReturn(id); |
| when(tab.getUserDataHost()).thenReturn(mUserDataHost); |
| mLastMockWebContents = mock(WebContents.class); |
| when(tab.getWebContents()).thenReturn(mLastMockWebContents); |
| mCache.getStateFor(tab).getWebContentsObserverForTesting().wasShown(); |
| when(tab.getContentView()).thenReturn(mMockContentView); |
| when(mMockActivity.getActivityTabProvider().get()).thenReturn(tab); |
| when(mMockTabModelSelector.getCurrentTab()).thenReturn(tab); |
| mediator.getTabModelObserverForTesting().didAddTab(tab, FROM_BROWSER_ACTIONS); |
| mediator.getTabObserverForTesting().onShown(tab, FROM_NEW); |
| mediator.getTabModelObserverForTesting().didSelectTab(tab, FROM_NEW, lastId); |
| setContentAreaDimensions(2.f, 300, 80); |
| return tab; |
| } |
| |
| /** |
| * Simulates switching to a different tab by calling observer events on the given |mediator|. |
| * @param mediator The mediator providing the observer instances. |
| * @param from The mocked {@link Tab} to be switched from. Needs |getId()|. May be null. |
| * @param to The mocked {@link Tab} to be switched to. Needs |getId()|. |
| */ |
| private void switchBrowserTab(ManualFillingMediator mediator, @Nullable Tab from, Tab to) { |
| int lastId = INVALID_TAB_ID; |
| if (from != null) { |
| lastId = from.getId(); |
| mediator.getTabObserverForTesting().onHidden(from, TabHidingType.CHANGED_TABS); |
| mCache.getStateFor(mLastMockWebContents).getWebContentsObserverForTesting().wasHidden(); |
| } |
| mLastMockWebContents = to.getWebContents(); |
| mCache.getStateFor(to).getWebContentsObserverForTesting().wasShown(); |
| when(mMockTabModelSelector.getCurrentTab()).thenReturn(to); |
| mediator.getTabModelObserverForTesting().didSelectTab(to, FROM_USER, lastId); |
| mediator.getTabObserverForTesting().onShown(to, FROM_USER); |
| } |
| |
| /** |
| * Simulates destroying the given tab by calling observer events on the given |mediator|. |
| * @param mediator The mediator providing the observer instances. |
| * @param tabToBeClosed The mocked {@link Tab} to be closed. Needs |getId()|. |
| */ |
| private void closeBrowserTab(ManualFillingMediator mediator, Tab tabToBeClosed) { |
| mediator.getTabModelObserverForTesting().willCloseTab(tabToBeClosed, false); |
| mediator.getTabObserverForTesting().onHidden(tabToBeClosed, TabHidingType.CHANGED_TABS); |
| mCache.getStateFor(mLastMockWebContents).getWebContentsObserverForTesting().wasHidden(); |
| mLastMockWebContents = null; |
| mediator.getTabModelObserverForTesting().tabClosureCommitted(tabToBeClosed); |
| mediator.getTabObserverForTesting().onDestroyed(tabToBeClosed); |
| } |
| |
| private void setContentAreaDimensions(float density, int widthDp, int heightDp) { |
| setContentAreaDimensions(density, widthDp, heightDp, Surface.ROTATION_0); |
| } |
| |
| private void setContentAreaDimensions(float density, int widthDp, int heightDp, int rotation) { |
| DisplayAndroid mockDisplay = mock(DisplayAndroid.class); |
| when(mockDisplay.getDipScale()).thenReturn(density); |
| when(mockDisplay.getRotation()).thenReturn(rotation); |
| when(mMockWindow.getDisplay()).thenReturn(mockDisplay); |
| when(mLastMockWebContents.getHeight()).thenReturn(heightDp); |
| when(mLastMockWebContents.getWidth()).thenReturn(widthDp); |
| // Return the correct keyboard_accessory_height for the current density: |
| when(mMockResources.getDimensionPixelSize(anyInt())).thenReturn((int) (density * 48)); |
| } |
| |
| /** |
| * This function initializes mocks and then calls the given mediator events in the order of a |
| * layout resize event (e.g. when extending/shrinking a multi-window window). |
| * It mains sets the {@link WebContents} size and calls |onLayoutChange| with the new bounds. |
| * @param density The logical screen density (e.g. 1.f). |
| * @param width The new {@link WebContents} width in dp. |
| * @param height The new {@link WebContents} height in dp. |
| */ |
| private void simulateLayoutSizeChange(float density, int width, int height) { |
| int oldHeight = mLastMockWebContents.getHeight(); |
| int oldWidth = mLastMockWebContents.getWidth(); |
| int newHeight = (int) (density * height); |
| int newWidth = (int) (density * width); |
| setContentAreaDimensions(2.f, width, height); |
| mMediator.onLayoutChange( |
| mMockContentView, 0, 0, newWidth, newHeight, 0, 0, oldWidth, oldHeight); |
| } |
| |
| /** |
| * @return A {@link ManualFillingState} that is never null. |
| */ |
| private ManualFillingState getStateForBrowserTab() { |
| assert mLastMockWebContents != null : "In testing, WebContents should never be null!"; |
| return mCache.getStateFor(mLastMockWebContents); |
| } |
| } |