diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardLayout.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardLayout.kt index 7f3ec28c70..d47e509ad8 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardLayout.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardLayout.kt @@ -57,8 +57,7 @@ import org.cru.godtools.base.ui.theme.GodToolsTheme import org.cru.godtools.model.Tool import org.cru.godtools.shared.analytics.AnalyticsScreenNames import org.cru.godtools.ui.dashboard.home.AllFavoritesScreen -import org.cru.godtools.ui.dashboard.home.DashboardHomeEvent -import org.cru.godtools.ui.dashboard.home.HomeContent +import org.cru.godtools.ui.dashboard.home.HomeScreen import org.cru.godtools.ui.dashboard.lessons.DashboardLessonsEvent import org.cru.godtools.ui.dashboard.lessons.LessonsLayout import org.cru.godtools.ui.dashboard.tools.ToolsScreen @@ -130,26 +129,12 @@ internal fun DashboardLayout(onEvent: (DashboardEvent) -> Unit, viewModel: Dashb }, ) - Page.HOME -> HomeContent( - onEvent = { - when (it) { - DashboardHomeEvent.ViewAllFavorites -> { - saveableStateHolder.removeState(Page.FAVORITE_TOOLS) - viewModel.updateCurrentPage(Page.FAVORITE_TOOLS, false) - } - DashboardHomeEvent.ViewAllTools -> viewModel.updateCurrentPage(Page.ALL_TOOLS) - is DashboardHomeEvent.OpenTool -> - onEvent(DashboardEvent.OpenTool(it.tool, it.type, it.lang1, it.lang2)) - is DashboardHomeEvent.OpenToolDetails -> - onEvent(DashboardEvent.OpenToolDetails(it.tool)) - } - } - ) - + Page.HOME, Page.FAVORITE_TOOLS, Page.ALL_TOOLS -> { CircuitContent( screen = when (page) { + Page.HOME -> HomeScreen Page.FAVORITE_TOOLS -> AllFavoritesScreen Page.ALL_TOOLS -> ToolsScreen else -> error("Page $page is not converted to Circuit yet") @@ -157,6 +142,10 @@ internal fun DashboardLayout(onEvent: (DashboardEvent) -> Unit, viewModel: Dashb onNavEvent = { when (it) { is NavEvent.GoTo -> when (val screen = it.screen) { + AllFavoritesScreen -> { + saveableStateHolder.removeState(Page.FAVORITE_TOOLS) + viewModel.updateCurrentPage(Page.FAVORITE_TOOLS, false) + } is IntentScreen -> onEvent(DashboardEvent.OpenIntent(screen.intent)) is ToolDetailsScreen -> onEvent( DashboardEvent.OpenToolDetails( @@ -165,6 +154,9 @@ internal fun DashboardLayout(onEvent: (DashboardEvent) -> Unit, viewModel: Dashb ) ) } + is NavEvent.ResetRoot -> when (it.newRoot) { + ToolsScreen -> viewModel.updateCurrentPage(Page.ALL_TOOLS) + } else -> Unit } }, diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayout.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayout.kt index e2b0a2cd9b..431db7efb1 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayout.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayout.kt @@ -21,60 +21,37 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import java.util.Locale -import org.cru.godtools.BuildConfig +import com.slack.circuit.codegen.annotations.CircuitInject +import dagger.hilt.components.SingletonComponent import org.cru.godtools.R -import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_LESSON -import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL -import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS -import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_FAVORITE -import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_FEATURED -import org.cru.godtools.model.Tool import org.cru.godtools.ui.banner.Banners +import org.cru.godtools.ui.dashboard.home.HomeScreen.UiEvent +import org.cru.godtools.ui.dashboard.home.HomeScreen.UiState import org.cru.godtools.ui.tools.LessonToolCard -import org.cru.godtools.ui.tools.PreloadTool import org.cru.godtools.ui.tools.SquareToolCard -import org.cru.godtools.ui.tools.ToolCardEvent private val PADDING_HORIZONTAL = 16.dp -internal sealed interface DashboardHomeEvent { - open class OpenTool(val tool: String?, val type: Tool.Type?, val lang1: Locale?, val lang2: Locale? = null) : - DashboardHomeEvent { - constructor(event: ToolCardEvent) : this(event.tool, event.toolType, event.lang1, event.lang2) - } - open class OpenToolDetails(val tool: String?) : DashboardHomeEvent { - constructor(event: ToolCardEvent.OpenToolDetails) : this(event.tool) - } - class OpenLesson(event: ToolCardEvent) : OpenTool(event.tool, Tool.Type.LESSON, event.lang1) - data object ViewAllFavorites : DashboardHomeEvent - data object ViewAllTools : DashboardHomeEvent -} - @Composable -internal fun HomeContent(onEvent: (DashboardHomeEvent) -> Unit, viewModel: HomeViewModel = viewModel()) { - val favoriteTools by viewModel.favoriteTools.collectAsState() - val spotlightLessons by viewModel.spotlightLessons.collectAsState() - val favoriteToolsLoaded by remember { derivedStateOf { favoriteTools != null } } - val hasFavoriteTools by remember { derivedStateOf { !favoriteTools.isNullOrEmpty() } } +@CircuitInject(HomeScreen::class, SingletonComponent::class) +internal fun HomeLayout(state: UiState, modifier: Modifier = Modifier) { + val banner by rememberUpdatedState(state.banner) + val favoriteToolsLoaded by rememberUpdatedState(state.favoriteToolsLoaded) + + val hasFavoriteTools by rememberUpdatedState(state.favoriteTools.isNotEmpty()) val columnState = rememberLazyListState() - val banner by viewModel.banner.collectAsState() LaunchedEffect(banner) { if (banner != null) columnState.animateScrollToItem(0) } - LazyColumn(state = columnState, contentPadding = PaddingValues(bottom = 16.dp)) { + LazyColumn(state = columnState, contentPadding = PaddingValues(bottom = 16.dp), modifier = modifier) { item("banners", "banners") { Banners( { banner }, @@ -94,7 +71,7 @@ internal fun HomeContent(onEvent: (DashboardHomeEvent) -> Unit, viewModel: HomeV } // featured lessons - if (spotlightLessons.isNotEmpty()) { + if (state.spotlightLessons.isNotEmpty()) { item("lesson-header", "lesson-header") { FeaturedLessonsHeader( modifier = Modifier @@ -104,20 +81,13 @@ internal fun HomeContent(onEvent: (DashboardHomeEvent) -> Unit, viewModel: HomeV ) } - items(spotlightLessons, key = { it }, contentType = { "lesson-tool-card" }) { lesson -> + items( + state.spotlightLessons, + key = { it.toolCode.orEmpty() }, + contentType = { "lesson-tool-card" } + ) { lessonState -> LessonToolCard( - lesson, - onEvent = { - when (it) { - is ToolCardEvent.Click, is ToolCardEvent.OpenTool -> { - viewModel.recordOpenClickInAnalytics(ACTION_OPEN_LESSON, it.tool, SOURCE_FEATURED) - onEvent(DashboardHomeEvent.OpenLesson(it)) - } - is ToolCardEvent.OpenToolDetails -> { - if (BuildConfig.DEBUG) error("$it is currently unsupported for Lesson Cards") - } - } - }, + lessonState, modifier = Modifier .animateItem() .padding(horizontal = PADDING_HORIZONTAL) @@ -130,8 +100,7 @@ internal fun HomeContent(onEvent: (DashboardHomeEvent) -> Unit, viewModel: HomeV if (favoriteToolsLoaded) { item("favorites-header") { FavoritesHeader( - showViewAll = { hasFavoriteTools }, - onEvent = onEvent, + state = state, modifier = Modifier .animateItem() .padding(horizontal = PADDING_HORIZONTAL) @@ -142,22 +111,7 @@ internal fun HomeContent(onEvent: (DashboardHomeEvent) -> Unit, viewModel: HomeV if (hasFavoriteTools) { item("favorites", "favorites") { HorizontalFavoriteTools( - { favoriteTools.orEmpty().take(5) }, - onEvent = { - when { - it is DashboardHomeEvent.OpenTool -> viewModel.recordOpenClickInAnalytics( - ACTION_OPEN_TOOL, - it.tool, - SOURCE_FAVORITE - ) - it is DashboardHomeEvent.OpenToolDetails -> viewModel.recordOpenClickInAnalytics( - ACTION_OPEN_TOOL_DETAILS, - it.tool, - SOURCE_FAVORITE - ) - } - onEvent(it) - }, + state, modifier = Modifier .animateItem() .fillMaxWidth() @@ -166,7 +120,7 @@ internal fun HomeContent(onEvent: (DashboardHomeEvent) -> Unit, viewModel: HomeV } else { item("favorites-empty", "favorites-empty") { NoFavoriteTools( - onEvent = onEvent, + state = state, modifier = Modifier .animateItem() .padding(horizontal = PADDING_HORIZONTAL) @@ -192,11 +146,9 @@ private fun FeaturedLessonsHeader(modifier: Modifier = Modifier) = Text( ) @Composable -private fun FavoritesHeader( - showViewAll: () -> Boolean, - onEvent: (DashboardHomeEvent) -> Unit, - modifier: Modifier = Modifier, -) = Row(modifier = modifier.fillMaxWidth()) { +private fun FavoritesHeader(state: UiState, modifier: Modifier = Modifier) = Row(modifier = modifier.fillMaxWidth()) { + val eventSink by rememberUpdatedState(state.eventSink) + Text( stringResource(R.string.dashboard_home_section_favorites_title), style = MaterialTheme.typography.titleLarge, @@ -206,7 +158,7 @@ private fun FavoritesHeader( ) AnimatedVisibility( - showViewAll(), + state.favoriteTools.isNotEmpty(), enter = fadeIn(), exit = fadeOut(), modifier = Modifier.alignByBaseline() @@ -215,47 +167,38 @@ private fun FavoritesHeader( stringResource(R.string.dashboard_home_section_favorites_action_view_all), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.clickable { onEvent(DashboardHomeEvent.ViewAllFavorites) } + modifier = Modifier.clickable { eventSink(UiEvent.ViewAllFavorites) } ) } } @Composable -private fun HorizontalFavoriteTools( - tools: () -> List, - onEvent: (DashboardHomeEvent) -> Unit, - modifier: Modifier = Modifier, -) = LazyRow( - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = modifier -) { - items(tools(), key = { it.code.orEmpty() }) { - PreloadTool(it) - - SquareToolCard( - toolCode = it.code.orEmpty(), - confirmRemovalFromFavorites = true, - onEvent = { - when (it) { - is ToolCardEvent.Click, is ToolCardEvent.OpenTool -> onEvent(DashboardHomeEvent.OpenTool(it)) - is ToolCardEvent.OpenToolDetails -> onEvent(DashboardHomeEvent.OpenToolDetails(it)) - } - }, - modifier = Modifier.animateItem() - ) +private fun HorizontalFavoriteTools(state: UiState, modifier: Modifier = Modifier) { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + items(state.favoriteTools, key = { it.toolCode.orEmpty() }) { toolState -> + SquareToolCard( + state = toolState, + confirmRemovalFromFavorites = true, + modifier = Modifier.animateItem() + ) + } } } -@Preview @Composable -private fun NoFavoriteTools(modifier: Modifier = Modifier, onEvent: (DashboardHomeEvent) -> Unit = {}) = Surface( +private fun NoFavoriteTools(state: UiState, modifier: Modifier = Modifier) = Surface( color = MaterialTheme.colorScheme.surfaceVariant, shape = RectangleShape, modifier = modifier .fillMaxWidth() .heightIn(min = 215.dp) ) { + val eventSink by rememberUpdatedState(state.eventSink) + Column(verticalArrangement = Arrangement.Center, modifier = Modifier.padding(16.dp)) { Text( stringResource(R.string.dashboard_home_section_favorites_no_tools_title), @@ -270,7 +213,7 @@ private fun NoFavoriteTools(modifier: Modifier = Modifier, onEvent: (DashboardHo modifier = Modifier.fillMaxWidth() ) Button( - onClick = { onEvent(DashboardHomeEvent.ViewAllTools) }, + onClick = { eventSink(UiEvent.ViewAllTools) }, modifier = Modifier .padding(top = 8.dp) .align(Alignment.CenterHorizontally) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomePresenter.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomePresenter.kt new file mode 100644 index 0000000000..91ce378e1d --- /dev/null +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomePresenter.kt @@ -0,0 +1,152 @@ +package org.cru.godtools.ui.dashboard.home + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.runtime.Navigator +import com.slack.circuit.runtime.presenter.Presenter +import com.slack.circuitx.android.IntentScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import org.cru.godtools.BuildConfig +import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent +import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_LESSON +import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL +import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS +import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_FAVORITE +import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_FEATURED +import org.cru.godtools.base.Settings +import org.cru.godtools.db.repository.ToolsRepository +import org.cru.godtools.tutorial.PageSet +import org.cru.godtools.ui.banner.BannerType +import org.cru.godtools.ui.dashboard.home.HomeScreen.UiEvent +import org.cru.godtools.ui.dashboard.home.HomeScreen.UiState +import org.cru.godtools.ui.dashboard.tools.ToolsScreen +import org.cru.godtools.ui.tooldetails.ToolDetailsScreen +import org.cru.godtools.ui.tools.ToolCard +import org.cru.godtools.ui.tools.ToolCardPresenter +import org.cru.godtools.util.createToolIntent +import org.greenrobot.eventbus.EventBus + +class HomePresenter @AssistedInject constructor( + @ApplicationContext + private val context: Context, + private val eventBus: EventBus, + private val settings: Settings, + private val toolCardPresenter: ToolCardPresenter, + private val toolsRepository: ToolsRepository, + @Assisted + private val navigator: Navigator, +) : Presenter { + @Composable + override fun present(): UiState { + val favoriteTools = rememberFavoriteTools() + + return UiState( + banner = rememberBanner(), + spotlightLessons = rememberSpotlightLessons(), + favoriteTools = favoriteTools.orEmpty(), + favoriteToolsLoaded = favoriteTools != null, + ) { + when (it) { + UiEvent.ViewAllFavorites -> navigator.goTo(AllFavoritesScreen) + UiEvent.ViewAllTools -> navigator.resetRoot(ToolsScreen, saveState = true, restoreState = true) + } + } + } + + @Composable + private fun rememberBanner() = remember { + settings.isFeatureDiscoveredFlow(Settings.FEATURE_TUTORIAL_FEATURES) + .combine(settings.appLanguageFlow) { discovered, language -> + when { + !discovered && PageSet.FEATURES.supportsLocale(language) -> BannerType.TUTORIAL_FEATURES + else -> null + } + } + }.collectAsState(null).value + + @Composable + private fun rememberSpotlightLessons() = + remember { toolsRepository.getLessonsFlow().map { it.filter { !it.isHidden && it.isSpotlight } } } + .collectAsState(emptyList()).value + .mapNotNull { lesson -> + val lessonCode = lesson.code ?: return@mapNotNull null + + key(lessonCode) { + lateinit var lessonState: ToolCard.State + lessonState = toolCardPresenter.present(lesson) { + when (it) { + ToolCard.Event.Click -> { + val intent = lesson.createToolIntent( + context = context, + languages = listOfNotNull(lessonState.translation?.languageCode), + ) + + if (intent != null) { + eventBus.post( + OpenAnalyticsActionEvent(ACTION_OPEN_LESSON, lessonCode, SOURCE_FEATURED) + ) + navigator.goTo(IntentScreen(intent)) + } + } + + else -> if (BuildConfig.DEBUG) error("$it is currently unsupported for Lesson Cards") + } + } + lessonState + } + } + + @Composable + private fun rememberFavoriteTools() = remember { toolsRepository.getFavoriteToolsFlow().map { it.take(5) } } + .collectAsState(null).value + ?.mapNotNull { tool -> + val toolCode = tool.code ?: return@mapNotNull null + + key(toolCode) { + lateinit var state: ToolCard.State + state = toolCardPresenter.present(tool) { + when (it) { + ToolCard.Event.Click, + ToolCard.Event.OpenTool -> { + val intent = tool.createToolIntent( + context = context, + languages = listOfNotNull(state.translation?.languageCode), + ) + + if (intent != null) { + eventBus.post( + OpenAnalyticsActionEvent(ACTION_OPEN_TOOL, tool.code, SOURCE_FAVORITE) + ) + navigator.goTo(IntentScreen(intent)) + } + } + ToolCard.Event.OpenToolDetails -> { + eventBus.post( + OpenAnalyticsActionEvent(ACTION_OPEN_TOOL_DETAILS, toolCode, SOURCE_FAVORITE) + ) + navigator.goTo(ToolDetailsScreen(toolCode)) + } + ToolCard.Event.PinTool, + ToolCard.Event.UnpinTool -> error("$it should be handled by the ToolCardPresenter") + } + } + state + } + } + + @AssistedFactory + @CircuitInject(HomeScreen::class, SingletonComponent::class) + interface Factory { + fun create(navigator: Navigator): HomePresenter + } +} diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeScreen.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeScreen.kt new file mode 100644 index 0000000000..03c27980a7 --- /dev/null +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeScreen.kt @@ -0,0 +1,24 @@ +package org.cru.godtools.ui.dashboard.home + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState +import com.slack.circuit.runtime.screen.Screen +import kotlinx.parcelize.Parcelize +import org.cru.godtools.ui.banner.BannerType +import org.cru.godtools.ui.tools.ToolCard + +@Parcelize +data object HomeScreen : Screen { + data class UiState( + val banner: BannerType? = null, + val spotlightLessons: List = emptyList(), + val favoriteTools: List = emptyList(), + val favoriteToolsLoaded: Boolean = false, + val eventSink: (UiEvent) -> Unit = {}, + ) : CircuitUiState + + sealed interface UiEvent : CircuitUiEvent { + data object ViewAllFavorites : UiEvent + data object ViewAllTools : UiEvent + } +} diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeViewModel.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeViewModel.kt deleted file mode 100644 index f40634c66b..0000000000 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/home/HomeViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.cru.godtools.ui.dashboard.home - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.Locale -import javax.inject.Inject -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent -import org.cru.godtools.base.Settings -import org.cru.godtools.db.repository.ToolsRepository -import org.cru.godtools.tutorial.PageSet -import org.cru.godtools.ui.banner.BannerType -import org.greenrobot.eventbus.EventBus - -@HiltViewModel -class HomeViewModel @Inject constructor( - private val eventBus: EventBus, - settings: Settings, - toolsRepository: ToolsRepository -) : ViewModel() { - val banner = settings.isFeatureDiscoveredFlow(Settings.FEATURE_TUTORIAL_FEATURES) - .map { featureTutorial -> - when { - !featureTutorial && PageSet.FEATURES.supportsLocale(Locale.getDefault()) -> BannerType.TUTORIAL_FEATURES - else -> null - } - } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - val spotlightLessons = toolsRepository.getLessonsFlow() - .map { it.filter { !it.isHidden && it.isSpotlight }.mapNotNull { it.code } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - val favoriteTools = toolsRepository.getFavoriteToolsFlow() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - // region Analytics - fun recordOpenClickInAnalytics(action: String, tool: String?, source: String) { - eventBus.post(OpenAnalyticsActionEvent(action, tool, source)) - } - // endregion Analytics -} diff --git a/app/src/main/kotlin/org/cru/godtools/ui/tools/SquareToolCard.kt b/app/src/main/kotlin/org/cru/godtools/ui/tools/SquareToolCard.kt index a56c6f10af..ccb3a76cc7 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/tools/SquareToolCard.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/tools/SquareToolCard.kt @@ -10,9 +10,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -23,59 +21,6 @@ import androidx.compose.ui.unit.dp import org.ccci.gto.android.common.androidx.compose.ui.draw.invisibleIf import org.cru.godtools.base.ui.util.ProvideLayoutDirectionFromLocale -@Composable -fun SquareToolCard( - toolCode: String, - modifier: Modifier = Modifier, - viewModel: ToolViewModels.ToolViewModel = toolViewModels[toolCode], - showCategory: Boolean = true, - showSecondLanguage: Boolean = false, - showActions: Boolean = true, - floatParallelLanguageUp: Boolean = true, - confirmRemovalFromFavorites: Boolean = false, - onEvent: (ToolCardEvent) -> Unit = {}, -) { - val tool by viewModel.tool.collectAsState() - val firstTranslation by viewModel.firstTranslation.collectAsState() - val secondTranslation by viewModel.secondTranslation.collectAsState() - - val eventSink: (ToolCard.Event) -> Unit = remember(viewModel) { - { - when (it) { - ToolCard.Event.Click -> onEvent( - ToolCardEvent.Click( - tool = tool?.code, - type = tool?.type, - lang1 = firstTranslation.value?.languageCode, - lang2 = secondTranslation?.languageCode - ) - ) - ToolCard.Event.OpenTool -> onEvent( - ToolCardEvent.OpenTool( - tool = tool?.code, - type = tool?.type, - lang1 = firstTranslation.value?.languageCode, - lang2 = secondTranslation?.languageCode - ) - ) - ToolCard.Event.OpenToolDetails -> onEvent(ToolCardEvent.OpenToolDetails(toolCode)) - ToolCard.Event.PinTool -> viewModel.pinTool() - ToolCard.Event.UnpinTool -> viewModel.unpinTool() - } - } - } - - SquareToolCard( - state = viewModel.toState(eventSink = eventSink), - modifier = modifier, - showCategory = showCategory, - showSecondLanguage = showSecondLanguage, - showActions = showActions, - floatParallelLanguageUp = floatParallelLanguageUp, - confirmRemovalFromFavorites = confirmRemovalFromFavorites, - ) -} - @Composable fun SquareToolCard( state: ToolCard.State, diff --git a/app/src/main/kotlin/org/cru/godtools/ui/tools/ToolCardLayouts.kt b/app/src/main/kotlin/org/cru/godtools/ui/tools/ToolCardLayouts.kt index 75dd70b52a..443dafd666 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/tools/ToolCardLayouts.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/tools/ToolCardLayouts.kt @@ -85,12 +85,6 @@ sealed class ToolCardEvent( class OpenToolDetails(tool: String?, val additionalLocale: Locale? = null) : ToolCardEvent(tool, null) } -@Composable -fun PreloadTool(tool: Tool) { - val code = tool.code ?: return - toolViewModels[code, tool] -} - @Composable fun ToolCard( state: ToolCard.State, diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..12d462a1ea --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a4b6701aecccd76a9484b6fc530a0e698186231639a55644551d147c893740d +size 87787 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..f8e33865a2 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7699b2d65e2e226b11b0a16301d28287bb2de4afb208e42d158adff6e32345e +size 106192 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..975005d140 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b8e32307bf09609981b4b9c0eaabedb64872e74e9a41c2ac98bafb24eebe05b +size 86974 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..0f59e4fa12 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca2e076ff359cae806b49cbcc783482d18ae5e919ca4f286f97446371bfdcb71 +size 66406 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..bace627ec8 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26c7c32288a5b35308a2738eebb7f4acc7553a8c4f5528b5d8d5debcc25af803 +size 66702 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..dd3de438c4 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59adfcb67a0d43395825cc6adeab4e9c3ce224c431f790cdc5f0823d5ada118b +size 39411 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..4ac911c68e --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:943698d7ecca1e36bad41cd5a7629dc62350d960639f6290b46a8fc98eadf37a +size 48876 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..2cbe3c7af5 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7953683a1c846a65b31cd26cb0c58c551302486372812c2945820c194b779909 +size 39681 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..0431e95be6 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:985db51b28fc87661d81e9ba70e613bdf63ab6f38476fee8b9ddadd82a8856c9 +size 29636 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..c83c617b6b --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_Favorites_Not_Loaded[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12a6a3db41713c2b4b07cee56cd9ec37c994b6fc8b1506726bc82b8d09e768c5 +size 29963 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..0dd9ad3242 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e83fd333f7a1b9e63a00ce70f04c0e8e4d5ce921be1c22338637d31075cf02b +size 60547 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..49c63402b4 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a477b5ae922d1c8acec84c33d55a41d67022fe21270843f72e12d6e8d75bd602 +size 83514 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..d537f12d94 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0224634b57f79ae5a53fb8c34c87a6af15a2724879fb32baaf65c0c95e8d91e2 +size 60586 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..c24bbb8216 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a403d4bbb9d73b35442a527e16a555b129b75d648aaf1321250aa316cd9883f2 +size 45197 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..1b97307363 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Favorites[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d0acca3e7388fd2efe37e687055d4387c3285d3540418e5bbd6e0cd171bb2dd +size 45525 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..a8e2dc2e3a --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa74908ef2cf2f67e42b0ee96d2a1344eb27e8b1c6b0d0422ca4808fbc7c5d37 +size 60494 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..46ba0fc688 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7ac3f9135eba57588390af6d6ef3a227dc18e434ea93abcbf544f37783e26da +size 94839 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..3444ecda45 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:085493a99d58f37e80a799b034ce2c363a89208437285ccfafe1c711db0a80ae +size 60159 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..a283458718 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a6b1e9a228c12c114a39f9706187bf2f251417739e4f03f545010f2b323b299 +size 45914 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..c184004234 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.home_HomeLayoutPaparazziTest_HomeLayout()_-_No_Spotlight_Lessons[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263e5bc55d90df32151a5eea403a735850f98e3435d62a1ce26f09e849dc6873 +size 45676 diff --git a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayoutPaparazziTest.kt b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayoutPaparazziTest.kt new file mode 100644 index 0000000000..a70a1cea27 --- /dev/null +++ b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/home/HomeLayoutPaparazziTest.kt @@ -0,0 +1,104 @@ +package org.cru.godtools.ui.dashboard.home + +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.Modifier +import app.cash.paparazzi.DeviceConfig +import coil.Coil +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.test.FakeImageLoaderEngine +import com.android.resources.NightMode +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.cru.godtools.ui.BasePaparazziTest +import org.cru.godtools.ui.banner.BannerType +import org.cru.godtools.ui.dashboard.home.HomeScreen.UiState +import org.cru.godtools.ui.tools.ToolCardStateTestData +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class HomeLayoutPaparazziTest( + @TestParameter(valuesProvider = DeviceConfigProvider::class) deviceConfig: DeviceConfig, + @TestParameter nightMode: NightMode, + @TestParameter accessibilityMode: AccessibilityMode, +) : BasePaparazziTest(deviceConfig = deviceConfig, nightMode = nightMode, accessibilityMode = accessibilityMode) { + private val state = UiState( + spotlightLessons = listOf( + ToolCardStateTestData.tool.copy(toolCode = "lesson", translation = null) + ), + favoriteTools = listOf( + ToolCardStateTestData.tool.copy(toolCode = "tool1", translation = null), + ToolCardStateTestData.tool.copy(toolCode = "tool2", translation = null), + ToolCardStateTestData.tool.copy(toolCode = "tool3", translation = null), + ToolCardStateTestData.tool.copy(toolCode = "tool4", translation = null), + ToolCardStateTestData.tool.copy(toolCode = "tool5", translation = null), + ), + favoriteToolsLoaded = true + ) + + @BeforeTest + @OptIn(ExperimentalCoilApi::class, ExperimentalCoroutinesApi::class) + fun setup() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + Coil.setImageLoader( + ImageLoader.Builder(paparazzi.context) + .components { + add( + FakeImageLoaderEngine.Builder() + .intercept(ToolCardStateTestData.banner, ToolCardStateTestData.bannerDrawable) + .build() + ) + } + .build() + ) + } + + @AfterTest + @OptIn(ExperimentalCoroutinesApi::class) + fun cleanup() { + Coil.reset() + Dispatchers.resetMain() + } + + @Test + fun `HomeLayout()`() { + snapshotHomeLayout(state) + } + + @Test + @Ignore("The Banner currently uses a ViewModel, which doesn't support Paparazzi") + fun `HomeLayout() - Banner - Tutorial`() { + snapshotHomeLayout(state.copy(banner = BannerType.TUTORIAL_FEATURES)) + } + + @Test + fun `HomeLayout() - Favorites Not Loaded`() { + snapshotHomeLayout(state.copy(favoriteTools = emptyList(), favoriteToolsLoaded = false)) + } + + @Test + fun `HomeLayout() - No Favorites`() { + snapshotHomeLayout(state.copy(favoriteTools = emptyList())) + } + + @Test + fun `HomeLayout() - No Spotlight Lessons`() { + snapshotHomeLayout(state.copy(spotlightLessons = emptyList())) + } + + private fun snapshotHomeLayout(state: UiState) { + snapshot { + HomeLayout(state, modifier = Modifier.background(MaterialTheme.colorScheme.background)) + } + } +} diff --git a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/home/HomePresenterTest.kt b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/home/HomePresenterTest.kt new file mode 100644 index 0000000000..c6ca58e5b1 --- /dev/null +++ b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/home/HomePresenterTest.kt @@ -0,0 +1,280 @@ +package org.cru.godtools.ui.dashboard.home + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.jeppeman.mockposable.mockk.everyComposable +import com.slack.circuit.test.FakeNavigator +import com.slack.circuit.test.test +import com.slack.circuitx.android.IntentScreen +import io.mockk.every +import io.mockk.mockk +import java.util.Locale +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.ccci.gto.android.common.androidx.compose.ui.platform.AndroidUiDispatcherUtil +import org.ccci.gto.android.common.util.content.equalsIntent +import org.cru.godtools.base.Settings +import org.cru.godtools.base.Settings.Companion.FEATURE_TUTORIAL_FEATURES +import org.cru.godtools.db.repository.ToolsRepository +import org.cru.godtools.model.Tool +import org.cru.godtools.model.randomTool +import org.cru.godtools.model.randomTranslation +import org.cru.godtools.ui.banner.BannerType +import org.cru.godtools.ui.dashboard.home.HomeScreen.UiEvent +import org.cru.godtools.ui.dashboard.tools.ToolsScreen +import org.cru.godtools.ui.tooldetails.ToolDetailsScreen +import org.cru.godtools.ui.tools.ToolCard +import org.cru.godtools.ui.tools.ToolCardPresenter +import org.cru.godtools.util.createToolIntent +import org.greenrobot.eventbus.EventBus +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(application = Application::class) +class HomePresenterTest { + private val appLanguageFlow = MutableStateFlow(Locale.ENGLISH) + private val lessonsFlow = MutableStateFlow(emptyList()) + private val toolsFlow = MutableSharedFlow>(replay = 1) + + private val context: Context = ApplicationProvider.getApplicationContext() + private val eventBus: EventBus = mockk(relaxUnitFun = true) + private val settings: Settings = mockk { + every { appLanguageFlow } returns this@HomePresenterTest.appLanguageFlow + every { isFeatureDiscoveredFlow(any()) } returns flowOf(true) + } + private val toolsRepository: ToolsRepository = mockk { + every { getLessonsFlow() } returns lessonsFlow + every { getFavoriteToolsFlow() } returns toolsFlow + } + private val toolCardPresenter: ToolCardPresenter = mockk { + everyComposable { present(tool = any(), eventSink = any()) }.answers { + ToolCard.State(toolCode = firstArg().code, eventSink = arg(4)) + } + } + + private val navigator = FakeNavigator(HomeScreen) + + private val presenter = HomePresenter( + context = context, + eventBus = eventBus, + settings = settings, + toolCardPresenter = toolCardPresenter, + toolsRepository = toolsRepository, + navigator = navigator, + ) + + @AfterTest + fun cleanup() { + AndroidUiDispatcherUtil.runScheduledDispatches() + + navigator.assertGoToIsEmpty() + navigator.assertPopIsEmpty() + navigator.assertResetRootIsEmpty() + } + + // region State.banner + @Test + fun `State - banner - Features Tutorial`() = runTest { + val featuresTutorialDiscovered = MutableStateFlow(true) + every { settings.isFeatureDiscoveredFlow(FEATURE_TUTORIAL_FEATURES) } returns featuresTutorialDiscovered + + presenter.test { + assertNull(expectMostRecentItem().banner) + + featuresTutorialDiscovered.value = false + assertEquals(BannerType.TUTORIAL_FEATURES, awaitItem().banner) + + featuresTutorialDiscovered.value = true + assertNull(awaitItem().banner) + } + } + + @Test + fun `State - banner - Features Tutorial - Only visible for supported languages`() = runTest { + every { settings.isFeatureDiscoveredFlow(FEATURE_TUTORIAL_FEATURES) } returns flowOf(false) + + presenter.test { + assertEquals(BannerType.TUTORIAL_FEATURES, expectMostRecentItem().banner) + + appLanguageFlow.value = Locale.forLanguageTag("x-test") + assertNull(awaitItem().banner) + + appLanguageFlow.value = Locale.ENGLISH + assertEquals(BannerType.TUTORIAL_FEATURES, expectMostRecentItem().banner) + } + } + // endregion State.banner + + // region State.spotlightLessons + @Test + fun `State - spotlightLessons`() = runTest { + lessonsFlow.value = emptyList() + + presenter.test { + assertEquals(emptyList(), expectMostRecentItem().spotlightLessons) + + lessonsFlow.value = List(3) { randomTool(type = Tool.Type.LESSON, isHidden = false, isSpotlight = true) } + assertEquals(lessonsFlow.value.map { it.code }, awaitItem().spotlightLessons.map { it.toolCode }) + + lessonsFlow.value = emptyList() + assertEquals(emptyList(), awaitItem().spotlightLessons) + } + } + + @Test + fun `State - spotlightLessons - Only Spotlight Lessons`() = runTest { + lessonsFlow.value = listOf( + randomTool(code = "valid", type = Tool.Type.LESSON, isHidden = false, isSpotlight = true), + randomTool(code = "invalid", type = Tool.Type.LESSON, isHidden = false, isSpotlight = false), + ) + + presenter.test { + assertEquals(listOf("valid"), expectMostRecentItem().spotlightLessons.map { it.toolCode }) + } + } + + @Test + fun `State - spotlightLessons - Exclude hidden Lessons`() = runTest { + lessonsFlow.value = listOf( + randomTool(code = "valid", type = Tool.Type.LESSON, isHidden = false, isSpotlight = true), + randomTool(code = "invalid", type = Tool.Type.LESSON, isHidden = true, isSpotlight = true), + ) + + presenter.test { + assertEquals(listOf("valid"), expectMostRecentItem().spotlightLessons.map { it.toolCode }) + } + } + + @Test + fun `State - spotlightLessons - Event - Click`() = runTest { + val lesson = randomTool(type = Tool.Type.LESSON, isHidden = false, isSpotlight = true) + val translation = randomTranslation(lesson.code, languageCode = Locale.FRENCH) + everyComposable { toolCardPresenter.present(tool = lesson, eventSink = any()) }.answers { + ToolCard.State(toolCode = lesson.code, translation = translation, eventSink = arg(4)) + } + lessonsFlow.value = listOf(lesson) + + presenter.test { + expectMostRecentItem().spotlightLessons[0].eventSink(ToolCard.Event.Click) + + assertIs(navigator.awaitNextScreen()).let { + val expected = lesson.createToolIntent(context, listOf(translation.languageCode)) + assertTrue(expected equalsIntent it.intent) + } + } + } + // endregion State.spotlightLessons + + // region State.favoriteTools + private val favoriteTool = randomTool(type = Tool.Type.TRACT, isHidden = false) + private val favoriteToolTranslation = randomTranslation(favoriteTool.code, languageCode = Locale.FRENCH) + init { + everyComposable { toolCardPresenter.present(tool = favoriteTool, eventSink = any()) }.answers { + ToolCard.State(toolCode = favoriteTool.code, translation = favoriteToolTranslation, eventSink = arg(4)) + } + } + + @Test + fun `State - favoriteTools`() = runTest { + val tools = List(3) { randomTool(type = Tool.Type.TRACT, isHidden = false) } + + presenter.test { + toolsFlow.emit(tools) + assertEquals(tools.map { it.code }, expectMostRecentItem().favoriteTools.map { it.toolCode }) + } + } + + @Test + fun `State - favoriteTools - limit to 5 tools`() = runTest { + val tools = List(10) { randomTool(type = Tool.Type.TRACT, isHidden = false) } + + presenter.test { + toolsFlow.emit(tools) + expectMostRecentItem().favoriteTools.let { + assertEquals(5, it.size) + assertEquals(tools.take(5).map { it.code }, it.map { it.toolCode }) + } + } + } + + @Test + fun `State - favoriteTools - Event - Click`() = runTest { + presenter.test { + toolsFlow.emit(listOf(favoriteTool)) + expectMostRecentItem().favoriteTools[0].eventSink(ToolCard.Event.Click) + + assertIs(navigator.awaitNextScreen()).let { + val expected = favoriteTool.createToolIntent(context, listOf(favoriteToolTranslation.languageCode)) + assertTrue(expected equalsIntent it.intent) + } + } + } + + @Test + fun `State - favoriteTools - Event - OpenTool`() = runTest { + presenter.test { + toolsFlow.emit(listOf(favoriteTool)) + expectMostRecentItem().favoriteTools[0].eventSink(ToolCard.Event.OpenTool) + + assertIs(navigator.awaitNextScreen()).let { + val expected = favoriteTool.createToolIntent(context, listOf(favoriteToolTranslation.languageCode)) + assertTrue(expected equalsIntent it.intent) + } + } + } + + @Test + fun `State - favoriteTools - Event - OpenToolDetails`() = runTest { + presenter.test { + toolsFlow.emit(listOf(favoriteTool)) + expectMostRecentItem().favoriteTools[0].eventSink(ToolCard.Event.OpenToolDetails) + + assertEquals(ToolDetailsScreen(favoriteTool.code!!), navigator.awaitNextScreen()) + } + } + // endregion State.favoriteTools + + @Test + fun `State - favoriteToolsLoaded`() = runTest { + presenter.test { + assertFalse(expectMostRecentItem().favoriteToolsLoaded) + + toolsFlow.emit(emptyList()) + assertTrue(expectMostRecentItem().favoriteToolsLoaded) + } + } + + @Test + fun `Event - ViewAllFavorites`() = runTest { + presenter.test { + awaitItem().eventSink(UiEvent.ViewAllFavorites) + + assertEquals(AllFavoritesScreen, navigator.awaitNextScreen()) + } + } + + @Test + fun `Event - ViewAllTools`() = runTest { + presenter.test { + awaitItem().eventSink(UiEvent.ViewAllTools) + + navigator.awaitResetRoot().let { + assertEquals(ToolsScreen, it.newRoot) + assertTrue(it.saveState) + assertTrue(it.restoreState) + } + } + } +}