From f5e956de15a9f98239f3ee1e33c15ee9d1216509 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 15 Jan 2026 16:11:35 +0200 Subject: [PATCH] fix: removed requests duplication --- .../data/repository/CourseRepository.kt | 407 ++++++++++++------ .../domain/interactor/CourseInteractor.kt | 33 +- .../assignments/CourseAssignmentViewModel.kt | 2 +- .../container/CourseContainerFragment.kt | 7 - .../container/CourseContainerViewModel.kt | 7 +- .../dates/CourseDatesViewModel.kt | 20 +- .../progress/CourseProgressViewModel.kt | 2 +- .../container/CourseContainerViewModelTest.kt | 12 +- .../dates/CourseDatesViewModelTest.kt | 21 +- .../home/CourseHomeViewModelTest.kt | 249 ++++++----- .../outline/CourseOutlineViewModelTest.kt | 54 ++- 11 files changed, 516 insertions(+), 298 deletions(-) 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..5faa89abe 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,13 @@ package org.openedx.course.data.repository +import kotlinx.coroutines.CompletableDeferred 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 +20,19 @@ 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. + * This is achieved using CompletableDeferred. + */ @Suppress("TooManyFunctions") class CourseRepository( private val api: CourseApi, @@ -33,66 +41,64 @@ class CourseRepository( private val preferencesManager: CorePreferences, private val networkConnection: NetworkConnection, ) { - private val courseStructure = mutableMapOf() - - private val courseStatusMap = mutableMapOf() - private val courseDatesMap = mutableMapOf() - - suspend fun removeDownloadModel(id: String) { - downloadDao.removeDownloadModel(id) + private val structureCache = ConcurrentHashMap() + private val statusCache = ConcurrentHashMap() + private val datesCache = ConcurrentHashMap() + private val progressCache = ConcurrentHashMap() + private val enrollmentCache = ConcurrentHashMap() + + // Pending requests - when a request is in progress, other callers wait for it + private val pendingStructure = ConcurrentHashMap>() + private val pendingStatus = + ConcurrentHashMap>() + private val pendingDates = ConcurrentHashMap>() + private val pendingProgress = ConcurrentHashMap>() + private val pendingEnrollment = + ConcurrentHashMap>() + + // Session tracking - when entering a course, mark that data needs refresh + private val needsRefresh = ConcurrentHashMap.newKeySet() + + /** + * Call when entering a course to mark that data should be refreshed. + */ + fun startCourseSession(courseId: String) { + needsRefresh.add(courseId) } - fun getDownloadModels() = downloadDao.getAllDataFlow().map { list -> - list.map { it.mapToDomain() } - } - - 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 { + if (!forceRefresh) { + structureCache[courseId]?.let { emit(it) } + + if (structureCache[courseId] == null) { + courseDao.getCourseStructureById(courseId)?.mapToDomain()?.let { + structureCache[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[courseId] == null || shouldRefresh)) { + emit(fetchOrAwaitStructure(courseId)) + } + + if (structureCache[courseId] == null) { throw NoCachedDataException() } } - suspend fun getCourseStructure(courseId: String, isNeedRefresh: Boolean): CourseStructure { - if (!isNeedRefresh) courseStructure[courseId]?.let { return it } + private suspend fun fetchOrAwaitStructure(courseId: String): CourseStructure { + pendingStructure[courseId]?.let { return it.await() } - if (networkConnection.isOnline()) { + val deferred = CompletableDeferred() + val existing = pendingStructure.putIfAbsent(courseId, deferred) + if (existing != null) return existing.await() + + return try { val response = api.getCourseStructure( "stale-if-error=0", "v4", @@ -100,57 +106,223 @@ class CourseRepository( 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() - } + val structure = response.mapToDomain() + structureCache[courseId] = structure + needsRefresh.remove(courseId) + deferred.complete(structure) + structure + } catch (e: Exception) { + deferred.completeExceptionally(e) + throw e + } finally { + pendingStructure.remove(courseId) } + } - return courseStructure[courseId]!! + suspend fun getCourseStructureFromCache(courseId: String): CourseStructure { + return structureCache[courseId] + ?: courseDao.getCourseStructureById(courseId)?.mapToDomain()?.also { + structureCache[courseId] = it + } + ?: throw NoCachedDataException() } - suspend fun getEnrollmentDetailsFlow(courseId: String): Flow = - channelFlowWithAwait { - getCourseEnrollmentDetailsFromCache(courseId)?.let { - trySend(it) + fun getEnrollmentDetailsFlow( + courseId: String, + forceRefresh: Boolean = false + ): Flow = flow { + if (!forceRefresh) { + enrollmentCache[courseId]?.let { emit(it) } + + if (enrollmentCache[courseId] == null) { + courseDao.getCourseEnrollmentDetailsById(courseId)?.mapToDomain()?.let { + enrollmentCache[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[courseId] == null || forceRefresh)) { + emit(fetchOrAwaitEnrollment(courseId)) + } + + if (enrollmentCache[courseId] == null) { + throw NoCachedDataException() + } + } + + private suspend fun fetchOrAwaitEnrollment(courseId: String): CourseEnrollmentDetails { + pendingEnrollment[courseId]?.let { return it.await() } + + val deferred = CompletableDeferred() + val existing = pendingEnrollment.putIfAbsent(courseId, deferred) + if (existing != null) return existing.await() + + return try { + val details = api.getEnrollmentDetails(courseId).mapToDomain() + courseDao.insertCourseEnrollmentDetailsEntity(details.mapToEntity()) + enrollmentCache[courseId] = details + deferred.complete(details) + details + } catch (e: Exception) { + deferred.completeExceptionally(e) + throw e + } finally { + pendingEnrollment.remove(courseId) + } } 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 { + if (!forceRefresh) { + statusCache[courseId]?.let { emit(it) } + } + + val shouldRefresh = forceRefresh || needsRefresh.contains(courseId) + if (networkConnection.isOnline() && (statusCache[courseId] == null || shouldRefresh)) { + emit(fetchOrAwaitStatus(courseId)) + } else if (statusCache[courseId] == null) { + emit(CourseComponentStatus("")) + } + } + + private suspend fun fetchOrAwaitStatus(courseId: String): CourseComponentStatus { + pendingStatus[courseId]?.let { return it.await() } + + val deferred = CompletableDeferred() + val existing = pendingStatus.putIfAbsent(courseId, deferred) + if (existing != null) return existing.await() + + return try { + val username = preferencesManager.user?.username ?: "" + val status = api.getCourseStatus(username, courseId).mapToDomain() + statusCache[courseId] = status + deferred.complete(status) + status + } catch (e: Exception) { + deferred.completeExceptionally(e) + throw e + } finally { + pendingStatus.remove(courseId) } + } 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[courseId] = status + return status + } + + fun getCourseDatesFlow( + courseId: String, + forceRefresh: Boolean = false + ): Flow = flow { + if (!forceRefresh) { + datesCache[courseId]?.let { emit(it) } + } + + val shouldRefresh = forceRefresh || needsRefresh.contains(courseId) + if (networkConnection.isOnline() && (datesCache[courseId] == null || shouldRefresh)) { + emit(fetchOrAwaitDates(courseId)) + } else if (datesCache[courseId] == null) { + emit(emptyCourseDatesResult()) + } + } + + private suspend fun fetchOrAwaitDates(courseId: String): CourseDatesResult { + pendingDates[courseId]?.let { return it.await() } + + val deferred = CompletableDeferred() + val existing = pendingDates.putIfAbsent(courseId, deferred) + if (existing != null) return existing.await() + + return try { + val datesResult = api.getCourseDates(courseId).getCourseDatesResult() + datesCache[courseId] = datesResult + deferred.complete(datesResult) + datesResult + } catch (e: Exception) { + deferred.completeExceptionally(e) + throw e + } finally { + pendingDates.remove(courseId) + } + } + + suspend fun getCourseDates(courseId: String, forceRefresh: Boolean = false): CourseDatesResult { + if (!forceRefresh) { + datesCache[courseId]?.let { return it } + } + if (networkConnection.isOnline()) { + return fetchOrAwaitDates(courseId) + } + datesCache[courseId]?.let { return it } + 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[courseId]?.let { emit(it) } + } + + if (!isRefresh && progressCache[courseId] == null) { + courseDao.getCourseProgressById(courseId)?.mapToDomain()?.let { + progressCache[courseId] = it + emit(it) + } + } + + val shouldRefresh = isRefresh || needsRefresh.contains(courseId) + val hasCache = progressCache[courseId] != null + val shouldFetch = shouldRefresh || !hasCache || !getOnlyCacheIfExist + + if (networkConnection.isOnline() && shouldFetch) { + emit(fetchOrAwaitProgress(courseId)) + } + } + + private suspend fun fetchOrAwaitProgress(courseId: String): CourseProgress { + pendingProgress[courseId]?.let { return it.await() } + + val deferred = CompletableDeferred() + val existing = pendingProgress.putIfAbsent(courseId, deferred) + if (existing != null) return existing.await() + + return try { + val response = api.getCourseProgress(courseId) + courseDao.insertCourseProgressEntity(response.mapToRoomEntity(courseId)) + val progress = response.mapToDomain() + progressCache[courseId] = progress + deferred.complete(progress) + progress + } catch (e: Exception) { + deferred.completeExceptionally(e) + throw e + } finally { + pendingProgress.remove(courseId) + } } suspend fun markBlocksCompletion(courseId: String, blocksId: List) { @@ -158,38 +330,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 +346,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 +411,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 57e0a3be4..d9fba0523 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/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 a150639c3..16695f5c3 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 @@ -139,12 +139,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, @@ -177,8 +182,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) @@ -196,12 +201,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, @@ -244,12 +254,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, @@ -293,12 +308,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, @@ -342,12 +362,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, @@ -399,12 +424,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, @@ -450,47 +480,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() @@ -511,47 +502,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() @@ -577,12 +529,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, @@ -633,12 +590,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, @@ -681,12 +643,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, @@ -729,12 +696,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, @@ -775,12 +747,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, @@ -825,12 +802,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, @@ -861,4 +843,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