From aee4013aee3faf485b473d28dbd7c8ca52b3a318 Mon Sep 17 00:00:00 2001 From: mike a Date: Mon, 13 Feb 2023 22:45:25 -0800 Subject: [PATCH] =?utf8?q?Bug=201816516=20=E2=80=93=C2=A0improve=20menu=20?= =?utf8?q?positioning=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../browser/menu2/BrowserMenuController.kt | 40 +- ...ositioningData.kt => BrowserMenuPositioning.kt} | 6 + .../components/browser/menu2/ext/PopupWindow.kt | 44 -- .../browser/menu2/BrowserMenuControllerTest.kt | 48 ++ .../menu2/ext/BrowserMenuPositioningTest.kt | 856 ++++++++++++++++++++ .../browser/menu2/ext/PopupWindowTest.kt | 859 --------------------- 6 files changed, 946 insertions(+), 907 deletions(-) rename mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/{MenuPositioningData.kt => BrowserMenuPositioning.kt} (98%) delete mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt delete mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt index 54502acfb0e1..cd1b22e598ec 100644 --- a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt +++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt @@ -4,10 +4,14 @@ package mozilla.components.browser.menu2 +import android.view.Gravity import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.PopupWindow -import mozilla.components.browser.menu2.ext.displayPopup +import androidx.annotation.VisibleForTesting +import androidx.core.widget.PopupWindowCompat +import mozilla.components.browser.menu2.ext.MenuPositioningData +import mozilla.components.browser.menu2.ext.inferMenuPositioningData import mozilla.components.browser.menu2.view.MenuView import mozilla.components.concept.menu.MenuController import mozilla.components.concept.menu.MenuStyle @@ -64,7 +68,14 @@ class BrowserMenuController( view.onDismiss = ::dismiss view.onReopenMenu = ::reopenMenu setOnDismissListener(menuDismissListener) - displayPopup(view, anchor, orientation, style) + inferMenuPositioningData( + containerView = view, + anchor = anchor, + style = style, + orientation = orientation, + )?.let { + displayPopup(it) + } }.also { currentPopupInfo = PopupMenuInfo( window = it, @@ -91,8 +102,15 @@ class BrowserMenuController( view.submitList(null) // Display the new nested list view.submitList(nested?.subMenuItems ?: menuCandidates) - // Reopen the menu - displayPopup(view, info.anchor, info.orientation, style) + // Attempt tp reopen the menu + inferMenuPositioningData( + containerView = view, + anchor = info.anchor, + style = style, + orientation = info.orientation, + )?.let { + displayPopup(it) + } } currentPopupInfo = info.copy(nested = nested) } @@ -143,3 +161,17 @@ class BrowserMenuController( val nested: NestedMenuCandidate? = null, ) } + +/** + * Show a [PopupWindow] given the positioning data. + */ +@VisibleForTesting +internal fun PopupWindow.displayPopup(positioningData: MenuPositioningData) { + inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED + + animationStyle = positioningData.animation + height = positioningData.containerHeight + + PopupWindowCompat.setOverlapAnchor(this, true) + showAtLocation(positioningData.anchor, Gravity.NO_GRAVITY, positioningData.x, positioningData.y) +} diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/MenuPositioningData.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt similarity index 98% rename from mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/MenuPositioningData.kt rename to mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt index cce28ad160b8..61fb98aacab8 100644 --- a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/MenuPositioningData.kt +++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt @@ -100,6 +100,7 @@ internal fun inferMenuPositioningData( } return MenuPositioningData( + anchor = anchor, x = anchorX + horizontalOffset, y = anchorY + verticalOffset, containerHeight = containerViewHeight, @@ -234,6 +235,11 @@ private fun getCroppedMenuContainerHeight( */ data class MenuPositioningData( /** + * Android View that the PopupWindow should be anchored to. + */ + val anchor: View, + + /** * [WindowManager#LayoutParams#x] of params the menu will be added with. */ @Px val x: Int = 0, diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt deleted file mode 100644 index 8e8f4799674f..000000000000 --- a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.browser.menu2.ext - -import android.view.Gravity -import android.view.View -import android.widget.PopupWindow -import androidx.core.widget.PopupWindowCompat -import mozilla.components.concept.menu.MenuStyle -import mozilla.components.concept.menu.Orientation - -/** - * If parameters passed are a valid configuration attempt to show a [PopupWindow]. - * - * @return True if the parameters passed are a valid configuration, otherwise false. - */ -internal fun PopupWindow.displayPopup( - containerView: View, - anchor: View, - orientation: Orientation? = null, - style: MenuStyle? = null, -): Boolean { - val positioningData = - inferMenuPositioningData(containerView, anchor, style, orientation) ?: return false - - inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED - - showAtAnchorLocation(anchor, positioningData) - return true -} - -private fun PopupWindow.showAtAnchorLocation( - anchor: View, - positioningData: MenuPositioningData, -) { - // Apply the best fit animation style based on positioning - animationStyle = positioningData.animation - height = positioningData.containerHeight - - PopupWindowCompat.setOverlapAnchor(this, true) - showAtLocation(anchor, Gravity.NO_GRAVITY, positioningData.x, positioningData.y) -} diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt index 5a40611cac2a..688b77b578f9 100644 --- a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt +++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt @@ -4,8 +4,14 @@ package mozilla.components.browser.menu2 +import android.view.Gravity import android.widget.Button +import android.widget.PopupWindow import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu2.ext.MenuPositioningData +import mozilla.components.browser.menu2.ext.createAnchor +import mozilla.components.browser.menu2.ext.createContainerView +import mozilla.components.browser.menu2.ext.getTargetCoordinates import mozilla.components.concept.menu.MenuController import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate import mozilla.components.concept.menu.candidate.MenuCandidate @@ -16,6 +22,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito @RunWith(AndroidJUnit4::class) class BrowserMenuControllerTest { @@ -32,6 +39,17 @@ class BrowserMenuControllerTest { } @Test + fun `WHEN an empty list is submitted to the menu THEN the menu isn't shown`() { + val menu: MenuController = BrowserMenuController() + menu.submitList(emptyList()) + + val anchor = Button(testContext) + val popup = menu.show(anchor) + + assertFalse(popup.isShowing) + } + + @Test fun `observer is notified when submitList is called`() { var submitted: List? = null val menu: MenuController = BrowserMenuController() @@ -81,4 +99,34 @@ class BrowserMenuControllerTest { assertFalse(popup.isShowing) assertTrue(dismissed) } + + @Test + fun `WHEN displayPopup is called with provided positioning data THEN showAtLocation is called with provided positioning values & menu height and animation are set`() { + val containerView = createContainerView() + val popupWindow = Mockito.spy(PopupWindow()) + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor) + val positioningData = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + popupWindow.displayPopup(positioningData) + + assertEquals(containerView.measuredHeight, popupWindow.height) + assertEquals(positioningData.animation, popupWindow.animationStyle) + + Mockito.verify(popupWindow).showAtLocation( + positioningData.anchor, + Gravity.NO_GRAVITY, + positioningData.x, + positioningData.y, + ) + } } diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt new file mode 100644 index 000000000000..d9be746793d8 --- /dev/null +++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt @@ -0,0 +1,856 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.menu2.ext + +import android.graphics.Rect +import android.view.View +import android.widget.PopupWindow +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.menu2.R +import mozilla.components.browser.menu2.adapter.MenuCandidateListAdapter +import mozilla.components.concept.menu.MenuStyle +import mozilla.components.concept.menu.Orientation +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.any +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` +import kotlin.math.roundToInt + +/** + * [PopupWindow] const's. + */ +private const val HALF_MENU_ITEM = 0.5 + +/** + * [PopupWindow] UI components. + */ +private const val SCREEN_ROOT_VIEW_HEIGHT = 1000 +private const val SCREEN_ROOT_VIEW_WIDTH = 400 +private const val MENU_ITEM_HEIGHT = 50 +private const val DEFAULT_ITEM_COUNT = 10 +private const val MENU_CONTAINER_WIDTH = 100 +private const val MENU_CONTAINER_PADDING = 10 + +@RunWith(AndroidJUnit4::class) +class BrowserMenuPositioningTest { + private lateinit var popupWindow: PopupWindow + private lateinit var overlapStyle: MenuStyle + private lateinit var offsetStyle: MenuStyle + private lateinit var offsetOverlapStyle: MenuStyle + + @Before + fun setUp() { + overlapStyle = MenuStyle(completelyOverlap = true) + offsetStyle = MenuStyle(horizontalOffset = 10, verticalOffset = 10) + offsetOverlapStyle = + MenuStyle(completelyOverlap = true, horizontalOffset = 10, verticalOffset = 10) + + popupWindow = spy(PopupWindow()) + } + + @Test + fun `WHEN recycler view has no adapter THEN menu positioning data is null`() { + val containerView = createContainerView(hasAdapter = false) + val anchor = mock(View::class.java) + + assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)) + } + + @Test + fun `WHEN container view has no measured height THEN menu positioning data is null`() { + val containerView = createContainerView() + val anchor = mock(View::class.java) + + `when`(containerView.measuredHeight).thenReturn(0) + + assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)) + } + + @Test + fun `WHEN container view has no measured width THEN menu positioning data is null`() { + val containerView = createContainerView() + val anchor = mock(View::class.java) + + `when`(containerView.measuredWidth).thenReturn(0) + + assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)) + } + + @Test + fun `WHEN recycler view has no measured height THEN menu positioning data is null`() { + val containerView = createContainerView(recyclerViewHeight = 0) + val anchor = mock(View::class.java) + + assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)) + } + + @Test + fun `WHEN recycler view has no items THEN menu positioning data is null`() { + val containerView = createContainerView(recyclerViewHeight = 100, itemCount = 0) + val anchor = mock(View::class.java) + + assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)) + } + + @Test + fun `WHEN orientation up & fits available height THEN original height & positioned from the bottom of the anchor with leftBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation up & fits available height & is RTL THEN original height & positioned from the bottom right of the anchor with rightBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation up & does not fit available height & fits down THEN original height & positioned from the top left of the anchor with leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation up & does not fit up or down THEN original height & positioned from the top left of the anchor with leftAnimation`() { + val containerView = createContainerView() + + val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) + val (x, y) = 20 to notEnoughHeight + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation up & does not fit up or down & is RTL THEN original height & positioned from the bottom right of the anchor with rightAnimation`() { + val containerView = createContainerView() + + val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) + val (x, y) = SCREEN_ROOT_VIEW_HEIGHT - 20 to notEnoughHeight + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation down & fits available height THEN original height & positioned from the top left of the anchor with leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation down & fits available height & is RTL THEN original height & positioned from the top right of the anchor with rightTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.DOWN) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN number of menu items don't fit available height THEN set popup with calculated height`() { + val containerView = createContainerView(itemCount = 22) + val anchor = createAnchor(20, 25) + + val positioningData = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP) + + val availableHeight = anchor.rootView.measuredHeight + val maxAvailableHeightForRecyclerView = availableHeight - MENU_CONTAINER_PADDING + val numberOfItemsFitExactly = + (maxAvailableHeightForRecyclerView.toFloat() / MENU_ITEM_HEIGHT.toFloat()).roundToInt() + val numberOfItemsFitWithOverFlow = numberOfItemsFitExactly - HALF_MENU_ITEM + val updatedRecyclerViewHeight = (numberOfItemsFitWithOverFlow * MENU_ITEM_HEIGHT).toInt() + val calculatedHeight = updatedRecyclerViewHeight + MENU_CONTAINER_PADDING + + assertEquals(calculatedHeight, positioningData?.containerHeight) + } + + @Test + fun `WHEN orientation is null & menu fits down THEN original height & positioned from the top left of the anchor with leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, orientation = null) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN orientation is null & menu does not fit & is RTL THEN showAtLocation with original height & positioned from the top right of the anchor with rightAnimation`() { + val containerView = createContainerView(itemCount = 20) + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, orientation = null) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should completely overlap & orientation up & fits up THEN original height & bottom of the menu positioned exactly to the bottom of the anchor with leftBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should completely overlap & orientation up & fits up & is RTL THEN original height & bottom right of the menu positioned exactly to the bottom right of the anchor with rightBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should completely overlap & orientation down & fits down THEN showAtLocation with original height & positioned exactly from the top of the anchor with leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.DOWN) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should completely overlap & orientation down & fits down & is RTL THEN original height & positioned exactly from the top of the anchor with rightTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.DOWN) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should completely overlap & orientation up & does not fit up & fits down THEN original height & positioned exactly from the top left of the anchor with leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should completely overlap & orientation up & does not fit up or down THEN original height & positioned exactly from the top left of the anchor with leftAnimation`() { + val containerView = createContainerView() + + val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) + val (x, y) = 20 to notEnoughHeight + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN has style offsets & orientation up & fits up THEN original height & positioned from the bottom left of the anchor with applied offsets and leftBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN has style offsets & orientation up & fits up & is RTL THEN original height & positioned from the bottom right of the anchor with applied offsets and rightBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN has style offsets & orientation down & fits down THEN original height & positioned from the top left of the anchor with applied offsets and leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN has style offsets & orientation down & fits down & is RTL THEN original height & positioned from the top right of the anchor with applied offsets and rightTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN has style offsets & orientation up & does not fit up & fits down THEN original height & positioned from the top left of the anchor with applied offsets and leftAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN has style offsets & orientation up & does not fit up or down THEN original height & positioned from the top left of the anchor with applied offsets leftAnimation`() { + val containerView = createContainerView() + + val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) + val (x, y) = 20 to notEnoughHeight + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should overlap & has style offsets & orientation up & fits up THEN original height & positioned exactly from the bottom left of the anchor with applied offsets leftBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should overlap & has style offsets & orientation up & fits up & is RTL THEN original height & positioned exactly to the bottom right of the anchor with applied offsets rightBottomAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should overlap & has style offsets & orientation down & fits down THEN original height & positioned exactly from the top of the anchor with applied offsets leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should overlap & has style offsets & orientation down & fits down & is RTL THEN original height & positioned exactly from the top of the anchor with applied offsets and rightTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 + val anchor = createAnchor(x, y, true) + + val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false, directionRight = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should overlap & has style offsets & orientation up & fits down only THEN original height & positioned exactly from the top left of the anchor with applied offsets and leftTopAnimation`() { + val containerView = createContainerView() + + val (x, y) = 20 to 25 + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, + ) + + assertEquals(expected, result) + } + + @Test + fun `WHEN should overlap & has style offsets & orientation up & does not fit up or down THEN original height & positioned exactly from the top left of the anchor with applied offsets and leftAnimation`() { + val containerView = createContainerView() + + val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) + val (x, y) = 20 to notEnoughHeight + val anchor = createAnchor(x, y) + + val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP) + + val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false) + val expected = MenuPositioningData( + anchor = anchor, + x = targetX, + y = targetY, + containerHeight = containerView.measuredHeight, + animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, + ) + + assertEquals(expected, result) + } +} + +internal fun createContainerView( + itemCount: Int = DEFAULT_ITEM_COUNT, + hasAdapter: Boolean = true, + recyclerViewHeight: Int? = null, +): View { + val containerView = mock(View::class.java) + + // Mimicking elevation added spacing. + val recyclerView = createRecyclerView(itemCount, hasAdapter, recyclerViewHeight) + val containerHeight = recyclerView.measuredHeight.plus(MENU_CONTAINER_PADDING) + val containerWidth = recyclerView.measuredWidth.plus(MENU_CONTAINER_PADDING) + `when`(containerView.measuredHeight).thenReturn(containerHeight) + `when`(containerView.measuredWidth).thenReturn(containerWidth) + + doReturn(recyclerView).`when`(containerView) + .findViewById(R.id.mozac_browser_menu_recyclerView) + return containerView +} + +private fun createRecyclerView( + itemCount: Int = DEFAULT_ITEM_COUNT, + hasAdapter: Boolean = true, + recyclerViewHeight: Int? = null, +): RecyclerView { + val recyclerView = mock(RecyclerView::class.java) + + if (hasAdapter) { + val adapter = mock(MenuCandidateListAdapter::class.java) + `when`(adapter.itemCount).thenReturn(itemCount) + `when`(recyclerView.adapter).thenReturn(adapter) + } + + `when`(recyclerView.measuredHeight).thenReturn( + recyclerViewHeight + ?: (itemCount * MENU_ITEM_HEIGHT), + ) + + `when`(recyclerView.measuredWidth).thenReturn(MENU_CONTAINER_WIDTH) + + return recyclerView +} + +internal fun createAnchor(x: Int, y: Int, isRTL: Boolean = false): View { + val view = spy(View(testContext)) + + doAnswer { invocation -> + val locationOnScreen = (invocation.getArgument(0) as IntArray) + locationOnScreen[0] = x + locationOnScreen[1] = y + locationOnScreen + }.`when`(view).getLocationOnScreen(any()) + + doAnswer { invocation -> + val locationInWindow = (invocation.getArgument(0) as IntArray) + locationInWindow[0] = x + locationInWindow[1] = y + locationInWindow + }.`when`(view).getLocationInWindow(any()) + + if (isRTL) { + doReturn(ViewCompat.LAYOUT_DIRECTION_RTL).`when`(view).layoutDirection + } else { + doReturn(ViewCompat.LAYOUT_DIRECTION_LTR).`when`(view).layoutDirection + } + doReturn(10).`when`(view).height + doReturn(15).`when`(view).width + + val anchorRootView = createAnchorRootView() + `when`(view.rootView).thenReturn(anchorRootView) + + return view +} + +private fun createAnchorRootView(): View { + val view = spy(View(testContext)) + doAnswer { invocation -> + val displayFrame = (invocation.getArgument(0) as Rect) + displayFrame.left = 0 + displayFrame.right = SCREEN_ROOT_VIEW_WIDTH + displayFrame.bottom = SCREEN_ROOT_VIEW_HEIGHT + displayFrame + }.`when`(view).getWindowVisibleDisplayFrame(any()) + + `when`(view.measuredHeight).thenReturn(SCREEN_ROOT_VIEW_HEIGHT) + + return view +} + +internal fun getTargetCoordinates( + anchorX: Int, + anchorY: Int, + containerView: View, + anchor: View, + style: MenuStyle? = null, + directionUp: Boolean = true, + directionRight: Boolean = true, +): Pair { + val targetX = getTargetX( + anchorX, + containerView, + anchor, + directionRight, + style?.completelyOverlap ?: false, + style?.horizontalOffset ?: 0, + ) + val targetY = getTargetY( + anchorY, + containerView, + anchor, + directionUp, + style?.completelyOverlap ?: false, + style?.verticalOffset ?: 0, + ) + return targetX to targetY +} + +private fun getTargetX( + anchorX: Int, + containerView: View, + anchor: View, + directionRight: Boolean, + shouldOverlap: Boolean, + horizontalOffset: Int, +): Int { + val targetX = when { + directionRight && shouldOverlap -> anchorX - (MENU_CONTAINER_PADDING / 2) + directionRight && !shouldOverlap -> anchorX + !directionRight && shouldOverlap -> anchorX - (containerView.measuredWidth - anchor.width) + (MENU_CONTAINER_PADDING / 2) + else -> anchorX - (containerView.measuredWidth - anchor.width) + } + + return if (directionRight) { + targetX + horizontalOffset + } else { + targetX - horizontalOffset + } +} + +private fun getTargetY( + anchorY: Int, + containerView: View, + anchor: View, + directionUp: Boolean, + shouldOverlap: Boolean, + verticalOffset: Int, +): Int { + val targetY = when { + directionUp && shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height) + (MENU_CONTAINER_PADDING / 2) + directionUp && !shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height) + !directionUp && shouldOverlap -> anchorY - (MENU_CONTAINER_PADDING / 2) + else -> anchorY + } + + return if (directionUp) { + targetY - verticalOffset + } else { + targetY + verticalOffset + } +} diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt deleted file mode 100644 index 82caf3859686..000000000000 --- a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt +++ /dev/null @@ -1,859 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.browser.menu2.ext - -import android.graphics.Rect -import android.view.Gravity -import android.view.View -import android.widget.PopupWindow -import androidx.core.view.ViewCompat -import androidx.recyclerview.widget.RecyclerView -import androidx.test.ext.junit.runners.AndroidJUnit4 -import mozilla.components.browser.menu2.R -import mozilla.components.browser.menu2.adapter.MenuCandidateListAdapter -import mozilla.components.concept.menu.MenuStyle -import mozilla.components.concept.menu.Orientation -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.any -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.doAnswer -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import kotlin.math.roundToInt - -/** - * [PopupWindow] const's. - */ -private const val HALF_MENU_ITEM = 0.5 - -/** - * [PopupWindow] UI components. - */ -private const val SCREEN_ROOT_VIEW_HEIGHT = 1000 -private const val SCREEN_ROOT_VIEW_WIDTH = 400 -private const val MENU_ITEM_HEIGHT = 50 -private const val DEFAULT_ITEM_COUNT = 10 -private const val MENU_CONTAINER_WIDTH = 100 -private const val MENU_CONTAINER_PADDING = 10 - -@RunWith(AndroidJUnit4::class) -class PopupWindowTest { - private lateinit var popupWindow: PopupWindow - private lateinit var overlapStyle: MenuStyle - private lateinit var offsetStyle: MenuStyle - private lateinit var offsetOverlapStyle: MenuStyle - - @Before - fun setUp() { - overlapStyle = MenuStyle(completelyOverlap = true) - offsetStyle = MenuStyle(horizontalOffset = 10, verticalOffset = 10) - offsetOverlapStyle = - MenuStyle(completelyOverlap = true, horizontalOffset = 10, verticalOffset = 10) - - popupWindow = spy(PopupWindow()) - } - - @Test - fun `WHEN recycler view has no adapter THEN show is never called`() { - val containerView = createContainerView(hasAdapter = false) - val anchor = mock(View::class.java) - - assertFalse(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - verify(popupWindow, never()).showAtLocation(any(), anyInt(), anyInt(), anyInt()) - } - - @Test - fun `WHEN container view has no measured height THEN show is never called`() { - val containerView = createContainerView() - val anchor = mock(View::class.java) - - `when`(containerView.measuredHeight).thenReturn(0) - - assertFalse(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - verify(popupWindow, never()).showAtLocation(any(), anyInt(), anyInt(), anyInt()) - } - - @Test - fun `WHEN container view has no measured width THEN show is never called`() { - val containerView = createContainerView() - val anchor = mock(View::class.java) - - `when`(containerView.measuredWidth).thenReturn(0) - - assertFalse(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - verify(popupWindow, never()).showAtLocation(any(), anyInt(), anyInt(), anyInt()) - } - - @Test - fun `WHEN recycler view has no measured height THEN show is never called`() { - val containerView = createContainerView(recyclerViewHeight = 0) - val anchor = mock(View::class.java) - - assertFalse(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - verify(popupWindow, never()).showAtLocation(any(), anyInt(), anyInt(), anyInt()) - } - - @Test - fun `WHEN recycler view has no items THEN show is never called`() { - val containerView = createContainerView(recyclerViewHeight = 100, itemCount = 0) - val anchor = mock(View::class.java) - - assertFalse(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - verify(popupWindow, never()).showAtLocation(any(), anyInt(), anyInt(), anyInt()) - } - - @Test - fun `WHEN orientation up & fits available height THEN showAtLocation with original height & positioned from the bottom of the anchor with leftBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation up & fits available height & is RTL THEN showAtLocation with original height & positioned from the bottom right of the anchor with rightBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation up & does not fit available height & fits down THEN showAtLocation with original height & positioned from the top left of the anchor with leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation up & does not fit up or down THEN showAtLocation with original height & positioned from the top left of the anchor with leftAnimation`() { - val containerView = createContainerView() - - val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) - val (x, y) = 20 to notEnoughHeight - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation up & does not fit up or down & is RTL THEN showAtLocation with original height & positioned from the bottom right of the anchor with rightAnimation`() { - val containerView = createContainerView() - - val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) - val (x, y) = SCREEN_ROOT_VIEW_HEIGHT - 20 to notEnoughHeight - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation down & fits available height THEN showAtLocation with original height & positioned from the top left of the anchor with leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.DOWN)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation down & fits available height & is RTL THEN showAtLocation with original height & positioned from the top right of the anchor with rightTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.DOWN)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN number of menu items don't fit available height THEN set popup with calculated height`() { - val containerView = createContainerView(itemCount = 22) - val anchor = createAnchor(20, 25) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP)) - - val availableHeight = anchor.rootView.measuredHeight - val maxAvailableHeightForRecyclerView = availableHeight - MENU_CONTAINER_PADDING - val numberOfItemsFitExactly = - (maxAvailableHeightForRecyclerView.toFloat() / MENU_ITEM_HEIGHT.toFloat()).roundToInt() - val numberOfItemsFitWithOverFlow = numberOfItemsFitExactly - HALF_MENU_ITEM - val updatedRecyclerViewHeight = (numberOfItemsFitWithOverFlow * MENU_ITEM_HEIGHT).toInt() - val calculatedHeight = updatedRecyclerViewHeight + MENU_CONTAINER_PADDING - - assertEquals(calculatedHeight, popupWindow.height) - } - - @Test - fun `WHEN orientation is null & menu fits down THEN showAtLocation with original height & positioned from the top left of the anchor with leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN orientation is null & menu does not fit & is RTL THEN showAtLocation with original height & positioned from the top right of the anchor with rightAnimation`() { - val containerView = createContainerView(itemCount = 20) - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should completely overlap & orientation up & fits up THEN showAtLocation with original height & bottom of the menu positioned exactly to the bottom of the anchor with leftBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, overlapStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should completely overlap & orientation up & fits up & is RTL THEN showAtLocation with original height & bottom right of the menu positioned exactly to the bottom right of the anchor with rightBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, overlapStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should completely overlap & orientation down & fits down THEN showAtLocation with original height & positioned exactly from the top of the anchor with leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.DOWN, overlapStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should completely overlap & orientation down & fits down & is RTL THEN showAtLocation with original height & positioned exactly from the top of the anchor with rightTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.DOWN, overlapStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should completely overlap & orientation up & does not fit up & fits down THEN showAtLocation with original height & positioned exactly from the top left of the anchor with leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, overlapStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should completely overlap & orientation up & does not fit up or down THEN showAtLocation with original height & positioned exactly from the top left of the anchor with leftAnimation`() { - val containerView = createContainerView() - - val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) - val (x, y) = 20 to notEnoughHeight - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, overlapStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN has style offsets & orientation up & fits up THEN showAtLocation with original height & positioned from the bottom left of the anchor with applied offsets and leftBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, offsetStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN has style offsets & orientation up & fits up & is RTL THEN showAtLocation with original height & positioned from the bottom right of the anchor with applied offsets and rightBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, offsetStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN has style offsets & orientation down & fits down THEN showAtLocation with original height & positioned from the top left of the anchor with applied offsets and leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, offsetStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN has style offsets & orientation down & fits down & is RTL THEN showAtLocation with original height & positioned from the top right of the anchor with applied offsets and rightTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 - val anchor = createAnchor(x, y, true) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, offsetStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN has style offsets & orientation up & does not fit up & fits down THEN showAtLocation with original height & positioned from the top left of the anchor with applied offsets and leftAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, offsetStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN has style offsets & orientation up & does not fit up or down THEN showAtLocation with original height & positioned from the top left of the anchor with applied offsets leftAnimation`() { - val containerView = createContainerView() - - val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) - val (x, y) = 20 to notEnoughHeight - val anchor = createAnchor(x, y) - - assertTrue(popupWindow.displayPopup(containerView, anchor, Orientation.UP, offsetStyle)) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should overlap & has style offsets & orientation up & fits up THEN showAtLocation with original height & positioned exactly from the bottom left of the anchor with applied offsets leftBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y) - - assertTrue( - popupWindow.displayPopup( - containerView, - anchor, - Orientation.UP, - offsetOverlapStyle, - ), - ) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should overlap & has style offsets & orientation up & fits up & is RTL THEN showAtLocation with original height & positioned exactly to the bottom right of the anchor with applied offsets rightBottomAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20 - val anchor = createAnchor(x, y, true) - - assertTrue( - popupWindow.displayPopup( - containerView, - anchor, - Orientation.UP, - offsetOverlapStyle, - ), - ) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should overlap & has style offsets & orientation down & fits down THEN showAsDropDown with original height & positioned exactly from the top of the anchor with applied offsets leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue( - popupWindow.displayPopup( - containerView, - anchor, - Orientation.UP, - offsetOverlapStyle, - ), - ) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should overlap & has style offsets & orientation down & fits down & is RTL THEN showAsDropDown with original height & positioned exactly from the top of the anchor with applied offsets and rightTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25 - val anchor = createAnchor(x, y, true) - - assertTrue( - popupWindow.displayPopup( - containerView, - anchor, - Orientation.UP, - offsetOverlapStyle, - ), - ) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false, directionRight = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should overlap & has style offsets & orientation up & fits down only THEN showAtLocation with original height & positioned exactly from the top left of the anchor with applied offsets and leftTopAnimation`() { - val containerView = createContainerView() - - val (x, y) = 20 to 25 - val anchor = createAnchor(x, y) - - assertTrue( - popupWindow.displayPopup( - containerView, - anchor, - Orientation.UP, - offsetOverlapStyle, - ), - ) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - @Test - fun `WHEN should overlap & has style offsets & orientation up & does not fit up or down THEN showAtLocation with original height & positioned exactly from the top left of the anchor with applied offsets and leftAnimation`() { - val containerView = createContainerView() - - val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1) - val (x, y) = 20 to notEnoughHeight - val anchor = createAnchor(x, y) - - assertTrue( - popupWindow.displayPopup( - containerView, - anchor, - Orientation.UP, - offsetOverlapStyle, - ), - ) - - assertEquals(containerView.measuredHeight, popupWindow.height) - assertEquals( - R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, - popupWindow.animationStyle, - ) - - val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false) - verify(popupWindow).showAtLocation(anchor, Gravity.NO_GRAVITY, targetX, targetY) - } - - private fun createContainerView( - itemCount: Int = DEFAULT_ITEM_COUNT, - hasAdapter: Boolean = true, - recyclerViewHeight: Int? = null, - ): View { - val containerView = mock(View::class.java) - - // Mimicking elevation added spacing. - val recyclerView = createRecyclerView(itemCount, hasAdapter, recyclerViewHeight) - val containerHeight = recyclerView.measuredHeight.plus(MENU_CONTAINER_PADDING) - val containerWidth = recyclerView.measuredWidth.plus(MENU_CONTAINER_PADDING) - `when`(containerView.measuredHeight).thenReturn(containerHeight) - `when`(containerView.measuredWidth).thenReturn(containerWidth) - - doReturn(recyclerView).`when`(containerView) - .findViewById(R.id.mozac_browser_menu_recyclerView) - return containerView - } - - private fun createRecyclerView( - itemCount: Int = DEFAULT_ITEM_COUNT, - hasAdapter: Boolean = true, - recyclerViewHeight: Int? = null, - ): RecyclerView { - val recyclerView = mock(RecyclerView::class.java) - - if (hasAdapter) { - val adapter = mock(MenuCandidateListAdapter::class.java) - `when`(adapter.itemCount).thenReturn(itemCount) - `when`(recyclerView.adapter).thenReturn(adapter) - } - - `when`(recyclerView.measuredHeight).thenReturn( - recyclerViewHeight - ?: (itemCount * MENU_ITEM_HEIGHT), - ) - - `when`(recyclerView.measuredWidth).thenReturn(MENU_CONTAINER_WIDTH) - - return recyclerView - } - - private fun createAnchor(x: Int, y: Int, isRTL: Boolean = false): View { - val view = spy(View(testContext)) - - doAnswer { invocation -> - val locationOnScreen = (invocation.getArgument(0) as IntArray) - locationOnScreen[0] = x - locationOnScreen[1] = y - locationOnScreen - }.`when`(view).getLocationOnScreen(any()) - - doAnswer { invocation -> - val locationInWindow = (invocation.getArgument(0) as IntArray) - locationInWindow[0] = x - locationInWindow[1] = y - locationInWindow - }.`when`(view).getLocationInWindow(any()) - - if (isRTL) { - doReturn(ViewCompat.LAYOUT_DIRECTION_RTL).`when`(view).layoutDirection - } else { - doReturn(ViewCompat.LAYOUT_DIRECTION_LTR).`when`(view).layoutDirection - } - doReturn(10).`when`(view).height - doReturn(15).`when`(view).width - - val anchorRootView = createAnchorRootView() - `when`(view.rootView).thenReturn(anchorRootView) - - return view - } - - private fun createAnchorRootView(): View { - val view = spy(View(testContext)) - doAnswer { invocation -> - val displayFrame = (invocation.getArgument(0) as Rect) - displayFrame.left = 0 - displayFrame.right = SCREEN_ROOT_VIEW_WIDTH - displayFrame.bottom = SCREEN_ROOT_VIEW_HEIGHT - displayFrame - }.`when`(view).getWindowVisibleDisplayFrame(any()) - - `when`(view.measuredHeight).thenReturn(SCREEN_ROOT_VIEW_HEIGHT) - - return view - } - - private fun getTargetCoordinates( - anchorX: Int, - anchorY: Int, - containerView: View, - anchor: View, - style: MenuStyle? = null, - directionUp: Boolean = true, - directionRight: Boolean = true, - ): Pair { - val targetX = getTargetX( - anchorX, - containerView, - anchor, - directionRight, - style?.completelyOverlap ?: false, - style?.horizontalOffset ?: 0, - ) - val targetY = getTargetY( - anchorY, - containerView, - anchor, - directionUp, - style?.completelyOverlap ?: false, - style?.verticalOffset ?: 0, - ) - return targetX to targetY - } - - private fun getTargetX( - anchorX: Int, - containerView: View, - anchor: View, - directionRight: Boolean, - shouldOverlap: Boolean, - horizontalOffset: Int, - ): Int { - val targetX = when { - directionRight && shouldOverlap -> anchorX - (MENU_CONTAINER_PADDING / 2) - directionRight && !shouldOverlap -> anchorX - !directionRight && shouldOverlap -> anchorX - (containerView.measuredWidth - anchor.width) + (MENU_CONTAINER_PADDING / 2) - else -> anchorX - (containerView.measuredWidth - anchor.width) - } - - return if (directionRight) { - targetX + horizontalOffset - } else { - targetX - horizontalOffset - } - } - - private fun getTargetY( - anchorY: Int, - containerView: View, - anchor: View, - directionUp: Boolean, - shouldOverlap: Boolean, - verticalOffset: Int, - ): Int { - val targetY = when { - directionUp && shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height) + (MENU_CONTAINER_PADDING / 2) - directionUp && !shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height) - !directionUp && shouldOverlap -> anchorY - (MENU_CONTAINER_PADDING / 2) - else -> anchorY - } - - return if (directionUp) { - targetY - verticalOffset - } else { - targetY + verticalOffset - } - } -} -- 2.11.4.GIT