/* 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/. */ @file:Suppress("TooManyFunctions") package org.mozilla.fenix.ui.robots import android.view.View import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsNotSelected import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.filter import androidx.compose.ui.test.hasAnyChild import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onChildAt import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performSemanticsAction import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeLeft import androidx.compose.ui.test.swipeRight import androidx.test.espresso.Espresso import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.action.GeneralLocation import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import com.google.android.material.bottomsheet.BottomSheetBehavior import junit.framework.TestCase import org.hamcrest.Matcher import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants import org.mozilla.fenix.helpers.HomeActivityComposeTestRule import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.clickAtLocationInView import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher import org.mozilla.fenix.tabstray.TabsTrayTestTag /** * Implementation of Robot Pattern for the Tabs Tray. */ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTestRule) { fun verifyNormalBrowsingButtonIsSelected(isSelected: Boolean = true) { if (isSelected) { composeTestRule.normalBrowsingButton().assertIsSelected() } else { composeTestRule.normalBrowsingButton().assertIsNotSelected() } } fun verifyPrivateBrowsingButtonIsSelected(isSelected: Boolean = true) { if (isSelected) { composeTestRule.privateBrowsingButton().assertIsSelected() } else { composeTestRule.privateBrowsingButton().assertIsNotSelected() } } fun verifySyncedTabsButtonIsSelected(isSelected: Boolean = true) { if (isSelected) { composeTestRule.syncedTabsButton().assertIsSelected() } else { composeTestRule.syncedTabsButton().assertIsNotSelected() } } fun verifySyncedTabsListWhenUserIsNotSignedIn() { verifySyncedTabsList() assertItemContainingTextExists( itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)), itemContainingText(getStringResource(R.string.sync_sign_in)), itemContainingText(getStringResource(R.string.tab_drawer_fab_sync)), ) } fun verifyExistingOpenTabs(vararg titles: String) { titles.forEach { title -> itemContainingText(title).waitForExists(waitingTime) composeTestRule.tabItem(title).assertExists() } } fun verifyOpenTabsOrder(title: String, position: Int) = composeTestRule.normalTabsList() .onChildAt(position - 1) .assert(hasTestTag(TabsTrayTestTag.tabItemRoot)) .assert(hasAnyChild(hasText(title))) fun verifyNoExistingOpenTabs(vararg titles: String) { titles.forEach { title -> TestCase.assertFalse( itemContainingText(title).waitForExists(TestAssetHelper.waitingTimeShort), ) } } fun verifyNormalTabsList() { composeTestRule.normalTabsList().assertExists() } fun verifyPrivateTabsList() { composeTestRule.privateTabsList().assertExists() } fun verifySyncedTabsList() { composeTestRule.syncedTabsList().assertExists() } fun verifyNoOpenTabsInNormalBrowsing() { composeTestRule.emptyNormalTabsList().assertExists() } fun verifyNoOpenTabsInPrivateBrowsing() { composeTestRule.emptyPrivateTabsList().assertExists() } fun verifyAccountSettingsButton() { composeTestRule.dropdownMenuItemAccountSettings().assertExists() } fun verifyCloseAllTabsButton() { composeTestRule.dropdownMenuItemCloseAllTabs().assertExists() } fun verifySelectTabsButton() { composeTestRule.dropdownMenuItemSelectTabs().assertExists() } fun verifyShareAllTabsButton() { composeTestRule.dropdownMenuItemShareAllTabs().assertExists() } fun verifyRecentlyClosedTabsButton() { composeTestRule.dropdownMenuItemRecentlyClosedTabs().assertExists() } fun verifyTabSettingsButton() { composeTestRule.dropdownMenuItemTabSettings().assertExists() } fun verifyThreeDotButton() { composeTestRule.threeDotButton().assertExists() } fun verifyFab() { composeTestRule.tabsTrayFab().assertExists() } fun verifyNormalTabCounter() { composeTestRule.normalTabsCounter().assertExists() } /** * Verifies a tab's thumbnail when there is only one tab open. */ fun verifyTabThumbnail() { composeTestRule.tabThumbnail().assertExists() } /** * Verifies a tab's close button when there is only one tab open. */ fun verifyTabCloseButton() { composeTestRule.closeTabButton().assertExists() } fun verifyTabsTrayBehaviorState(expectedState: Int) { tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorStateMatcher(expectedState))) } fun verifyMinusculeHalfExpandedRatio() { tabsTrayView().check(ViewAssertions.matches(BottomSheetBehaviorHalfExpandedMaxRatioMatcher(0.001f))) } fun verifyTabTrayIsOpen() { composeTestRule.tabsTray().assertExists() } fun verifyTabTrayIsClosed() { composeTestRule.tabsTray().assertDoesNotExist() } /** * Closes a tab when there is only one tab open. */ fun closeTab() { composeTestRule.closeTabButton().performClick() } /** * Swipes a tab with [title] left. */ fun swipeTabLeft(title: String) { composeTestRule.tabItem(title).performTouchInput { swipeLeft() } } /** * Swipes a tab with [title] right. */ fun swipeTabRight(title: String) { composeTestRule.tabItem(title).performTouchInput { swipeRight() } } /** * Creates a collection from the provided [tabTitles]. */ fun createCollection( vararg tabTitles: String, collectionName: String, firstCollection: Boolean = true, ) { composeTestRule.threeDotButton().performClick() composeTestRule.dropdownMenuItemSelectTabs().performClick() for (tab in tabTitles) { selectTab(tab) } clickCollectionsButton(composeTestRule) { if (!firstCollection) { clickAddNewCollection() } typeCollectionNameAndSave(collectionName) } } /** * Selects a tab with [title]. */ @OptIn(ExperimentalTestApi::class) fun selectTab(title: String) { composeTestRule.waitUntilExactlyOneExists(hasText(title), TestAssetHelper.waitingTime) composeTestRule.tabItem(title).performClick() } /** * Performs a long click on a tab with [title]. */ fun longClickTab(title: String) { composeTestRule.tabItem(title) .performTouchInput { longClick(durationMillis = Constants.LONG_CLICK_DURATION) } } /** * Verifies the multi selection counter displays [numOfTabs]. */ fun verifyTabsMultiSelectionCounter(numOfTabs: Int) { composeTestRule.multiSelectionCounter() .assert(hasText("$numOfTabs selected")) } /** * Verifies a tab's media button matches [action] when there is only one tab with media. */ @OptIn(ExperimentalTestApi::class) fun verifyTabMediaControlButtonState(action: String) { composeTestRule.waitUntilAtLeastOneExists(hasContentDescription(action), waitingTime) composeTestRule.tabMediaControlButton(action) .assertExists() } /** * Clicks a tab's media button when there is only one tab with media. */ @OptIn(ExperimentalTestApi::class) fun clickTabMediaControlButton(action: String) { composeTestRule.waitUntilAtLeastOneExists(hasContentDescription(action), waitingTime) composeTestRule.tabMediaControlButton(action) .performClick() } /** * Closes a tab with a given [title]. */ fun closeTabWithTitle(title: String) { composeTestRule.onAllNodesWithTag(TabsTrayTestTag.tabItemClose) .filter(hasParent(hasText(title))) .onFirst() .performClick() } class Transition(private val composeTestRule: HomeActivityComposeTestRule) { fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition { mDevice.waitForIdle() composeTestRule.tabsTrayFab().performClick() SearchRobot().interact() return SearchRobot.Transition() } fun toggleToNormalTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition { composeTestRule.normalBrowsingButton().performClick() ComposeTabDrawerRobot(composeTestRule).interact() return Transition(composeTestRule) } fun toggleToPrivateTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition { composeTestRule.privateBrowsingButton().performClick() ComposeTabDrawerRobot(composeTestRule).interact() return Transition(composeTestRule) } fun toggleToSyncedTabs(interact: ComposeTabDrawerRobot.() -> Unit): Transition { composeTestRule.syncedTabsButton().performClick() ComposeTabDrawerRobot(composeTestRule).interact() return Transition(composeTestRule) } fun clickSignInToSyncButton(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition { itemContainingText(getStringResource(R.string.sync_sign_in)) .clickAndWaitForNewWindow(TestAssetHelper.waitingTimeShort) SyncSignInRobot().interact() return SyncSignInRobot.Transition() } fun openThreeDotMenu(interact: ComposeTabDrawerRobot.() -> Unit): Transition { composeTestRule.threeDotButton().performClick() ComposeTabDrawerRobot(composeTestRule).interact() return Transition(composeTestRule) } fun closeAllTabs(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { composeTestRule.dropdownMenuItemCloseAllTabs().performClick() HomeScreenRobot().interact() return HomeScreenRobot.Transition() } fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { composeTestRule.tabItem(title) .performScrollTo() .performClick() BrowserRobot().interact() return BrowserRobot.Transition() } fun openPrivateTab(position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { composeTestRule.privateTabsList() .onChildren()[position] .performClick() BrowserRobot().interact() return BrowserRobot.Transition() } fun openNormalTab(position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { composeTestRule.normalTabsList() .onChildren()[position] .performClick() BrowserRobot().interact() return BrowserRobot.Transition() } fun clickTopBar(interact: ComposeTabDrawerRobot.() -> Unit): Transition { // The topBar contains other views. // Don't do the default click in the middle, rather click in some free space - top right. Espresso.onView(ViewMatchers.withId(R.id.topBar)).clickAtLocationInView(GeneralLocation.TOP_RIGHT) ComposeTabDrawerRobot(composeTestRule).interact() return Transition(composeTestRule) } fun waitForTabTrayBehaviorToIdle(interact: ComposeTabDrawerRobot.() -> Unit): Transition { // Need to get the behavior of tab_wrapper and wait for that to idle. var behavior: BottomSheetBehavior<*>? = null // Null check here since it's possible that the view is already animated away from the screen. tabsTrayView()?.perform( object : ViewAction { override fun getDescription(): String { return "Postpone actions to after the BottomSheetBehavior has settled" } override fun getConstraints(): Matcher { return ViewMatchers.isAssignableFrom(View::class.java) } override fun perform(uiController: UiController?, view: View?) { behavior = BottomSheetBehavior.from(view!!) } }, ) behavior?.let { runWithIdleRes(BottomSheetBehaviorStateIdlingResource(it)) { ComposeTabDrawerRobot(composeTestRule).interact() } } return Transition(composeTestRule) } fun advanceToHalfExpandedState(interact: ComposeTabDrawerRobot.() -> Unit): Transition { tabsTrayView().perform( object : ViewAction { override fun getDescription(): String { return "Advance a BottomSheetBehavior to STATE_HALF_EXPANDED" } override fun getConstraints(): Matcher { return ViewMatchers.isAssignableFrom(View::class.java) } override fun perform(uiController: UiController?, view: View?) { val behavior = BottomSheetBehavior.from(view!!) behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED } }, ) ComposeTabDrawerRobot(composeTestRule).interact() return Transition(composeTestRule) } fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { composeTestRule.bannerHandle().performSemanticsAction(SemanticsActions.OnClick) BrowserRobot().interact() return BrowserRobot.Transition() } fun clickSaveCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition { composeTestRule.collectionsButton().performClick() CollectionRobot().interact() return CollectionRobot.Transition() } fun clickShareAllTabsButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition { composeTestRule.dropdownMenuItemShareAllTabs().performClick() ShareOverlayRobot().interact() return ShareOverlayRobot.Transition() } } } /** * Opens a transition in the [ComposeTabDrawerRobot]. */ fun composeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition { ComposeTabDrawerRobot(composeTestRule).interact() return ComposeTabDrawerRobot.Transition(composeTestRule) } /** * Clicks on the Collections button in the Tabs Tray banner and opens a transition in the [CollectionRobot]. */ private fun clickCollectionsButton(composeTestRule: HomeActivityComposeTestRule, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition { composeTestRule.collectionsButton().performClick() CollectionRobot().interact() return CollectionRobot.Transition() } /** * Obtains the root [View] that wraps the Tabs Tray. */ private fun tabsTrayView() = Espresso.onView(ViewMatchers.withId(R.id.tabs_tray_root)) /** * Obtains the root Tabs Tray. */ private fun ComposeTestRule.tabsTray() = onNodeWithTag(TabsTrayTestTag.tabsTray) /** * Obtains the Tabs Tray FAB. */ private fun ComposeTestRule.tabsTrayFab() = onNodeWithTag(TabsTrayTestTag.fab) /** * Obtains the normal browsing page button of the Tabs Tray banner. */ private fun ComposeTestRule.normalBrowsingButton() = onNodeWithTag(TabsTrayTestTag.normalTabsPageButton) /** * Obtains the private browsing page button of the Tabs Tray banner. */ private fun ComposeTestRule.privateBrowsingButton() = onNodeWithTag(TabsTrayTestTag.privateTabsPageButton) /** * Obtains the synced tabs page button of the Tabs Tray banner. */ private fun ComposeTestRule.syncedTabsButton() = onNodeWithTag(TabsTrayTestTag.syncedTabsPageButton) /** * Obtains the normal tabs list. */ private fun ComposeTestRule.normalTabsList() = onNodeWithTag(TabsTrayTestTag.normalTabsList) /** * Obtains the private tabs list. */ private fun ComposeTestRule.privateTabsList() = onNodeWithTag(TabsTrayTestTag.privateTabsList) /** * Obtains the synced tabs list. */ private fun ComposeTestRule.syncedTabsList() = onNodeWithTag(TabsTrayTestTag.syncedTabsList) /** * Obtains the empty normal tabs list. */ private fun ComposeTestRule.emptyNormalTabsList() = onNodeWithTag(TabsTrayTestTag.emptyNormalTabsList) /** * Obtains the empty private tabs list. */ private fun ComposeTestRule.emptyPrivateTabsList() = onNodeWithTag(TabsTrayTestTag.emptyPrivateTabsList) /** * Obtains the tab with the provided [title] */ private fun ComposeTestRule.tabItem(title: String) = onAllNodesWithTag(TabsTrayTestTag.tabItemRoot) .filter(hasAnyChild(hasText(title))) .onFirst() /** * Obtains an open tab's close button when there's only one tab open. */ private fun ComposeTestRule.closeTabButton() = onNodeWithTag(TabsTrayTestTag.tabItemClose) /** * Obtains an open tab's thumbnail when there's only one tab open. */ private fun ComposeTestRule.tabThumbnail() = onNodeWithTag(TabsTrayTestTag.tabItemThumbnail) /** * Obtains the three dot button in the Tabs Tray banner. */ private fun ComposeTestRule.threeDotButton() = onNodeWithTag(TabsTrayTestTag.threeDotButton) /** * Obtains the dropdown menu item to access account settings. */ private fun ComposeTestRule.dropdownMenuItemAccountSettings() = onNodeWithTag(TabsTrayTestTag.accountSettings) /** * Obtains the dropdown menu item to close all tabs. */ private fun ComposeTestRule.dropdownMenuItemCloseAllTabs() = onNodeWithTag(TabsTrayTestTag.closeAllTabs) /** * Obtains the dropdown menu item to access recently closed tabs. */ private fun ComposeTestRule.dropdownMenuItemRecentlyClosedTabs() = onNodeWithTag(TabsTrayTestTag.recentlyClosedTabs) /** * Obtains the dropdown menu item to select tabs. */ private fun ComposeTestRule.dropdownMenuItemSelectTabs() = onNodeWithTag(TabsTrayTestTag.selectTabs) /** * Obtains the dropdown menu item to share all tabs. */ private fun ComposeTestRule.dropdownMenuItemShareAllTabs() = onNodeWithTag(TabsTrayTestTag.shareAllTabs) /** * Obtains the dropdown menu item to access tab settings. */ private fun ComposeTestRule.dropdownMenuItemTabSettings() = onNodeWithTag(TabsTrayTestTag.tabSettings) /** * Obtains the normal tabs counter. */ private fun ComposeTestRule.normalTabsCounter() = onNodeWithTag(TabsTrayTestTag.normalTabsCounter) /** * Obtains the Tabs Tray banner collections button. */ private fun ComposeTestRule.collectionsButton() = onNodeWithTag(TabsTrayTestTag.collectionsButton) /** * Obtains the Tabs Tray banner multi selection counter. */ private fun ComposeTestRule.multiSelectionCounter() = onNodeWithTag(TabsTrayTestTag.selectionCounter) /** * Obtains the Tabs Tray banner handle. */ private fun ComposeTestRule.bannerHandle() = onNodeWithTag(TabsTrayTestTag.bannerHandle) /** * Obtains the media control button with the given [action] as its content description. */ private fun ComposeTestRule.tabMediaControlButton(action: String) = onNodeWithContentDescription(action)