Skip to content
8 changes: 8 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,11 @@ jobs:

- name: Check code test coverage
run: ./gradlew jvmTest -Pkover koverVerify

- name: Upload JVM test report
if: always()
uses: actions/upload-artifact@v4
with:
name: jvm-test-report
path: foliary/build/reports/tests/jvmTest/
if-no-files-found: ignore
5 changes: 5 additions & 0 deletions foliary/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

<!-- region Main -->
<string name="main_nav_today">Today</string>
<string name="main_nav_upcoming">Upcoming</string>
<string name="main_nav_profile">Profile</string>
<!-- endregion -->

Expand All @@ -44,6 +45,10 @@
<string name="profile_title">Profile</string>
<!-- endregion -->

<!-- region Upcoming -->
<string name="upcoming_title">Upcoming</string>
<!-- endregion -->

<!-- region Today -->
<string name="today_title">Today</string>
<string name="today_welcome">Welcome back, %1$s</string>
Expand Down
Comment thread
MessiasLima marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.CalendarCheck
import com.composables.icons.lucide.CalendarDays
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.User
import dev.appoutlet.foliary.core.ui.component.layout.Screen
import dev.appoutlet.foliary.feature.profile.ProfileScreen
import dev.appoutlet.foliary.feature.today.TodayScreen
import dev.appoutlet.foliary.feature.upcoming.UpcomingScreen
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.main_nav_profile
import foliary.foliary.generated.resources.main_nav_today
import foliary.foliary.generated.resources.main_nav_upcoming
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel

Expand All @@ -41,64 +45,89 @@ fun MainScreen() {
screenName = "MainScreen",
viewModelProvider = { viewModel }
) { viewData: MainViewData ->
val itemColors = getItemColors()
val todayLazyListState = rememberLazyListState()
val upcomingLazyListState = rememberLazyListState()

val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
adaptiveInfo = currentWindowAdaptiveInfoV2(),
)

val (windowDecorationPadding, itemTopPadding) = remember(layoutType) {
when (layoutType) {
NavigationSuiteType.NavigationRail,
NavigationSuiteType.WideNavigationRailExpanded,
NavigationSuiteType.WideNavigationRailCollapsed -> getWindowDecorationPadding() to 16.dp

else -> 0.dp to 0.dp
MainScreenNavigation(
selectedTab = viewData.selectedTab,
onTabSelect = viewModel::onTabSelected
) { selectedTab ->
when (selectedTab) {
MainTab.Today -> TodayScreen(todayLazyListState)
MainTab.Upcoming -> UpcomingScreen(upcomingLazyListState)
MainTab.Profile -> ProfileScreen()
}
}
}
}

NavigationSuiteScaffold(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface),
layoutType = layoutType,
navigationSuiteItems = {
item(
modifier = Modifier.padding(top = windowDecorationPadding + itemTopPadding),
selected = viewData.selectedTab == MainTab.Today,
onClick = { viewModel.onTabSelected(MainTab.Today) },
icon = { Icon(Lucide.CalendarCheck, contentDescription = null) },
label = { Text(stringResource(Res.string.main_nav_today)) },
colors = itemColors,
)
@Composable
private fun MainScreenNavigation(
selectedTab: MainTab,
onTabSelect: (MainTab) -> Unit,
content: @Composable (MainTab) -> Unit
) {
val itemColors = getItemColors()

item(
modifier = Modifier.padding(top = itemTopPadding),
selected = viewData.selectedTab == MainTab.Profile,
onClick = { viewModel.onTabSelected(MainTab.Profile) },
icon = { Icon(Lucide.User, contentDescription = null) },
label = { Text(stringResource(Res.string.main_nav_profile)) },
colors = itemColors,
)
},
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationBarContainerColor = MaterialTheme.colorScheme.surface,
navigationRailContainerColor = MaterialTheme.colorScheme.surface,
),
) {
val todayLazyListState = rememberLazyListState()
val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
adaptiveInfo = currentWindowAdaptiveInfoV2(),
)

Crossfade(
modifier = Modifier.fillMaxSize(),
targetState = viewData.selectedTab,
) { selectedTab ->
when (selectedTab) {
MainTab.Today -> TodayScreen(todayLazyListState)
MainTab.Profile -> ProfileScreen()
}
}
val (windowDecorationPadding, itemTopPadding) = remember(layoutType) {
when (layoutType) {
NavigationSuiteType.NavigationRail,
NavigationSuiteType.WideNavigationRailExpanded,
NavigationSuiteType.WideNavigationRailCollapsed -> getWindowDecorationPadding() to 16.dp

else -> 0.dp to 0.dp
}
}

