diff --git a/course/src/main/java/org/openedx/course/data/repository/CoalescingCache.kt b/course/src/main/java/org/openedx/course/data/repository/CoalescingCache.kt new file mode 100644 index 000000000..78a16d8a8 --- /dev/null +++ b/course/src/main/java/org/openedx/course/data/repository/CoalescingCache.kt @@ -0,0 +1,73 @@ +package org.openedx.course.data.repository + +import kotlinx.coroutines.CompletableDeferred +import java.util.concurrent.ConcurrentHashMap + +/** + * A cache with request coalescing support. + * + * When multiple callers request the same data simultaneously, + * only one fetch operation is performed and all callers receive the same result. + * + * @param K the type of cache keys + * @param V the type of cached values + * @param fetch the suspend function to fetch data for a given key + * @param persist optional callback invoked after successful fetch (e.g., to save to database) + */ +class CoalescingCache( + private val fetch: suspend (K) -> V, + private val persist: (suspend (K, V) -> Unit)? = null +) { + private val cache = ConcurrentHashMap() + private val pending = ConcurrentHashMap>() + + /** + * Returns cached value for the key, or null if not cached. + */ + fun getCached(key: K): V? = cache[key] + + /** + * Manually sets a cached value. + */ + fun setCached(key: K, value: V) { + cache[key] = value + } + + /** + * Gets the value from cache or fetches it. + * + * If [forceRefresh] is false and a cached value exists, returns it immediately. + * Otherwise, fetches the value. If another fetch for the same key is already + * in progress, waits for that result instead of making a duplicate request. + */ + suspend fun getOrFetch(key: K, forceRefresh: Boolean = false): V { + if (!forceRefresh) { + cache[key]?.let { return it } + } + + val (deferred, isOwner) = getOrCreateDeferred(key) + return if (isOwner) { + try { + val result = fetch(key) + cache[key] = result + persist?.invoke(key, result) + deferred.complete(result) + result + } catch (e: Exception) { + deferred.completeExceptionally(e) + throw e + } finally { + pending.remove(key) + } + } else { + deferred.await() + } + } + + private fun getOrCreateDeferred(key: K): Pair, Boolean> { + pending[key]?.let { return it to false } + val deferred = CompletableDeferred() + val existing = pending.putIfAbsent(key, deferred) + return if (existing != null) existing to false else deferred to true + } +} diff --git a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt index a6db3df5a..37a049a68 100644 --- a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt +++ b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt @@ -1,12 +1,12 @@ package org.openedx.course.data.repository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import okhttp3.MultipartBody import org.openedx.core.ApiConstants import org.openedx.core.data.api.CourseApi import org.openedx.core.data.model.BlocksCompletionBody -import org.openedx.core.data.model.room.CourseProgressEntity import org.openedx.core.data.model.room.OfflineXBlockProgress import org.openedx.core.data.model.room.VideoProgressEntity import org.openedx.core.data.model.room.XBlockProgressData @@ -19,12 +19,18 @@ import org.openedx.core.domain.model.CourseEnrollmentDetails import org.openedx.core.domain.model.CourseProgress import org.openedx.core.domain.model.CourseStructure import org.openedx.core.exception.NoCachedDataException -import org.openedx.core.extension.channelFlowWithAwait import org.openedx.core.module.db.DownloadDao import org.openedx.core.system.connection.NetworkConnection import java.net.URLDecoder import java.nio.charset.StandardCharsets - +import java.util.concurrent.ConcurrentHashMap + +/** + * Repository for course data with request coalescing. + * + * When multiple callers request the same data simultaneously, + * only one network request is made and all callers receive the same result. + */ @Suppress("TooManyFunctions") class CourseRepository( private val api: CourseApi, @@ -33,124 +39,190 @@ class CourseRepository( private val preferencesManager: CorePreferences, private val networkConnection: NetworkConnection, ) { - private val courseStructure = mutableMapOf() + // Session tracking - when entering a course, mark that data needs refresh + private val needsRefresh = ConcurrentHashMap.newKeySet() - private val courseStatusMap = mutableMapOf() - private val courseDatesMap = mutableMapOf() + private val structureCache = CoalescingCache( + fetch = { courseId -> + val response = api.getCourseStructure( + "stale-if-error=0", + "v4", + preferencesManager.user?.username, + courseId + ) + courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) + response.mapToDomain() + }, + persist = { courseId, _ -> needsRefresh.remove(courseId) } + ) + + private val statusCache = CoalescingCache( + fetch = { courseId -> + val username = preferencesManager.user?.username ?: "" + api.getCourseStatus(username, courseId).mapToDomain() + } + ) - suspend fun removeDownloadModel(id: String) { - downloadDao.removeDownloadModel(id) - } + private val datesCache = CoalescingCache( + fetch = { courseId -> api.getCourseDates(courseId).getCourseDatesResult() } + ) - fun getDownloadModels() = downloadDao.getAllDataFlow().map { list -> - list.map { it.mapToDomain() } + private val progressCache = CoalescingCache( + fetch = { courseId -> + val response = api.getCourseProgress(courseId) + courseDao.insertCourseProgressEntity(response.mapToRoomEntity(courseId)) + response.mapToDomain() + } + ) + + private val enrollmentCache = CoalescingCache( + fetch = { courseId -> api.getEnrollmentDetails(courseId).mapToDomain() }, + persist = { _, details -> courseDao.insertCourseEnrollmentDetailsEntity(details.mapToEntity()) } + ) + + /** + * Call when entering a course to mark that data should be refreshed. + */ + fun startCourseSession(courseId: String) { + needsRefresh.add(courseId) } - suspend fun getAllDownloadModels() = downloadDao.readAllData().map { it.mapToDomain() } - - suspend fun getCourseStructureFlow( + fun getCourseStructureFlow( courseId: String, - forceRefresh: Boolean = true - ): Flow = - channelFlowWithAwait { - var hasCourseStructure = false - val cachedCourseStructure = courseStructure[courseId] ?: ( - courseDao.getCourseStructureById(courseId)?.mapToDomain() - ) - if (cachedCourseStructure != null) { - hasCourseStructure = true - trySend(cachedCourseStructure) - } - val fetchRemoteCourse = !hasCourseStructure || forceRefresh - if (networkConnection.isOnline() && fetchRemoteCourse) { - val response = api.getCourseStructure( - "stale-if-error=0", - "v4", - preferencesManager.user?.username, - courseId - ) - courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) - val courseDomainModel = response.mapToDomain() - courseStructure[courseId] = courseDomainModel - trySend(courseDomainModel) - hasCourseStructure = true - } - if (!hasCourseStructure) { - throw NoCachedDataException() + forceRefresh: Boolean = false + ): Flow = flow { + // Always emit cached data first if available + structureCache.getCached(courseId)?.let { emit(it) } + + if (structureCache.getCached(courseId) == null) { + courseDao.getCourseStructureById(courseId)?.mapToDomain()?.let { + structureCache.setCached(courseId, it) + emit(it) } } - suspend fun getCourseStructureFromCache(courseId: String): CourseStructure { - val cachedCourseStructure = courseDao.getCourseStructureById(courseId) - if (cachedCourseStructure != null) { - return cachedCourseStructure.mapToDomain() - } else { + val shouldRefresh = forceRefresh || needsRefresh.contains(courseId) + if (networkConnection.isOnline() && (structureCache.getCached(courseId) == null || shouldRefresh)) { + emit(structureCache.getOrFetch(courseId, forceRefresh = true)) + } + + if (structureCache.getCached(courseId) == null) { throw NoCachedDataException() } } - suspend fun getCourseStructure(courseId: String, isNeedRefresh: Boolean): CourseStructure { - if (!isNeedRefresh) courseStructure[courseId]?.let { return it } - - if (networkConnection.isOnline()) { - val response = api.getCourseStructure( - "stale-if-error=0", - "v4", - preferencesManager.user?.username, - courseId - ) - courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) - courseStructure[courseId] = response.mapToDomain() - } else { - val cachedCourseStructure = courseDao.getCourseStructureById(courseId) - if (cachedCourseStructure != null) { - courseStructure[courseId] = cachedCourseStructure.mapToDomain() - } else { - throw NoCachedDataException() + suspend fun getCourseStructureFromCache(courseId: String): CourseStructure { + return structureCache.getCached(courseId) + ?: courseDao.getCourseStructureById(courseId)?.mapToDomain()?.also { + structureCache.setCached(courseId, it) } - } - - return courseStructure[courseId]!! + ?: throw NoCachedDataException() } - suspend fun getEnrollmentDetailsFlow(courseId: String): Flow = - channelFlowWithAwait { - getCourseEnrollmentDetailsFromCache(courseId)?.let { - trySend(it) + fun getEnrollmentDetailsFlow( + courseId: String, + forceRefresh: Boolean = false + ): Flow = flow { + // Always emit cached data first if available + enrollmentCache.getCached(courseId)?.let { emit(it) } + + if (enrollmentCache.getCached(courseId) == null) { + courseDao.getCourseEnrollmentDetailsById(courseId)?.mapToDomain()?.let { + enrollmentCache.setCached(courseId, it) + emit(it) } - val details = getEnrollmentDetails(courseId) - courseDao.insertCourseEnrollmentDetailsEntity(details.mapToEntity()) - trySend(details) } - private suspend fun getCourseEnrollmentDetailsFromCache(courseId: String): CourseEnrollmentDetails? { - return courseDao.getCourseEnrollmentDetailsById(id = courseId) - ?.mapToDomain() + if (networkConnection.isOnline() && (enrollmentCache.getCached(courseId) == null || forceRefresh)) { + emit(enrollmentCache.getOrFetch(courseId, forceRefresh = true)) + } + + if (enrollmentCache.getCached(courseId) == null) { + throw NoCachedDataException() + } } suspend fun getEnrollmentDetails(courseId: String): CourseEnrollmentDetails { - return api.getEnrollmentDetails(courseId = courseId).mapToDomain() + return api.getEnrollmentDetails(courseId).mapToDomain() } - suspend fun getCourseStatusFlow(courseId: String): Flow = - channelFlowWithAwait { - val localStatus = courseStatusMap[courseId] - localStatus?.let { trySend(it) } - - if (networkConnection.isOnline()) { - val username = preferencesManager.user?.username ?: "" - val status = api.getCourseStatus(username, courseId).mapToDomain() - courseStatusMap[courseId] = status - trySend(status) - } else { - val status = localStatus ?: CourseComponentStatus("") - trySend(status) - } + fun getCourseStatusFlow( + courseId: String, + forceRefresh: Boolean = false + ): Flow = flow { + // Always emit cached data first if available, otherwise emit empty status + val cached = statusCache.getCached(courseId) + emit(cached ?: CourseComponentStatus("")) + + val shouldRefresh = forceRefresh || needsRefresh.contains(courseId) + if (networkConnection.isOnline() && (cached == null || shouldRefresh)) { + emit(statusCache.getOrFetch(courseId, forceRefresh = true)) } + } suspend fun getCourseStatus(courseId: String): CourseComponentStatus { val username = preferencesManager.user?.username ?: "" - return api.getCourseStatus(username, courseId).mapToDomain() + val status = api.getCourseStatus(username, courseId).mapToDomain() + statusCache.setCached(courseId, status) + return status + } + + fun getCourseDatesFlow( + courseId: String, + forceRefresh: Boolean = false + ): Flow = flow { + // Always emit cached data first if available, otherwise emit empty result + val cached = datesCache.getCached(courseId) + emit(cached ?: emptyCourseDatesResult()) + + val shouldRefresh = forceRefresh || needsRefresh.contains(courseId) + if (networkConnection.isOnline() && (cached == null || shouldRefresh)) { + emit(datesCache.getOrFetch(courseId, forceRefresh = true)) + } + } + + suspend fun getCourseDates(courseId: String, forceRefresh: Boolean = false): CourseDatesResult { + return when { + !forceRefresh && datesCache.getCached(courseId) != null -> datesCache.getCached(courseId)!! + networkConnection.isOnline() -> datesCache.getOrFetch(courseId, forceRefresh = true) + else -> datesCache.getCached(courseId) ?: throw NoCachedDataException() + } + } + + private fun emptyCourseDatesResult() = CourseDatesResult( + datesSection = linkedMapOf(), + courseBanner = CourseDatesBannerInfo( + missedDeadlines = false, + missedGatedContent = false, + verifiedUpgradeLink = "", + contentTypeGatingEnabled = false, + hasEnded = false + ) + ) + + fun getCourseProgress( + courseId: String, + isRefresh: Boolean, + getOnlyCacheIfExist: Boolean + ): Flow = flow { + if (!isRefresh) { + progressCache.getCached(courseId)?.let { emit(it) } + } + + if (!isRefresh && progressCache.getCached(courseId) == null) { + courseDao.getCourseProgressById(courseId)?.mapToDomain()?.let { + progressCache.setCached(courseId, it) + emit(it) + } + } + + val shouldRefresh = isRefresh || needsRefresh.contains(courseId) + val hasCache = progressCache.getCached(courseId) != null + val shouldFetch = shouldRefresh || !hasCache || !getOnlyCacheIfExist + + if (networkConnection.isOnline() && shouldFetch) { + emit(progressCache.getOrFetch(courseId, forceRefresh = true)) + } } suspend fun markBlocksCompletion(courseId: String, blocksId: List) { @@ -158,38 +230,11 @@ class CourseRepository( val blocksCompletionBody = BlocksCompletionBody( username, courseId, - blocksId.associateWith { "1" }.toMap() + blocksId.associateWith { "1" } ) - return api.markBlocksCompletion(blocksCompletionBody) + api.markBlocksCompletion(blocksCompletionBody) } - suspend fun getCourseDatesFlow(courseId: String): Flow = - channelFlowWithAwait { - val localDates = courseDatesMap[courseId] - localDates?.let { trySend(it) } - - if (networkConnection.isOnline()) { - val datesResult = api.getCourseDates(courseId).getCourseDatesResult() - courseDatesMap[courseId] = datesResult - trySend(datesResult) - } else { - val datesResult = localDates ?: CourseDatesResult( - datesSection = linkedMapOf(), - courseBanner = CourseDatesBannerInfo( - missedDeadlines = false, - missedGatedContent = false, - verifiedUpgradeLink = "", - contentTypeGatingEnabled = false, - hasEnded = false - ) - ) - trySend(datesResult) - } - } - - suspend fun getCourseDates(courseId: String) = - api.getCourseDates(courseId).getCourseDatesResult() - suspend fun resetCourseDates(courseId: String) = api.resetCourseDates(mapOf(ApiConstants.COURSE_KEY to courseId)).mapToDomain() @@ -201,6 +246,16 @@ class CourseRepository( suspend fun getAnnouncements(courseId: String) = api.getAnnouncements(courseId).map { it.mapToDomain() } + suspend fun removeDownloadModel(id: String) { + downloadDao.removeDownloadModel(id) + } + + fun getDownloadModels() = downloadDao.getAllDataFlow().map { list -> + list.map { it.mapToDomain() } + } + + suspend fun getAllDownloadModels() = downloadDao.readAllData().map { it.mapToDomain() } + suspend fun saveOfflineXBlockProgress(blockId: String, courseId: String, jsonProgress: String) { val offlineXBlockProgress = OfflineXBlockProgress( blockId = blockId, @@ -256,24 +311,4 @@ class CourseRepository( return courseDao.getVideoProgressByBlockId(blockId) ?: VideoProgressEntity(blockId, "", null, null) } - - fun getCourseProgress( - courseId: String, - isRefresh: Boolean, - getOnlyCacheIfExist: Boolean // If true, only returns cached data if available, otherwise fetches from network - ): Flow = - channelFlowWithAwait { - var courseProgress: CourseProgressEntity? = null - if (!isRefresh) { - courseProgress = courseDao.getCourseProgressById(courseId) - if (courseProgress != null) { - trySend(courseProgress.mapToDomain()) - } - } - if (networkConnection.isOnline() && (!getOnlyCacheIfExist || courseProgress == null)) { - val response = api.getCourseProgress(courseId) - courseDao.insertCourseProgressEntity(response.mapToRoomEntity(courseId)) - trySend(response.mapToDomain()) - } - } } diff --git a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt index 86788f34c..04224a698 100644 --- a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt +++ b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt @@ -1,6 +1,7 @@ package org.openedx.course.domain.interactor import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import org.openedx.core.BlockType import org.openedx.core.domain.interactor.CourseInteractor import org.openedx.core.domain.model.Block @@ -13,9 +14,13 @@ class CourseInteractor( private val repository: CourseRepository ) : CourseInteractor { - suspend fun getCourseStructureFlow( + fun startCourseSession(courseId: String) { + repository.startCourseSession(courseId) + } + + fun getCourseStructureFlow( courseId: String, - forceRefresh: Boolean = true + forceRefresh: Boolean = false ): Flow { return repository.getCourseStructureFlow(courseId, forceRefresh) } @@ -24,15 +29,18 @@ class CourseInteractor( courseId: String, isNeedRefresh: Boolean ): CourseStructure { - return repository.getCourseStructure(courseId, isNeedRefresh) + return repository.getCourseStructureFlow(courseId, isNeedRefresh).first() } override suspend fun getCourseStructureFromCache(courseId: String): CourseStructure { return repository.getCourseStructureFromCache(courseId) } - suspend fun getEnrollmentDetailsFlow(courseId: String): Flow { - return repository.getEnrollmentDetailsFlow(courseId) + fun getEnrollmentDetailsFlow( + courseId: String, + forceRefresh: Boolean = false + ): Flow { + return repository.getEnrollmentDetailsFlow(courseId, forceRefresh) } suspend fun getEnrollmentDetails(courseId: String): CourseEnrollmentDetails { @@ -43,7 +51,7 @@ class CourseInteractor( courseId: String, isNeedRefresh: Boolean = false ): CourseStructure { - val courseStructure = repository.getCourseStructure(courseId, isNeedRefresh) + val courseStructure = getCourseStructure(courseId, isNeedRefresh) val blocks = courseStructure.blockData val videoBlocks = blocks.filter { it.type == BlockType.VIDEO } val resultBlocks = mutableListOf() @@ -87,13 +95,20 @@ class CourseInteractor( } } - suspend fun getCourseStatusFlow(courseId: String) = repository.getCourseStatusFlow(courseId) + fun getCourseStatusFlow( + courseId: String, + forceRefresh: Boolean = false + ) = repository.getCourseStatusFlow(courseId, forceRefresh) suspend fun getCourseStatus(courseId: String) = repository.getCourseStatus(courseId) - suspend fun getCourseDatesFlow(courseId: String) = repository.getCourseDatesFlow(courseId) + fun getCourseDatesFlow( + courseId: String, + forceRefresh: Boolean = false + ) = repository.getCourseDatesFlow(courseId, forceRefresh) - suspend fun getCourseDates(courseId: String) = repository.getCourseDates(courseId) + suspend fun getCourseDates(courseId: String, forceRefresh: Boolean = false) = + repository.getCourseDates(courseId, forceRefresh) suspend fun resetCourseDates(courseId: String) = repository.resetCourseDates(courseId) diff --git a/course/src/main/java/org/openedx/course/presentation/assignments/CourseAssignmentViewModel.kt b/course/src/main/java/org/openedx/course/presentation/assignments/CourseAssignmentViewModel.kt index 11da8d792..7bbc574ac 100644 --- a/course/src/main/java/org/openedx/course/presentation/assignments/CourseAssignmentViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/assignments/CourseAssignmentViewModel.kt @@ -39,7 +39,7 @@ class CourseAssignmentViewModel( private fun collectData() { viewModelScope.launch { val courseProgressFlow = interactor.getCourseProgress(courseId, false, true) - val courseStructureFlow = interactor.getCourseStructureFlow(courseId) + val courseStructureFlow = interactor.getCourseStructureFlow(courseId, false) combine( courseProgressFlow, diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index 80bbe2091..300b10bb3 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -143,13 +143,6 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { observe() } - override fun onResume() { - super.onResume() - if (viewModel.courseAccessStatus.value == CourseAccessError.NONE) { - viewModel.updateData() - } - } - override fun onDestroyView() { snackBar?.dismiss() super.onDestroyView() diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index 98501ae1e..3c20c7ef6 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -164,13 +164,18 @@ class CourseContainerViewModel( _showProgress.value = true + interactor.startCourseSession(courseId) + viewModelScope.launch { val courseStructureFlow = interactor.getCourseStructureFlow(courseId) .catch { e -> handleFetchError(e) emit(null) } - val courseDetailsFlow = interactor.getEnrollmentDetailsFlow(courseId) + val courseDetailsFlow = interactor.getEnrollmentDetailsFlow( + courseId, + forceRefresh = true + ) .catch { emit(null) } courseStructureFlow.combine(courseDetailsFlow) { courseStructure, courseEnrollmentDetails -> courseStructure to courseEnrollmentDetails diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index c059d1e73..f18b57f79 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -66,7 +66,7 @@ class CourseDatesViewModel( courseNotifier.notifier.collect { event -> when (event) { is RefreshDates -> { - loadingCourseDatesInternal() + loadingCourseDatesInternal(forceRefresh = true) } } } @@ -82,15 +82,23 @@ class CourseDatesViewModel( } } - loadingCourseDatesInternal() + loadingCourseDatesInternal(forceRefresh = false) } - private fun loadingCourseDatesInternal() { + private fun loadingCourseDatesInternal(forceRefresh: Boolean) { viewModelScope.launch { try { - courseStructure = interactor.getCourseStructure(courseId = courseId) + courseStructure = if (forceRefresh) { + interactor.getCourseStructure(courseId = courseId, isNeedRefresh = true) + } else { + interactor.getCourseStructure(courseId = courseId) + } isSelfPaced = courseStructure?.isSelfPaced ?: false - val datesResponse = interactor.getCourseDates(courseId = courseId) + val datesResponse = if (forceRefresh) { + interactor.getCourseDates(courseId = courseId, forceRefresh = true) + } else { + interactor.getCourseDates(courseId = courseId) + } if (datesResponse.datesSection.isEmpty()) { _uiState.value = CourseDatesUIState.Error } else { @@ -117,7 +125,7 @@ class CourseDatesViewModel( viewModelScope.launch { try { interactor.resetCourseDates(courseId = courseId) - loadingCourseDatesInternal() + loadingCourseDatesInternal(forceRefresh = true) courseNotifier.send(CourseDatesShifted) onResetDates(true) } catch (e: Exception) { diff --git a/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt b/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt index 5a3ac9fed..6c218619f 100644 --- a/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt @@ -165,8 +165,17 @@ class CourseHomeViewModel( private fun getCourseDataInternal() { viewModelScope.launch { + if (_uiState.value !is CourseHomeUIState.CourseData) { + _uiState.value = CourseHomeUIState.Loading + } + var hasReceivedData = false val courseStructureFlow = interactor.getCourseStructureFlow(courseId, false) - .catch { emit(null) } + .catch { e -> + if (!hasReceivedData) { + handleCourseDataError(e) + } + emit(null) + } val courseStatusFlow = interactor.getCourseStatusFlow(courseId) val courseDatesFlow = interactor.getCourseDatesFlow(courseId) val courseProgressFlow = interactor.getCourseProgress(courseId, false, true) @@ -177,6 +186,7 @@ class CourseHomeViewModel( courseProgressFlow ) { courseStructure, courseStatus, courseDatesResult, courseProgress -> if (courseStructure == null) return@combine + hasReceivedData = true val blocks = courseStructure.blockData initializeCourseData( diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt index 18e2901b6..a9d7ae7ee 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseContentAllViewModel.kt @@ -171,8 +171,17 @@ class CourseContentAllViewModel( private fun getCourseDataInternal() { viewModelScope.launch { + if (_uiState.value !is CourseContentAllUIState.CourseData) { + _uiState.value = CourseContentAllUIState.Loading + } + var hasReceivedData = false val courseStructureFlow = interactor.getCourseStructureFlow(courseId, false) - .catch { emit(null) } + .catch { e -> + if (!hasReceivedData) { + handleCourseDataError(e) + } + emit(null) + } val courseStatusFlow = interactor.getCourseStatusFlow(courseId) val courseDatesFlow = interactor.getCourseDatesFlow(courseId) combine( @@ -185,6 +194,7 @@ class CourseContentAllViewModel( handleCourseDataError(e) }.collect { (courseStructure, courseStatus, courseDates) -> if (courseStructure == null) return@collect + hasReceivedData = true val blocks = courseStructure.blockData checkIfCalendarOutOfDate(courseDates.datesSection.values.flatten()) diff --git a/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt b/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt index 395ad82f9..a83204a03 100644 --- a/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt @@ -35,7 +35,7 @@ class CourseProgressViewModel( private fun collectData(isRefresh: Boolean) { viewModelScope.launch { val courseProgressFlow = interactor.getCourseProgress(courseId, isRefresh, false) - val courseStructureFlow = interactor.getCourseStructureFlow(courseId) + val courseStructureFlow = interactor.getCourseStructureFlow(courseId, false) combine( courseProgressFlow, diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index ba23dc140..f2e5de6b6 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -5,6 +5,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.spyk import io.mockk.verify @@ -81,6 +82,7 @@ class CourseContainerViewModelTest { every { corePreferences.appConfig } returns CoreMocks.mockAppConfig every { courseNotifier.notifier } returns emptyFlow() every { config.getApiHostURL() } returns "baseUrl" + justRun { interactor.startCourseSession(any()) } coEvery { interactor.getEnrollmentDetails(any()) } returns CoreMocks.mockCourseEnrollmentDetails every { imageProcessor.loadImage(any(), any(), any()) } returns Unit every { imageProcessor.applyBlur(any(), any()) } returns mockBitmap @@ -114,7 +116,7 @@ class CourseContainerViewModelTest { interactor.getCourseStructureFlow(any(), any()) } returns flowOf(null) coEvery { - interactor.getEnrollmentDetailsFlow(any()) + interactor.getEnrollmentDetailsFlow(any(), any()) } returns flow { throw Exception() } every { analytics.logScreenEvent( @@ -131,7 +133,7 @@ class CourseContainerViewModelTest { viewModel.fetchCourseDetails() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrollmentDetailsFlow(any()) } + coVerify(exactly = 1) { interactor.getEnrollmentDetailsFlow(any(), any()) } verify(exactly = 1) { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, @@ -169,7 +171,7 @@ class CourseContainerViewModelTest { coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( CoreMocks.mockCourseStructure ) - coEvery { interactor.getEnrollmentDetailsFlow(any()) } returns flowOf( + coEvery { interactor.getEnrollmentDetailsFlow(any(), any()) } returns flowOf( CoreMocks.mockCourseEnrollmentDetails ) every { @@ -187,7 +189,7 @@ class CourseContainerViewModelTest { viewModel.fetchCourseDetails() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getEnrollmentDetailsFlow(any()) } + coVerify(exactly = 1) { interactor.getEnrollmentDetailsFlow(any(), any()) } verify(exactly = 1) { analytics.logScreenEvent( CourseAnalyticsEvent.DASHBOARD.eventName, @@ -226,7 +228,7 @@ class CourseContainerViewModelTest { coEvery { interactor.getCourseStructureFlow(any(), any()) } returns flowOf( CoreMocks.mockCourseStructure ) - coEvery { interactor.getEnrollmentDetailsFlow(any()) } returns flowOf( + coEvery { interactor.getEnrollmentDetailsFlow(any(), any()) } returns flowOf( CoreMocks.mockCourseEnrollmentDetails ) every { diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index f3ca889db..c80c6ce6a 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -112,7 +112,7 @@ class CourseDatesViewModelTest { calendarRouter, resourceManager, ) - coEvery { interactor.getCourseDates(any()) } throws UnknownHostException() + coEvery { interactor.getCourseDates(any(), any()) } throws UnknownHostException() val message = async { withTimeoutOrNull(5000) { viewModel.uiMessage.first() as? UIMessage.SnackBarMessage @@ -120,7 +120,7 @@ class CourseDatesViewModelTest { } advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseDates(any()) } + coVerify(exactly = 1) { interactor.getCourseDates(any(), any()) } Assert.assertEquals(noInternet, message.await()?.message) assert(viewModel.uiState.value is CourseDatesUIState.Error) @@ -142,7 +142,7 @@ class CourseDatesViewModelTest { calendarRouter, resourceManager, ) - coEvery { interactor.getCourseDates(any()) } throws Exception() + coEvery { interactor.getCourseDates(any(), any()) } throws Exception() val message = async { withTimeoutOrNull(5000) { viewModel.uiMessage.first() as? UIMessage.SnackBarMessage @@ -150,7 +150,7 @@ class CourseDatesViewModelTest { } advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseDates(any()) } + coVerify(exactly = 1) { interactor.getCourseDates(any(), any()) } assert(message.await()?.message.isNullOrEmpty()) assert(viewModel.uiState.value is CourseDatesUIState.Error) @@ -172,7 +172,12 @@ class CourseDatesViewModelTest { calendarRouter, resourceManager, ) - coEvery { interactor.getCourseDates(any()) } returns CourseMocks.courseDatesResultWithData + coEvery { + interactor.getCourseDates( + any(), + any() + ) + } returns CourseMocks.courseDatesResultWithData val message = async { withTimeoutOrNull(5000) { viewModel.uiMessage.first() as? UIMessage.SnackBarMessage @@ -180,7 +185,7 @@ class CourseDatesViewModelTest { } advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseDates(any()) } + coVerify(exactly = 1) { interactor.getCourseDates(any(), any()) } assert(message.await()?.message.isNullOrEmpty()) assert(viewModel.uiState.value is CourseDatesUIState.CourseDates) @@ -202,7 +207,7 @@ class CourseDatesViewModelTest { calendarRouter, resourceManager, ) - coEvery { interactor.getCourseDates(any()) } returns CourseDatesResult( + coEvery { interactor.getCourseDates(any(), any()) } returns CourseDatesResult( datesSection = linkedMapOf(), courseBanner = CoreMocks.mockCourseDatesBannerInfo, ) @@ -213,7 +218,7 @@ class CourseDatesViewModelTest { } advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseDates(any()) } + coVerify(exactly = 1) { interactor.getCourseDates(any(), any()) } assert(message.await()?.message.isNullOrEmpty()) assert(viewModel.uiState.value is CourseDatesUIState.Error) diff --git a/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt index 0b661dece..b65977052 100644 --- a/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/home/CourseHomeViewModelTest.kt @@ -137,12 +137,17 @@ class CourseHomeViewModelTest { ) ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -175,8 +180,8 @@ class CourseHomeViewModelTest { advanceUntilIdle() coVerify { interactor.getCourseStructureFlow(courseId, false) } - coVerify { interactor.getCourseStatusFlow(courseId) } - coVerify { interactor.getCourseDatesFlow(courseId) } + coVerify { interactor.getCourseStatusFlow(courseId, any()) } + coVerify { interactor.getCourseDatesFlow(courseId, any()) } coVerify { interactor.getCourseProgress(courseId, false, true) } assertTrue(viewModel.uiState.value is CourseHomeUIState.CourseData) @@ -194,12 +199,17 @@ class CourseHomeViewModelTest { false ) } returns flow { throw UnknownHostException() } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -242,12 +252,17 @@ class CourseHomeViewModelTest { false ) } returns flow { throw Exception() } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -291,12 +306,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -334,47 +354,8 @@ class CourseHomeViewModelTest { @Test fun `logVideoClick analytics event`() = runTest { - coEvery { interactor.getCourseStructureFlow(courseId, false) } returns flow { - emit( - CoreMocks.mockCourseStructure.copy( - id = courseId, - name = courseTitle - ) - ) - } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { - emit( - CoreMocks.mockCourseComponentStatus - ) - } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } - coEvery { - interactor.getCourseProgress( - courseId, - false, - true - ) - } returns flow { emit(CoreMocks.mockCourseProgress) } - - val viewModel = CourseHomeViewModel( - courseId = courseId, - courseTitle = courseTitle, - config = config, - interactor = interactor, - resourceManager = resourceManager, - courseNotifier = courseNotifier, - networkConnection = networkConnection, - preferencesManager = preferencesManager, - analytics = analytics, - downloadDialogManager = downloadDialogManager, - fileUtil = fileUtil, - courseRouter = courseRouter, - videoPreviewHelper = videoPreviewHelper, - coreAnalytics = coreAnalytics, - downloadDao = downloadDao, - workerController = workerController, - downloadHelper = downloadHelper - ) + stubCourseDataFlows() + val viewModel = createViewModel() advanceUntilIdle() @@ -395,47 +376,8 @@ class CourseHomeViewModelTest { @Test fun `logAssignmentClick analytics event`() = runTest { - coEvery { interactor.getCourseStructureFlow(courseId, false) } returns flow { - emit( - CoreMocks.mockCourseStructure.copy( - id = courseId, - name = courseTitle - ) - ) - } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { - emit( - CoreMocks.mockCourseComponentStatus - ) - } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } - coEvery { - interactor.getCourseProgress( - courseId, - false, - true - ) - } returns flow { emit(CoreMocks.mockCourseProgress) } - - val viewModel = CourseHomeViewModel( - courseId = courseId, - courseTitle = courseTitle, - config = config, - interactor = interactor, - resourceManager = resourceManager, - courseNotifier = courseNotifier, - networkConnection = networkConnection, - preferencesManager = preferencesManager, - analytics = analytics, - downloadDialogManager = downloadDialogManager, - fileUtil = fileUtil, - courseRouter = courseRouter, - videoPreviewHelper = videoPreviewHelper, - coreAnalytics = coreAnalytics, - downloadDao = downloadDao, - workerController = workerController, - downloadHelper = downloadHelper - ) + stubCourseDataFlows() + val viewModel = createViewModel() advanceUntilIdle() @@ -461,12 +403,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -517,12 +464,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -565,12 +517,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -613,12 +570,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -659,12 +621,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -709,12 +676,17 @@ class CourseHomeViewModelTest { CoreMocks.mockCourseStructure ) } - coEvery { interactor.getCourseStatusFlow(courseId) } returns flow { + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { emit( CoreMocks.mockCourseComponentStatus ) } - coEvery { interactor.getCourseDatesFlow(courseId) } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } coEvery { interactor.getCourseProgress( courseId, @@ -745,4 +717,55 @@ class CourseHomeViewModelTest { assertTrue(viewModel.isCourseDropdownNavigationEnabled) } + + private fun stubCourseDataFlows() { + coEvery { interactor.getCourseStructureFlow(courseId, false) } returns flow { + emit( + CoreMocks.mockCourseStructure.copy( + id = courseId, + name = courseTitle + ) + ) + } + coEvery { interactor.getCourseStatusFlow(courseId, any()) } returns flow { + emit( + CoreMocks.mockCourseComponentStatus + ) + } + coEvery { + interactor.getCourseDatesFlow( + courseId, + any() + ) + } returns flow { emit(CoreMocks.mockCourseDatesResult) } + coEvery { + interactor.getCourseProgress( + courseId, + false, + true + ) + } returns flow { emit(CoreMocks.mockCourseProgress) } + } + + private fun createViewModel(): CourseHomeViewModel { + return CourseHomeViewModel( + courseId = courseId, + courseTitle = courseTitle, + config = config, + interactor = interactor, + resourceManager = resourceManager, + courseNotifier = courseNotifier, + networkConnection = networkConnection, + preferencesManager = preferencesManager, + analytics = analytics, + downloadDialogManager = downloadDialogManager, + fileUtil = fileUtil, + courseRouter = courseRouter, + videoPreviewHelper = videoPreviewHelper, + coreAnalytics = coreAnalytics, + downloadDao = downloadDao, + workerController = workerController, + downloadHelper = downloadHelper + ) + } } diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 33ae2dcb9..89f8fec0a 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -94,8 +94,13 @@ class CourseOutlineViewModelTest { every { downloadDialogManager.showDownloadFailedPopup(any(), any()) } returns Unit every { preferencesManager.isRelativeDatesEnabled } returns true - coEvery { interactor.getCourseDates(any()) } returns CoreMocks.mockCourseDatesResult - coEvery { interactor.getCourseDatesFlow(any()) } returns flowOf(CoreMocks.mockCourseDatesResult) + coEvery { interactor.getCourseDates(any(), any()) } returns CoreMocks.mockCourseDatesResult + coEvery { + interactor.getCourseDatesFlow( + any(), + any() + ) + } returns flowOf(CoreMocks.mockCourseDatesResult) } @After @@ -124,7 +129,12 @@ class CourseOutlineViewModelTest { any(), ) } returns Unit - coEvery { interactor.getCourseStatusFlow(any()) } returns flow { throw UnknownHostException() } + coEvery { + interactor.getCourseStatusFlow( + any(), + any() + ) + } returns flow { throw UnknownHostException() } val viewModel = CourseContentAllViewModel( "", @@ -152,7 +162,7 @@ class CourseOutlineViewModelTest { advanceUntilIdle() coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } - coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any(), any()) } assertEquals(noInternet, message.await()?.message) assert(viewModel.uiState.value is CourseContentAllUIState.Error) @@ -166,7 +176,7 @@ class CourseOutlineViewModelTest { ) every { networkConnection.isOnline() } returns true every { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } - coEvery { interactor.getCourseStatusFlow(any()) } returns flow { throw Exception() } + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flow { throw Exception() } val viewModel = CourseContentAllViewModel( "", "", @@ -193,7 +203,7 @@ class CourseOutlineViewModelTest { advanceUntilIdle() coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } - coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any(), any()) } assertEquals(somethingWrong, message.await()?.message) assert(viewModel.uiState.value is CourseContentAllUIState.Error) @@ -215,7 +225,9 @@ class CourseOutlineViewModelTest { ) ) } - coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flowOf( + CourseComponentStatus("id") + ) every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseContentAllViewModel( @@ -247,7 +259,7 @@ class CourseOutlineViewModelTest { advanceUntilIdle() coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } - coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any(), any()) } assert(message.await() == null) assert(viewModel.uiState.value is CourseContentAllUIState.CourseData) @@ -269,7 +281,9 @@ class CourseOutlineViewModelTest { ) ) } - coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flowOf( + CourseComponentStatus("id") + ) every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseContentAllViewModel( @@ -300,7 +314,7 @@ class CourseOutlineViewModelTest { advanceUntilIdle() coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } - coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any(), any()) } assert(message.await() == null) assert(viewModel.uiState.value is CourseContentAllUIState.CourseData) @@ -322,7 +336,9 @@ class CourseOutlineViewModelTest { ) ) } - coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flowOf( + CourseComponentStatus("id") + ) every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false val viewModel = CourseContentAllViewModel( @@ -353,7 +369,7 @@ class CourseOutlineViewModelTest { advanceUntilIdle() coVerify(exactly = 2) { interactor.getCourseStructureFlow(any(), any()) } - coVerify(exactly = 2) { interactor.getCourseStatusFlow(any()) } + coVerify(exactly = 2) { interactor.getCourseStatusFlow(any(), any()) } assert(message.await() == null) assert(viewModel.uiState.value is CourseContentAllUIState.CourseData) @@ -367,7 +383,9 @@ class CourseOutlineViewModelTest { ) coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("")) } every { networkConnection.isOnline() } returns true - coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flowOf( + CourseComponentStatus("id") + ) val viewModel = CourseContentAllViewModel( "", @@ -397,7 +415,7 @@ class CourseOutlineViewModelTest { advanceUntilIdle() coVerify(exactly = 3) { interactor.getCourseStructureFlow(any(), any()) } - coVerify(exactly = 3) { interactor.getCourseStatusFlow(any()) } + coVerify(exactly = 3) { interactor.getCourseStatusFlow(any(), any()) } } @Test @@ -417,7 +435,9 @@ class CourseOutlineViewModelTest { } returns Unit coEvery { workerController.saveModels(any()) } returns Unit coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flowOf( + CourseComponentStatus("id") + ) coEvery { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false @@ -464,7 +484,9 @@ class CourseOutlineViewModelTest { CoreMocks.mockCourseStructure ) coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") - coEvery { interactor.getCourseStatusFlow(any()) } returns flowOf(CourseComponentStatus("id")) + coEvery { interactor.getCourseStatusFlow(any(), any()) } returns flowOf( + CourseComponentStatus("id") + ) every { preferencesManager.videoSettings.wifiDownloadOnly } returns true every { networkConnection.isWifiConnected() } returns true every { networkConnection.isOnline() } returns true