From aa292b82518d86427a56e25fffd94f4a8f374786 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 26 Jul 2026 14:55:39 +0000 Subject: [PATCH 01/10] Redesigned navigation with bottom tabs. Co-authored-by: MessiasLima --- .../composeResources/values/strings.xml | 6 + .../foliary/feature/main/MainScreen.kt | 124 +++++++----------- .../foliary/feature/main/MainViewModel.kt | 2 +- .../foliary/feature/profile/ProfileScreen.kt | 69 +++++++--- .../feature/profile/ProfileViewModel.kt | 6 + .../feature/upcoming/UpcomingScreen.kt | 37 ++++++ .../foliary/feature/main/MainScreenTest.kt | 71 ++++++++++ .../foliary/feature/main/MainViewModelTest.kt | 34 +++++ .../feature/profile/ProfileScreenTest.kt | 102 ++++++++++++++ .../feature/profile/ProfileViewModelTest.kt | 16 ++- .../feature/upcoming/UpcomingScreenTest.kt | 23 ++++ 11 files changed, 396 insertions(+), 94 deletions(-) create mode 100644 foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt create mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt create mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainViewModelTest.kt create mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt create mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreenTest.kt diff --git a/foliary/src/commonMain/composeResources/values/strings.xml b/foliary/src/commonMain/composeResources/values/strings.xml index 04cc92b8..5c9a0f7d 100644 --- a/foliary/src/commonMain/composeResources/values/strings.xml +++ b/foliary/src/commonMain/composeResources/values/strings.xml @@ -34,14 +34,20 @@ Today + Upcoming Profile Log out + Statistics Profile + + Upcoming + + Today Welcome back, %1$s diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt index 5b33f66c..99b0c33b 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt @@ -1,36 +1,31 @@ package dev.appoutlet.foliary.feature.main import androidx.compose.animation.Crossfade -import androidx.compose.foundation.background +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults -import androidx.compose.material3.NavigationDrawerItemDefaults -import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults -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.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 @@ -41,60 +36,51 @@ fun MainScreen() { screenName = "MainScreen", viewModelProvider = { viewModel } ) { viewData: MainViewData -> - val itemColors = getItemColors() + val todayLazyListState = rememberLazyListState() + val upcomingLazyListState = rememberLazyListState() + val profileLazyListState = rememberLazyListState() - val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo( - adaptiveInfo = currentWindowAdaptiveInfoV2(), - ) + Scaffold( + modifier = Modifier.fillMaxSize(), + contentWindowInsets = WindowInsets(0, 0, 0, 0), + bottomBar = { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surface, + ) { + NavigationBarItem( + 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 = navigationBarItemColors(), + ) - val (windowDecorationPadding, itemTopPadding) = remember(layoutType) { - when (layoutType) { - NavigationSuiteType.NavigationRail, - NavigationSuiteType.WideNavigationRailExpanded, - NavigationSuiteType.WideNavigationRailCollapsed -> getWindowDecorationPadding() to 16.dp + NavigationBarItem( + selected = viewData.selectedTab == MainTab.Upcoming, + onClick = { viewModel.onTabSelected(MainTab.Upcoming) }, + icon = { Icon(Lucide.CalendarDays, contentDescription = null) }, + label = { Text(stringResource(Res.string.main_nav_upcoming)) }, + colors = navigationBarItemColors(), + ) - else -> 0.dp to 0.dp - } - } - - 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, - ) - - 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, - ) + NavigationBarItem( + 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 = navigationBarItemColors(), + ) + } }, - navigationSuiteColors = NavigationSuiteDefaults.colors( - navigationBarContainerColor = MaterialTheme.colorScheme.surface, - navigationRailContainerColor = MaterialTheme.colorScheme.surface, - ), - ) { - val todayLazyListState = rememberLazyListState() - + ) { _ -> Crossfade( modifier = Modifier.fillMaxSize(), targetState = viewData.selectedTab, ) { selectedTab -> when (selectedTab) { MainTab.Today -> TodayScreen(todayLazyListState) - MainTab.Profile -> ProfileScreen() + MainTab.Upcoming -> UpcomingScreen(upcomingLazyListState) + MainTab.Profile -> ProfileScreen(profileLazyListState) } } } @@ -102,24 +88,12 @@ fun MainScreen() { } @Composable -private fun getItemColors(): NavigationSuiteItemColors { - return NavigationSuiteItemColors( - navigationBarItemColors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - selectedTextColor = MaterialTheme.colorScheme.primary, - indicatorColor = MaterialTheme.colorScheme.secondary, - unselectedIconColor = MaterialTheme.colorScheme.onBackground, - unselectedTextColor = MaterialTheme.colorScheme.onBackground, - ), - navigationRailItemColors = NavigationRailItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - selectedTextColor = MaterialTheme.colorScheme.primary, - indicatorColor = MaterialTheme.colorScheme.secondary, - unselectedIconColor = MaterialTheme.colorScheme.onBackground, - unselectedTextColor = MaterialTheme.colorScheme.onBackground, - ), - navigationDrawerItemColors = NavigationDrawerItemDefaults.colors(), - ) -} +private fun navigationBarItemColors() = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + indicatorColor = MaterialTheme.colorScheme.secondary, + unselectedIconColor = MaterialTheme.colorScheme.onBackground, + unselectedTextColor = MaterialTheme.colorScheme.onBackground, +) expect fun getWindowDecorationPadding(): Dp diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainViewModel.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainViewModel.kt index 1b9d6faa..7238a2a4 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainViewModel.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainViewModel.kt @@ -18,7 +18,7 @@ data class MainViewData( ) enum class MainTab { - Today, Profile + Today, Upcoming, Profile } object MainAction : Action diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt index 6dede911..2a74d754 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt @@ -1,42 +1,77 @@ package dev.appoutlet.foliary.feature.profile -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.clickable +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.material3.Scaffold +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.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import dev.appoutlet.foliary.core.navigation.Navigator -import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton import dev.appoutlet.foliary.core.ui.component.layout.Screen import dev.appoutlet.foliary.feature.signin.SignInNavKey import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.profile_logout +import foliary.foliary.generated.resources.profile_statistics +import foliary.foliary.generated.resources.profile_title import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Composable -fun ProfileScreen() { +fun ProfileScreen(lazyListState: LazyListState) { val viewModel = koinViewModel() + ProfileScreenContent(lazyListState, viewModel) +} + +@Composable +internal fun ProfileScreenContent( + lazyListState: LazyListState, + viewModel: ProfileViewModel +) { Screen( screenName = "ProfileScreen", viewModelProvider = { viewModel }, onAction = ::onAction ) { - Scaffold( - modifier = Modifier.fillMaxSize() - ) { paddingValues -> - Box( - modifier = Modifier.padding(paddingValues).fillMaxSize(), - contentAlignment = Alignment.Center - ) { - FoliaryPrimaryButton( - onClick = { viewModel.onEvent(ProfileEvent.OnLogOutClick) } - ) { - Text(text = stringResource(Res.string.profile_logout)) - } + LazyColumn( + modifier = Modifier + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) + .fillMaxSize(), + state = lazyListState, + ) { + item { + Text( + modifier = Modifier.padding(16.dp), + text = stringResource(Res.string.profile_title), + style = MaterialTheme.typography.displaySmall + ) + } + + item { + ListItem( + headlineContent = { Text(text = stringResource(Res.string.profile_statistics)) }, + modifier = Modifier.clickable { + viewModel.onEvent(ProfileEvent.OnStatisticsClick) + } + ) + } + + item { + ListItem( + headlineContent = { Text(text = stringResource(Res.string.profile_logout)) }, + modifier = Modifier.clickable { + viewModel.onEvent(ProfileEvent.OnLogOutClick) + } + ) } } } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt index 68c42c45..84e727c8 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt @@ -16,9 +16,14 @@ class ProfileViewModel( fun onEvent(event: ProfileEvent) { when (event) { ProfileEvent.OnLogOutClick -> onLogOutClick() + ProfileEvent.OnStatisticsClick -> onStatisticsClick() } } + private fun onStatisticsClick() = intent { + analytics.trackEvent("profile_statistics_clicked") + } + private fun onLogOutClick() = intent { authenticationRepository.signOut() analytics.trackEvent("user_logged_out") @@ -34,4 +39,5 @@ sealed interface ProfileAction : Action { sealed interface ProfileEvent { data object OnLogOutClick : ProfileEvent + data object OnStatisticsClick : ProfileEvent } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt new file mode 100644 index 00000000..999bb043 --- /dev/null +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt @@ -0,0 +1,37 @@ +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.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 + .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 + ) + } + } +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt new file mode 100644 index 00000000..76e0295f --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt @@ -0,0 +1,71 @@ +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.hasText +import androidx.compose.ui.test.onNodeWithText +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 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 foliary.foliary.generated.resources.profile_title +import foliary.foliary.generated.resources.today_title +import foliary.foliary.generated.resources.upcoming_title +import org.jetbrains.compose.resources.getString +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() + } + + @AfterTest + fun tearDown() { + stopKoin() + } + + @Test + fun `should render bottom navigation with three tabs`() = runComposeUiTest { + setContent { + CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { + MainScreen() + } + } + + onNodeWithText(getString(Res.string.main_nav_today)).assertIsDisplayed() + onNodeWithText(getString(Res.string.main_nav_upcoming)).assertIsDisplayed() + onNodeWithText(getString(Res.string.main_nav_profile)).assertIsDisplayed() + } + + @Test + fun `should switch tabs and display corresponding screen`() = runComposeUiTest { + setContent { + CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { + MainScreen() + } + } + + waitUntilAtLeastOneExists(hasText(getString(Res.string.today_title))) + + onNodeWithText(getString(Res.string.main_nav_upcoming)).performClick() + waitUntilAtLeastOneExists(hasText(getString(Res.string.upcoming_title))) + + onNodeWithText(getString(Res.string.main_nav_profile)).performClick() + waitUntilAtLeastOneExists(hasText(getString(Res.string.profile_title))) + } +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainViewModelTest.kt new file mode 100644 index 00000000..16c05c74 --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainViewModelTest.kt @@ -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() { + 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) } + } + } +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt new file mode 100644 index 00000000..d704dd81 --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt @@ -0,0 +1,102 @@ +package dev.appoutlet.foliary.feature.profile + +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.navigation3.runtime.NavKey +import dev.appoutlet.foliary.core.analytics.LocalAnalytics +import dev.appoutlet.foliary.core.analytics.MockAnalytics +import dev.appoutlet.foliary.core.navigation.LocalNavigator +import dev.appoutlet.foliary.core.navigation.Navigator +import dev.appoutlet.foliary.data.authentication.AuthenticationRepository +import dev.appoutlet.foliary.feature.signin.SignInNavKey +import dev.mokkery.MockMode +import dev.mokkery.mock +import dev.mokkery.verifySuspend +import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.profile_logout +import foliary.foliary.generated.resources.profile_statistics +import foliary.foliary.generated.resources.profile_title +import io.kotest.matchers.shouldBe +import org.jetbrains.compose.resources.getString +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class ProfileScreenTest { + private val mockAnalytics = MockAnalytics() + private val mockAuthenticationRepository = mock(mode = MockMode.autoUnit) + + @Test + fun `should render profile screen with options`() = runComposeUiTest { + setContent { + CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { + ProfileScreenContent( + lazyListState = rememberLazyListState(), + viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics), + ) + } + } + + onNodeWithText(getString(Res.string.profile_title)).assertIsDisplayed() + onNodeWithText(getString(Res.string.profile_statistics)).assertIsDisplayed() + onNodeWithText(getString(Res.string.profile_logout)).assertIsDisplayed() + } + + @Test + fun `should track statistics click`() = runComposeUiTest { + setContent { + CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { + ProfileScreenContent( + lazyListState = rememberLazyListState(), + viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics), + ) + } + } + + onNodeWithText(getString(Res.string.profile_statistics)).performClick() + + waitUntil("Statistics click is tracked") { + mockAnalytics.events.containsKey("profile_statistics_clicked") + } + } + + @Test + fun `should log out and navigate to sign in`() = runComposeUiTest { + val fakeNavigator = FakeNavigator() + + setContent { + CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { + CompositionLocalProvider(LocalNavigator provides fakeNavigator) { + ProfileScreenContent( + lazyListState = rememberLazyListState(), + viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics), + ) + } + } + } + + onNodeWithText(getString(Res.string.profile_logout)).performClick() + + waitUntil("Navigated to sign in") { fakeNavigator.roots.isNotEmpty() } + + verifySuspend { mockAuthenticationRepository.signOut() } + mockAnalytics.events.containsKey("user_logged_out") shouldBe true + fakeNavigator.roots shouldBe listOf(SignInNavKey) + } +} + +private class FakeNavigator : Navigator { + val roots = mutableListOf() + + override fun navigate(destination: NavKey) = Unit + + override fun setRoot(destination: NavKey) { + roots.add(destination) + } + + override fun goBack() = Unit +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt index 45376ac7..1327d5ab 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt @@ -10,14 +10,19 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest import kotlin.test.Test @OptIn(ExperimentalCoroutinesApi::class) class ProfileViewModelTest { private val mockAuthenticationRepository = mock(mode = MockMode.autoUnit) private val mockAnalytics = mock(mode = MockMode.autoUnit) + private lateinit var viewModel: ProfileViewModel - private val viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics) + @BeforeTest + fun setup() { + viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics) + } @Test fun `should load profile screen`() = runTest { @@ -36,4 +41,13 @@ class ProfileViewModelTest { viewModel.container.sideEffectFlow.first() shouldBe ProfileAction.NavigateToSignIn } + + @Test + fun `should track statistics click`() = runTest { + viewModel.onEvent(ProfileEvent.OnStatisticsClick) + + advanceUntilIdle() + + verifySuspend { mockAnalytics.trackEvent("profile_statistics_clicked") } + } } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreenTest.kt new file mode 100644 index 00000000..9b5ad5fb --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreenTest.kt @@ -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() + } +} From a7837dec0e440ce7bcc3ce0283214bb69ec31d66 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 27 Jul 2026 15:58:42 +0000 Subject: [PATCH 02/10] Restored nav scaffold, added Upcoming. Co-authored-by: MessiasLima --- .../foliary/feature/main/MainScreen.kt | 130 ++++++++++++------ .../foliary/feature/profile/ProfileScreen.kt | 3 +- 2 files changed, 87 insertions(+), 46 deletions(-) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt index 99b0c33b..36d7ae21 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt @@ -1,19 +1,27 @@ package dev.appoutlet.foliary.feature.main import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults -import androidx.compose.material3.Scaffold +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults +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.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 @@ -36,43 +44,63 @@ fun MainScreen() { screenName = "MainScreen", viewModelProvider = { viewModel } ) { viewData: MainViewData -> - val todayLazyListState = rememberLazyListState() - val upcomingLazyListState = rememberLazyListState() - val profileLazyListState = rememberLazyListState() + val itemColors = getItemColors() - Scaffold( - modifier = Modifier.fillMaxSize(), - contentWindowInsets = WindowInsets(0, 0, 0, 0), - bottomBar = { - NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, - ) { - NavigationBarItem( - 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 = navigationBarItemColors(), - ) + val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo( + adaptiveInfo = currentWindowAdaptiveInfoV2(), + ) - NavigationBarItem( - selected = viewData.selectedTab == MainTab.Upcoming, - onClick = { viewModel.onTabSelected(MainTab.Upcoming) }, - icon = { Icon(Lucide.CalendarDays, contentDescription = null) }, - label = { Text(stringResource(Res.string.main_nav_upcoming)) }, - colors = navigationBarItemColors(), - ) + val (windowDecorationPadding, itemTopPadding) = remember(layoutType) { + when (layoutType) { + NavigationSuiteType.NavigationRail, + NavigationSuiteType.WideNavigationRailExpanded, + NavigationSuiteType.WideNavigationRailCollapsed -> getWindowDecorationPadding() to 16.dp - NavigationBarItem( - 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 = navigationBarItemColors(), - ) - } + else -> 0.dp to 0.dp + } + } + + 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, + ) + + item( + modifier = Modifier.padding(top = itemTopPadding), + selected = viewData.selectedTab == MainTab.Upcoming, + onClick = { viewModel.onTabSelected(MainTab.Upcoming) }, + icon = { Icon(Lucide.CalendarDays, contentDescription = null) }, + label = { Text(stringResource(Res.string.main_nav_upcoming)) }, + colors = itemColors, + ) + + 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 upcomingLazyListState = rememberLazyListState() + Crossfade( modifier = Modifier.fillMaxSize(), targetState = viewData.selectedTab, @@ -80,7 +108,7 @@ fun MainScreen() { when (selectedTab) { MainTab.Today -> TodayScreen(todayLazyListState) MainTab.Upcoming -> UpcomingScreen(upcomingLazyListState) - MainTab.Profile -> ProfileScreen(profileLazyListState) + MainTab.Profile -> ProfileScreen() } } } @@ -88,12 +116,24 @@ fun MainScreen() { } @Composable -private fun navigationBarItemColors() = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - selectedTextColor = MaterialTheme.colorScheme.primary, - indicatorColor = MaterialTheme.colorScheme.secondary, - unselectedIconColor = MaterialTheme.colorScheme.onBackground, - unselectedTextColor = MaterialTheme.colorScheme.onBackground, -) +private fun getItemColors(): NavigationSuiteItemColors { + return NavigationSuiteItemColors( + navigationBarItemColors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + indicatorColor = MaterialTheme.colorScheme.secondary, + unselectedIconColor = MaterialTheme.colorScheme.onBackground, + unselectedTextColor = MaterialTheme.colorScheme.onBackground, + ), + navigationRailItemColors = NavigationRailItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + indicatorColor = MaterialTheme.colorScheme.secondary, + unselectedIconColor = MaterialTheme.colorScheme.onBackground, + unselectedTextColor = MaterialTheme.colorScheme.onBackground, + ), + navigationDrawerItemColors = NavigationDrawerItemDefaults.colors(), + ) +} expect fun getWindowDecorationPadding(): Dp diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt index 2a74d754..dfa7b1b5 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt @@ -10,6 +10,7 @@ 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.foundation.lazy.rememberLazyListState import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -27,7 +28,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Composable -fun ProfileScreen(lazyListState: LazyListState) { +fun ProfileScreen(lazyListState: LazyListState = rememberLazyListState()) { val viewModel = koinViewModel() ProfileScreenContent(lazyListState, viewModel) } From 3b18c284e9b886c90ae8cabcdcfef13a3513f101 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 27 Jul 2026 16:17:06 +0000 Subject: [PATCH 03/10] Reverted ProfileScreen redesign. Co-authored-by: MessiasLima --- .../composeResources/values/strings.xml | 1 - .../foliary/feature/profile/ProfileScreen.kt | 70 +++--------- .../feature/profile/ProfileViewModel.kt | 6 -- .../foliary/feature/main/MainScreenTest.kt | 4 +- .../feature/profile/ProfileScreenTest.kt | 102 ------------------ .../feature/profile/ProfileViewModelTest.kt | 16 +-- 6 files changed, 20 insertions(+), 179 deletions(-) delete mode 100644 foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt diff --git a/foliary/src/commonMain/composeResources/values/strings.xml b/foliary/src/commonMain/composeResources/values/strings.xml index 5c9a0f7d..7c091c05 100644 --- a/foliary/src/commonMain/composeResources/values/strings.xml +++ b/foliary/src/commonMain/composeResources/values/strings.xml @@ -40,7 +40,6 @@ Log out - Statistics Profile diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt index dfa7b1b5..6dede911 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreen.kt @@ -1,78 +1,42 @@ package dev.appoutlet.foliary.feature.profile -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.Box 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.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import dev.appoutlet.foliary.core.navigation.Navigator +import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton import dev.appoutlet.foliary.core.ui.component.layout.Screen import dev.appoutlet.foliary.feature.signin.SignInNavKey import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.profile_logout -import foliary.foliary.generated.resources.profile_statistics -import foliary.foliary.generated.resources.profile_title import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Composable -fun ProfileScreen(lazyListState: LazyListState = rememberLazyListState()) { +fun ProfileScreen() { val viewModel = koinViewModel() - ProfileScreenContent(lazyListState, viewModel) -} - -@Composable -internal fun ProfileScreenContent( - lazyListState: LazyListState, - viewModel: ProfileViewModel -) { Screen( screenName = "ProfileScreen", viewModelProvider = { viewModel }, onAction = ::onAction ) { - LazyColumn( - modifier = Modifier - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) - .fillMaxSize(), - state = lazyListState, - ) { - item { - Text( - modifier = Modifier.padding(16.dp), - text = stringResource(Res.string.profile_title), - style = MaterialTheme.typography.displaySmall - ) - } - - item { - ListItem( - headlineContent = { Text(text = stringResource(Res.string.profile_statistics)) }, - modifier = Modifier.clickable { - viewModel.onEvent(ProfileEvent.OnStatisticsClick) - } - ) - } - - item { - ListItem( - headlineContent = { Text(text = stringResource(Res.string.profile_logout)) }, - modifier = Modifier.clickable { - viewModel.onEvent(ProfileEvent.OnLogOutClick) - } - ) + Scaffold( + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues).fillMaxSize(), + contentAlignment = Alignment.Center + ) { + FoliaryPrimaryButton( + onClick = { viewModel.onEvent(ProfileEvent.OnLogOutClick) } + ) { + Text(text = stringResource(Res.string.profile_logout)) + } } } } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt index 84e727c8..68c42c45 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModel.kt @@ -16,14 +16,9 @@ class ProfileViewModel( fun onEvent(event: ProfileEvent) { when (event) { ProfileEvent.OnLogOutClick -> onLogOutClick() - ProfileEvent.OnStatisticsClick -> onStatisticsClick() } } - private fun onStatisticsClick() = intent { - analytics.trackEvent("profile_statistics_clicked") - } - private fun onLogOutClick() = intent { authenticationRepository.signOut() analytics.trackEvent("user_logged_out") @@ -39,5 +34,4 @@ sealed interface ProfileAction : Action { sealed interface ProfileEvent { data object OnLogOutClick : ProfileEvent - data object OnStatisticsClick : ProfileEvent } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt index 76e0295f..51be9071 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt @@ -15,7 +15,7 @@ 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 foliary.foliary.generated.resources.profile_title +import foliary.foliary.generated.resources.profile_logout import foliary.foliary.generated.resources.today_title import foliary.foliary.generated.resources.upcoming_title import org.jetbrains.compose.resources.getString @@ -66,6 +66,6 @@ class MainScreenTest { waitUntilAtLeastOneExists(hasText(getString(Res.string.upcoming_title))) onNodeWithText(getString(Res.string.main_nav_profile)).performClick() - waitUntilAtLeastOneExists(hasText(getString(Res.string.profile_title))) + waitUntilAtLeastOneExists(hasText(getString(Res.string.profile_logout))) } } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt deleted file mode 100644 index d704dd81..00000000 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileScreenTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package dev.appoutlet.foliary.feature.profile - -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.v2.runComposeUiTest -import androidx.navigation3.runtime.NavKey -import dev.appoutlet.foliary.core.analytics.LocalAnalytics -import dev.appoutlet.foliary.core.analytics.MockAnalytics -import dev.appoutlet.foliary.core.navigation.LocalNavigator -import dev.appoutlet.foliary.core.navigation.Navigator -import dev.appoutlet.foliary.data.authentication.AuthenticationRepository -import dev.appoutlet.foliary.feature.signin.SignInNavKey -import dev.mokkery.MockMode -import dev.mokkery.mock -import dev.mokkery.verifySuspend -import foliary.foliary.generated.resources.Res -import foliary.foliary.generated.resources.profile_logout -import foliary.foliary.generated.resources.profile_statistics -import foliary.foliary.generated.resources.profile_title -import io.kotest.matchers.shouldBe -import org.jetbrains.compose.resources.getString -import kotlin.test.Test - -@OptIn(ExperimentalTestApi::class) -class ProfileScreenTest { - private val mockAnalytics = MockAnalytics() - private val mockAuthenticationRepository = mock(mode = MockMode.autoUnit) - - @Test - fun `should render profile screen with options`() = runComposeUiTest { - setContent { - CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { - ProfileScreenContent( - lazyListState = rememberLazyListState(), - viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics), - ) - } - } - - onNodeWithText(getString(Res.string.profile_title)).assertIsDisplayed() - onNodeWithText(getString(Res.string.profile_statistics)).assertIsDisplayed() - onNodeWithText(getString(Res.string.profile_logout)).assertIsDisplayed() - } - - @Test - fun `should track statistics click`() = runComposeUiTest { - setContent { - CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { - ProfileScreenContent( - lazyListState = rememberLazyListState(), - viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics), - ) - } - } - - onNodeWithText(getString(Res.string.profile_statistics)).performClick() - - waitUntil("Statistics click is tracked") { - mockAnalytics.events.containsKey("profile_statistics_clicked") - } - } - - @Test - fun `should log out and navigate to sign in`() = runComposeUiTest { - val fakeNavigator = FakeNavigator() - - setContent { - CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { - CompositionLocalProvider(LocalNavigator provides fakeNavigator) { - ProfileScreenContent( - lazyListState = rememberLazyListState(), - viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics), - ) - } - } - } - - onNodeWithText(getString(Res.string.profile_logout)).performClick() - - waitUntil("Navigated to sign in") { fakeNavigator.roots.isNotEmpty() } - - verifySuspend { mockAuthenticationRepository.signOut() } - mockAnalytics.events.containsKey("user_logged_out") shouldBe true - fakeNavigator.roots shouldBe listOf(SignInNavKey) - } -} - -private class FakeNavigator : Navigator { - val roots = mutableListOf() - - override fun navigate(destination: NavKey) = Unit - - override fun setRoot(destination: NavKey) { - roots.add(destination) - } - - override fun goBack() = Unit -} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt index 1327d5ab..45376ac7 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/profile/ProfileViewModelTest.kt @@ -10,19 +10,14 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import kotlin.test.BeforeTest import kotlin.test.Test @OptIn(ExperimentalCoroutinesApi::class) class ProfileViewModelTest { private val mockAuthenticationRepository = mock(mode = MockMode.autoUnit) private val mockAnalytics = mock(mode = MockMode.autoUnit) - private lateinit var viewModel: ProfileViewModel - @BeforeTest - fun setup() { - viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics) - } + private val viewModel = ProfileViewModel(mockAuthenticationRepository, mockAnalytics) @Test fun `should load profile screen`() = runTest { @@ -41,13 +36,4 @@ class ProfileViewModelTest { viewModel.container.sideEffectFlow.first() shouldBe ProfileAction.NavigateToSignIn } - - @Test - fun `should track statistics click`() = runTest { - viewModel.onEvent(ProfileEvent.OnStatisticsClick) - - advanceUntilIdle() - - verifySuspend { mockAnalytics.trackEvent("profile_statistics_clicked") } - } } From 556ee180e46c50eeafa682022e5e85e67569f39f Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Mon, 27 Jul 2026 22:34:06 +0100 Subject: [PATCH 04/10] extract navigaton --- .../foliary/feature/main/MainScreen.kt | 133 ++++++++++-------- 1 file changed, 73 insertions(+), 60 deletions(-) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt index 36d7ae21..a3b7ee2a 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt @@ -44,75 +44,88 @@ 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.Upcoming, - onClick = { viewModel.onTabSelected(MainTab.Upcoming) }, - icon = { Icon(Lucide.CalendarDays, contentDescription = null) }, - label = { Text(stringResource(Res.string.main_nav_upcoming)) }, - colors = itemColors, - ) + val layoutType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo( + adaptiveInfo = currentWindowAdaptiveInfoV2(), + ) - 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 upcomingLazyListState = rememberLazyListState() + val (windowDecorationPadding, itemTopPadding) = remember(layoutType) { + when (layoutType) { + NavigationSuiteType.NavigationRail, + NavigationSuiteType.WideNavigationRailExpanded, + NavigationSuiteType.WideNavigationRailCollapsed -> getWindowDecorationPadding() to 16.dp - Crossfade( - modifier = Modifier.fillMaxSize(), - targetState = viewData.selectedTab, - ) { selectedTab -> - when (selectedTab) { - MainTab.Today -> TodayScreen(todayLazyListState) - MainTab.Upcoming -> UpcomingScreen(upcomingLazyListState) - MainTab.Profile -> ProfileScreen() - } - } + else -> 0.dp to 0.dp } } + + NavigationSuiteScaffold( + modifier = Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.surface), + layoutType = layoutType, + navigationSuiteItems = { + item( + modifier = Modifier.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.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.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 From b25fa78146aef8da71230b892e68db7860eddfef Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 28 Jul 2026 09:48:19 +0100 Subject: [PATCH 05/10] bump compose test version --- .../core/ui/component/button/FoliaryOutlinedButtonTest.kt | 2 +- .../core/ui/component/button/FoliaryPrimaryButtonTest.kt | 2 +- .../core/ui/component/button/FoliarySecondaryButtonTest.kt | 2 +- .../foliary/feature/signin/composable/AuthenticatedTest.kt | 2 +- .../foliary/feature/signin/composable/MagicLinkSentTest.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryOutlinedButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryOutlinedButtonTest.kt index b76bf6ae..133e0f72 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryOutlinedButtonTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryOutlinedButtonTest.kt @@ -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 diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt index d901aa92..cdb2f2be 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliaryPrimaryButtonTest.kt @@ -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 diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt index 9702b781..d629684e 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/button/FoliarySecondaryButtonTest.kt @@ -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 diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/AuthenticatedTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/AuthenticatedTest.kt index 1cef7993..205b6d8e 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/AuthenticatedTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/AuthenticatedTest.kt @@ -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 diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/MagicLinkSentTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/MagicLinkSentTest.kt index 6d29badb..c7c4ff5e 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/MagicLinkSentTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/signin/composable/MagicLinkSentTest.kt @@ -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 From 6212b84620ae15b51c0048a6a51c842d97dd6bb1 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 28 Jul 2026 09:57:30 +0100 Subject: [PATCH 06/10] headless tests --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5c43799f..f7415e52 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -36,4 +36,4 @@ jobs: run: ./gradlew detekt - name: Check code test coverage - run: ./gradlew jvmTest -Pkover koverVerify + run: ./gradlew jvmTest -Pkover koverVerify --quiet -Djava.awt.headless=true -Djava.awt.headless.screen=0 From 649d259cc9d7a4148c5a3a170ef32967baaf2471 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 28 Jul 2026 10:09:39 +0100 Subject: [PATCH 07/10] info on test fails --- .github/workflows/pull-request.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f7415e52..380120b3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -36,4 +36,11 @@ jobs: run: ./gradlew detekt - name: Check code test coverage - run: ./gradlew jvmTest -Pkover koverVerify --quiet -Djava.awt.headless=true -Djava.awt.headless.screen=0 + run: ./gradlew jvmTest -Pkover koverVerify --info --stacktrace + + - name: Upload JVM test report + uses: actions/upload-artifact@v4 + with: + name: jvm-test-report + path: foliary/build/reports/tests/jvmTest/ + if-no-files-found: ignore From 13acd2bfff8f9594b156c345bad420a133e7b82c Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 28 Jul 2026 10:12:13 +0100 Subject: [PATCH 08/10] always --- .github/workflows/pull-request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 380120b3..88d44a91 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -39,6 +39,7 @@ jobs: run: ./gradlew jvmTest -Pkover koverVerify --info --stacktrace - name: Upload JVM test report + if: always() uses: actions/upload-artifact@v4 with: name: jvm-test-report From 9cfa5bb8cfc8e810cc4a511f012df2daa77f2451 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 28 Jul 2026 10:22:29 +0100 Subject: [PATCH 09/10] test tags instead of strings --- .../foliary/feature/main/MainScreen.kt | 8 +++-- .../feature/upcoming/UpcomingScreen.kt | 2 ++ .../foliary/feature/main/MainScreenTest.kt | 35 +++++-------------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt index a3b7ee2a..4359c1e8 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/main/MainScreen.kt @@ -20,6 +20,7 @@ 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 @@ -89,7 +90,8 @@ private fun MainScreenNavigation( layoutType = layoutType, navigationSuiteItems = { item( - modifier = Modifier.padding(top = windowDecorationPadding + itemTopPadding), + modifier = Modifier.testTag("MainScreen:TodayTab") + .padding(top = windowDecorationPadding + itemTopPadding), selected = selectedTab == MainTab.Today, onClick = { onTabSelect(MainTab.Today) }, icon = { Icon(Lucide.CalendarCheck, contentDescription = null) }, @@ -98,7 +100,7 @@ private fun MainScreenNavigation( ) item( - modifier = Modifier.padding(top = itemTopPadding), + modifier = Modifier.testTag("MainScreen:UpcomingTab").padding(top = itemTopPadding), selected = selectedTab == MainTab.Upcoming, onClick = { onTabSelect(MainTab.Upcoming) }, icon = { Icon(Lucide.CalendarDays, contentDescription = null) }, @@ -107,7 +109,7 @@ private fun MainScreenNavigation( ) item( - modifier = Modifier.padding(top = itemTopPadding), + modifier = Modifier.testTag("MainScreen:ProfileTab").padding(top = itemTopPadding), selected = selectedTab == MainTab.Profile, onClick = { onTabSelect(MainTab.Profile) }, icon = { Icon(Lucide.User, contentDescription = null) }, diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt index 999bb043..d347660c 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/upcoming/UpcomingScreen.kt @@ -13,6 +13,7 @@ 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 @@ -22,6 +23,7 @@ import org.jetbrains.compose.resources.stringResource fun UpcomingScreen(lazyListState: LazyListState) { LazyColumn( modifier = Modifier + .testTag("UpcomingScreen") .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) .fillMaxSize(), state = lazyListState, diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt index 51be9071..ac9b613e 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/main/MainScreenTest.kt @@ -3,22 +3,14 @@ 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.hasText -import androidx.compose.ui.test.onNodeWithText +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 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 foliary.foliary.generated.resources.profile_logout -import foliary.foliary.generated.resources.today_title -import foliary.foliary.generated.resources.upcoming_title -import org.jetbrains.compose.resources.getString import org.koin.core.context.stopKoin import org.koin.plugin.module.dsl.startKoin import kotlin.test.AfterTest @@ -47,25 +39,14 @@ class MainScreenTest { } } - onNodeWithText(getString(Res.string.main_nav_today)).assertIsDisplayed() - onNodeWithText(getString(Res.string.main_nav_upcoming)).assertIsDisplayed() - onNodeWithText(getString(Res.string.main_nav_profile)).assertIsDisplayed() - } - - @Test - fun `should switch tabs and display corresponding screen`() = runComposeUiTest { - setContent { - CompositionLocalProvider(LocalAnalytics provides mockAnalytics) { - MainScreen() - } - } + waitUntilAtLeastOneExists(hasTestTag("TodayScreen")) - waitUntilAtLeastOneExists(hasText(getString(Res.string.today_title))) + onNodeWithTag("MainScreen:TodayTab").assertIsDisplayed() - onNodeWithText(getString(Res.string.main_nav_upcoming)).performClick() - waitUntilAtLeastOneExists(hasText(getString(Res.string.upcoming_title))) + onNodeWithTag("MainScreen:UpcomingTab").assertIsDisplayed().performClick() + waitUntilAtLeastOneExists(hasTestTag("UpcomingScreen")) - onNodeWithText(getString(Res.string.main_nav_profile)).performClick() - waitUntilAtLeastOneExists(hasText(getString(Res.string.profile_logout))) + onNodeWithTag("MainScreen:ProfileTab").assertIsDisplayed().performClick() + waitUntilAtLeastOneExists(hasTestTag("ProfileScreen")) } } From 53cdb1c069bfdf5639391b22640cb0a05bca676e Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Tue, 28 Jul 2026 10:28:13 +0100 Subject: [PATCH 10/10] remove stack trace --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 88d44a91..1b0fc7ad 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -36,7 +36,7 @@ jobs: run: ./gradlew detekt - name: Check code test coverage - run: ./gradlew jvmTest -Pkover koverVerify --info --stacktrace + run: ./gradlew jvmTest -Pkover koverVerify - name: Upload JVM test report if: always()