NavigationSuiteScaffold(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface),
layoutType = layoutType,
navigationSuiteItems = {
item(
modifier = Modifier.testTag("MainScreen:TodayTab")
.padding(top = windowDecorationPadding + itemTopPadding),
selected = selectedTab == MainTab.Today,
onClick = { onTabSelect(MainTab.Today) },
icon = { Icon(Lucide.CalendarCheck, contentDescription = null) },
label = { Text(stringResource(Res.string.main_nav_today)) },
colors = itemColors,
)

item(
modifier = Modifier.testTag("MainScreen:UpcomingTab").padding(top = itemTopPadding),
selected = selectedTab == MainTab.Upcoming,
onClick = { onTabSelect(MainTab.Upcoming) },
icon = { Icon(Lucide.CalendarDays, contentDescription = null) },
label = { Text(stringResource(Res.string.main_nav_upcoming)) },
colors = itemColors,
)

item(
modifier = Modifier.testTag("MainScreen:ProfileTab").padding(top = itemTopPadding),
selected = selectedTab == MainTab.Profile,
onClick = { onTabSelect(MainTab.Profile) },
icon = { Icon(Lucide.User, contentDescription = null) },
label = { Text(stringResource(Res.string.main_nav_profile)) },
colors = itemColors,
)
},
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationBarContainerColor = MaterialTheme.colorScheme.surface,
navigationRailContainerColor = MaterialTheme.colorScheme.surface,
),
) {
Crossfade(
modifier = Modifier.fillMaxSize(),
targetState = selectedTab,
content = content
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ data class MainViewData(
)

enum class MainTab {
Today, Profile
Today, Upcoming, Profile
}

object MainAction : Action
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.appoutlet.foliary.feature.upcoming

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.upcoming_title
import org.jetbrains.compose.resources.stringResource

@Composable
fun UpcomingScreen(lazyListState: LazyListState) {
LazyColumn(
modifier = Modifier
.testTag("UpcomingScreen")
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
.fillMaxSize(),
state = lazyListState,
) {
item {
Text(
modifier = Modifier.padding(16.dp),
text = stringResource(Res.string.upcoming_title),
style = MaterialTheme.typography.displaySmall
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.test.v2.runComposeUiTest
import io.kotest.matchers.shouldBe
import kotlin.test.Test

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.test.v2.runComposeUiTest
import io.kotest.matchers.shouldBe
import kotlin.test.Test

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.test.v2.runComposeUiTest
import io.kotest.matchers.shouldBe
import kotlin.test.Test

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dev.appoutlet.foliary.feature.main

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest
import androidx.compose.ui.test.waitUntilAtLeastOneExists
import dev.appoutlet.foliary.FoliaryKoinApplication
import dev.appoutlet.foliary.core.analytics.LocalAnalytics
import dev.appoutlet.foliary.core.analytics.MockAnalytics
import org.koin.core.context.stopKoin
import org.koin.plugin.module.dsl.startKoin
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test

@OptIn(ExperimentalTestApi::class)
class MainScreenTest {
private val mockAnalytics = MockAnalytics()

@BeforeTest
fun setup() {
startKoin<FoliaryKoinApplication>()
}

@AfterTest
fun tearDown() {
stopKoin()
}

@Test
fun `should render bottom navigation with three tabs`() = runComposeUiTest {
setContent {
CompositionLocalProvider(LocalAnalytics provides mockAnalytics) {
MainScreen()
}
}

waitUntilAtLeastOneExists(hasTestTag("TodayScreen"))

onNodeWithTag("MainScreen:TodayTab").assertIsDisplayed()

onNodeWithTag("MainScreen:UpcomingTab").assertIsDisplayed().performClick()
waitUntilAtLeastOneExists(hasTestTag("UpcomingScreen"))

onNodeWithTag("MainScreen:ProfileTab").assertIsDisplayed().performClick()
waitUntilAtLeastOneExists(hasTestTag("ProfileScreen"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dev.appoutlet.foliary.feature.main

import dev.appoutlet.foliary.core.testing.ViewModelTest
import io.kotest.matchers.shouldBe
import kotlin.test.Test

class MainViewModelTest : ViewModelTest<MainViewModel, MainViewData, MainAction>() {
override fun createViewModel() = MainViewModel()

@Test
fun `should select today tab by default`() {
test {
currentState.selectedTab shouldBe MainTab.Today
}
}

@Test
fun `should switch to upcoming tab`() {
test {
viewModel.onTabSelected(MainTab.Upcoming)

expectState { copy(selectedTab = MainTab.Upcoming) }
}
}

@Test
fun `should switch to profile tab`() {
test {
viewModel.onTabSelected(MainTab.Profile)

expectState { copy(selectedTab = MainTab.Profile) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package dev.appoutlet.foliary.feature.signin.composable
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.test.v2.runComposeUiTest
import dev.appoutlet.foliary.feature.signin.SignInViewData
import kotlin.test.Test

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.test.v2.runComposeUiTest
import dev.appoutlet.foliary.feature.signin.SignInEvent
import dev.appoutlet.foliary.feature.signin.SignInViewData
import io.kotest.matchers.shouldBe
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.appoutlet.foliary.feature.upcoming

import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runComposeUiTest
import foliary.foliary.generated.resources.Res
import foliary.foliary.generated.resources.upcoming_title
import org.jetbrains.compose.resources.getString
import kotlin.test.Test

@OptIn(ExperimentalTestApi::class)
class UpcomingScreenTest {
@Test
fun `should render upcoming title`() = runComposeUiTest {
setContent {
UpcomingScreen(lazyListState = rememberLazyListState())
}

onNodeWithText(getString(Res.string.upcoming_title)).assertIsDisplayed()
}
}
Loading