test: make viewmodel and notification receiver tests deterministic (#1207)
This commit is contained in:
9 files changed
+52
-66
No files matched your search
@@ -20,6 +20,7 @@ import com.m57.hermescontrol.ui.common.ToastHost
|
|||||||
import com.m57.hermescontrol.ui.common.refreshOnChange
|
import com.m57.hermescontrol.ui.common.refreshOnChange
|
||||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||||
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -99,6 +100,7 @@ class CronJobsViewModel :
|
|||||||
val uiState: StateFlow<CronJobsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<CronJobsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private var loadJob: Job? = null
|
private var loadJob: Job? = null
|
||||||
|
internal var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
private val jobsCache = SwrCache<String, List<CronJob>>()
|
private val jobsCache = SwrCache<String, List<CronJob>>()
|
||||||
|
|
||||||
fun clearScopeOwnedState() {
|
fun clearScopeOwnedState() {
|
||||||
@@ -166,7 +168,7 @@ class CronJobsViewModel :
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.pauseCronJob(id) }
|
safeApiCall { ApiClient.hermesApi.pauseCronJob(id) }
|
||||||
}
|
}
|
||||||
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
||||||
@@ -197,7 +199,7 @@ class CronJobsViewModel :
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.resumeCronJob(id) }
|
safeApiCall { ApiClient.hermesApi.resumeCronJob(id) }
|
||||||
}
|
}
|
||||||
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
||||||
@@ -218,7 +220,7 @@ class CronJobsViewModel :
|
|||||||
fun triggerCronJob(id: String) {
|
fun triggerCronJob(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.triggerCronJob(id) }
|
safeApiCall { ApiClient.hermesApi.triggerCronJob(id) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -241,7 +243,7 @@ class CronJobsViewModel :
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.deleteCronJob(id) }
|
safeApiCall { ApiClient.hermesApi.deleteCronJob(id) }
|
||||||
}
|
}
|
||||||
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
||||||
@@ -292,7 +294,7 @@ class CronJobsViewModel :
|
|||||||
fun loadEditorBlueprints() {
|
fun loadEditorBlueprints() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.getCronBlueprints() }
|
safeApiCall { ApiClient.hermesApi.getCronBlueprints() }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -319,7 +321,7 @@ class CronJobsViewModel :
|
|||||||
fun loadDeliveryTargets() {
|
fun loadDeliveryTargets() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.getCronDeliveryTargets() }
|
safeApiCall { ApiClient.hermesApi.getCronDeliveryTargets() }
|
||||||
}
|
}
|
||||||
if (result is NetworkResult.Success) {
|
if (result is NetworkResult.Success) {
|
||||||
@@ -345,7 +347,7 @@ class CronJobsViewModel :
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.getCronJob(id) }
|
safeApiCall { ApiClient.hermesApi.getCronJob(id) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -462,7 +464,7 @@ class CronJobsViewModel :
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
if (editor.isNew && editor.selectedBlueprintKey != null) {
|
if (editor.isNew && editor.selectedBlueprintKey != null) {
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
ApiClient.hermesApi.instantiateBlueprint(
|
ApiClient.hermesApi.instantiateBlueprint(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.m57.hermescontrol.data.repository.KanbanRepositoryImpl
|
|||||||
import com.m57.hermescontrol.data.ws.KanbanEventsClient
|
import com.m57.hermescontrol.data.ws.KanbanEventsClient
|
||||||
import com.m57.hermescontrol.data.ws.KanbanLiveStatus
|
import com.m57.hermescontrol.data.ws.KanbanLiveStatus
|
||||||
import com.m57.hermescontrol.ui.common.ToastHost
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -166,6 +167,7 @@ class KanbanViewModel(
|
|||||||
private var eventsBoard: String? = null
|
private var eventsBoard: String? = null
|
||||||
private var reloadJob: Job? = null
|
private var reloadJob: Job? = null
|
||||||
private var currentLoadGen: Int = 0
|
private var currentLoadGen: Int = 0
|
||||||
|
internal var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
fun loadBoards() {
|
fun loadBoards() {
|
||||||
val endpoint = endpointProvider()
|
val endpoint = endpointProvider()
|
||||||
@@ -184,7 +186,7 @@ class KanbanViewModel(
|
|||||||
coroutineScope {
|
coroutineScope {
|
||||||
val prefetchedBoardDeferred =
|
val prefetchedBoardDeferred =
|
||||||
if (previouslySelectedId != null) {
|
if (previouslySelectedId != null) {
|
||||||
async(Dispatchers.IO) {
|
async(ioDispatcher) {
|
||||||
repository.getBoard(
|
repository.getBoard(
|
||||||
board = previouslySelectedId,
|
board = previouslySelectedId,
|
||||||
includeArchived = _uiState.value.includeArchived,
|
includeArchived = _uiState.value.includeArchived,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.m57.hermescontrol.ui.common.ToastHost
|
|||||||
import com.m57.hermescontrol.ui.common.refreshOnChange
|
import com.m57.hermescontrol.ui.common.refreshOnChange
|
||||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||||
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -40,6 +41,7 @@ class PairingViewModel :
|
|||||||
val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private var launchJob: Job? = null
|
private var launchJob: Job? = null
|
||||||
|
internal var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
private val pairingCache = SwrCache<String, PairingResponse>()
|
private val pairingCache = SwrCache<String, PairingResponse>()
|
||||||
|
|
||||||
fun clearScopeOwnedState() {
|
fun clearScopeOwnedState() {
|
||||||
@@ -140,7 +142,7 @@ class PairingViewModel :
|
|||||||
if (_uiState.value.actionKey != null) return
|
if (_uiState.value.actionKey != null) return
|
||||||
_uiState.update { it.copy(actionKey = actionKey) }
|
_uiState.update { it.copy(actionKey = actionKey) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = withContext(Dispatchers.IO) { apiCall() }
|
val result = withContext(ioDispatcher) { apiCall() }
|
||||||
when (result) {
|
when (result) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { onSuccess(it.copy(actionKey = null)) }
|
_uiState.update { onSuccess(it.copy(actionKey = null)) }
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.m57.hermescontrol.data.remote.safeApiCall
|
|||||||
import com.m57.hermescontrol.ui.common.ToastHost
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||||
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -98,6 +99,7 @@ class SkillsViewModel(
|
|||||||
private var loadJob: Job? = null
|
private var loadJob: Job? = null
|
||||||
private var searchJob: Job? = null
|
private var searchJob: Job? = null
|
||||||
private val skillsCache = SwrCache<String, List<Skill>>()
|
private val skillsCache = SwrCache<String, List<Skill>>()
|
||||||
|
internal var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
fun clearScopeOwnedState() {
|
fun clearScopeOwnedState() {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@@ -252,7 +254,7 @@ class SkillsViewModel(
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.scanHubSkill(identifier = identifier) }
|
safeApiCall { ApiClient.hermesApi.scanHubSkill(identifier = identifier) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -307,7 +309,7 @@ class SkillsViewModel(
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.previewHubSkill(identifier = identifier) }
|
safeApiCall { ApiClient.hermesApi.previewHubSkill(identifier = identifier) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -343,7 +345,7 @@ class SkillsViewModel(
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.installHubSkill(SkillHubInstallRequest(identifier = identifier)) }
|
safeApiCall { ApiClient.hermesApi.installHubSkill(SkillHubInstallRequest(identifier = identifier)) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -381,7 +383,7 @@ class SkillsViewModel(
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.uninstallHubSkill(SkillHubUninstallRequest(name = name)) }
|
safeApiCall { ApiClient.hermesApi.uninstallHubSkill(SkillHubUninstallRequest(name = name)) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -438,7 +440,7 @@ class SkillsViewModel(
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) }
|
safeApiCall { ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) }
|
||||||
}
|
}
|
||||||
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
|
||||||
@@ -489,7 +491,7 @@ class SkillsViewModel(
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.getSkillContent(skillName) }
|
safeApiCall { ApiClient.hermesApi.getSkillContent(skillName) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
@@ -522,7 +524,7 @@ class SkillsViewModel(
|
|||||||
_uiState.update { it.copy(isSavingContent = true, saveContentSuccess = false) }
|
_uiState.update { it.copy(isSavingContent = true, saveContentSuccess = false) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
ApiClient.hermesApi.saveSkillContent(
|
ApiClient.hermesApi.saveSkillContent(
|
||||||
SaveSkillContentRequest(
|
SaveSkillContentRequest(
|
||||||
@@ -560,7 +562,7 @@ class SkillsViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(toastMessage = "Updating skills from hub…") }
|
_uiState.update { it.copy(toastMessage = "Updating skills from hub…") }
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall { ApiClient.hermesApi.updateSkillsFromHub() }
|
safeApiCall { ApiClient.hermesApi.updateSkillsFromHub() }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|||||||
@@ -70,19 +70,12 @@ class CronJobsViewModelTest {
|
|||||||
|
|
||||||
private fun createViewModel(): CronJobsViewModel {
|
private fun createViewModel(): CronJobsViewModel {
|
||||||
val vm = CronJobsViewModel()
|
val vm = CronJobsViewModel()
|
||||||
|
vm.ioDispatcher = testDispatcher
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pump the test scheduler while letting the real Dispatchers.IO hops
|
|
||||||
* (safeLaunchLoad / withContext(IO)) land their resumptions.
|
|
||||||
*/
|
|
||||||
private fun settle() {
|
private fun settle() {
|
||||||
repeat(20) {
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
Thread.sleep(10)
|
|
||||||
}
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import com.m57.hermescontrol.data.model.KanbanTask
|
|||||||
import com.m57.hermescontrol.data.model.UpdateTaskResponse
|
import com.m57.hermescontrol.data.model.UpdateTaskResponse
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.KanbanApiService
|
import com.m57.hermescontrol.data.remote.KanbanApiService
|
||||||
|
import com.m57.hermescontrol.data.repository.KanbanDispatcherNudger
|
||||||
|
import com.m57.hermescontrol.data.repository.KanbanRepositoryImpl
|
||||||
import com.m57.hermescontrol.data.ws.KanbanEvent
|
import com.m57.hermescontrol.data.ws.KanbanEvent
|
||||||
import com.m57.hermescontrol.data.ws.KanbanEventsClient
|
import com.m57.hermescontrol.data.ws.KanbanEventsClient
|
||||||
import com.m57.hermescontrol.data.ws.KanbanEventsEnvelope
|
import com.m57.hermescontrol.data.ws.KanbanEventsEnvelope
|
||||||
@@ -42,20 +44,24 @@ class KanbanViewModelTest {
|
|||||||
private val mockEventsClient = mockk<KanbanEventsClient>(relaxed = true)
|
private val mockEventsClient = mockk<KanbanEventsClient>(relaxed = true)
|
||||||
|
|
||||||
private fun createViewModel(): KanbanViewModel {
|
private fun createViewModel(): KanbanViewModel {
|
||||||
val vm = KanbanViewModel(eventsClientProvider = { mockEventsClient })
|
val mockNudger = mockk<KanbanDispatcherNudger>(relaxed = true)
|
||||||
|
val repo =
|
||||||
|
KanbanRepositoryImpl(
|
||||||
|
apiProvider = { mockApi },
|
||||||
|
ioDispatcher = testDispatcher,
|
||||||
|
nudger = mockNudger,
|
||||||
|
)
|
||||||
|
val vm =
|
||||||
|
KanbanViewModel(
|
||||||
|
repository = repo,
|
||||||
|
eventsClientProvider = { mockEventsClient },
|
||||||
|
)
|
||||||
|
vm.ioDispatcher = testDispatcher
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pump the test scheduler while letting the real Dispatchers.IO hops
|
|
||||||
* (safeLaunchLoad / withContext(IO)) land their resumptions.
|
|
||||||
*/
|
|
||||||
private fun settle() {
|
private fun settle() {
|
||||||
repeat(20) {
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
Thread.sleep(10)
|
|
||||||
}
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,15 +26,7 @@ class LogsViewModelTest {
|
|||||||
private val testDispatcher = StandardTestDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
private val mockApi = mockk<HermesApiService>(relaxed = true)
|
private val mockApi = mockk<HermesApiService>(relaxed = true)
|
||||||
|
|
||||||
/**
|
|
||||||
* Pump the test scheduler while letting the real Dispatchers.IO hops
|
|
||||||
* (safeLaunchLoad / withContext(IO)) land their resumptions.
|
|
||||||
*/
|
|
||||||
private fun settle() {
|
private fun settle() {
|
||||||
repeat(20) {
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
Thread.sleep(10)
|
|
||||||
}
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,19 +36,12 @@ class PairingViewModelTest {
|
|||||||
|
|
||||||
private fun createViewModel(): PairingViewModel {
|
private fun createViewModel(): PairingViewModel {
|
||||||
val vm = PairingViewModel()
|
val vm = PairingViewModel()
|
||||||
|
vm.ioDispatcher = testDispatcher
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pump the test scheduler while letting the real Dispatchers.IO hops
|
|
||||||
* (safeLaunchLoad / withContext(IO)) land their resumptions.
|
|
||||||
*/
|
|
||||||
private fun settle() {
|
private fun settle() {
|
||||||
repeat(20) {
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
Thread.sleep(10)
|
|
||||||
}
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,9 @@ class SkillsViewModelTest {
|
|||||||
private val mockApi = mockk<HermesApiService>(relaxed = true)
|
private val mockApi = mockk<HermesApiService>(relaxed = true)
|
||||||
private val app = mockk<Application>(relaxed = true)
|
private val app = mockk<Application>(relaxed = true)
|
||||||
|
|
||||||
/**
|
private fun createViewModel(): SkillsViewModel = SkillsViewModel(app).also { it.ioDispatcher = testDispatcher }
|
||||||
* Pump the test scheduler while letting the real Dispatchers.IO hops
|
|
||||||
* (safeLaunchLoad / withContext(IO)) land their resumptions.
|
|
||||||
*/
|
|
||||||
private fun settle() {
|
private fun settle() {
|
||||||
repeat(20) {
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
Thread.sleep(10)
|
|
||||||
}
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +99,7 @@ class SkillsViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `loadHubSources stores sources and featured skills`() {
|
fun `loadHubSources stores sources and featured skills`() {
|
||||||
coEvery { mockApi.getSkillHubSources() } returns Response.success(sourcesResponse)
|
coEvery { mockApi.getSkillHubSources() } returns Response.success(sourcesResponse)
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.loadHubSources()
|
vm.loadHubSources()
|
||||||
settle()
|
settle()
|
||||||
|
|
||||||
@@ -122,7 +116,7 @@ class SkillsViewModelTest {
|
|||||||
404,
|
404,
|
||||||
"""{"detail":"Hub sources unavailable"}""".toResponseBody("application/json".toMediaTypeOrNull()),
|
"""{"detail":"Hub sources unavailable"}""".toResponseBody("application/json".toMediaTypeOrNull()),
|
||||||
)
|
)
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.loadHubSources()
|
vm.loadHubSources()
|
||||||
settle()
|
settle()
|
||||||
|
|
||||||
@@ -137,7 +131,7 @@ class SkillsViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `setViewMode HUB triggers sources load when landing is empty`() {
|
fun `setViewMode HUB triggers sources load when landing is empty`() {
|
||||||
coEvery { mockApi.getSkillHubSources() } returns Response.success(sourcesResponse)
|
coEvery { mockApi.getSkillHubSources() } returns Response.success(sourcesResponse)
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.setViewMode(SkillsViewMode.HUB)
|
vm.setViewMode(SkillsViewMode.HUB)
|
||||||
settle()
|
settle()
|
||||||
|
|
||||||
@@ -149,7 +143,7 @@ class SkillsViewModelTest {
|
|||||||
fun `scanHubSkill stores the scan result for the identifier`() {
|
fun `scanHubSkill stores the scan result for the identifier`() {
|
||||||
coEvery { mockApi.scanHubSkill(identifier = "hermes-index:web-research") } returns
|
coEvery { mockApi.scanHubSkill(identifier = "hermes-index:web-research") } returns
|
||||||
Response.success(scanResponse)
|
Response.success(scanResponse)
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.scanHubSkill("hermes-index:web-research")
|
vm.scanHubSkill("hermes-index:web-research")
|
||||||
settle()
|
settle()
|
||||||
|
|
||||||
@@ -166,7 +160,7 @@ class SkillsViewModelTest {
|
|||||||
404,
|
404,
|
||||||
"""{"detail":"Skill not found"}""".toResponseBody("application/json".toMediaTypeOrNull()),
|
"""{"detail":"Skill not found"}""".toResponseBody("application/json".toMediaTypeOrNull()),
|
||||||
)
|
)
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.scanHubSkill("broken:skill")
|
vm.scanHubSkill("broken:skill")
|
||||||
settle()
|
settle()
|
||||||
|
|
||||||
@@ -178,7 +172,7 @@ class SkillsViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `scanHubSkill ignores blank identifiers`() {
|
fun `scanHubSkill ignores blank identifiers`() {
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.scanHubSkill(" ")
|
vm.scanHubSkill(" ")
|
||||||
settle()
|
settle()
|
||||||
|
|
||||||
@@ -190,7 +184,7 @@ class SkillsViewModelTest {
|
|||||||
fun `clearHubScan resets scan state`() {
|
fun `clearHubScan resets scan state`() {
|
||||||
coEvery { mockApi.scanHubSkill(identifier = "hermes-index:web-research") } returns
|
coEvery { mockApi.scanHubSkill(identifier = "hermes-index:web-research") } returns
|
||||||
Response.success(scanResponse)
|
Response.success(scanResponse)
|
||||||
val vm = SkillsViewModel(app)
|
val vm = createViewModel()
|
||||||
vm.scanHubSkill("hermes-index:web-research")
|
vm.scanHubSkill("hermes-index:web-research")
|
||||||
settle()
|
settle()
|
||||||
vm.clearHubScan()
|
vm.clearHubScan()
|
||||||
|
|||||||
Reference in new issue
Block a user