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 e6b92e4249..88ec2756e4 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 @@ -56,7 +56,6 @@ import org.cru.godtools.base.ui.circuit.screen.dashboard.page.HomeScreen import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen import org.cru.godtools.base.ui.compose.LocalEventBus -import org.cru.godtools.base.ui.theme.GodToolsTheme import org.cru.godtools.shared.analytics.AnalyticsScreenNames import org.cru.godtools.ui.dashboard.DashboardPresenter.UiEvent import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState @@ -91,7 +90,6 @@ internal fun DashboardLayout(state: UiState, modifier: Modifier = Modifier) { } } }, - colors = GodToolsTheme.topAppBarColors, ) }, bottomBar = { diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenter.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenter.kt index 2e93776a2d..675a47149f 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenter.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenter.kt @@ -2,11 +2,13 @@ package org.cru.godtools.ui.dashboard import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.foundation.NavEvent import com.slack.circuit.foundation.onNavEvent +import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator @@ -18,18 +20,20 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import org.ccci.gto.android.common.sync.SyncTracker -import org.ccci.gto.android.common.sync.rememberSyncTracker import org.cru.godtools.base.ui.circuit.screen.dashboard.DashboardScreen import org.cru.godtools.base.ui.circuit.screen.dashboard.page.DashboardPage import org.cru.godtools.base.ui.circuit.screen.dashboard.page.HomeScreen import org.cru.godtools.sync.GodToolsSyncService import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState +import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.rememberSyncRegistry +import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry import org.cru.godtools.ui.drawer.DrawerMenuPresenter import org.cru.godtools.ui.drawer.DrawerMenuScreen class DashboardPresenter @AssistedInject internal constructor( private val drawerMenuPresenter: DrawerMenuPresenter, private val syncService: GodToolsSyncService, + @Assisted private val circuitContext: CircuitContext, @Assisted private val navigator: Navigator, @Assisted private val screen: DashboardScreen, ) : Presenter { @@ -49,16 +53,25 @@ class DashboardPresenter @AssistedInject internal constructor( @Composable override fun present(): UiState { - val syncTracker = rememberSyncTracker { it.syncData() } + val syncRegistry = rememberSyncRegistry() + DisposableEffect(syncRegistry) { + circuitContext.syncTaskRegistry = syncRegistry + val id = syncRegistry.registerSyncTask { syncData(it) } + + onDispose { + circuitContext.syncTaskRegistry = null + syncRegistry.unregisterSyncTask(id) + } + } return UiState( drawerState = drawerMenuPresenter.present(), - isSyncing = syncTracker.isSyncing.collectAsState().value, + isSyncing = syncRegistry.syncTracker.isSyncing.collectAsState().value, initialPage = screen.initialPage, snackbarState = remember { SnackbarHostState() }, ) { when (it) { - UiEvent.TriggerSync -> syncTracker.syncData(force = true) + UiEvent.TriggerSync -> syncRegistry.triggerSyncTasks(force = true) is UiEvent.NestedNavEvent -> navigator.onNavEvent(it.event) } } @@ -81,6 +94,6 @@ class DashboardPresenter @AssistedInject internal constructor( @AssistedFactory @CircuitInject(DashboardScreen::class, SingletonComponent::class) interface Factory { - fun create(navigator: Navigator, screen: DashboardScreen): DashboardPresenter + fun create(context: CircuitContext, navigator: Navigator, screen: DashboardScreen): DashboardPresenter } } diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/SyncTaskRegistry.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/SyncTaskRegistry.kt new file mode 100644 index 0000000000..5a039a2126 --- /dev/null +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/SyncTaskRegistry.kt @@ -0,0 +1,41 @@ +package org.cru.godtools.ui.dashboard + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.slack.circuit.runtime.CircuitContext +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +import org.ccci.gto.android.common.sync.SyncTracker +import org.ccci.gto.android.common.sync.rememberSyncTracker + +internal class SyncTaskRegistry(val syncTracker: SyncTracker) { + private val tasks = mutableMapOf Unit>() + + @OptIn(ExperimentalUuidApi::class) + fun registerSyncTask(task: SyncTracker.(force: Boolean) -> Unit): String { + val id = Uuid.generateV7().toString() + synchronized(tasks) { tasks[id] = task } + syncTracker.task(false) + return id + } + + fun unregisterSyncTask(id: String) { + synchronized(tasks) { tasks.remove(id) } + } + + fun triggerSyncTasks(force: Boolean = false) { + synchronized(tasks) { tasks.values.toList() }.forEach { syncTracker.it(force) } + } + + companion object { + internal var CircuitContext.syncTaskRegistry: SyncTaskRegistry? + get() = tag() ?: parent?.syncTaskRegistry + set(value) = putTag(value) + + @Composable + internal fun rememberSyncRegistry(): SyncTaskRegistry { + val syncTracker = rememberSyncTracker() + return remember(syncTracker) { SyncTaskRegistry(syncTracker) } + } + } +} diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducer.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducer.kt index 955d370bcd..c411c3584a 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducer.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducer.kt @@ -2,27 +2,49 @@ package org.cru.godtools.ui.dashboard.tools import java.util.Locale import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import org.ccci.gto.android.common.kotlin.coroutines.flow.combineTransformLatest +import org.cru.godtools.base.Settings import org.cru.godtools.db.repository.ToolsRepository import org.cru.godtools.model.Tool +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode -internal class FilteredToolsFlowProducer @Inject constructor(private val toolsRepository: ToolsRepository) { - fun getFlow(category: String? = null, language: Locale? = null): Flow> { - val baseFlow = when (language) { - null -> toolsRepository.getNormalToolsFlow() - else -> toolsRepository.getNormalToolsFlowByLanguage(language) - } - - val defaultVariantsFlow = toolsRepository.getMetaToolsFlow() - .map { it.associateBy({ it.code }, { it.defaultVariantCode }) } +internal class FilteredToolsFlowProducer @Inject constructor( + private val settings: Settings, + private val toolsRepository: ToolsRepository +) { + @OptIn(ExperimentalCoroutinesApi::class) + fun getFlow(mode: Mode, category: String? = null, language: Locale? = null): Flow> { + val baseFlow = when { + mode == Mode.PERSONALIZATION -> { + val languageFlow = if (language != null) flowOf(language) else settings.appLanguageFlow + val fallbackFlow = languageFlow.flatMapLatest { toolsRepository.getPersonalizedToolsFlow(it, null) } - return baseFlow - .map { it.filterNot { it.isHidden }.sortedBy { it.defaultOrder } } - .combine(defaultVariantsFlow) { tools, defaultVariants -> - tools.filter { it.metatoolCode == null || it.code == defaultVariants[it.metatoolCode] } + languageFlow + .combineTransformLatest(settings.getPersonalizationCountryFlow()) { language, country -> + emitAll(toolsRepository.getPersonalizedToolsFlow(language, country)) + } + .combine(fallbackFlow) { personalized, fallback -> personalized.ifEmpty { fallback } } + .distinctUntilChanged() } - .map { tools -> if (category == null) tools else tools.filter { it.category == category } } + + language != null -> toolsRepository.getNormalToolsFlowByLanguage(language) + .map { it.sortedBy { it.defaultOrder } } + + else -> toolsRepository.getNormalToolsFlow().map { it.sortedBy { it.defaultOrder } } + } + + return baseFlow.map { + it + .filterNot { it.isHidden } + .filter { category == null || it.category == category } + } } } diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFiltersStateProducer.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFiltersStateProducer.kt index d52be5b76d..6e93c7d31a 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFiltersStateProducer.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFiltersStateProducer.kt @@ -34,6 +34,7 @@ import org.cru.godtools.model.Language import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName import org.cru.godtools.ui.dashboard.filters.FilterMenu import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode interface ToolFiltersStateProducer { data class Filters( @@ -42,7 +43,7 @@ interface ToolFiltersStateProducer { ) : CircuitUiState @Composable - fun produce(): Filters + fun produce(mode: Mode): Filters } internal class DefaultToolFiltersStateProducer @Inject constructor( @@ -54,7 +55,7 @@ internal class DefaultToolFiltersStateProducer @Inject constructor( @param:DispatcherType(IO) private val ioDispatcher: CoroutineDispatcher, ) : ToolFiltersStateProducer { @Composable - override fun produce(): Filters { + override fun produce(mode: Mode): Filters { val scope = rememberCoroutineScope() val selectedCategory by remember { settings.getDashboardFilterCategoryFlow() }.collectAsState(null) @@ -69,7 +70,7 @@ internal class DefaultToolFiltersStateProducer @Inject constructor( return Filters( categoryFilter = FilterMenu.UiState( menuExpanded = rememberSaveable { mutableStateOf(false) }, - items = rememberFilterCategories(selectedLocale), + items = rememberFilterCategories(mode, selectedLocale), query = remember { mutableStateOf("") }, selectedItem = selectedCategory, eventSink = { @@ -82,7 +83,7 @@ internal class DefaultToolFiltersStateProducer @Inject constructor( ), languageFilter = FilterMenu.UiState( menuExpanded = languageMenuExpanded, - items = rememberFilterLanguages(selectedCategory, languageQuery.value), + items = rememberFilterLanguages(mode, selectedCategory, languageQuery.value), selectedItem = languagesRepository.rememberLanguage(selectedLocale), query = languageQuery, eventSink = { @@ -97,8 +98,8 @@ internal class DefaultToolFiltersStateProducer @Inject constructor( } @Composable - private fun rememberFilterCategories(selectedLanguage: Locale?) = remember(selectedLanguage) { - filteredToolsFlowProducer.getFlow(language = selectedLanguage).map { + private fun rememberFilterCategories(mode: Mode, selectedLanguage: Locale?) = remember(mode, selectedLanguage) { + filteredToolsFlowProducer.getFlow(mode, language = selectedLanguage).map { it.groupBy { it.category } .map { (category, tools) -> FilterMenu.UiState.Item(category, tools.size) } } @@ -106,7 +107,11 @@ internal class DefaultToolFiltersStateProducer @Inject constructor( @Composable @OptIn(ExperimentalCoroutinesApi::class) - private fun rememberFilterLanguages(category: String?, query: String): List> { + private fun rememberFilterLanguages( + mode: Mode, + category: String?, + query: String, + ): List> { val scope = rememberCoroutineScope() val categoryFlow = rememberStateFlow(category) @@ -127,8 +132,8 @@ internal class DefaultToolFiltersStateProducer @Inject constructor( .shareIn(scope, started = SharingStarted.WhileSubscribed(5_000), replay = 1) } - return remember(category) { - val toolCountsFlow = filteredToolsFlowProducer.getFlow(category = category) + return remember(mode, category) { + val toolCountsFlow = filteredToolsFlowProducer.getFlow(mode, category = category) .map { it.mapNotNullTo(mutableSetOf()) { it.code } } .distinctUntilChanged() .flatMapLatest { translationsRepository.getTranslationsFlowForTools(it) } diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayout.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayout.kt index 54f6998575..2f694017b4 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayout.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayout.kt @@ -5,17 +5,22 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -25,6 +30,7 @@ import org.ccci.gto.android.common.compose.foundation.layout.padding import org.cru.godtools.R import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen import org.cru.godtools.ui.banner.Banners +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiEvent import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState import org.cru.godtools.ui.tools.SquareToolCard import org.cru.godtools.ui.tools.ToolCard @@ -34,7 +40,6 @@ internal val MARGIN_TOOLS_LAYOUT_HORIZONTAL = 16.dp @Composable @CircuitInject(ToolsScreen::class, SingletonComponent::class) internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) { - val spotlightTools by rememberUpdatedState(state.spotlightTools) val filters by rememberUpdatedState(state.filters) val tools by rememberUpdatedState(state.tools) @@ -53,24 +58,51 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) { ) } - if (spotlightTools.isNotEmpty()) { - item("tool-spotlight", "tool-spotlight") { - ToolSpotlight( - spotlightTools, + if (state.isPersonalizationEnabled) { + item("mode-toggle", "mode-toggle") { + PersonalizationToggle( + state, modifier = Modifier - .animateItem() - .padding(top = 16.dp) + .padding(horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL) + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) ) + } + } + item("header", "header") { + ToolsHeader( + state.mode, + modifier = Modifier + .padding(horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL, top = 16.dp) + .fillMaxWidth() + ) + } + + if (state.spotlightTools.isNotEmpty()) { + item("featured-tools", "featured-tools") { HorizontalDivider( modifier = Modifier .animateItem() .padding(horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL, top = 16.dp) ) + + ToolSpotlight( + state.spotlightTools, + modifier = Modifier + .animateItem() + .padding(top = 16.dp) + ) } } item("tool-filters", "tool-filters") { + HorizontalDivider( + modifier = Modifier + .animateItem() + .padding(horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL, top = 16.dp) + ) + ToolFilters( filters = filters, modifier = Modifier @@ -85,12 +117,51 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) { showActions = false, modifier = Modifier .animateItem() - .padding(bottom = 16.dp, horizontal = 16.dp) + .padding(bottom = 16.dp, horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL) ) } } } +@Composable +private fun PersonalizationToggle(state: UiState, modifier: Modifier = Modifier) { + SingleChoiceSegmentedButtonRow(modifier = modifier) { + SegmentedButton( + selected = state.mode == UiState.Mode.PERSONALIZATION, + onClick = { state.eventSink(UiEvent.ChangeMode(UiState.Mode.PERSONALIZATION)) }, + shape = SegmentedButtonDefaults.itemShape(0, 2), + ) { + Text(stringResource(R.string.dashboard_tools_toggle_personalized)) + } + + SegmentedButton( + selected = state.mode == UiState.Mode.ALL_TOOLS, + onClick = { state.eventSink(UiEvent.ChangeMode(UiState.Mode.ALL_TOOLS)) }, + shape = SegmentedButtonDefaults.itemShape(1, 2), + ) { + Text(stringResource(R.string.dashboard_tools_toggle_all)) + } + } +} + +@Composable +private fun ToolsHeader(mode: UiState.Mode, modifier: Modifier = Modifier) = Column(modifier) { + Text( + stringResource( + when (mode) { + UiState.Mode.PERSONALIZATION -> R.string.dashboard_tools_header_title_personalized + UiState.Mode.ALL_TOOLS -> R.string.dashboard_tools_header_title_all + } + ), + style = MaterialTheme.typography.headlineMedium, + ) + Text( + stringResource(R.string.dashboard_tools_header_description), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 4.dp) + ) +} + @Composable private fun ToolSpotlight(tools: List, modifier: Modifier = Modifier) { Column(modifier = modifier.fillMaxWidth()) { @@ -98,22 +169,22 @@ private fun ToolSpotlight(tools: List, modifier: Modifier = Modi stringResource(R.string.dashboard_tools_section_spotlight_label), style = MaterialTheme.typography.titleLarge, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL) .fillMaxWidth() ) Text( stringResource(R.string.dashboard_tools_section_spotlight_description), style = MaterialTheme.typography.bodyMedium, modifier = Modifier - .padding(top = 4.dp, horizontal = 16.dp) + .padding(top = 4.dp, horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL) .fillMaxWidth() ) LazyRow( - contentPadding = PaddingValues(horizontal = 16.dp), + contentPadding = PaddingValues(horizontal = MARGIN_TOOLS_LAYOUT_HORIZONTAL), horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding(vertical = 8.dp) ) { - items(tools, key = { it.tool?.code.orEmpty() }) { tool -> + items(tools, key = { it.toolCode.orEmpty() }) { tool -> SquareToolCard( state = tool, showCategory = false, diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt index 0f8b62c1a3..b80f852a3d 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt @@ -1,12 +1,18 @@ package org.cru.godtools.ui.dashboard.tools import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.runtime.CircuitContext import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator @@ -15,20 +21,28 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.components.SingletonComponent +import java.util.Locale +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import org.ccci.gto.android.common.sync.SyncTracker import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_ALL_TOOLS import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_SPOTLIGHT +import org.cru.godtools.base.CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED +import org.cru.godtools.base.Settings import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen import org.cru.godtools.db.repository.ToolsRepository import org.cru.godtools.model.Language import org.cru.godtools.model.Tool +import org.cru.godtools.sync.GodToolsSyncService import org.cru.godtools.ui.banner.Banner import org.cru.godtools.ui.banner.BannerPresenter import org.cru.godtools.ui.banner.favoritetools.FavoriteToolsBannerPresenter +import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode import org.cru.godtools.ui.tooldetails.ToolDetailsScreen import org.cru.godtools.ui.tools.ToolCard import org.cru.godtools.ui.tools.ToolCardPresenter @@ -36,36 +50,57 @@ import org.greenrobot.eventbus.EventBus class ToolsPresenter @AssistedInject internal constructor( private val eventBus: EventBus, + private val remoteConfig: FirebaseRemoteConfig, + private val settings: Settings, private val toolCardPresenter: ToolCardPresenter, private val toolsRepository: ToolsRepository, private val favoriteToolsBannerPresenter: BannerPresenter, private val filteredToolsFlowProducer: FilteredToolsFlowProducer, private val toolFiltersStateProducer: ToolFiltersStateProducer, + private val syncService: GodToolsSyncService, + @Assisted private val circuitContext: CircuitContext, @Assisted private val navigator: Navigator, ) : Presenter { // region UiState / UiEvent data class UiState( + val mode: Mode = Mode.ALL_TOOLS, val banner: Banner.UiState? = null, val dataLoaded: Boolean = true, val spotlightTools: List = emptyList(), val filters: Filters = Filters(), val tools: List = emptyList(), + // TODO: temporary until personalization is rolled out to everyone, + // then this can be removed and the mode logic simplified + val isPersonalizationEnabled: Boolean = false, val eventSink: (UiEvent) -> Unit = {}, - ) : CircuitUiState + ) : CircuitUiState { + enum class Mode { PERSONALIZATION, ALL_TOOLS } + } sealed interface UiEvent : CircuitUiEvent { + data class ChangeMode(val mode: Mode) : UiEvent data class OpenToolDetails(val tool: String, val source: String? = null) : UiEvent } // endregion UiState / UiEvent @Composable override fun present(): UiState { - val filters = toolFiltersStateProducer.produce() + val isPersonalizationEnabled = rememberSaveable { + remoteConfig.getBoolean(CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED) + } + var mode by rememberSaveable { + mutableStateOf(if (isPersonalizationEnabled) Mode.PERSONALIZATION else Mode.ALL_TOOLS) + } + val filters = toolFiltersStateProducer.produce(mode) val selectedLocale by rememberUpdatedState(filters.languageFilter.selectedItem?.code) + RegisterSyncTask(selectedLocale) + val eventSink: (UiEvent) -> Unit = remember { { when (it) { + is UiEvent.ChangeMode -> mode = it.mode + is UiEvent.OpenToolDetails -> { if (it.source != null) { eventBus.post(OpenAnalyticsActionEvent(ACTION_OPEN_TOOL_DETAILS, it.tool, it.source)) @@ -81,21 +116,37 @@ class ToolsPresenter @AssistedInject internal constructor( eventSink = eventSink ) val tools = rememberTools( + mode = mode, category = filters.categoryFilter.selectedItem, language = filters.languageFilter.selectedItem, eventSink = eventSink, ) return UiState( + mode = mode, banner = favoriteToolsBannerPresenter.present(), dataLoaded = spotlightTools != null && tools != null, - spotlightTools = spotlightTools.orEmpty(), + spotlightTools = when { + isPersonalizationEnabled && mode == Mode.ALL_TOOLS -> emptyList() + else -> spotlightTools.orEmpty() + }, filters = filters, tools = tools.orEmpty(), + isPersonalizationEnabled = isPersonalizationEnabled, eventSink = eventSink, ) } + @Composable + private fun RegisterSyncTask(selectedLocale: Locale?) { + val syncRegistry = circuitContext.syncTaskRegistry + DisposableEffect(syncRegistry, selectedLocale) { + if (syncRegistry == null) return@DisposableEffect onDispose { } + val id = syncRegistry.registerSyncTask { force -> syncData(selectedLocale ?: settings.appLanguage, force) } + onDispose { syncRegistry.unregisterSyncTask(id) } + } + } + @Composable private fun rememberSpotlightTools( secondLanguage: Language?, @@ -130,12 +181,13 @@ class ToolsPresenter @AssistedInject internal constructor( @Composable private fun rememberTools( + mode: Mode, category: String?, language: Language?, eventSink: (UiEvent) -> Unit, ): List? { val locale = language?.code - val tools by remember(category, locale) { filteredToolsFlowProducer.getFlow(category, locale) } + val tools by remember(mode, category, locale) { filteredToolsFlowProducer.getFlow(mode, category, locale) } .collectAsState(null) val eventSink by rememberUpdatedState(eventSink) @@ -161,9 +213,14 @@ class ToolsPresenter @AssistedInject internal constructor( } } + private fun SyncTracker.syncData(locale: Locale, force: Boolean = false) = launchSync { + val country = settings.getCountrySettingFlow().first() + syncService.syncToolOrder(locale, country, force) + } + @AssistedFactory @CircuitInject(ToolsScreen::class, SingletonComponent::class) interface Factory { - fun create(navigator: Navigator): ToolsPresenter + fun create(circuitContext: CircuitContext, navigator: Navigator): ToolsPresenter } } diff --git a/app/src/main/res/values/strings_dashboard.xml b/app/src/main/res/values/strings_dashboard.xml index 42ccf169eb..16416dd988 100644 --- a/app/src/main/res/values/strings_dashboard.xml +++ b/app/src/main/res/values/strings_dashboard.xml @@ -63,6 +63,11 @@ An online version can be found at https://knowgod.com/ Tools + Personalized + All Tools + Tools made for you + Tools + Unique tools designed to help you have more and better conversations about Jesus. Filter Any category Any language @@ -73,7 +78,7 @@ An online version can be found at https://knowgod.com/ All Tools available Categories All Tools - Tool Spotlight + Featured Here are some tools we thought you might like diff --git a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenterTest.kt b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenterTest.kt index 5313633280..d1e0b781d8 100644 --- a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenterTest.kt +++ b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenterTest.kt @@ -5,6 +5,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.ReceiveTurbine import com.jeppeman.mockposable.mockk.everyComposable import com.slack.circuit.foundation.NavEvent +import com.slack.circuit.runtime.CircuitContext +import com.slack.circuit.runtime.InternalCircuitApi import com.slack.circuit.test.FakeNavigator import com.slack.circuit.test.test import io.mockk.coEvery @@ -14,6 +16,8 @@ import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.sync.Mutex @@ -27,6 +31,7 @@ import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen import org.cru.godtools.sync.GodToolsSyncService import org.cru.godtools.ui.dashboard.DashboardPresenter.UiEvent import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState +import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry import org.cru.godtools.ui.drawer.DrawerMenuPresenter import org.cru.godtools.ui.drawer.DrawerMenuScreen import org.cru.godtools.ui.tooldetails.ToolDetailsScreen @@ -36,9 +41,11 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(application = Application::class) @Suppress("DeferredResultUnused") +@OptIn(InternalCircuitApi::class) class DashboardPresenterTest { private val screen = DashboardScreen(HomeScreen) private val syncLock = Mutex(true) + private val circuitContext = CircuitContext(null) private val drawerMenuPresenter: DrawerMenuPresenter = mockk { everyComposable { present() } returns DrawerMenuScreen.State() @@ -55,6 +62,7 @@ class DashboardPresenterTest { private val presenter = DashboardPresenter( drawerMenuPresenter = drawerMenuPresenter, syncService = syncService, + circuitContext = circuitContext, navigator = navigator, screen = screen, ) @@ -72,6 +80,7 @@ class DashboardPresenterTest { val presenter = DashboardPresenter( drawerMenuPresenter = drawerMenuPresenter, syncService = syncService, + circuitContext = circuitContext, navigator = navigator, screen = DashboardScreen(LessonsScreen), ) @@ -140,5 +149,17 @@ class DashboardPresenterTest { } // endregion UiEvent.TriggerSync + // region SideEffect - SyncTaskRegistry + @Test + fun `SideEffect - SyncTaskRegistry - set on CircuitContext while presenter is active`() = runTest { + assertNull(circuitContext.syncTaskRegistry) + presenter.test { + awaitInitialState() + assertNotNull(circuitContext.syncTaskRegistry) + } + assertNull(circuitContext.syncTaskRegistry) + } + // endregion SideEffect - SyncTaskRegistry + private suspend fun ReceiveTurbine.awaitInitialState() = awaitItemMatching { it.isSyncing } } diff --git a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/SyncTaskRegistryTest.kt b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/SyncTaskRegistryTest.kt new file mode 100644 index 0000000000..d86033329c --- /dev/null +++ b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/SyncTaskRegistryTest.kt @@ -0,0 +1,121 @@ +package org.cru.godtools.ui.dashboard + +import com.slack.circuit.runtime.CircuitContext +import com.slack.circuit.runtime.InternalCircuitApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlinx.coroutines.test.TestScope +import org.ccci.gto.android.common.sync.SyncTracker +import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry + +@OptIn(InternalCircuitApi::class) +class SyncTaskRegistryTest { + private val syncTracker = SyncTracker(TestScope()) + private val registry = SyncTaskRegistry(syncTracker) + + // region registerSyncTask() + @Test + fun `registerSyncTask - returns unique IDs`() { + val id1 = registry.registerSyncTask {} + val id2 = registry.registerSyncTask {} + assertNotEquals(id1, id2) + } + + @Test + fun `registerSyncTask - immediately executes task with force = false`() { + val calls = mutableListOf() + registry.registerSyncTask { force -> calls += force } + assertEquals(listOf(false), calls) + } + // endregion registerSyncTask() + + // region unregisterSyncTask() + @Test + fun `unregisterSyncTask - task not called after unregister`() { + val calls = mutableListOf() + val id = registry.registerSyncTask { force -> calls += force } + calls.clear() + + registry.unregisterSyncTask(id) + registry.triggerSyncTasks() + assertEquals(emptyList(), calls) + } + + @Test + fun `unregisterSyncTask - unknown id is safe`() { + registry.unregisterSyncTask("unknown-id") + } + // endregion unregisterSyncTask() + + // region triggerSyncTasks() + @Test + fun `triggerSyncTasks - calls all registered tasks`() { + val calls1 = mutableListOf() + val calls2 = mutableListOf() + registry.registerSyncTask { force -> calls1 += force } + registry.registerSyncTask { force -> calls2 += force } + calls1.clear() + calls2.clear() + + registry.triggerSyncTasks() + assertEquals(1, calls1.size) + assertEquals(1, calls2.size) + } + + @Test + fun `triggerSyncTasks - passes force = false by default`() { + val calls = mutableListOf() + registry.registerSyncTask { force -> calls += force } + calls.clear() + + registry.triggerSyncTasks() + assertEquals(listOf(false), calls) + } + + @Test + fun `triggerSyncTasks - passes force = true when specified`() { + val calls = mutableListOf() + registry.registerSyncTask { force -> calls += force } + calls.clear() + + registry.triggerSyncTasks(force = true) + assertEquals(listOf(true), calls) + } + // endregion triggerSyncTasks() + + // region CircuitContext.syncTaskRegistry + @Test + fun `syncTaskRegistry - null by default`() { + val context = CircuitContext(null) + assertNull(context.syncTaskRegistry) + } + + @Test + fun `syncTaskRegistry - returns set value`() { + val context = CircuitContext(null) + context.syncTaskRegistry = registry + assertSame(registry, context.syncTaskRegistry) + } + + @Test + fun `syncTaskRegistry - traverses parent context`() { + val parent = CircuitContext(null) + parent.syncTaskRegistry = registry + val child = CircuitContext(parent) + assertSame(registry, child.syncTaskRegistry) + } + + @Test + fun `syncTaskRegistry - child value takes precedence over parent`() { + val parentRegistry = SyncTaskRegistry(syncTracker) + val parent = CircuitContext(null) + parent.syncTaskRegistry = parentRegistry + val child = CircuitContext(parent) + child.syncTaskRegistry = registry + assertSame(registry, child.syncTaskRegistry) + } + // endregion CircuitContext.syncTaskRegistry +} diff --git a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/DefaultToolFiltersStateProducerTest.kt b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/DefaultToolFiltersStateProducerTest.kt index 36668b6956..8846ab2725 100644 --- a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/DefaultToolFiltersStateProducerTest.kt +++ b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/DefaultToolFiltersStateProducerTest.kt @@ -32,6 +32,7 @@ import org.cru.godtools.model.Translation import org.cru.godtools.model.randomTool import org.cru.godtools.model.randomTranslation import org.cru.godtools.ui.dashboard.filters.FilterMenu +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -40,6 +41,7 @@ import org.robolectric.annotation.Config @Suppress("UnusedFlow") @OptIn(ExperimentalCoroutinesApi::class) class DefaultToolFiltersStateProducerTest { + private val mode = Mode.ALL_TOOLS private val appLanguage = MutableStateFlow(Locale.ENGLISH) private val filteredToolsFlow = MutableStateFlow(emptyList()) private val languagesFlow = MutableStateFlow(emptyList()) @@ -50,7 +52,7 @@ class DefaultToolFiltersStateProducerTest { private val testScope = TestScope() private val filteredToolsFlowProducer: FilteredToolsFlowProducer = mockk { - every { getFlow(any(), any()) } returns filteredToolsFlow + every { getFlow(mode = any(), category = any(), language = any()) } returns filteredToolsFlow } private val languagesRepository: LanguagesRepository = mockk { every { findLanguageFlow(any()) } returns flowOf(null) @@ -97,7 +99,7 @@ class DefaultToolFiltersStateProducerTest { randomTool(category = Tool.CATEGORY_ARTICLES), ) - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { assertEquals( listOf( FilterMenu.UiState.Item(Tool.CATEGORY_ARTICLES, 2), @@ -108,21 +110,39 @@ class DefaultToolFiltersStateProducerTest { } } + @Test + fun `Filters - categoryFilter - items - uses mode`() = testScope.runTest { + presenterTestOf(presentFunction = { producer.produce(Mode.PERSONALIZATION) }) { + expectMostRecentItem() + verify { filteredToolsFlowProducer.getFlow(mode = Mode.PERSONALIZATION, language = null) } + verify(inverse = true) { filteredToolsFlowProducer.getFlow(mode = Mode.ALL_TOOLS, language = any()) } + } + } + @Test fun `Filters - categoryFilter - items - uses selected language`() = testScope.runTest { - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { awaitItem().languageFilter.eventSink(FilterMenu.Event.SelectItem(Language(Locale.FRENCH))) expectMostRecentItem() - verify { filteredToolsFlowProducer.getFlow(language = Locale.FRENCH) } + verify { filteredToolsFlowProducer.getFlow(mode = mode, language = Locale.FRENCH) } } } // endregion Filters.categoryFilter.items // region Filters.languageFilter.items + @Test + fun `Filters - languageFilter - items - uses mode`() = testScope.runTest { + presenterTestOf(presentFunction = { producer.produce(Mode.PERSONALIZATION) }) { + expectMostRecentItem() + verify { filteredToolsFlowProducer.getFlow(mode = Mode.PERSONALIZATION, category = null) } + verify(inverse = true) { filteredToolsFlowProducer.getFlow(mode = Mode.ALL_TOOLS, category = any()) } + } + } + @Test fun `Filters - languageFilter - items - no category`() = testScope.runTest { - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { languagesFlow.value = listOf(Language(Locale.ENGLISH), Language(Locale.FRENCH)) assertEquals( listOf( @@ -139,7 +159,7 @@ class DefaultToolFiltersStateProducerTest { @Test fun `Filters - languageFilter - items - for category`() = testScope.runTest { - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { awaitItem().categoryFilter.eventSink(FilterMenu.Event.SelectItem(Tool.CATEGORY_GOSPEL)) gospelLanguagesFlow.value = listOf(Language(Locale.ENGLISH), Language(Locale.FRENCH)) @@ -159,7 +179,7 @@ class DefaultToolFiltersStateProducerTest { val translationsFlow = MutableStateFlow(emptyList()) every { translationsRepository.getTranslationsFlowForTools(setOf("tool1", "tool2")) } returns translationsFlow - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { filteredToolsFlow.value = listOf( randomTool("tool1", isHidden = false), randomTool("tool2", isHidden = false), @@ -187,7 +207,7 @@ class DefaultToolFiltersStateProducerTest { fun `Filters - languageFilter - items - filtered by query`() = testScope.runTest { languagesFlow.value = listOf(Language(Locale.ENGLISH), Language(Locale.FRENCH)) - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { expectMostRecentItem().languageFilter.let { it.menuExpanded.value = true assertEquals( @@ -217,7 +237,7 @@ class DefaultToolFiltersStateProducerTest { // region Filters.languageFilter.selectedItem @Test fun `Filters - languageFilter - selectedItem - no language selected`() = testScope.runTest { - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { assertNull(expectMostRecentItem().languageFilter.selectedItem) } @@ -226,7 +246,7 @@ class DefaultToolFiltersStateProducerTest { @Test fun `Filters - languageFilter - selectedItem - language not found`() = testScope.runTest { - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { awaitItem().languageFilter.eventSink(FilterMenu.Event.SelectItem(Language(Locale.ENGLISH))) assertNull(expectMostRecentItem().languageFilter.selectedItem) @@ -240,7 +260,7 @@ class DefaultToolFiltersStateProducerTest { val language = Language(Locale.ENGLISH) every { languagesRepository.findLanguageFlow(Locale.ENGLISH) } returns flowOf(language) - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { awaitItem().languageFilter.eventSink(FilterMenu.Event.SelectItem(language)) assertEquals(language, expectMostRecentItem().languageFilter.selectedItem) @@ -253,7 +273,7 @@ class DefaultToolFiltersStateProducerTest { // region Filters.languageFilter.menuExpanded @Test fun `Filters - languageFilter - menuExpanded - resets query when set to false`() = testScope.runTest { - presenterTestOf(presentFunction = { producer.produce() }) { + presenterTestOf(presentFunction = { producer.produce(mode) }) { val state = expectMostRecentItem().languageFilter state.menuExpanded.value = true diff --git a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FakeToolFiltersStateProducer.kt b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FakeToolFiltersStateProducer.kt index 0c00538b08..3ef47bdcc1 100644 --- a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FakeToolFiltersStateProducer.kt +++ b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FakeToolFiltersStateProducer.kt @@ -4,10 +4,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import kotlinx.coroutines.flow.MutableStateFlow import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode class FakeToolFiltersStateProducer : ToolFiltersStateProducer { val filters = MutableStateFlow(Filters()) + var lastMode: Mode? = null @Composable - override fun produce() = filters.collectAsState().value + override fun produce(mode: Mode): Filters { + lastMode = mode + return filters.collectAsState().value + } } diff --git a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducerTest.kt b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducerTest.kt index dd7e943666..2f9c414c2b 100644 --- a/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducerTest.kt +++ b/app/src/test/kotlin/org/cru/godtools/ui/dashboard/tools/FilteredToolsFlowProducerTest.kt @@ -11,39 +11,67 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.cru.godtools.base.Settings import org.cru.godtools.db.repository.ToolsRepository import org.cru.godtools.model.Tool import org.cru.godtools.model.randomTool +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode @Suppress("UnusedFlow") class FilteredToolsFlowProducerTest { - private val metatoolsFlow = MutableStateFlow(emptyList()) + private val appLanguageFlow = MutableStateFlow(Locale.ENGLISH) + private val countryFlow = MutableStateFlow(null) private val normalToolsFlow = MutableStateFlow(emptyList()) + private val settings: Settings = mockk { + every { appLanguageFlow } returns this@FilteredToolsFlowProducerTest.appLanguageFlow + every { getPersonalizationCountryFlow() } returns countryFlow + } private val toolsRepository: ToolsRepository = mockk { every { getNormalToolsFlow() } returns normalToolsFlow every { getNormalToolsFlowByLanguage(any()) } returns flowOf(emptyList()) - every { getMetaToolsFlow() } returns metatoolsFlow + every { getPersonalizedToolsFlow(any(), any()) } returns flowOf(emptyList()) } - private val producer = FilteredToolsFlowProducer(toolsRepository) + private val producer = FilteredToolsFlowProducer( + settings = settings, + toolsRepository = toolsRepository + ) // region language selection @Test - fun `getFlow - no language - uses getNormalToolsFlow`() = runTest { - producer.getFlow(language = null).first() + fun `getFlow - All Tools - no language - uses getNormalToolsFlow`() = runTest { + producer.getFlow(mode = Mode.ALL_TOOLS, language = null).first() verify { toolsRepository.getNormalToolsFlow() } verify(exactly = 0) { toolsRepository.getNormalToolsFlowByLanguage(any()) } } @Test - fun `getFlow - with language - uses getNormalToolsFlowByLanguage`() = runTest { - producer.getFlow(language = Locale.FRENCH).first() + fun `getFlow - All Tools - with language - uses getNormalToolsFlowByLanguage`() = runTest { + producer.getFlow(mode = Mode.ALL_TOOLS, language = Locale.FRENCH).first() verify { toolsRepository.getNormalToolsFlowByLanguage(Locale.FRENCH) } verify(exactly = 0) { toolsRepository.getNormalToolsFlow() } } // endregion language selection + // region sort order + @Test + fun `getFlow - All Tools - no language - tools sorted by defaultOrder`() = runTest { + val tools = List(5) { createTool(defaultOrder = it) } + + normalToolsFlow.value = tools.shuffled() + assertEquals(tools, producer.getFlow(mode = Mode.ALL_TOOLS).first()) + } + + @Test + fun `getFlow - All Tools - with language - tools sorted by defaultOrder`() = runTest { + val tools = List(5) { createTool(defaultOrder = it) } + every { toolsRepository.getNormalToolsFlowByLanguage(Locale.FRENCH) } returns flowOf(tools.shuffled()) + + assertEquals(tools, producer.getFlow(mode = Mode.ALL_TOOLS, language = Locale.FRENCH).first()) + } + // endregion sort order + // region hidden filter @Test fun `getFlow - hidden tools are excluded`() = runTest { @@ -51,7 +79,7 @@ class FilteredToolsFlowProducerTest { val visible = createTool(isHidden = false) normalToolsFlow.value = listOf(hidden, visible) - assertEquals(listOf(visible), producer.getFlow().first()) + assertEquals(listOf(visible), producer.getFlow(mode = Mode.ALL_TOOLS).first()) } @Test @@ -59,129 +87,157 @@ class FilteredToolsFlowProducerTest { val tool = createTool(isHidden = false) normalToolsFlow.value = listOf(tool) - assertEquals(listOf(tool), producer.getFlow().first()) + assertEquals(listOf(tool), producer.getFlow(mode = Mode.ALL_TOOLS).first()) } // endregion hidden filter - // region sort order + // region category filter @Test - fun `getFlow - tools sorted by defaultOrder`() = runTest { - val tools = List(5) { createTool(defaultOrder = it) } + fun `getFlow - null category includes all tools`() = runTest { + val gospel = createTool(category = Tool.CATEGORY_GOSPEL) + val articles = createTool(category = Tool.CATEGORY_ARTICLES) - normalToolsFlow.value = tools.shuffled() - assertEquals(tools, producer.getFlow().first()) + normalToolsFlow.value = listOf(gospel, articles) + assertEquals(setOf(gospel, articles), producer.getFlow(mode = Mode.ALL_TOOLS, category = null).first().toSet()) } - // endregion sort order - // region default variant filtering @Test - fun `getFlow - non-variant tools always included`() = runTest { - val tool = createTool(metatoolCode = null) + fun `getFlow - category filter includes only matching tools`() = runTest { + val gospel = createTool(category = Tool.CATEGORY_GOSPEL) + val articles = createTool(category = Tool.CATEGORY_ARTICLES) + + normalToolsFlow.value = listOf(gospel, articles) + assertEquals(listOf(gospel), producer.getFlow(mode = Mode.ALL_TOOLS, category = Tool.CATEGORY_GOSPEL).first()) + } + + @Test + fun `getFlow - category filter excludes tools with different category`() = runTest { + val tool = createTool(category = Tool.CATEGORY_ARTICLES) normalToolsFlow.value = listOf(tool) - assertEquals(listOf(tool), producer.getFlow().first()) + assertEquals(emptyList(), producer.getFlow(mode = Mode.ALL_TOOLS, category = Tool.CATEGORY_GOSPEL).first()) } + // endregion category filter + // region combined filters @Test - fun `getFlow - default variant is included`() = runTest { - val meta = randomTool("meta", type = Tool.Type.META, defaultVariantCode = "default") - val defaultVariant = randomTool("default", metatoolCode = "meta", isHidden = false) + fun `getFlow - All Tools - category and language filters applied together`() = runTest { + val languageToolsFlow = MutableStateFlow(emptyList()) + every { toolsRepository.getNormalToolsFlowByLanguage(Locale.FRENCH) } returns languageToolsFlow - metatoolsFlow.value = listOf(meta) - normalToolsFlow.value = listOf(defaultVariant) - assertEquals(listOf(defaultVariant), producer.getFlow().first()) + val gospel = createTool(category = Tool.CATEGORY_GOSPEL) + val articles = createTool(category = Tool.CATEGORY_ARTICLES) + + languageToolsFlow.value = listOf(gospel, articles) + assertEquals( + listOf(gospel), + producer.getFlow(mode = Mode.ALL_TOOLS, category = Tool.CATEGORY_GOSPEL, language = Locale.FRENCH).first() + ) } + // endregion combined filters + // region Personalization mode @Test - fun `getFlow - non-default variant is excluded`() = runTest { - val meta = randomTool("meta", type = Tool.Type.META, defaultVariantCode = "default") - val nonDefault = randomTool("other", metatoolCode = "meta", isHidden = false) + fun `getFlow - Personalization mode - uses getPersonalizedToolsFlow`() = runTest { + producer.getFlow(mode = Mode.PERSONALIZATION).first() + verify { toolsRepository.getPersonalizedToolsFlow(any(), any()) } + verify(exactly = 0) { toolsRepository.getNormalToolsFlow() } + verify(exactly = 0) { toolsRepository.getNormalToolsFlowByLanguage(any()) } + } - metatoolsFlow.value = listOf(meta) - normalToolsFlow.value = listOf(nonDefault) - assertEquals(emptyList(), producer.getFlow().first()) + @Test + fun `getFlow - Personalization mode - uses appLanguageFlow when no language provided`() = runTest { + appLanguageFlow.value = Locale.FRENCH + producer.getFlow(mode = Mode.PERSONALIZATION).first() + verify { toolsRepository.getPersonalizedToolsFlow(Locale.FRENCH, any()) } } @Test - fun `getFlow - variant with no matching metatool is excluded`() = runTest { - val orphan = randomTool("orphan", metatoolCode = "missing-meta", isHidden = false) + fun `getFlow - Personalization mode - uses provided language when specified`() = runTest { + producer.getFlow(mode = Mode.PERSONALIZATION, language = Locale.GERMAN).first() + verify { toolsRepository.getPersonalizedToolsFlow(Locale.GERMAN, any()) } + verify(exactly = 0) { toolsRepository.getPersonalizedToolsFlow(Locale.ENGLISH, any()) } + } - normalToolsFlow.value = listOf(orphan) - assertEquals(emptyList(), producer.getFlow().first()) + @Test + fun `getFlow - Personalization mode - uses Settings getPersonalizationCountryFlow for country`() = runTest { + countryFlow.value = "US" + producer.getFlow(mode = Mode.PERSONALIZATION).first() + verify { toolsRepository.getPersonalizedToolsFlow(any(), "US") } } @Test - fun `getFlow - default variant updates when metatool changes`() = runTest { - val metaV1 = randomTool("meta", type = Tool.Type.META, defaultVariantCode = "v1") - val metaV2 = randomTool("meta", type = Tool.Type.META, defaultVariantCode = "v2") - val v1 = randomTool("v1", metatoolCode = "meta", isHidden = false) - val v2 = randomTool("v2", metatoolCode = "meta", isHidden = false) + fun `getFlow - Personalization mode - returns personalized tools when non-empty`() = runTest { + val tool = createTool() + val fallbackTool = createTool() + countryFlow.value = "US" + every { toolsRepository.getPersonalizedToolsFlow(Locale.ENGLISH, "US") } returns flowOf(listOf(tool)) + every { toolsRepository.getPersonalizedToolsFlow(Locale.ENGLISH, null) } returns flowOf(listOf(fallbackTool)) + + assertEquals(listOf(tool), producer.getFlow(mode = Mode.PERSONALIZATION).first()) + } - producer.getFlow().test { - normalToolsFlow.value = listOf(v1, v2) - metatoolsFlow.value = listOf(metaV1) - assertEquals(listOf(v1), expectMostRecentItem()) + @Test + fun `getFlow - Personalization mode - falls back to language only when no country-specific tools`() = runTest { + val fallbackTool = createTool() + countryFlow.value = "US" + every { toolsRepository.getPersonalizedToolsFlow(Locale.ENGLISH, "US") } returns flowOf(emptyList()) + every { toolsRepository.getPersonalizedToolsFlow(Locale.ENGLISH, null) } returns flowOf(listOf(fallbackTool)) - metatoolsFlow.value = listOf(metaV2) - assertEquals(listOf(v2), expectMostRecentItem()) - } + assertEquals(listOf(fallbackTool), producer.getFlow(mode = Mode.PERSONALIZATION).first()) } - // endregion default variant filtering - // region category filter @Test - fun `getFlow - null category includes all tools`() = runTest { - val gospel = createTool(category = Tool.CATEGORY_GOSPEL) - val articles = createTool(category = Tool.CATEGORY_ARTICLES) + fun `getFlow - Personalization mode - hidden tools are excluded`() = runTest { + val hidden = createTool(isHidden = true) + val visible = createTool(isHidden = false) + every { toolsRepository.getPersonalizedToolsFlow(any(), any()) } returns flowOf(listOf(hidden, visible)) - normalToolsFlow.value = listOf(gospel, articles) - assertEquals(setOf(gospel, articles), producer.getFlow(category = null).first().toSet()) + assertEquals(listOf(visible), producer.getFlow(mode = Mode.PERSONALIZATION).first()) } @Test - fun `getFlow - category filter includes only matching tools`() = runTest { + fun `getFlow - Personalization mode - category filter applies`() = runTest { val gospel = createTool(category = Tool.CATEGORY_GOSPEL) val articles = createTool(category = Tool.CATEGORY_ARTICLES) + every { toolsRepository.getPersonalizedToolsFlow(any(), any()) } returns flowOf(listOf(gospel, articles)) - normalToolsFlow.value = listOf(gospel, articles) - assertEquals(listOf(gospel), producer.getFlow(category = Tool.CATEGORY_GOSPEL).first()) + assertEquals( + listOf(gospel), + producer.getFlow(mode = Mode.PERSONALIZATION, category = Tool.CATEGORY_GOSPEL).first() + ) } @Test - fun `getFlow - category filter excludes tools with different category`() = runTest { - val tool = createTool(category = Tool.CATEGORY_ARTICLES) + fun `getFlow - Personalization mode - updates when appLanguage changes`() = runTest { + val frenchTool = createTool() + every { toolsRepository.getPersonalizedToolsFlow(Locale.FRENCH, null) } returns flowOf(listOf(frenchTool)) - normalToolsFlow.value = listOf(tool) - assertEquals(emptyList(), producer.getFlow(category = Tool.CATEGORY_GOSPEL).first()) + producer.getFlow(mode = Mode.PERSONALIZATION).test { + assertEquals(emptyList(), awaitItem()) + + appLanguageFlow.value = Locale.FRENCH + assertEquals(listOf(frenchTool), awaitItem()) + } } - // endregion category filter - // region combined filters @Test - fun `getFlow - category and language filters applied together`() = runTest { - val languageToolsFlow = MutableStateFlow(emptyList()) - every { toolsRepository.getNormalToolsFlowByLanguage(Locale.FRENCH) } returns languageToolsFlow + fun `getFlow - Personalization mode - updates when country changes`() = runTest { + val usTool = createTool() + every { toolsRepository.getPersonalizedToolsFlow(Locale.ENGLISH, "US") } returns flowOf(listOf(usTool)) - val gospel = createTool(category = Tool.CATEGORY_GOSPEL) - val articles = createTool(category = Tool.CATEGORY_ARTICLES) + producer.getFlow(mode = Mode.PERSONALIZATION).test { + assertEquals(emptyList(), awaitItem()) - languageToolsFlow.value = listOf(gospel, articles) - assertEquals( - listOf(gospel), - producer.getFlow(category = Tool.CATEGORY_GOSPEL, language = Locale.FRENCH).first() - ) + countryFlow.value = "US" + assertEquals(listOf(usTool), awaitItem()) + } } - // endregion combined filters + // endregion Personalization mode - private fun createTool( - category: String? = null, - defaultOrder: Int = 0, - isHidden: Boolean = false, - metatoolCode: String? = null, - ) = randomTool( + private fun createTool(category: String? = null, defaultOrder: Int = 0, isHidden: Boolean = false) = randomTool( category = category, defaultOrder = defaultOrder, isHidden = isHidden, - metatoolCode = metatoolCode ) } diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index f6ef6741b0..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c521a5f0ca7abaa766dd890fccf8ae2773e7d7ffeca7d39e74c45420933544dc -size 99998 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NOTNIGHT,ACCESSIBILITY].png deleted file mode 100644 index b1a1447f5c..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NOTNIGHT,ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee908c9730c082eb1ee60bf369f45364917fc52b9cf30a96d63ae6390693c024 -size 146571 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index b2818bb08e..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e58eb279628082ce0bea4a9a885440684acb9512a3122b4a5fe1cd5a66c12fb -size 101435 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 4bb8b70666..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b0c650e58a33353f613e7cb89ce24e2def0fd2265483781a00eb78830dd47ba5 -size 105495 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 4bee9d53b1..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8de948724eb1a01bdf6348a66cf66b0cedb274cc8ff53a49a251aff288e7fe1f -size 106654 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 52c30373e9..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:668f6204e199d19e69863bf2bf0788ad58d47bbe79dbbc258eb7d6ba16d9f87a -size 3658 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index df3c4ebb6e..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:159b7cafe520cf5a9b24e794561b070899286ac3ed96ee95a798bdd82a3b2213 -size 98864 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NOTNIGHT,ACCESSIBILITY].png deleted file mode 100644 index cd4b63c682..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NOTNIGHT,ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6b430aeb8e5e359b14241e673e8ac4f4f6cfbd138f2ee479191d1ad273e01e7 -size 144852 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 96c588e0ae..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a1826540d8af03566f318ed4bf898b042112246b7723cb84479958f149bd7c93 -size 100480 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 46e7c665cf..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e6534cbbaf4ab53deb4cefba4e4408086ed4116554fce34424519578936e03d -size 105006 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index e00e57994f..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_Filters_Selected[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bead36be9b0e24a1b2902a3555dac9c48ef69a696c07533df38f5427c712a3d8 -size 106186 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 1549e98da2..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4fd109bd46943a3edf37302dcb4857c7c96f9c308398a8bfef803e9fdb181c4 -size 107960 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png deleted file mode 100644 index 184355c863..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:954c452793b64fa1fa518a9f8c6bd679b3bdf62d58222864482f4db4c010ebc1 -size 101057 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index fc86246dfe..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6117c940e6b62fc5c749bf3a821aeb8dfb6c04bbce8573cafb9310d55a6677ea -size 110616 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index db7404e37c..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a04caf39b427d7628b815750494b972ee844bcf89b5d09c66c6028587af0853 -size 79151 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 609d08a418..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Spotlight_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:25a83643f321889acfd9f7e546de46f04fbc0c771c1e38ffcff0f189b4ed0e6f -size 80243 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 21dc681ee1..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0685c6649fc8d38430a5b69d382072ac3b2c33d552110084b68743674dc03f1c -size 57027 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png deleted file mode 100644 index 542547ce90..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a91497ae582436fcb814f8af79de79cb0fa7cbefa8ae0cc504ac9b34e9aafce7 -size 96777 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index 1ac4160eaf..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b2a543a6ed56076146d179254bf04e15e678b9c2ebe71eab1ec31065e1d2703 -size 57462 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index d80cccf1dd..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09e63d2019b7960483eb4cfbbfd3b9eb5931698441296ffb2f674718e46684e1 -size 42580 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png deleted file mode 100644 index f98162c54e..0000000000 --- a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard.tools_ToolsLayoutPaparazziTest_ToolsLayout()_-_No_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:04452bd2a81bebb34bb4cd209b1e373f1a689e0e8893cb4f05e4ab3ab0f99a48 -size 43078 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..c5b31b0c22 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c04065dd8c2c3707147e03c5a646a9ef89aca47cb079dd3460ef5d7c5801245a +size 87199 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..3c672fa623 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d79d242f0c8e001f9608731a235ea51e1a8d6410ef97c8b54f80dbe091fe058 +size 168033 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..ea8a0d83d9 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7cc11d57d0f64ac7e7f0ccb4a6a38918d39832ad0f0e5bf7a034a992ed72a2b +size 87665 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..226663e6ce --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60085e32a741dc0ca3fda9aee848505e385b64686997aaae047485cbbedefbd4 +size 93025 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..3135da2894 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53dcf52d41515014b2f309f45a8afaf80373b448ece7b71768c12e5a751d37be +size 93695 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..31eb979e2d --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fb8171739e537cd78eeb67f4cf7590446decad65721f7ab4f6297805420bd97 +size 86172 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..0b92b8f8c7 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce5727abc09579275c0ac6244e0a0a685c232a6410ec919d4033c2e73a35b8e0 +size 166509 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..591f6b4797 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11cba28a1561f03c76151ada8ce3282807c7e5ec0e934b6caac999c18174086f +size 86645 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..529e44ef50 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a3d8d44fd80d547fda43ea5ecb0dca63d0f18cfebfa79f80c74a3b994cb4035 +size 92507 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..adfa6f0678 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_All_Tools_-_Filters_Selected[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f76f3842ef807c61b781ae227cdc0be0582ffd3c774b6fb48d73817a95a3dbc +size 93254 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..132a8a907c --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b882bbc26ade695ebdaae2c9998aadb7333bc92e74dc6a57230303a0cebf0595 +size 10873 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..b4114e354d --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cf73bc6120371695c7aff913fc13a7b9a6a8b104ac1a98f67bf104647676743 +size 10702 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..c16a04c199 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673aab4e3affdda965eb6ed7daa3342dab51aa0ac35fd13712d3d89c34f4e400 +size 8633 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..73738712af --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Data_Not_Loaded[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fecdd28ddcfe6ae2280234056573bd88626e96551534de90be6fd2092f47be0 +size 8510 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..c6461b66ad --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f8ae10f9e32a52adb657556cfe7c58fb150cdce36fcaacb945a922a8eb09844 +size 69657 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..d4ffebbcfe --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8a68d7a9fb1616448a51f274122154cfd04965bb436a588e7a76f828bd26356 +size 163602 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..b91ce24d4d --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb96c789143e8f3388843be52f3380bc0fce1b0eb84f40986b9623b313a8068f +size 69160 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..92efb257ba --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:729a4fdeff47a891212345278a495581f4dae2ec389cfdc15d385bb05ffee969 +size 84218 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..1c7e9841ec --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_No_Personalization[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:520632ac54442a632531f1d03c4b4826a5ddfb5471f76e5e2bacef105c5a1db4 +size 84757 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..8185829894 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a254784b924adcee9fb52609fabe916e5fdfab7283b6959f4042f3332bf9b603 +size 77911 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..a16f5e11ab --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52ab4f3b8068dc4cd0149e9ef4b4fe7de164c937ff96a83acad1a769d7570fb3 +size 168034 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..42a4dc4553 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42bdba962d0eb9fec7f1aafa7f3ad678a58060789d7159118aaf38cc1153f353 +size 76719 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..07de1607c6 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91efabec9c61cca425e99805e3a763acb6c60701acab1600f88270db3808d47e +size 86790 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..43b7103a65 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9afdc07c2d04cc6295cd3e2b0c22a93b60d1cb09a9047630c02cb6a309d83bb9 +size 87153 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..8185829894 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a254784b924adcee9fb52609fabe916e5fdfab7283b6959f4042f3332bf9b603 +size 77911 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png new file mode 100644 index 0000000000..a16f5e11ab --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NOTNIGHT,ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52ab4f3b8068dc4cd0149e9ef4b4fe7de164c937ff96a83acad1a769d7570fb3 +size 168034 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..42a4dc4553 --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Nexus_5,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42bdba962d0eb9fec7f1aafa7f3ad678a58060789d7159118aaf38cc1153f353 +size 76719 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..4e81bb320e --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Pixel_6_Pro,NIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd0395f220bd21cbfc50a9bfd758c4bae1f359748802942bcf58e939fa361f48 +size 64070 diff --git a/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png new file mode 100644 index 0000000000..b4e6a8d05d --- /dev/null +++ b/app/src/test/snapshots/images/org.cru.godtools.ui.dashboard_DashboardLayoutPaparazziTest_ToolsLayout()_-_Personalization_-_No_Tools[Pixel_6_Pro,NOTNIGHT,NO_ACCESSIBILITY].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9586615ef859302a0e04da4cf4abfe510a7e6e7b616710241a1c23d351d42685 +size 64336 diff --git a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/DashboardLayoutPaparazziTest.kt b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/DashboardLayoutPaparazziTest.kt new file mode 100644 index 0000000000..134391a53b --- /dev/null +++ b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/DashboardLayoutPaparazziTest.kt @@ -0,0 +1,194 @@ +package org.cru.godtools.ui.dashboard + +import androidx.activity.compose.LocalActivityResultRegistryOwner +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +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 com.slack.circuit.foundation.Circuit +import com.slack.circuit.foundation.CircuitCompositionLocals +import com.slack.circuit.runtime.presenter.presenterOf +import io.mockk.mockk +import java.util.Locale +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlinx.collections.immutable.persistentListOf +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.base.ui.BasePaparazziTest +import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen +import org.cru.godtools.model.Language +import org.cru.godtools.model.Tool +import org.cru.godtools.model.randomTool +import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState +import org.cru.godtools.ui.dashboard.filters.FilterMenu +import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters +import org.cru.godtools.ui.dashboard.tools.ToolsLayout +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter +import org.cru.godtools.ui.tools.ToolCardStateTestData +import org.junit.Assume.assumeTrue +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class DashboardLayoutPaparazziTest( + @TestParameter(valuesProvider = DeviceConfigProvider::class) deviceConfig: DeviceConfig, + @TestParameter nightMode: NightMode, + @TestParameter accessibilityMode: AccessibilityMode, +) : BasePaparazziTest(deviceConfig = deviceConfig, nightMode = nightMode, accessibilityMode = accessibilityMode) { + private val state = UiState() + + @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() + } + + // region ToolsLayout + private var toolsState = ToolsPresenter.UiState( + isPersonalizationEnabled = true, + dataLoaded = true, + spotlightTools = listOf( + ToolCardStateTestData.tool.copy( + toolCode = "spotlight1", + tool = randomTool("spotlight1", isFavorite = false) + ), + ToolCardStateTestData.tool.copy( + toolCode = "spotlight2", + tool = randomTool("spotlight2", isFavorite = true) + ), + ToolCardStateTestData.tool.copy( + toolCode = "spotlight3", + tool = randomTool("spotlight3", isFavorite = false) + ), + ), + tools = listOf( + ToolCardStateTestData.tool.copy(toolCode = "tool1"), + ToolCardStateTestData.tool.copy(toolCode = "tool2"), + ToolCardStateTestData.tool.copy(toolCode = "tool3"), + ), + ) + + @Test + fun `ToolsLayout() - Data Not Loaded`() { + assumeTrue( + "Disable Accessibility screenshots since this doesn't have any addition", + accessibilityMode == AccessibilityMode.NO_ACCESSIBILITY + ) + toolsState = toolsState.copy(dataLoaded = false, tools = emptyList()) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + + @Test + fun `ToolsLayout() - Personalization`() { + toolsState = toolsState.copy(mode = ToolsPresenter.UiState.Mode.PERSONALIZATION) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + + @Test + fun `ToolsLayout() - Personalization - No Tools`() { + toolsState = toolsState.copy( + mode = ToolsPresenter.UiState.Mode.PERSONALIZATION, + tools = emptyList() + ) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + + @Test + fun `ToolsLayout() - All Tools`() { + toolsState = toolsState.copy( + mode = ToolsPresenter.UiState.Mode.ALL_TOOLS, + spotlightTools = emptyList(), + ) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + + @Test + fun `ToolsLayout() - All Tools - Filters Selected`() { + toolsState = toolsState.copy( + mode = ToolsPresenter.UiState.Mode.ALL_TOOLS, + filters = Filters( + categoryFilter = FilterMenu.UiState(selectedItem = Tool.CATEGORY_GOSPEL), + languageFilter = FilterMenu.UiState( + selectedItem = Language(Locale.ENGLISH), + menuExpanded = mutableStateOf(false), + ), + ), + spotlightTools = emptyList(), + ) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + + @Test + @Ignore("LayoutLib does not correctly support Popups/Windows currently") + fun `ToolsLayout() - All Tools - Language Filter Expanded`() { + toolsState = toolsState.copy( + filters = Filters( + languageFilter = FilterMenu.UiState( + selectedItem = Language(Locale.ENGLISH), + menuExpanded = mutableStateOf(true), + items = persistentListOf( + FilterMenu.UiState.Item(null, 0), + FilterMenu.UiState.Item(Language(Locale.ENGLISH), 12345), + FilterMenu.UiState.Item(Language(Locale.FRENCH), 1), + FilterMenu.UiState.Item(Language(Locale("es")), 3), + ), + ) + ) + ) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + + @Test + fun `ToolsLayout() - No Personalization`() { + toolsState = toolsState.copy(isPersonalizationEnabled = false) + snapshotDashboardLayout(state.copy(initialPage = ToolsScreen)) + } + // endregion ToolsLayout + + private val circuit = Circuit.Builder() + .addPresenter { _, _, _ -> presenterOf { toolsState } } + .addUi { state, modifier -> ToolsLayout(state, modifier) } + .build() + + private fun snapshotDashboardLayout(state: UiState = this.state) = snapshot { + CircuitCompositionLocals(circuit) { + CompositionLocalProvider( + // mock required for AppUpdateSnackbar + LocalActivityResultRegistryOwner provides mockk(relaxed = true), + // mock required for AppUpdateSnackbar + LocalAppUpdateManager provides mockk(relaxed = true) + ) { + DashboardLayout(state) + } + } + } +} diff --git a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayoutPaparazziTest.kt b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayoutPaparazziTest.kt deleted file mode 100644 index ddb87a67e2..0000000000 --- a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayoutPaparazziTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -package org.cru.godtools.ui.dashboard.tools - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.mutableStateOf -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 java.util.Locale -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Ignore -import kotlin.test.Test -import kotlinx.collections.immutable.persistentListOf -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.base.ui.BasePaparazziTest -import org.cru.godtools.model.Language -import org.cru.godtools.model.Tool -import org.cru.godtools.model.randomTool -import org.cru.godtools.ui.dashboard.filters.FilterMenu -import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters -import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState -import org.cru.godtools.ui.tools.ToolCardStateTestData -import org.junit.Assume.assumeTrue -import org.junit.runner.RunWith - -@RunWith(TestParameterInjector::class) -class ToolsLayoutPaparazziTest( - @TestParameter(valuesProvider = DeviceConfigProvider::class) deviceConfig: DeviceConfig, - @TestParameter nightMode: NightMode, - @TestParameter accessibilityMode: AccessibilityMode, -) : BasePaparazziTest(deviceConfig = deviceConfig, nightMode = nightMode, accessibilityMode = accessibilityMode) { - private val tools = listOf( - ToolCardStateTestData.tool.copy(toolCode = "tool1"), - ToolCardStateTestData.tool.copy(toolCode = "tool2"), - ToolCardStateTestData.tool.copy(toolCode = "tool3"), - ) - private val spotlightTools = listOf( - ToolCardStateTestData.tool.copy(toolCode = "spotlight1", tool = randomTool("spotlight1", isFavorite = false)), - ToolCardStateTestData.tool.copy(toolCode = "spotlight2", tool = randomTool("spotlight2", isFavorite = true)), - ToolCardStateTestData.tool.copy(toolCode = "spotlight3", tool = randomTool("spotlight3", isFavorite = false)), - ) - - private val state = UiState( - dataLoaded = true, - spotlightTools = spotlightTools, - tools = tools, - ) - - @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 `ToolsLayout()`() = snapshotToolsLayout(state) - - @Test - fun `ToolsLayout() - Data Not Loaded`() { - assumeTrue( - "Only do a single screenshot since this is currently a blank screen", - deviceConfig == DeviceConfig.NEXUS_5 && - nightMode == NightMode.NOTNIGHT && - accessibilityMode == AccessibilityMode.NO_ACCESSIBILITY - ) - snapshotToolsLayout(state.copy(dataLoaded = false, tools = emptyList())) - } - - @Test - fun `ToolsLayout() - No Tools`() = snapshotToolsLayout(state.copy(tools = emptyList())) - - @Test - fun `ToolsLayout() - No Spotlight Tools`() = snapshotToolsLayout(state.copy(spotlightTools = emptyList())) - - @Test - fun `ToolsLayout() - Filters Selected`() = snapshotToolsLayout( - state.copy( - filters = Filters( - categoryFilter = FilterMenu.UiState(selectedItem = Tool.CATEGORY_GOSPEL), - languageFilter = FilterMenu.UiState( - selectedItem = Language(Locale.ENGLISH), - menuExpanded = mutableStateOf(false), - ), - ) - ) - ) - - @Test - @Ignore("LayoutLib does not correctly support Popups/Windows currently") - fun `ToolsLayout() - Language Filter Expanded`() = snapshotToolsLayout( - state.copy( - filters = Filters( - languageFilter = FilterMenu.UiState( - selectedItem = Language(Locale.ENGLISH), - menuExpanded = mutableStateOf(true), - items = persistentListOf( - FilterMenu.UiState.Item(null, 0), - FilterMenu.UiState.Item(Language(Locale.ENGLISH), 12345), - FilterMenu.UiState.Item(Language(Locale.FRENCH), 1), - FilterMenu.UiState.Item(Language(Locale("es")), 3), - ), - ) - ) - ) - ) - - private fun snapshotToolsLayout(state: UiState) = snapshot { - ToolsLayout( - state, - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - ) - } -} diff --git a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt index 2ce78b7e08..0bcbcc10cc 100644 --- a/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt +++ b/app/src/testDebug/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenterTest.kt @@ -3,9 +3,16 @@ package org.cru.godtools.ui.dashboard.tools import android.app.Application import androidx.compose.runtime.mutableStateOf import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.ReceiveTurbine +import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.jeppeman.mockposable.mockk.everyComposable +import com.slack.circuit.runtime.CircuitContext +import com.slack.circuit.runtime.InternalCircuitApi import com.slack.circuit.test.FakeNavigator import com.slack.circuit.test.test +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyAll import io.mockk.every import io.mockk.mockk import java.util.Locale @@ -17,34 +24,65 @@ import kotlin.test.assertNull import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.ccci.gto.android.common.androidx.compose.ui.platform.AndroidUiDispatcherUtil +import org.ccci.gto.android.common.sync.SyncTracker +import org.ccci.gto.support.turbine.awaitItemMatching +import org.cru.godtools.base.CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED +import org.cru.godtools.base.Settings import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen import org.cru.godtools.db.repository.ToolsRepository import org.cru.godtools.model.Language import org.cru.godtools.model.Tool import org.cru.godtools.model.randomTool +import org.cru.godtools.sync.GodToolsSyncService import org.cru.godtools.ui.banner.FakeBannerPresenter import org.cru.godtools.ui.banner.favoritetools.FavoriteToolsBannerPresenter +import org.cru.godtools.ui.dashboard.SyncTaskRegistry +import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry import org.cru.godtools.ui.dashboard.filters.FilterMenu +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiEvent +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState +import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode import org.cru.godtools.ui.tools.ToolCard import org.cru.godtools.ui.tools.ToolCardPresenter import org.junit.runner.RunWith import org.robolectric.annotation.Config +@Suppress("UnusedFlow") @RunWith(AndroidJUnit4::class) @Config(application = Application::class) @OptIn(ExperimentalCoroutinesApi::class) class ToolsPresenterTest { + private var isPersonalizationEnabled = false + private val countryFlow = MutableStateFlow("US") private val toolsFlow = MutableStateFlow(emptyList()) private val filteredToolsFlow = MutableStateFlow(emptyList()) + private val toolOrderSync = Channel() + private val testScope = TestScope() + @OptIn(InternalCircuitApi::class) + private val circuitContext = CircuitContext(null).apply { + syncTaskRegistry = SyncTaskRegistry(SyncTracker(testScope.backgroundScope)) + } + private val settings: Settings = mockk { + every { appLanguage } returns Locale.ENGLISH + every { getCountrySettingFlow() } returns countryFlow + } + private val syncService: GodToolsSyncService = mockk { + coEvery { syncToolOrder(any(), any(), any()) } coAnswers { toolOrderSync.receive() } + } private val favoriteToolsBannerPresenter = FakeBannerPresenter(null) private val filteredToolsFlowProducer: FilteredToolsFlowProducer = mockk { - every { getFlow(any(), any()) } returns filteredToolsFlow + every { getFlow(any(), any(), any()) } returns filteredToolsFlow } private val navigator = FakeNavigator(ToolsScreen) + private val remoteConfig: FirebaseRemoteConfig = mockk { + every { getBoolean(CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED) } answers { isPersonalizationEnabled } + } private val toolsRepository: ToolsRepository = mockk { every { getNormalToolsFlow() } returns toolsFlow } @@ -57,11 +95,15 @@ class ToolsPresenterTest { private val presenter = ToolsPresenter( eventBus = mockk(), + remoteConfig = remoteConfig, + settings = settings, toolCardPresenter = toolCardPresenter, toolsRepository = toolsRepository, favoriteToolsBannerPresenter = favoriteToolsBannerPresenter, filteredToolsFlowProducer = filteredToolsFlowProducer, toolFiltersStateProducer = toolFiltersStateProducer, + syncService = syncService, + circuitContext = circuitContext, navigator = navigator, ) @@ -70,7 +112,7 @@ class ToolsPresenterTest { // region State.banner @Test - fun `State - banner - none`() = runTest { + fun `State - banner - none`() = testScope.runTest { favoriteToolsBannerPresenter.updateState(null) presenter.test { assertNull(expectMostRecentItem().banner) @@ -78,7 +120,7 @@ class ToolsPresenterTest { } @Test - fun `State - banner - favorites`() = runTest { + fun `State - banner - favorites`() = testScope.runTest { val bannerState = FavoriteToolsBannerPresenter.UiState() favoriteToolsBannerPresenter.updateState(bannerState) presenter.test { @@ -87,9 +129,67 @@ class ToolsPresenterTest { } // endregion State.banner + // region State.mode + @Test + fun `State - mode - personalization enabled`() = testScope.runTest { + isPersonalizationEnabled = true + + presenter.test { + val state = awaitInitialItem() + assertEquals(Mode.PERSONALIZATION, state.mode) + + state.eventSink(UiEvent.ChangeMode(Mode.ALL_TOOLS)) + assertEquals(Mode.ALL_TOOLS, awaitItem().mode) + } + } + // endregion State.mode + + // region State.tools + @Test + fun `State - tools - shows tools from filteredToolsFlowProducer`() = testScope.runTest { + val tool = randomTool(isHidden = false) + filteredToolsFlow.value = listOf(tool) + + presenter.test { + assertEquals(listOf(tool), awaitInitialItem().tools.map { it.tool }) + } + } + + @Test + fun `State - tools - updates when filtered tools change`() = testScope.runTest { + val tool = randomTool(isHidden = false) + + presenter.test { + assertEquals(emptyList(), awaitInitialItem().tools) + + filteredToolsFlow.value = listOf(tool) + assertEquals(listOf(tool), awaitItem().tools.map { it.tool }) + } + } + + @Test + fun `State - tools - shows tools from correct mode flow`() = testScope.runTest { + isPersonalizationEnabled = true + val personalizationTools = List(2) { randomTool(isHidden = false) } + val allToolsList = List(3) { randomTool(isHidden = false) } + val personalizationFlow = MutableStateFlow(personalizationTools) + val allToolsFlow = MutableStateFlow(allToolsList) + every { filteredToolsFlowProducer.getFlow(Mode.PERSONALIZATION, any(), any()) } returns personalizationFlow + every { filteredToolsFlowProducer.getFlow(Mode.ALL_TOOLS, any(), any()) } returns allToolsFlow + + presenter.test { + val initial = awaitInitialItem() + assertEquals(personalizationTools, initial.tools.map { it.tool }) + + initial.eventSink(UiEvent.ChangeMode(Mode.ALL_TOOLS)) + assertEquals(allToolsList, awaitItemMatching { it.tools.size == allToolsList.size }.tools.map { it.tool }) + } + } + // endregion State.tools + // region State.spotlightTools @Test - fun `Property spotlightTools`() = runTest { + fun `Property spotlightTools`() = testScope.runTest { val normalTool = randomTool("normal", isHidden = false, isSpotlight = false) val spotlightTool = randomTool("spotlight", isHidden = false, isSpotlight = true) @@ -100,7 +200,7 @@ class ToolsPresenterTest { } @Test - fun `Property spotlightTools - Don't show hidden tools`() = runTest { + fun `Property spotlightTools - Don't show hidden tools`() = testScope.runTest { val hiddenTool = randomTool("normal", isHidden = true, isSpotlight = true) val spotlightTool = randomTool("spotlight", isHidden = false, isSpotlight = true) @@ -111,7 +211,7 @@ class ToolsPresenterTest { } @Test - fun `Property spotlightTools - Sorted by default order`() = runTest { + fun `Property spotlightTools - Sorted by default order`() = testScope.runTest { val tools = List(10) { randomTool("tool$it", Tool.Type.TRACT, defaultOrder = it, isHidden = false, isSpotlight = true) } @@ -121,12 +221,41 @@ class ToolsPresenterTest { assertEquals(tools, expectMostRecentItem().spotlightTools.map { it.tool }) } } + + @Test + fun `Property spotlightTools - Don't show spotlight tools for ALL_TOOLS`() = testScope.runTest { + isPersonalizationEnabled = true + val normalTool = randomTool("normal", isHidden = false, isSpotlight = false) + val spotlightTool = randomTool("spotlight", isHidden = false, isSpotlight = true) + toolsFlow.value = listOf(normalTool, spotlightTool) + + presenter.test { + val initialState = awaitInitialItem() + assertEquals(listOf(spotlightTool), initialState.spotlightTools.map { it.tool }) + + initialState.eventSink(UiEvent.ChangeMode(Mode.ALL_TOOLS)) + assertEquals(emptyList(), expectMostRecentItem().spotlightTools) + } + } // endregion State.spotlightTools // region State.filters + @Test + fun `State - filters - uses current mode`() = testScope.runTest { + isPersonalizationEnabled = true + + presenter.test { + assertEquals(Mode.PERSONALIZATION, toolFiltersStateProducer.lastMode) + + awaitInitialItem().eventSink(UiEvent.ChangeMode(Mode.ALL_TOOLS)) + awaitItem() + assertEquals(Mode.ALL_TOOLS, toolFiltersStateProducer.lastMode) + } + } + @Test @OptIn(ExperimentalUuidApi::class) - fun `State - filters`() = runTest { + fun `State - filters`() = testScope.runTest { val filters = ToolFiltersStateProducer.Filters( categoryFilter = FilterMenu.UiState( menuExpanded = mutableStateOf(Random.nextBoolean()), @@ -158,4 +287,60 @@ class ToolsPresenterTest { } } // endregion State.filters + + // region SideEffect - RegisterSyncTask + @Test + fun `SideEffect - RegisterSyncTask - Triggers initial sync`() = testScope.runTest { + presenter.test { + awaitInitialItem() + toolOrderSync.send(true) + coVerifyAll { syncService.syncToolOrder(Locale.ENGLISH, "US", false) } + } + } + + @Test + fun `SideEffect - RegisterSyncTask - uses locale from language filter`() = testScope.runTest { + toolFiltersStateProducer.filters.value = ToolFiltersStateProducer.Filters( + languageFilter = FilterMenu.UiState(selectedItem = Language(Locale.FRENCH)) + ) + + presenter.test { + awaitInitialItem() + toolOrderSync.send(true) + coVerifyAll { syncService.syncToolOrder(Locale.FRENCH, "US", false) } + } + } + + @Test + fun `SideEffect - RegisterSyncTask - re-syncs when locale changes`() = testScope.runTest { + presenter.test { + awaitInitialItem() + toolOrderSync.send(true) + coVerify { syncService.syncToolOrder(Locale.ENGLISH, "US", false) } + + toolFiltersStateProducer.filters.value = ToolFiltersStateProducer.Filters( + languageFilter = FilterMenu.UiState(selectedItem = Language(Locale.FRENCH)) + ) + awaitItemMatching { it.filters.languageFilter.selectedItem?.code == Locale.FRENCH } + toolOrderSync.send(true) + coVerify { syncService.syncToolOrder(Locale.FRENCH, "US", false) } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `SideEffect - RegisterSyncTask - passes force on triggered sync`() = testScope.runTest { + presenter.test { + awaitInitialItem() + toolOrderSync.send(true) + coVerify { syncService.syncToolOrder(Locale.ENGLISH, "US", false) } + + circuitContext.syncTaskRegistry!!.triggerSyncTasks(force = true) + toolOrderSync.send(true) + coVerify { syncService.syncToolOrder(Locale.ENGLISH, "US", true) } + } + } + // endregion SideEffect - RegisterSyncTask + + private suspend fun ReceiveTurbine.awaitInitialItem() = awaitItemMatching { it.dataLoaded } } diff --git a/library/api/src/main/kotlin/org/cru/godtools/api/ToolsApi.kt b/library/api/src/main/kotlin/org/cru/godtools/api/ToolsApi.kt index 1e5aeb244a..2386477c05 100644 --- a/library/api/src/main/kotlin/org/cru/godtools/api/ToolsApi.kt +++ b/library/api/src/main/kotlin/org/cru/godtools/api/ToolsApi.kt @@ -25,16 +25,10 @@ interface ToolsApi { @QueryMap params: JsonApiParams, ): Response> - @GET("$PATH_RESOURCES/featured") - suspend fun getToolOrder( - @Query(PARAM_FILTER_LANGUAGE) locale: Locale, - @Query(PARAM_FILTER_COUNTRY) country: String, - @QueryMap params: JsonApiParams, - ): Response> - @GET("$PATH_RESOURCES/default_order") - suspend fun getDefaultToolOrder( + suspend fun getToolOrder( @Query(PARAM_FILTER_LANGUAGE) locale: Locale, + @Query(PARAM_FILTER_COUNTRY) country: String? = null, @QueryMap params: JsonApiParams, ): Response> } diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/Config.kt b/library/base/src/main/kotlin/org/cru/godtools/base/Config.kt index 31dad0f274..a4743b3d06 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/Config.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/Config.kt @@ -2,6 +2,7 @@ package org.cru.godtools.base const val CONFIG_TOOL_CONTENT_FEATURE_PAGE_COLLECTION = "tool_content_feature_page_collection_page_enabled" const val CONFIG_TUTORIAL_LESSON_PAGE_SWIPE = "tutorial_lesson_page_swipe_enabled" +const val CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED = "ui_dashboard_personalization_enabled" const val CONFIG_UI_DASHBOARD_HOME_FAVORITE_TOOLS = "ui_dashboard_home_favorite_tool_cards_count" const val CONFIG_UI_GLOBAL_ACTIVITY_ENABLED = "ui_account_globalactivity_enabled" @@ -14,6 +15,7 @@ const val CONFIG_UI_OPT_IN_NOTIFICATION_PROMPT_LIMIT = "ui_opt_in_notification_p internal val CONFIG_DEFAULTS = mapOf( CONFIG_TOOL_CONTENT_FEATURE_PAGE_COLLECTION to true, CONFIG_TUTORIAL_LESSON_PAGE_SWIPE to true, + CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED to false, CONFIG_UI_DASHBOARD_HOME_FAVORITE_TOOLS to 5, CONFIG_UI_GLOBAL_ACTIVITY_ENABLED to true, @@ -22,5 +24,4 @@ internal val CONFIG_DEFAULTS = mapOf( CONFIG_UI_OPT_IN_NOTIFICATION_TIME_INTERVAL to 41, CONFIG_UI_OPT_IN_NOTIFICATION_PROMPT_LIMIT to 5, // endregion optInNotification - ) diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt b/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt index 0a24ae9f69..d5e2e452e7 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/Settings.kt @@ -68,8 +68,8 @@ class Settings internal constructor(private val context: Context, coroutineScope private val KEY_DASHBOARD_FILTER_CATEGORY = stringPreferencesKey("dashboardFilterCategory") private val KEY_DASHBOARD_FILTER_LOCALE = stringPreferencesKey("dashboardFilterLocale") - // Country Settings - private val KEY_COUNTRY_SETTING = stringPreferencesKey("CountrySetting") + // Personalization Settings + private val KEY_PERSONALIZATION_COUNTRY = stringPreferencesKey("personalizationCountry") // optInNotification const val LAST_PROMPTED_OPT_IN_NOTIFICATION = "lastPromptedOptInNotification" @@ -168,22 +168,24 @@ class Settings internal constructor(private val context: Context, coroutineScope } // endregion Dashboard Settings - // region Country Settings - fun getCountrySettingFlow() = dataStorePreferences.data - .map { it[KEY_COUNTRY_SETTING] } + // region Personalization Settings + fun getCountrySettingFlow() = getPersonalizationCountryFlow() + + fun getPersonalizationCountryFlow() = dataStorePreferences.data + .map { it[KEY_PERSONALIZATION_COUNTRY] } .distinctUntilChanged() suspend fun updateCountrySetting(isoCode: String?) { dataStorePreferences.updateData { it.toMutablePreferences().apply { when (isoCode) { - null -> remove(KEY_COUNTRY_SETTING) - else -> set(KEY_COUNTRY_SETTING, isoCode) + null -> remove(KEY_PERSONALIZATION_COUNTRY) + else -> set(KEY_PERSONALIZATION_COUNTRY, isoCode) } } } } - // endregion Country Settings + // endregion Personalization Settings // region optInNotification fun getLastPromptedOptInNotification(): LocalDate = diff --git a/library/sync/src/main/kotlin/org/cru/godtools/sync/GodToolsSyncService.kt b/library/sync/src/main/kotlin/org/cru/godtools/sync/GodToolsSyncService.kt index a01689eb08..cba6cd5e97 100644 --- a/library/sync/src/main/kotlin/org/cru/godtools/sync/GodToolsSyncService.kt +++ b/library/sync/src/main/kotlin/org/cru/godtools/sync/GodToolsSyncService.kt @@ -86,8 +86,8 @@ class GodToolsSyncService @VisibleForTesting internal constructor( suspend fun syncTool(toolCode: String, force: Boolean = false) = executeSync { syncTool(toolCode, force) } - suspend fun syncPersonalizedTools(locale: Locale, country: String?, force: Boolean = false) = - executeSync { syncPersonalizedTools(locale, country, force) } + suspend fun syncToolOrder(locale: Locale, country: String?, force: Boolean = false) = + executeSync { this.syncToolOrder(locale, country, force) } suspend fun syncGlobalActivity(force: Boolean = false) = executeSync { syncGlobalActivity(force) } diff --git a/library/sync/src/main/kotlin/org/cru/godtools/sync/task/ToolSyncTasks.kt b/library/sync/src/main/kotlin/org/cru/godtools/sync/task/ToolSyncTasks.kt index f4baf11786..bd25c27956 100644 --- a/library/sync/src/main/kotlin/org/cru/godtools/sync/task/ToolSyncTasks.kt +++ b/library/sync/src/main/kotlin/org/cru/godtools/sync/task/ToolSyncTasks.kt @@ -39,7 +39,7 @@ internal class ToolSyncTasks @Inject internal constructor( ) : BaseSyncTasks() { internal companion object { const val SYNC_TIME_TOOLS = "last_synced.tools" - const val SYNC_TIME_PERSONALIZED_TOOLS = "last_synced.personalized_tools." + const val SYNC_TIME_TOOL_ORDER = "last_synced.tool_order." private val INCLUDES_GET_TOOL = Includes( Tool.JSON_ATTACHMENTS, @@ -52,7 +52,7 @@ internal class ToolSyncTasks @Inject internal constructor( .fields(Tool.JSONAPI_TYPE, *Tool.JSONAPI_FIELDS) .fields(Language.JSONAPI_TYPE, *Language.JSONAPI_FIELDS) - private fun buildPersonalizedToolsApiParams() = JsonApiParams() + private fun buildToolOrderApiParams() = JsonApiParams() .fields(Tool.JSONAPI_TYPE, Tool.JSON_CODE) } @@ -102,57 +102,34 @@ internal class ToolSyncTasks @Inject internal constructor( true } - // region Personalized Tools - private val personalizedToolsMutex = MutexMap() + // region Tool Order + private val toolOrderMutex = MutexMap() - internal suspend fun syncPersonalizedTools(locale: Locale, country: String?, force: Boolean = false) = - coroutineScope { - val order = async { country?.let { syncPersonalizedToolOrder(locale, it.uppercase(), force) } ?: true } - val defaultOrder = async { syncDefaultPersonalizedToolOrder(locale, force) } - order.await() && defaultOrder.await() - } + internal suspend fun syncToolOrder(locale: Locale, country: String?, force: Boolean = false): Boolean { + val normalizedCountry = country?.uppercase() - private suspend fun syncPersonalizedToolOrder(locale: Locale, country: String, force: Boolean) = - personalizedToolsMutex.withLock(locale to country) { + toolOrderMutex.withLock(locale to normalizedCountry) { if (!force && !lastSyncTimeRepository.isLastSyncStale( - SYNC_TIME_PERSONALIZED_TOOLS, + SYNC_TIME_TOOL_ORDER, locale, - country, + normalizedCountry.orEmpty(), staleAfter = STALE_DURATION_TOOLS, ) ) { - return@withLock true + return true } - val tools = toolsApi.getToolOrder(locale, country, buildPersonalizedToolsApiParams()) - .takeIf { it.code() == HTTP_OK }?.body()?.data ?: return@withLock false + val tools = toolsApi.getToolOrder(locale, country, buildToolOrderApiParams()) + .takeIf { it.code() == HTTP_OK }?.body()?.data ?: return false toolsRepository.storePersonalizedToolOrderFromSync(locale, country, tools) - lastSyncTimeRepository.updateLastSyncTime(SYNC_TIME_PERSONALIZED_TOOLS, locale, country) - true - } + lastSyncTimeRepository.updateLastSyncTime(SYNC_TIME_TOOL_ORDER, locale, normalizedCountry.orEmpty()) - private suspend fun syncDefaultPersonalizedToolOrder(locale: Locale, force: Boolean) = - personalizedToolsMutex.withLock(locale) { - if (!force && - !lastSyncTimeRepository.isLastSyncStale( - SYNC_TIME_PERSONALIZED_TOOLS, - locale, - staleAfter = STALE_DURATION_TOOLS - ) - ) { - return@withLock true - } - - val tools = toolsApi.getDefaultToolOrder(locale, buildPersonalizedToolsApiParams()) - .takeIf { it.code() == HTTP_OK }?.body()?.data ?: return@withLock false - - toolsRepository.storePersonalizedToolOrderFromSync(locale, null, tools) - lastSyncTimeRepository.updateLastSyncTime(SYNC_TIME_PERSONALIZED_TOOLS, locale) - true + return true } - // endregion Personalized Tools + } + // endregion Tool Order /** * @return true if all pending share counts were successfully synced. false if any failed to sync. diff --git a/library/sync/src/test/kotlin/org/cru/godtools/sync/task/ToolSyncTasksTest.kt b/library/sync/src/test/kotlin/org/cru/godtools/sync/task/ToolSyncTasksTest.kt index 60b683f7c6..e3cbea87f1 100644 --- a/library/sync/src/test/kotlin/org/cru/godtools/sync/task/ToolSyncTasksTest.kt +++ b/library/sync/src/test/kotlin/org/cru/godtools/sync/task/ToolSyncTasksTest.kt @@ -3,7 +3,6 @@ package org.cru.godtools.sync.task import io.mockk.Called import io.mockk.Runs import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.coVerifyAll import io.mockk.coVerifySequence import io.mockk.just @@ -19,7 +18,7 @@ import org.cru.godtools.db.repository.InMemoryLastSyncTimeRepository import org.cru.godtools.db.repository.ToolsRepository import org.cru.godtools.model.randomTool import org.cru.godtools.sync.repository.SyncRepository -import org.cru.godtools.sync.task.ToolSyncTasks.Companion.SYNC_TIME_PERSONALIZED_TOOLS +import org.cru.godtools.sync.task.ToolSyncTasks.Companion.SYNC_TIME_TOOL_ORDER import retrofit2.Response class ToolSyncTasksTest { @@ -28,14 +27,12 @@ class ToolSyncTasksTest { private val tool = randomTool() private val existingTools = listOf(randomTool()) - private val personalizedTools = listOf(randomTool(), randomTool()) + private val apiToolOrder = listOf(randomTool(), randomTool()) private val toolsApi: ToolsApi = mockk { coEvery { list(any()) } returns Response.success(JsonApiObject.single(tool)) coEvery { getToolOrder(any(), any(), any()) } returns - Response.success(JsonApiObject.of(*personalizedTools.toTypedArray())) - coEvery { getDefaultToolOrder(any(), any()) } returns - Response.success(JsonApiObject.of(*personalizedTools.toTypedArray())) + Response.success(JsonApiObject.of(*apiToolOrder.toTypedArray())) } private val viewsApi: ViewsApi = mockk() private val syncRepository: SyncRepository = mockk { @@ -102,73 +99,68 @@ class ToolSyncTasksTest { } // endregion syncTools() - // region syncPersonalizedTools() + // region syncToolOrder() @Test - fun `syncPersonalizedTools(country = non-null)`() = runTest { - tasks.syncPersonalizedTools(locale, country) + fun `syncToolOrder()`() = runTest { + tasks.syncToolOrder(locale, country) coVerifyAll { toolsApi.getToolOrder(locale, country, any()) - toolsApi.getDefaultToolOrder(locale, any()) - toolsRepository.storePersonalizedToolOrderFromSync(locale, country, personalizedTools) - toolsRepository.storePersonalizedToolOrderFromSync(locale, null, personalizedTools) + toolsRepository.storePersonalizedToolOrderFromSync(locale, country, apiToolOrder) } assertFalse( - lastSyncTimeRepository.isLastSyncStale(SYNC_TIME_PERSONALIZED_TOOLS, locale, country, staleAfter = 60_000) - ) - assertFalse( - lastSyncTimeRepository.isLastSyncStale(SYNC_TIME_PERSONALIZED_TOOLS, locale, staleAfter = 60_000) + lastSyncTimeRepository.isLastSyncStale( + SYNC_TIME_TOOL_ORDER, + locale, + country, + staleAfter = 60_000 + ) ) } @Test - fun `syncPersonalizedTools(country = null)`() = runTest { - tasks.syncPersonalizedTools(locale, null) + fun `syncToolOrder(country = null)`() = runTest { + tasks.syncToolOrder(locale, null) coVerifyAll { - toolsApi.getDefaultToolOrder(locale, any()) - toolsRepository.storePersonalizedToolOrderFromSync(locale, null, personalizedTools) + toolsApi.getToolOrder(locale, null, any()) + toolsRepository.storePersonalizedToolOrderFromSync(locale, null, apiToolOrder) } - coVerify(exactly = 0) { toolsApi.getToolOrder(any(), any(), any()) } assertFalse( - lastSyncTimeRepository.isLastSyncStale(SYNC_TIME_PERSONALIZED_TOOLS, locale, staleAfter = 60_000) + lastSyncTimeRepository.isLastSyncStale(SYNC_TIME_TOOL_ORDER, locale, "", staleAfter = 60_000) ) } @Test - fun `syncPersonalizedTools(force = false) - already synced`() = runTest { + fun `syncToolOrder(force = false) - already synced`() = runTest { with(lastSyncTimeRepository) { - setLastSyncTime(SYNC_TIME_PERSONALIZED_TOOLS, locale, country, time = System.currentTimeMillis()) - setLastSyncTime(SYNC_TIME_PERSONALIZED_TOOLS, locale, time = System.currentTimeMillis()) + setLastSyncTime(SYNC_TIME_TOOL_ORDER, locale, country, time = System.currentTimeMillis()) } - tasks.syncPersonalizedTools(locale, country, force = false) - coVerifyAll { toolsApi wasNot Called } + tasks.syncToolOrder(locale, country, force = false) + coVerifyAll { + toolsApi wasNot Called + toolsRepository wasNot Called + } } @Test - fun `syncPersonalizedTools(force = true) - already synced`() = runTest { + fun `syncToolOrder(force = true) - already synced`() = runTest { with(lastSyncTimeRepository) { - setLastSyncTime(SYNC_TIME_PERSONALIZED_TOOLS, locale, country, time = System.currentTimeMillis()) - setLastSyncTime(SYNC_TIME_PERSONALIZED_TOOLS, locale, time = System.currentTimeMillis()) + setLastSyncTime(SYNC_TIME_TOOL_ORDER, locale, country, time = System.currentTimeMillis()) } - tasks.syncPersonalizedTools(locale, country, force = true) + tasks.syncToolOrder(locale, country, force = true) coVerifyAll { toolsApi.getToolOrder(locale, country, any()) - toolsApi.getDefaultToolOrder(locale, any()) - toolsRepository.storePersonalizedToolOrderFromSync(locale, country, personalizedTools) - toolsRepository.storePersonalizedToolOrderFromSync(locale, null, personalizedTools) + toolsRepository.storePersonalizedToolOrderFromSync(locale, country, apiToolOrder) } assertFalse( lastSyncTimeRepository.isLastSyncStale( - SYNC_TIME_PERSONALIZED_TOOLS, + SYNC_TIME_TOOL_ORDER, locale, - country.uppercase(), + country, staleAfter = 60_000, ) ) - assertFalse( - lastSyncTimeRepository.isLastSyncStale(SYNC_TIME_PERSONALIZED_TOOLS, locale, staleAfter = 60_000) - ) } - // endregion syncPersonalizedTools() + // endregion syncToolOrder() }