From 498f0ef64cfeb8b88c62817f465d4dea19662ef4 Mon Sep 17 00:00:00 2001 From: droid Date: Mon, 15 Apr 2024 16:54:02 +0200 Subject: [PATCH] fix: crash when restoring the app after a long period of inactivity --- .../java/org/openedx/app/di/ScreenModule.kt | 25 +++- .../core/system/notifier/CourseDataReady.kt | 5 - .../core/system/notifier/CourseNotifier.kt | 1 - .../data/repository/CourseRepository.kt | 53 +++---- .../domain/interactor/CourseInteractor.kt | 22 +-- .../container/CourseContainerFragment.kt | 33 +++-- .../container/CourseContainerViewModel.kt | 11 +- .../dates/CourseDatesViewModel.kt | 31 ++-- .../outline/CourseOutlineViewModel.kt | 10 +- .../section/CourseSectionViewModel.kt | 4 +- .../container/CourseUnitContainerFragment.kt | 3 +- .../container/CourseUnitContainerViewModel.kt | 35 +++-- .../videos/CourseVideoViewModel.kt | 15 +- .../container/CourseContainerViewModelTest.kt | 62 +++++--- .../dates/CourseDatesViewModelTest.kt | 19 ++- .../outline/CourseOutlineViewModelTest.kt | 39 ++--- .../section/CourseSectionViewModelTest.kt | 31 ++-- .../CourseUnitContainerViewModelTest.kt | 133 +++++++++--------- .../videos/CourseVideoViewModelTest.kt | 26 ++-- .../topics/DiscussionTopicsScreen.kt | 2 +- .../topics/DiscussionTopicsViewModel.kt | 20 +-- .../topics/DiscussionTopicsViewModelTest.kt | 15 +- 22 files changed, 319 insertions(+), 276 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/system/notifier/CourseDataReady.kt diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 4efd1a19e..c9c395a01 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -148,10 +148,23 @@ val screenModule = module { viewModel { (qualityType: String) -> VideoQualityViewModel(qualityType, get(), get(), get()) } viewModel { DeleteProfileViewModel(get(), get(), get(), get(), get()) } viewModel { (username: String) -> AnothersProfileViewModel(get(), get(), username) } - viewModel { SettingsViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { + SettingsViewModel( + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() + ) + } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - single { CourseRepository(get(), get(), get(), get()) } + single { CourseRepository(get(), get(), get(), get(), get()) } factory { CourseInteractor(get()) } viewModel { (pathId: String, infoType: String) -> CourseInfoViewModel( @@ -279,8 +292,10 @@ val screenModule = module { get(), ) } - viewModel { (enrollmentMode: String) -> + viewModel { (courseId: String, courseTitle: String, enrollmentMode: String) -> CourseDatesViewModel( + courseId, + courseTitle, enrollmentMode, get(), get(), @@ -305,8 +320,10 @@ val screenModule = module { single { DiscussionRepository(get(), get(), get()) } factory { DiscussionInteractor(get()) } - viewModel { + viewModel { (courseId: String, courseTitle: String) -> DiscussionTopicsViewModel( + courseId, + courseTitle, get(), get(), get(), diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseDataReady.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseDataReady.kt deleted file mode 100644 index 0ad123d17..000000000 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseDataReady.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.openedx.core.system.notifier - -import org.openedx.core.domain.model.CourseStructure - -data class CourseDataReady(val courseStructure: CourseStructure) : CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt index 63660b4de..f4908bdef 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseNotifier.kt @@ -18,6 +18,5 @@ class CourseNotifier { suspend fun send(event: CalendarSyncEvent) = channel.emit(event) suspend fun send(event: CourseDatesShifted) = channel.emit(event) suspend fun send(event: CourseLoading) = channel.emit(event) - suspend fun send(event: CourseDataReady) = channel.emit(event) suspend fun send(event: CourseRefresh) = channel.emit(event) } 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 17dc6a240..c32397a48 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 @@ -5,9 +5,11 @@ import org.openedx.core.ApiConstants import org.openedx.core.data.api.CourseApi import org.openedx.core.data.model.BlocksCompletionBody import org.openedx.core.data.storage.CorePreferences -import org.openedx.core.domain.model.* +import org.openedx.core.domain.model.CourseComponentStatus +import org.openedx.core.domain.model.CourseStructure import org.openedx.core.exception.NoCachedDataException import org.openedx.core.module.db.DownloadDao +import org.openedx.core.system.connection.NetworkConnection import org.openedx.course.data.storage.CourseDao class CourseRepository( @@ -15,8 +17,9 @@ class CourseRepository( private val courseDao: CourseDao, private val downloadDao: DownloadDao, private val preferencesManager: CorePreferences, + private val networkConnection: NetworkConnection, ) { - private var courseStructure: CourseStructure? = null + private var courseStructure = mutableMapOf() suspend fun removeDownloadModel(id: String) { downloadDao.removeDownloadModel(id) @@ -26,35 +29,33 @@ class CourseRepository( list.map { it.mapToDomain() } } - suspend fun preloadCourseStructure(courseId: String) { - val response = api.getCourseStructure( - "stale-if-error=0", - "v3", - preferencesManager.user?.username, - courseId - ) - courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) - courseStructure = null - courseStructure = response.mapToDomain() + fun hasCourses(courseId: String): Boolean { + return courseStructure[courseId] != null } - suspend fun preloadCourseStructureFromCache(courseId: String) { - val cachedCourseStructure = courseDao.getCourseStructureById(courseId) - courseStructure = null - if (cachedCourseStructure != null) { - courseStructure = cachedCourseStructure.mapToDomain() - } else { - 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", + "v3", + preferencesManager.user?.username, + courseId + ) + courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) + courseStructure[courseId] = response.mapToDomain() - @Throws(IllegalStateException::class) - fun getCourseStructureFromCache(): CourseStructure { - if (courseStructure != null) { - return courseStructure!! } else { - throw IllegalStateException("Course structure is empty") + val cachedCourseStructure = courseDao.getCourseStructureById(courseId) + if (cachedCourseStructure != null) { + courseStructure[courseId] = cachedCourseStructure.mapToDomain() + } else { + throw NoCachedDataException() + } } + + return courseStructure[courseId]!! } suspend fun getCourseStatus(courseId: String): CourseComponentStatus { 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 6c8bd1009..5bc859120 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 @@ -9,18 +9,18 @@ class CourseInteractor( private val repository: CourseRepository ) { - suspend fun preloadCourseStructure(courseId: String) = - repository.preloadCourseStructure(courseId) - - suspend fun preloadCourseStructureFromCache(courseId: String) = - repository.preloadCourseStructureFromCache(courseId) - - @Throws(IllegalStateException::class) - fun getCourseStructureFromCache() = repository.getCourseStructureFromCache() + suspend fun getCourseStructure( + courseId: String, + isNeedRefresh: Boolean = false + ): CourseStructure { + return repository.getCourseStructure(courseId, isNeedRefresh) + } - @Throws(IllegalStateException::class) - fun getCourseStructureForVideos(): CourseStructure { - val courseStructure = repository.getCourseStructureFromCache() + suspend fun getCourseStructureForVideos( + courseId: String, + isNeedRefresh: Boolean = false + ): CourseStructure { + val courseStructure = repository.getCourseStructure(courseId, isNeedRefresh) val blocks = courseStructure.blockData val videoBlocks = blocks.filter { it.type == BlockType.VIDEO } val resultBlocks = ArrayList() 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 c44733948..669b1f661 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 @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -294,6 +295,8 @@ fun CourseDashboard( val refreshing by viewModel.refreshing.collectAsState(true) val courseImage by viewModel.courseImage.collectAsState() val uiMessage by viewModel.uiMessage.collectAsState(null) + val dataReady = viewModel.dataReady.observeAsState() + val pagerState = rememberPagerState(pageCount = { CourseContainerTab.entries.size }) val tabState = rememberLazyListState() val snackState = remember { SnackbarHostState() } @@ -351,15 +354,17 @@ fun CourseDashboard( fragmentManager.popBackStack() }, bodyContent = { - DashboardPager( - windowSize = windowSize, - viewModel = viewModel, - pagerState = pagerState, - isNavigationEnabled = isNavigationEnabled, - isResumed = isResumed, - fragmentManager = fragmentManager, - bundle = bundle - ) + if (dataReady.value == true) { + DashboardPager( + windowSize = windowSize, + viewModel = viewModel, + pagerState = pagerState, + isNavigationEnabled = isNavigationEnabled, + isResumed = isResumed, + fragmentManager = fragmentManager, + bundle = bundle + ) + } } ) PullRefreshIndicator( @@ -462,6 +467,8 @@ fun DashboardPager( courseDatesViewModel = koinViewModel( parameters = { parametersOf( + bundle.getString(CourseContainerFragment.ARG_COURSE_ID, ""), + bundle.getString(CourseContainerFragment.ARG_TITLE, ""), bundle.getString(CourseContainerFragment.ARG_ENROLLMENT_MODE, "") ) } @@ -478,6 +485,14 @@ fun DashboardPager( CourseContainerTab.DISCUSSIONS -> { DiscussionTopicsScreen( + discussionTopicsViewModel = koinViewModel( + parameters = { + parametersOf( + bundle.getString(CourseContainerFragment.ARG_COURSE_ID, ""), + bundle.getString(CourseContainerFragment.ARG_TITLE, ""), + ) + } + ), windowSize = windowSize, fragmentManager = fragmentManager ) 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 c61d7e165..8562289af 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 @@ -31,7 +31,6 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseCompletionSet -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier @@ -169,12 +168,7 @@ class CourseContainerViewModel( _showProgress.value = true viewModelScope.launch { try { - if (networkConnection.isOnline()) { - interactor.preloadCourseStructure(courseId) - } else { - interactor.preloadCourseStructureFromCache(courseId) - } - val courseStructure = interactor.getCourseStructureFromCache() + val courseStructure = interactor.getCourseStructure(courseId) courseName = courseStructure.name _organization = courseStructure.org _isSelfPaced = courseStructure.isSelfPaced @@ -183,7 +177,6 @@ class CourseContainerViewModel( val isReady = start < Date() if (isReady) { _isNavigationEnabled.value = true - courseNotifier.send(CourseDataReady(courseStructure)) } isReady } @@ -248,7 +241,7 @@ class CourseContainerViewModel( fun updateData() { viewModelScope.launch { try { - interactor.preloadCourseStructure(courseId) + interactor.getCourseStructure(courseId, isNeedRefresh = true) } catch (e: Exception) { if (e.isInternetError()) { _errorMessage.value = 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 79f866ba7..e5ce08ed7 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 @@ -19,6 +19,7 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseBannerType import org.openedx.core.domain.model.CourseDateBlock +import org.openedx.core.domain.model.CourseStructure import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks import org.openedx.core.extension.isInternetError @@ -26,7 +27,6 @@ import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier @@ -41,6 +41,8 @@ import org.openedx.course.presentation.calendarsync.CalendarSyncUIState import org.openedx.core.R as CoreR class CourseDatesViewModel( + val courseId: String, + courseTitle: String, private val enrollmentMode: String, private val courseNotifier: CourseNotifier, private val interactor: CourseInteractor, @@ -51,8 +53,6 @@ class CourseDatesViewModel( private val config: Config, ) : BaseViewModel() { - var courseId = "" - var courseName = "" var isSelfPaced = true private val _uiState = MutableLiveData(DatesUIState.Loading) @@ -66,7 +66,7 @@ class CourseDatesViewModel( private val _calendarSyncUIState = MutableStateFlow( CalendarSyncUIState( isCalendarSyncEnabled = isCalendarSyncEnabled(), - calendarTitle = calendarManager.getCourseCalendarTitle(courseName), + calendarTitle = calendarManager.getCourseCalendarTitle(courseTitle), isSynced = false, ) ) @@ -74,6 +74,7 @@ class CourseDatesViewModel( _calendarSyncUIState.asStateFlow() private var courseBannerType: CourseBannerType = CourseBannerType.BLANK + private var courseStructure: CourseStructure? = null val isCourseExpandableSectionsEnabled get() = config.isCourseNestedListEnabled() @@ -90,22 +91,19 @@ class CourseDatesViewModel( loadingCourseDatesInternal() } } - - is CourseDataReady -> { - courseId = event.courseStructure.id - courseName = event.courseStructure.name - isSelfPaced = event.courseStructure.isSelfPaced - loadingCourseDatesInternal() - updateAndFetchCalendarSyncState() - } } } } + + loadingCourseDatesInternal() + updateAndFetchCalendarSyncState() } private fun loadingCourseDatesInternal() { viewModelScope.launch { try { + courseStructure = interactor.getCourseStructure(courseId = courseId) + isSelfPaced = courseStructure?.isSelfPaced ?: false val datesResponse = interactor.getCourseDates(courseId = courseId) if (datesResponse.datesSection.isEmpty()) { _uiState.value = DatesUIState.Empty @@ -146,8 +144,8 @@ class CourseDatesViewModel( fun getVerticalBlock(blockId: String): Block? { return try { - val courseStructure = interactor.getCourseStructureFromCache() - courseStructure.blockData.getVerticalBlocks().find { it.descendants.contains(blockId) } + courseStructure?.blockData?.getVerticalBlocks() + ?.find { it.descendants.contains(blockId) } } catch (e: Exception) { null } @@ -155,9 +153,8 @@ class CourseDatesViewModel( fun getSequentialBlock(blockId: String): Block? { return try { - val courseStructure = interactor.getCourseStructureFromCache() - courseStructure.blockData.getSequentialBlocks() - .find { it.descendants.contains(blockId) } + courseStructure?.blockData?.getSequentialBlocks() + ?.find { it.descendants.contains(blockId) } } catch (e: Exception) { null } diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index 569498ab6..7a6e08b58 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -28,7 +28,6 @@ import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier @@ -84,15 +83,12 @@ class CourseOutlineViewModel( init { viewModelScope.launch { courseNotifier.notifier.collect { event -> - when(event) { + when (event) { is CourseStructureUpdated -> { if (event.courseId == courseId) { updateCourseData() } } - is CourseDataReady -> { - getCourseData() - } } } } @@ -113,6 +109,8 @@ class CourseOutlineViewModel( } } } + + getCourseData() } override fun saveDownloadModels(folder: String, id: String) { @@ -166,7 +164,7 @@ class CourseOutlineViewModel( private fun getCourseDataInternal() { viewModelScope.launch { try { - var courseStructure = interactor.getCourseStructureFromCache() + var courseStructure = interactor.getCourseStructure(courseId) val blocks = courseStructure.blockData val courseStatus = if (networkConnection.isOnline()) { diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt index 97f241650..33870c69c 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt @@ -89,8 +89,8 @@ class CourseSectionViewModel( viewModelScope.launch { try { val courseStructure = when (mode) { - CourseViewMode.FULL -> interactor.getCourseStructureFromCache() - CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() + CourseViewMode.FULL -> interactor.getCourseStructure(courseId) + CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos(courseId) } val blocks = courseStructure.blockData setBlocks(blocks) diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt index fc7c9213f..1bc26e1a4 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -134,8 +134,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) componentId = requireArguments().getString(ARG_COMPONENT_ID, "") - viewModel.loadBlocks(requireArguments().serializable(ARG_MODE)!!) - viewModel.setupCurrentIndex(componentId) + viewModel.loadBlocks(requireArguments().serializable(ARG_MODE)!!, componentId) viewModel.courseUnitContainerShowedEvent() } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 323adb7cb..f479f08c0 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -76,23 +76,28 @@ class CourseUnitContainerViewModel( var hasNextBlock = false private var currentMode: CourseViewMode? = null + private var currentComponentId = "" private var courseName = "" private val _descendantsBlocks = MutableStateFlow>(listOf()) val descendantsBlocks = _descendantsBlocks.asStateFlow() - fun loadBlocks(mode: CourseViewMode) { + fun loadBlocks(mode: CourseViewMode, componentId: String = "") { currentMode = mode - try { - val courseStructure = when (mode) { - CourseViewMode.FULL -> interactor.getCourseStructureFromCache() - CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos() + viewModelScope.launch { + try { + val courseStructure = when (mode) { + CourseViewMode.FULL -> interactor.getCourseStructure(courseId) + CourseViewMode.VIDEOS -> interactor.getCourseStructureForVideos(courseId) + } + val blocks = courseStructure.blockData + courseName = courseStructure.name + this@CourseUnitContainerViewModel.blocks.clearAndAddAll(blocks) + + setupCurrentIndex(componentId) + } catch (e: Exception) { + e.printStackTrace() } - val blocks = courseStructure.blockData - courseName = courseStructure.name - this.blocks.clearAndAddAll(blocks) - } catch (e: Exception) { - //ignore e.printStackTrace() } } @@ -104,7 +109,7 @@ class CourseUnitContainerViewModel( if (event is CourseStructureUpdated) { if (event.courseId != courseId) return@collect - currentMode?.let { loadBlocks(it) } + currentMode?.let { loadBlocks(it, currentComponentId) } val blockId = blocks[currentVerticalIndex].id _subSectionUnitBlocks.value = getSubSectionUnitBlocks(blocks, getSubSectionId(blockId)) @@ -113,10 +118,10 @@ class CourseUnitContainerViewModel( } } - fun setupCurrentIndex(componentId: String = "") { - if (currentSectionIndex != -1) { - return - } + private fun setupCurrentIndex(componentId: String = "") { + if (currentSectionIndex != -1) return + currentComponentId = componentId + blocks.forEachIndexed { index, block -> if (block.id == unitId) { currentVerticalIndex = index diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt index dc88105a8..f5e9be934 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt @@ -20,7 +20,6 @@ import org.openedx.core.module.download.BaseDownloadViewModel import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated @@ -75,13 +74,9 @@ class CourseVideoViewModel( when (event) { is CourseStructureUpdated -> { if (event.courseId == courseId) { - updateVideos() + getVideos() } } - - is CourseDataReady -> { - getVideos() - } } } } @@ -114,6 +109,8 @@ class CourseVideoViewModel( } _videoSettings.value = preferencesManager.videoSettings + + getVideos() } override fun saveDownloadModels(folder: String, id: String) { @@ -141,13 +138,9 @@ class CourseVideoViewModel( super.saveAllDownloadModels(folder) } - private fun updateVideos() { - getVideos() - } - fun getVideos() { viewModelScope.launch { - var courseStructure = interactor.getCourseStructureForVideos() + var courseStructure = interactor.getCourseStructureForVideos(courseId) val blocks = courseStructure.blockData if (blocks.isEmpty()) { _uiState.value = CourseVideosUIState.Empty( 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 63dce6272..1b2cb6cca 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 @@ -25,6 +25,8 @@ import org.junit.rules.TestRule import org.openedx.core.ImageProcessor import org.openedx.core.R import org.openedx.core.config.Config +import org.openedx.core.data.api.CourseApi +import org.openedx.core.data.model.CourseStructureModel import org.openedx.core.data.model.User import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.AppConfig @@ -64,6 +66,7 @@ class CourseContainerViewModelTest { private val mockBitmap = mockk() private val imageProcessor = mockk() private val courseRouter = mockk() + private val courseApi = mockk() private val openEdx = "OpenEdx" private val calendarTitle = "OpenEdx - Abc" @@ -108,6 +111,23 @@ class CourseContainerViewModelTest { isSelfPaced = false ) + private val courseStructureModel = CourseStructureModel( + root = "", + blockData = mapOf(), + id = "id", + name = "Course name", + number = "", + org = "Org", + start = "", + startDisplay = "", + startType = "", + end = null, + coursewareAccess = null, + media = null, + certificate = null, + isSelfPaced = false + ) + @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -129,7 +149,7 @@ class CourseContainerViewModelTest { } @Test - fun `preloadCourseStructure internet connection exception`() = runTest { + fun `getCourseStructure internet connection exception`() = runTest { val viewModel = CourseContainerViewModel( "", "", @@ -147,12 +167,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.preloadCourseStructure(any()) } throws UnknownHostException() + coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 1) { interactor.preloadCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } val message = viewModel.errorMessage.value @@ -162,7 +182,7 @@ class CourseContainerViewModelTest { } @Test - fun `preloadCourseStructure unknown exception`() = runTest { + fun `getCourseStructure unknown exception`() = runTest { val viewModel = CourseContainerViewModel( "", "", @@ -180,12 +200,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.preloadCourseStructure(any()) } throws Exception() + coEvery { interactor.getCourseStructure(any()) } throws Exception() every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 1) { interactor.preloadCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } val message = viewModel.errorMessage.value @@ -195,7 +215,7 @@ class CourseContainerViewModelTest { } @Test - fun `preloadCourseStructure success with internet`() = runTest { + fun `getCourseStructure success with internet`() = runTest { val viewModel = CourseContainerViewModel( "", "", @@ -213,13 +233,12 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns true - coEvery { interactor.preloadCourseStructure(any()) } returns Unit - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 1) { interactor.preloadCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } assert(viewModel.errorMessage.value == null) @@ -228,7 +247,7 @@ class CourseContainerViewModelTest { } @Test - fun `preloadCourseStructure success without internet`() = runTest { + fun `getCourseStructure success without internet`() = runTest { val viewModel = CourseContainerViewModel( "", "", @@ -246,14 +265,15 @@ class CourseContainerViewModelTest { courseRouter ) every { networkConnection.isOnline() } returns false - coEvery { interactor.preloadCourseStructureFromCache(any()) } returns Unit - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { analytics.logEvent(any(), any()) } returns Unit + coEvery { + courseApi.getCourseStructure(any(), any(), any(), any()) + } returns courseStructureModel viewModel.preloadCourseStructure() advanceUntilIdle() - coVerify(exactly = 0) { interactor.preloadCourseStructure(any()) } - coVerify(exactly = 1) { interactor.preloadCourseStructureFromCache(any()) } + coVerify(exactly = 0) { courseApi.getCourseStructure(any(), any(), any(), any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } assert(viewModel.errorMessage.value == null) @@ -279,12 +299,12 @@ class CourseContainerViewModelTest { imageProcessor, courseRouter ) - coEvery { interactor.preloadCourseStructure(any()) } throws UnknownHostException() + coEvery { interactor.getCourseStructure(any(), true) } throws UnknownHostException() coEvery { notifier.send(CourseStructureUpdated("")) } returns Unit viewModel.updateData() advanceUntilIdle() - coVerify(exactly = 1) { interactor.preloadCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any(), true) } val message = viewModel.errorMessage.value assertEquals(noInternet, message) @@ -309,12 +329,12 @@ class CourseContainerViewModelTest { imageProcessor, courseRouter ) - coEvery { interactor.preloadCourseStructure(any()) } throws Exception() + coEvery { interactor.getCourseStructure(any(), true) } throws Exception() coEvery { notifier.send(CourseStructureUpdated("")) } returns Unit viewModel.updateData() advanceUntilIdle() - coVerify(exactly = 1) { interactor.preloadCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any(), true) } val message = viewModel.errorMessage.value assertEquals(somethingWrong, message) @@ -339,12 +359,12 @@ class CourseContainerViewModelTest { imageProcessor, courseRouter ) - coEvery { interactor.preloadCourseStructure(any()) } returns Unit + coEvery { interactor.getCourseStructure(any(), true) } returns courseStructure coEvery { notifier.send(CourseStructureUpdated("")) } returns Unit viewModel.updateData() advanceUntilIdle() - coVerify(exactly = 1) { interactor.preloadCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructure(any(), true) } assert(viewModel.errorMessage.value == null) assert(!viewModel.refreshing.value) 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 40a2d41c0..13e78fe91 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 @@ -39,7 +39,6 @@ import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.DatesSection import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.domain.interactor.CourseInteractor @@ -143,15 +142,15 @@ class CourseDatesViewModelTest { every { resourceManager.getString(id = R.string.platform_name) } returns openEdx every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { corePreferences.user } returns user every { corePreferences.appConfig } returns appConfig - every { notifier.notifier } returns flowOf(CourseDataReady(courseStructure)) + every { notifier.notifier } returns flowOf(CourseLoading(false)) every { calendarManager.getCourseCalendarTitle(any()) } returns calendarTitle every { calendarManager.isCalendarExists(any()) } returns true coEvery { notifier.send(any()) } returns Unit coEvery { notifier.send(any()) } returns Unit - coEvery { notifier.send(any()) } returns Unit + coEvery { notifier.send(any()) } returns Unit } @After @@ -162,6 +161,8 @@ class CourseDatesViewModelTest { @Test fun `getCourseDates no internet connection exception`() = runTest(UnconfinedTestDispatcher()) { val viewModel = CourseDatesViewModel( + "id", + "", "", notifier, interactor, @@ -179,15 +180,17 @@ class CourseDatesViewModelTest { } advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseDates(any()) } + coVerify(exactly = 1) { interactor.getCourseDates(any()) } Assert.assertEquals(noInternet, message.await()?.message) assert(viewModel.uiState.value is DatesUIState.Loading) } @Test - fun `getCourseDates unknown exception`() = runTest(UnconfinedTestDispatcher()) { + fun `getCourseDates unknown exception`() = runTest(UnconfinedTestDispatcher()) { val viewModel = CourseDatesViewModel( + "id", + "", "", notifier, interactor, @@ -214,6 +217,8 @@ class CourseDatesViewModelTest { @Test fun `getCourseDates success with internet`() = runTest(UnconfinedTestDispatcher()) { val viewModel = CourseDatesViewModel( + "id", + "", "", notifier, interactor, @@ -240,6 +245,8 @@ class CourseDatesViewModelTest { @Test fun `getCourseDates success with EmptyList`() = runTest(UnconfinedTestDispatcher()) { val viewModel = CourseDatesViewModel( + "id", + "", "", notifier, interactor, 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 098960a2a..c2b2cff57 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 @@ -218,7 +218,7 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal no internet connection exception`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isOnline() } returns true every { downloadDao.readAllData() } returns flow { emit(emptyList()) } coEvery { interactor.getCourseStatus(any()) } throws UnknownHostException() @@ -244,8 +244,8 @@ class CourseOutlineViewModelTest { viewModel.getCourseData() advanceUntilIdle() - verify(exactly = 1) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 1) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructure(any()) } + coVerify(exactly = 2) { interactor.getCourseStatus(any()) } assertEquals(noInternet, message.await()?.message) assert(viewModel.uiState.value is CourseOutlineUIState.Loading) @@ -253,7 +253,7 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal unknown exception`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isOnline() } returns true every { downloadDao.readAllData() } returns flow { emit(emptyList()) } coEvery { interactor.getCourseStatus(any()) } throws Exception() @@ -278,8 +278,8 @@ class CourseOutlineViewModelTest { viewModel.getCourseData() advanceUntilIdle() - verify(exactly = 1) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 1) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructure(any()) } + coVerify(exactly = 2) { interactor.getCourseStatus(any()) } assertEquals(somethingWrong, message.await()?.message) assert(viewModel.uiState.value is CourseOutlineUIState.Loading) @@ -287,7 +287,7 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal success with internet connection`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isOnline() } returns true coEvery { downloadDao.readAllData() } returns flow { emit( @@ -321,11 +321,12 @@ class CourseOutlineViewModelTest { viewModel.uiMessage.first() as? UIMessage.SnackBarMessage } } + viewModel.getCourseData() advanceUntilIdle() - verify(exactly = 1) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 1) { interactor.getCourseStatus(any()) } + coVerify(exactly = 2) { interactor.getCourseStructure(any()) } + coVerify(exactly = 2) { interactor.getCourseStatus(any()) } assert(message.await() == null) assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) @@ -333,7 +334,7 @@ class CourseOutlineViewModelTest { @Test fun `getCourseDataInternal success without internet connection`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isOnline() } returns false coEvery { downloadDao.readAllData() } returns flow { emit( @@ -370,7 +371,7 @@ class CourseOutlineViewModelTest { viewModel.getCourseData() advanceUntilIdle() - verify(exactly = 1) { interactor.getCourseStructureFromCache() } + coVerify(exactly = 2) { interactor.getCourseStructure(any()) } coVerify(exactly = 0) { interactor.getCourseStatus(any()) } assert(message.await() == null) @@ -379,7 +380,7 @@ class CourseOutlineViewModelTest { @Test fun `updateCourseData success with internet connection`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isOnline() } returns true coEvery { downloadDao.readAllData() } returns flow { emit( @@ -417,8 +418,8 @@ class CourseOutlineViewModelTest { viewModel.updateCourseData() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 2) { interactor.getCourseStatus(any()) } + coVerify(exactly = 3) { interactor.getCourseStructure(any()) } + coVerify(exactly = 3) { interactor.getCourseStatus(any()) } assert(message.await() == null) assert(viewModel.uiState.value is CourseOutlineUIState.CourseData) @@ -442,7 +443,7 @@ class CourseOutlineViewModelTest { workerController ) coEvery { notifier.notifier } returns flow { emit(CourseStructureUpdated("")) } - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") @@ -454,14 +455,14 @@ class CourseOutlineViewModelTest { viewModel.getCourseData() advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureFromCache() } + coVerify(exactly = 2) { interactor.getCourseStructure(any()) } coVerify(exactly = 1) { interactor.getCourseStatus(any()) } } @Test fun `saveDownloadModels test`() = runTest(UnconfinedTestDispatcher()) { every { preferencesManager.videoSettings.wifiDownloadOnly } returns false - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { networkConnection.isWifiConnected() } returns true every { networkConnection.isOnline() } returns true every { @@ -508,7 +509,7 @@ class CourseOutlineViewModelTest { @Test fun `saveDownloadModels only wifi download, with connection`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure coEvery { interactor.getCourseStatus(any()) } returns CourseComponentStatus("id") every { preferencesManager.videoSettings.wifiDownloadOnly } returns true every { networkConnection.isWifiConnected() } returns true @@ -545,7 +546,7 @@ class CourseOutlineViewModelTest { @Test fun `saveDownloadModels only wifi download, without connection`() = runTest(UnconfinedTestDispatcher()) { - every { interactor.getCourseStructureFromCache() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure every { preferencesManager.videoSettings.wifiDownloadOnly } returns true every { networkConnection.isWifiConnected() } returns false every { networkConnection.isOnline() } returns false diff --git a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt index ba6aa779c..0a398371b 100644 --- a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt @@ -185,14 +185,14 @@ class CourseSectionViewModelTest { downloadDao, ) - coEvery { interactor.getCourseStructureFromCache() } throws UnknownHostException() - coEvery { interactor.getCourseStructureForVideos() } throws UnknownHostException() + coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() + coEvery { interactor.getCourseStructureForVideos(any()) } throws UnknownHostException() viewModel.getBlocks("", CourseViewMode.FULL) advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 0) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } + coVerify(exactly = 0) { interactor.getCourseStructureForVideos(any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -215,14 +215,14 @@ class CourseSectionViewModelTest { downloadDao, ) - coEvery { interactor.getCourseStructureFromCache() } throws Exception() - coEvery { interactor.getCourseStructureForVideos() } throws Exception() + coEvery { interactor.getCourseStructure(any()) } throws Exception() + coEvery { interactor.getCourseStructureForVideos(any()) } throws Exception() viewModel.getBlocks("id2", CourseViewMode.FULL) advanceUntilIdle() - coVerify(exactly = 1) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 0) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } + coVerify(exactly = 0) { interactor.getCourseStructureForVideos(any()) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -247,14 +247,17 @@ class CourseSectionViewModelTest { downloadDao, ) - coEvery { interactor.getCourseStructureFromCache() } returns courseStructure - coEvery { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { downloadDao.readAllData() } returns flow { + emit(listOf(DownloadModelEntity.createFrom(downloadModel))) + } + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure viewModel.getBlocks("id", CourseViewMode.VIDEOS) advanceUntilIdle() - coVerify(exactly = 0) { interactor.getCourseStructureFromCache() } - coVerify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos(any()) } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is CourseSectionUIState.Blocks) @@ -366,8 +369,8 @@ class CourseSectionViewModelTest { ) coEvery { notifier.notifier } returns flow { } - coEvery { interactor.getCourseStructureFromCache() } returns courseStructure - coEvery { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) diff --git a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt index b92a02f5a..1e5354a95 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt @@ -1,13 +1,18 @@ package org.openedx.course.presentation.unit.container import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Rule @@ -147,159 +152,161 @@ class CourseUnitContainerViewModelTest { val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } throws UnknownHostException() - every { interactor.getCourseStructureForVideos() } throws UnknownHostException() + coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() + coEvery { interactor.getCourseStructureForVideos(any()) } throws UnknownHostException() viewModel.loadBlocks(CourseViewMode.FULL) advanceUntilIdle() - verify(exactly = 1) { interactor.getCourseStructureFromCache() } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } } @Test fun `getBlocks unknown exception`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } throws UnknownHostException() - every { interactor.getCourseStructureForVideos() } throws UnknownHostException() + coEvery { interactor.getCourseStructure(any()) } throws UnknownHostException() + coEvery { interactor.getCourseStructureForVideos(any()) } throws UnknownHostException() viewModel.loadBlocks(CourseViewMode.FULL) advanceUntilIdle() - verify(exactly = 1) { interactor.getCourseStructureFromCache() } + coVerify(exactly = 1) { interactor.getCourseStructure(any()) } } @Test fun `getBlocks unknown success`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure viewModel.loadBlocks(CourseViewMode.VIDEOS) advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos(any()) } } @Test fun setupCurrentIndex() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos(any()) } } @Test fun `getCurrentBlock test`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure("") } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos("") } assert(viewModel.getCurrentBlock().id == "id") } @Test fun `moveToPrevBlock null`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure("") } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos("") } assert(viewModel.moveToPrevBlock() == null) } @Test fun `moveToPrevBlock not null`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "id", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "id", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id1") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id1") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure("") } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos("") } assert(viewModel.moveToPrevBlock() != null) } @Test fun `moveToNextBlock null`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id3") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id3") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure("") } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos("") } assert(viewModel.moveToNextBlock() == null) } @Test fun `moveToNextBlock not null`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "id", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "id", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure("") } returns courseStructure + coEvery { interactor.getCourseStructureForVideos("") } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure("") } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos("") } assert(viewModel.moveToNextBlock() != null) } @Test fun `currentIndex isLastIndex`() = runTest { every { notifier.notifier } returns MutableSharedFlow() - val viewModel = CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) - every { interactor.getCourseStructureFromCache() } returns courseStructure - every { interactor.getCourseStructureForVideos() } returns courseStructure + val viewModel = + CourseUnitContainerViewModel("", "", config, interactor, notifier, analytics) + coEvery { interactor.getCourseStructure(any()) } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure - viewModel.loadBlocks(CourseViewMode.VIDEOS) - viewModel.setupCurrentIndex("id3") + viewModel.loadBlocks(CourseViewMode.VIDEOS, "id3") advanceUntilIdle() - verify(exactly = 0) { interactor.getCourseStructureFromCache() } - verify(exactly = 1) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 0) { interactor.getCourseStructure(any()) } + coVerify(exactly = 1) { interactor.getCourseStructureForVideos(any()) } } } \ No newline at end of file diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt index 43d057a6c..a2dae8b2e 100644 --- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt @@ -46,7 +46,7 @@ import org.openedx.core.module.db.FileType import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.CourseDataReady +import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.system.notifier.VideoNotifier @@ -171,7 +171,7 @@ class CourseVideoViewModelTest { every { resourceManager.getString(R.string.course_can_download_only_with_wifi) } returns cantDownload Dispatchers.setMain(dispatcher) every { config.getApiHostURL() } returns "http://localhost:8000" - every { courseNotifier.notifier } returns flowOf(CourseDataReady(courseStructure)) + every { courseNotifier.notifier } returns flowOf(CourseLoading(false)) } @After @@ -182,7 +182,8 @@ class CourseVideoViewModelTest { @Test fun `getVideos empty list`() = runTest { every { config.isCourseNestedListEnabled() } returns false - every { interactor.getCourseStructureForVideos() } returns courseStructure.copy(blockData = emptyList()) + coEvery { interactor.getCourseStructureForVideos(any()) } returns + courseStructure.copy(blockData = emptyList()) every { downloadDao.readAllData() } returns flow { emit(emptyList()) } every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( @@ -204,7 +205,7 @@ class CourseVideoViewModelTest { viewModel.getVideos() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 2) { interactor.getCourseStructureForVideos(any()) } assert(viewModel.uiState.value is CourseVideosUIState.Empty) } @@ -212,7 +213,7 @@ class CourseVideoViewModelTest { @Test fun `getVideos success`() = runTest { every { config.isCourseNestedListEnabled() } returns false - every { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure every { downloadDao.readAllData() } returns flow { emit(emptyList()) } every { preferencesManager.videoSettings } returns VideoSettings.default @@ -236,7 +237,7 @@ class CourseVideoViewModelTest { viewModel.getVideos() advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 2) { interactor.getCourseStructureForVideos(any()) } assert(viewModel.uiState.value is CourseVideosUIState.CourseData) } @@ -244,10 +245,9 @@ class CourseVideoViewModelTest { @Test fun `updateVideos success`() = runTest { every { config.isCourseNestedListEnabled() } returns false - every { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { courseNotifier.notifier } returns flow { emit(CourseStructureUpdated("")) - emit(CourseDataReady(courseStructure)) } every { downloadDao.readAllData() } returns flow { repeat(5) { @@ -279,7 +279,7 @@ class CourseVideoViewModelTest { advanceUntilIdle() - coVerify(exactly = 2) { interactor.getCourseStructureForVideos() } + coVerify(exactly = 2) { interactor.getCourseStructureForVideos(any()) } assert(viewModel.uiState.value is CourseVideosUIState.CourseData) } @@ -288,7 +288,7 @@ class CourseVideoViewModelTest { fun `setIsUpdating success`() = runTest { every { config.isCourseNestedListEnabled() } returns false every { preferencesManager.videoSettings } returns VideoSettings.default - coEvery { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { downloadDao.readAllData() } returns flow { emit(listOf(downloadModelEntity)) } advanceUntilIdle() } @@ -312,7 +312,7 @@ class CourseVideoViewModelTest { downloadDao, workerController ) - coEvery { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { downloadDao.readAllData() } returns flow { emit(listOf(downloadModelEntity)) } every { preferencesManager.videoSettings.wifiDownloadOnly } returns false every { networkConnection.isWifiConnected() } returns true @@ -348,7 +348,7 @@ class CourseVideoViewModelTest { downloadDao, workerController ) - coEvery { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { downloadDao.readAllData() } returns flow { emit(listOf(downloadModelEntity)) } every { preferencesManager.videoSettings.wifiDownloadOnly } returns true every { networkConnection.isWifiConnected() } returns true @@ -391,7 +391,7 @@ class CourseVideoViewModelTest { every { preferencesManager.videoSettings.wifiDownloadOnly } returns true every { networkConnection.isWifiConnected() } returns false every { networkConnection.isOnline() } returns false - coEvery { interactor.getCourseStructureForVideos() } returns courseStructure + coEvery { interactor.getCourseStructureForVideos(any()) } returns courseStructure coEvery { downloadDao.readAllData() } returns flow { emit(listOf(downloadModelEntity)) } coEvery { workerController.saveModels(any()) } returns Unit val message = async { diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt index 2797ed1a6..62ec564b6 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt @@ -58,7 +58,7 @@ import org.openedx.discussion.R as discussionR @Composable fun DiscussionTopicsScreen( - discussionTopicsViewModel: DiscussionTopicsViewModel = koinViewModel(), + discussionTopicsViewModel: DiscussionTopicsViewModel, windowSize: WindowSize, fragmentManager: FragmentManager ) { diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt index 5bdd90d70..46552edc9 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt @@ -13,7 +13,6 @@ import org.openedx.core.UIMessage import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseRefresh @@ -22,6 +21,8 @@ import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.discussion.presentation.DiscussionRouter class DiscussionTopicsViewModel( + val courseId: String, + private val courseTitle: String, private val interactor: DiscussionInteractor, private val resourceManager: ResourceManager, private val analytics: DiscussionAnalytics, @@ -29,9 +30,6 @@ class DiscussionTopicsViewModel( val discussionRouter: DiscussionRouter, ) : BaseViewModel() { - var courseId: String = "" - var courseName: String = "" - private val _uiState = MutableLiveData() val uiState: LiveData get() = _uiState @@ -42,6 +40,8 @@ class DiscussionTopicsViewModel( init { collectCourseNotifier() + + getCourseTopic() } private fun getCourseTopic() { @@ -64,15 +64,15 @@ class DiscussionTopicsViewModel( fun discussionClickedEvent(action: String, data: String, title: String) { when (action) { ALL_POSTS -> { - analytics.discussionAllPostsClickedEvent(courseId, courseName) + analytics.discussionAllPostsClickedEvent(courseId, courseTitle) } FOLLOWING_POSTS -> { - analytics.discussionFollowingClickedEvent(courseId, courseName) + analytics.discussionFollowingClickedEvent(courseId, courseTitle) } TOPIC -> { - analytics.discussionTopicClickedEvent(courseId, courseName, data, title) + analytics.discussionTopicClickedEvent(courseId, courseTitle, data, title) } } } @@ -81,12 +81,6 @@ class DiscussionTopicsViewModel( viewModelScope.launch { courseNotifier.notifier.collect { event -> when (event) { - is CourseDataReady -> { - courseId = event.courseStructure.id - courseName = event.courseStructure.name - getCourseTopic() - } - is CourseRefresh -> { if (event.courseContainerTab == CourseContainerTab.DISCUSSIONS) { getCourseTopic() diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt index fcff13a30..b74cc6644 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt @@ -31,7 +31,6 @@ import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.CourseDataReady import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.discussion.domain.interactor.DiscussionInteractor @@ -136,7 +135,7 @@ class DiscussionTopicsViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { courseNotifier.notifier } returns flowOf(CourseDataReady(courseStructure)) + every { courseNotifier.notifier } returns flowOf(CourseLoading(false)) coEvery { courseNotifier.send(any()) } returns Unit } @@ -147,7 +146,7 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics no internet exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel(interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) coEvery { interactor.getCourseTopics(any()) } throws UnknownHostException() val message = async { @@ -164,7 +163,7 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics unknown exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel(interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) coEvery { interactor.getCourseTopics(any()) } throws Exception() val message = async { @@ -181,7 +180,7 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics success`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel(interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) coEvery { interactor.getCourseTopics(any()) } returns mockk() advanceUntilIdle() @@ -198,7 +197,7 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics no internet exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel(interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) coEvery { interactor.getCourseTopics(any()) } throws UnknownHostException() val message = async { @@ -215,7 +214,7 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics unknown exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel(interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) coEvery { interactor.getCourseTopics(any()) } throws Exception() val message = async { @@ -232,7 +231,7 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics success`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel(interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) coEvery { interactor.getCourseTopics(any()) } returns mockk() val message = async {