Skip to content
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ android {
applicationId = "dev.typetype.android"
minSdk = 23
targetSdk = 37
versionCode = 10601
versionName = "1.6.1"
versionCode = 10700
versionName = "1.7.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resValue("string", "app_name", "TypeType")
}
Expand Down
171 changes: 48 additions & 123 deletions app/src/androidTest/java/dev/typetype/android/AppShellAdaptiveTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.NavHostController
import androidx.test.espresso.Espresso.closeSoftKeyboard
import dev.typetype.android.core.ui.navigation.HomeRoute
import dev.typetype.android.core.ui.navigation.ChannelRoute
import dev.typetype.android.core.ui.navigation.LibraryRoute
import dev.typetype.android.core.ui.navigation.SearchRoute
import dev.typetype.android.core.ui.navigation.SubscriptionsRoute
Expand All @@ -62,76 +64,6 @@ class AppShellAdaptiveTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun wideWindowUsesNavigationRail() {
setShellWidth(700.dp)

assertNodeCount(APP_NAVIGATION_RAIL_TAG, 1)
assertNodeCount(APP_BOTTOM_NAVIGATION_TAG, 0)
}

@Test
fun compactWindowUsesBottomNavigation() {
setShellWidth(400.dp)

assertNodeCount(APP_BOTTOM_NAVIGATION_TAG, 1)
assertNodeCount(APP_NAVIGATION_RAIL_TAG, 0)
}

@Test
fun landscapePhoneUsesBottomNavigation() {
setShellSize(width = 800.dp, height = 400.dp)

assertNodeCount(APP_BOTTOM_NAVIGATION_TAG, 1)
assertNodeCount(APP_NAVIGATION_RAIL_TAG, 0)
}

@Test
fun shortsTabFollowsTheServerVisibilitySetting() {
val showShorts = mutableStateOf(false)
setShellSize(width = 300.dp, height = 500.dp, showShorts = showShorts)

composeRule.onNodeWithText("Shorts").assertDoesNotExist()

composeRule.runOnIdle { showShorts.value = true }

composeRule.onNodeWithText("Shorts").assertIsDisplayed()
}

@Test
fun compactNavigationRemainsVisibleAtTwoHundredPercentText() {
setShellSize(width = 320.dp, height = 500.dp, fontScale = 2f)

listOf("Home", "Shorts", "Subscriptions", "Library").forEach {
composeRule.onNodeWithText(it).assertIsDisplayed()
}
}

@Test
fun rightToLeftLayoutMirrorsTheTopLevelTabs() {
setShellSize(
width = 400.dp,
height = 800.dp,
layoutDirection = LayoutDirection.Rtl,
)

val home = composeRule.onNodeWithText("Home").bounds()
val library = composeRule.onNodeWithText("Library").bounds()
assertTrue(home.left > library.left)
}

@Test
fun directionalKeysMoveFocusAcrossTopLevelTabs() {
setShellSize(width = 400.dp, height = 800.dp, keyboardInput = true)
val home = composeRule.onNodeWithText("Home")

home.performSemanticsAction(SemanticsActions.RequestFocus)
home.assertIsFocused()
home.performKeyInput { pressKey(Key.DirectionRight) }

composeRule.onNodeWithText("Shorts").assertIsFocused()
}

@Test
fun topLevelNavigationDoesNotRestoreSearchOverItsOpeningTab() {
composeRule.setContent {
Expand Down Expand Up @@ -174,6 +106,52 @@ class AppShellAdaptiveTest {
composeRule.onNodeWithText("Search content").assertDoesNotExist()
}

@Test
fun selectingActiveTabReturnsToTheOriginalTabPage() {
var navController: NavHostController? = null
composeRule.setContent {
val controller = rememberNavController()
navController = controller
AppShell(
navController = controller,
playerHostController = PlayerHostController(FakePlaybackQueueController()),
onOpenSearch = {},
onOpenSettings = {},
onPlayVideo = {},
onOpenChannel = {},
onOpenAccounts = {},
onClosePlayback = {},
) { contentModifier ->
NavHost(
navController = controller,
startDestination = HomeRoute,
modifier = contentModifier,
) {
composable<HomeRoute> { androidx.compose.material3.Text("Home content") }
composable<SubscriptionsRoute> {
androidx.compose.material3.Text("Subscriptions content")
}
composable<ChannelRoute> {
androidx.compose.material3.Text("Channel content")
}
composable<LibraryRoute> { androidx.compose.material3.Text("Library content") }
}
}
}

composeRule.onNodeWithText("Subscriptions").performClick()
composeRule.onNodeWithText("Subscriptions content").assertIsDisplayed()

composeRule.runOnIdle {
navController?.navigateToChannel("https://example.com/channel")
}
composeRule.onNodeWithText("Channel content").assertIsDisplayed()

composeRule.onNodeWithText("Subscriptions").performClick()
composeRule.onNodeWithText("Subscriptions content").assertIsDisplayed()
composeRule.onNodeWithText("Channel content").assertDoesNotExist()
}

@Test
fun closedKeyboardDoesNotTrapSearchNavigationOrBack() {
composeRule.setContent {
Expand Down Expand Up @@ -270,59 +248,6 @@ class AppShellAdaptiveTest {
composeRule.onNodeWithTag(SEARCH_FIELD_TAG).assertTextEquals("")
}

private fun setShellWidth(width: Dp) {
setShellSize(width = width, height = 800.dp)
}

private fun setShellSize(
width: Dp,
height: Dp,
showShorts: MutableState<Boolean> = mutableStateOf(true),
fontScale: Float = 1f,
layoutDirection: LayoutDirection = LayoutDirection.Ltr,
keyboardInput: Boolean = false,
) {
composeRule.setContent {
val systemDensity = LocalDensity.current
val inputModeManager = LocalInputModeManager.current
LaunchedEffect(keyboardInput) {
if (keyboardInput) inputModeManager.requestInputMode(InputMode.Keyboard)
}
CompositionLocalProvider(
LocalDensity provides Density(systemDensity.density, fontScale),
LocalLayoutDirection provides layoutDirection,
) {
val navController = rememberNavController()
AppShell(
navController = navController,
playerHostController = PlayerHostController(FakePlaybackQueueController()),
onOpenSettings = {},
onPlayVideo = {},
onOpenChannel = {},
onOpenAccounts = {},
onClosePlayback = {},
showShorts = showShorts.value,
modifier = Modifier.requiredWidth(width).requiredHeight(height),
) { contentModifier ->
NavHost(
navController = navController,
startDestination = HomeRoute,
modifier = contentModifier,
) {
composable<HomeRoute> { }
}
}
}
}
}

private fun androidx.compose.ui.test.SemanticsNodeInteraction.bounds(): Rect =
fetchSemanticsNode().boundsInRoot

private fun assertNodeCount(tag: String, expected: Int) {
val count = composeRule.onAllNodesWithTag(tag).fetchSemanticsNodes().size
assertEquals(expected, count)
}
}

private const val SEARCH_FIELD_TAG = "search_field"
Expand Down
Loading