Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package com.android.messaging.ui.conversation.entry

import androidx.activity.ComponentActivity
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasScrollToIndexAction
import androidx.compose.ui.test.hasSetTextAction
Expand All @@ -14,12 +24,24 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTextInput
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import com.android.messaging.ui.common.components.ListDetailPaneSide
import com.android.messaging.ui.common.components.LocalIsListDetailPane
import com.android.messaging.ui.common.components.LocalListDetailPaneSide
import com.android.messaging.ui.conversation.NEW_CHAT_CONTACT_RESOLVING_INDICATOR_TEST_TAG
import com.android.messaging.ui.conversation.NEW_CHAT_CREATE_GROUP_BUTTON_TEST_TAG
import com.android.messaging.ui.conversation.NEW_CHAT_CREATE_GROUP_NEXT_BUTTON_TEST_TAG
import com.android.messaging.ui.conversation.entry.model.NewChatUiState
import com.android.messaging.ui.conversation.navigation.NewChatNavKey
import com.android.messaging.ui.conversation.navigation.conversationDetailPaneMetadata
import com.android.messaging.ui.conversation.navigation.conversationListPaneMetadata
import com.android.messaging.ui.conversation.newChatContactRowTestTag
import com.android.messaging.ui.conversationlist.navigation.ConversationListNavKey
import com.android.messaging.ui.core.AppTheme
import com.android.messaging.ui.navigation.AppNavDisplay
import com.android.messaging.ui.navigation.rememberAppSceneStrategies
import com.android.messaging.ui.recipientselection.component.row.CONTACT_ID
import com.android.messaging.ui.recipientselection.component.row.MOBILE_NORMALIZED_DESTINATION
import com.android.messaging.ui.recipientselection.component.row.contactItem
Expand All @@ -37,6 +59,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
class NewChatScreenTest {
Expand Down Expand Up @@ -253,17 +276,100 @@ class NewChatScreenTest {
.assertIsDisplayed()
}

private fun setContent(screenModel: NewChatScreenModel) {
@Test
fun opening_inSinglePane_focusesTheRecipientField() {
setContent(screenModel = createScreenModel(initialUiState = NewChatUiState()))

composeTestRule
.onNode(matcher = hasSetTextAction())
.assertIsFocused()
}

@Test
fun opening_inListDetailPane_focusesTheRecipientField() {
setContent(
screenModel = createScreenModel(initialUiState = NewChatUiState()),
isListDetailPane = true,
)

composeTestRule
.onNode(matcher = hasSetTextAction())
.assertIsFocused()
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Config(qualifiers = TABLET_QUALIFIERS)
@Test
fun paneCountChange_keepsTheRecipientFieldFocused() {
val screenModel = createScreenModel(initialUiState = NewChatUiState())
val backStack = mutableStateListOf<NavKey>(ConversationListNavKey, NewChatNavKey)
var showsTwoPanes by mutableStateOf(false)

composeTestRule.setContent {
val maxHorizontalPartitions = when {
showsTwoPanes -> TWO_PANES
else -> SINGLE_PANE
}

AppTheme {
NewChatScreen(
screenModel = screenModel,
effectHandler = mockk(relaxed = true),
onNavigateBack = {},
onNavigateToConversation = { _, _ -> },
AppNavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<ConversationListNavKey>(metadata = conversationListPaneMetadata()) {}
entry<NewChatNavKey>(metadata = conversationDetailPaneMetadata()) {
NewChatScreen(
screenModel = screenModel,
effectHandler = mockk(relaxed = true),
onNavigateBack = {},
onNavigateToConversation = { _, _ -> },
autoFocusQueryKey = LocalNavAnimatedContentScope.current,
)
}
},
onBack = {},
sceneStrategies = rememberAppSceneStrategies(
additionalStrategies = listOf(
rememberListDetailSceneStrategy(
backNavigationBehavior = BackNavigationBehavior.PopLatest,
directive = PaneScaffoldDirective.Default.copy(
maxHorizontalPartitions = maxHorizontalPartitions,
),
),
),
),
showsTwoPanes = showsTwoPanes,
)
}
}

for (twoPanes in listOf(true, false)) {
showsTwoPanes = twoPanes

composeTestRule
.onNode(matcher = hasSetTextAction())
.assertIsFocused()
}
}

private fun setContent(
screenModel: NewChatScreenModel,
isListDetailPane: Boolean = false,
) {
composeTestRule.setContent {
AppTheme {
CompositionLocalProvider(
LocalIsListDetailPane provides isListDetailPane,
LocalListDetailPaneSide provides ListDetailPaneSide.End,
) {
NewChatScreen(
screenModel = screenModel,
effectHandler = mockk(relaxed = true),
onNavigateBack = {},
onNavigateToConversation = { _, _ -> },
)
}
}
}
}

private fun contactItems(count: Int): ImmutableList<RecipientPickerListItem.Contact> {
Expand Down Expand Up @@ -294,4 +400,10 @@ class NewChatScreenTest {
every { uiState } returns uiStateFlow
}
}

private companion object {
private const val SINGLE_PANE = 1
private const val TWO_PANES = 2
private const val TABLET_QUALIFIERS = "w1280dp-h800dp"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
@file:OptIn(ExperimentalMaterial3AdaptiveApi::class)

package com.android.messaging.ui.conversation.navigation

import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.get
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SceneStrategyScope
import com.android.messaging.testutil.TEST_CONVERSATION_ID as CONVERSATION_ID
import com.android.messaging.ui.common.components.ListDetailPaneSide
import com.android.messaging.ui.conversationlist.navigation.ConversationListNavKey
import com.android.messaging.ui.conversationlist.navigation.conversationListEntries
import com.android.messaging.ui.navigation.ListDetailPaneKey
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

internal class ConversationNavEntriesTest {

private val entryProvider = entryProvider<NavKey> {
conversationListEntries()
conversationEntries()
}

@Test
fun newChat_inTwoPaneWindow_opensInTheDetailPaneNextToTheList() {
val entries = navEntries(ConversationListNavKey, NewChatNavKey)

val scene = listDetailScene(
entries = entries,
maxHorizontalPartitions = TWO_PANES,
)

assertEquals(entries, scene?.entries)
}

@Test
fun newChat_overOpenConversationInTwoPaneWindow_goesBackToThatConversation() {
val entries = navEntries(
ConversationListNavKey,
ConversationNavKey(conversationId = CONVERSATION_ID),
NewChatNavKey,
)

val scene = listDetailScene(
entries = entries,
maxHorizontalPartitions = TWO_PANES,
)

assertEquals(entries, scene?.entries)
assertEquals(entries.dropLast(n = 1), scene?.previousEntries)
}

@Test
fun newChat_inSinglePaneWindow_isLeftToFillTheWindow() {
val scene = listDetailScene(
entries = navEntries(ConversationListNavKey, NewChatNavKey),
maxHorizontalPartitions = SINGLE_PANE,
)

assertNull(scene)
}

@Test
fun newChat_inListDetailLayout_takesTheEndSide() {
val entry = entryProvider(NewChatNavKey)

assertEquals(ListDetailPaneSide.End, entry.metadata[ListDetailPaneKey])
}

private fun navEntries(vararg keys: NavKey): List<NavEntry<NavKey>> {
return keys.map(entryProvider)
}

private fun listDetailScene(
entries: List<NavEntry<NavKey>>,
maxHorizontalPartitions: Int,
): Scene<NavKey>? {
val sceneStrategy = ListDetailSceneStrategy<NavKey>(
shouldHandleSinglePaneLayout = false,
backNavigationBehavior = BackNavigationBehavior.PopLatest,
directive = PaneScaffoldDirective.Default.copy(
maxHorizontalPartitions = maxHorizontalPartitions,
),
adaptStrategies = ListDetailPaneScaffoldDefaults.adaptStrategies(),
paneExpansionDragHandle = null,
paneExpansionState = null,
)

return with(SceneStrategyScope<NavKey>()) {
with(sceneStrategy) {
calculateScene(entries)
}
}
}

private companion object {
private const val SINGLE_PANE = 1
private const val TWO_PANES = 2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.android.messaging.ui.conversation.navigation

import com.android.messaging.testutil.TEST_CONVERSATION_ID as CONVERSATION_ID
import com.android.messaging.ui.conversationlist.navigation.ArchivedConversationListNavKey
import com.android.messaging.ui.conversationlist.navigation.ConversationListNavKey
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

internal class ConversationPaneMetadataTest {

@Test
fun detailPaneConversationId_conversationOnTop_returnsItsId() {
val backStack = listOf(
ConversationListNavKey,
ConversationNavKey(conversationId = CONVERSATION_ID),
)

assertEquals(CONVERSATION_ID, detailPaneConversationId(backStack = backStack))
}

@Test
fun detailPaneConversationId_newChatOverConversation_returnsNull() {
val backStack = listOf(
ConversationListNavKey,
ConversationNavKey(conversationId = CONVERSATION_ID),
NewChatNavKey,
)

assertNull(detailPaneConversationId(backStack = backStack))
}

@Test
fun detailPaneConversationId_listPaneOverConversation_returnsTheConversation() {
val backStack = listOf(
ConversationListNavKey,
ConversationNavKey(conversationId = CONVERSATION_ID),
ArchivedConversationListNavKey,
)

assertEquals(CONVERSATION_ID, detailPaneConversationId(backStack = backStack))
}

@Test
fun detailPaneConversationId_withoutDetailDestination_returnsNull() {
assertNull(detailPaneConversationId(backStack = listOf(ConversationListNavKey)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.android.messaging.ui.navigation

import androidx.activity.ComponentActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.android.messaging.testutil.assertThat
import com.android.messaging.ui.common.components.ListDetailPaneSide
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class DisplayCornerDecoratorTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun listDetailEntry_whenTheWindowGainsAndLosesASecondPane_keepsItsState() {
var showsTwoPanes by mutableStateOf(false)
val entryStates = mutableSetOf<Any>()
composeTestRule.setContent {
NavDisplay(
backStack = listOf(TestKey),
onBack = {},
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberListDetailPaneNavEntryDecorator(showsTwoPanes = showsTwoPanes),
rememberDisplayCornerNavEntryDecorator(
topContentKey = NavEntry(TestKey) {}.contentKey,
),
),
entryProvider = { key ->
NavEntry(
key = key,
metadata = listDetailPaneMetadata(side = ListDetailPaneSide.End),
) {
entryStates += remember { Any() }
}
},
)
}

showsTwoPanes = true
composeTestRule.waitForIdle()
showsTwoPanes = false
composeTestRule.waitForIdle()

assertThat(entryStates.size).isEqualTo(1)
}

private data object TestKey : NavKey
}
Loading
Loading