feat(scene): add scene editor, executor and fakes (phase 1) (#5)
## Summary Adds scene integration phase 1: domain model, fakes, UI, navigation, and tests. ## Changes - **domain**: Add `Scene`, `SceneAction` (SocketPower/InfraredKey), `SceneRepository`, `SceneExecutor`, and execution result models. - **testing/fakes**: Add `FakeSceneRepository` (isolated by `ProviderProfileId`, seeded defaults for the local profile) and `FakeSceneExecutor` (sequential execution, continue-on-failure). - **android/app**: Add scene UI module - list screen, editor screen, execution progress screen - with Hilt-bound ViewModels. - **navigation**: Wire scene routes into `ABoxManagerApp` NavHost. - **strings**: Add `scene_*` entries for en and zh-rCN. - **tests**: Cover `FakeSceneRepository` CRUD/isolation and `FakeSceneExecutor` ordering/continue-on-failure. ## Test plan - [x] `./gradlew :testing:fakes:test` passes. - [x] `FakeSceneRepository` isolates scenes by provider profile. - [x] `FakeSceneExecutor` runs actions in order and continues on failure. ## Notes Phase 1 ships fakes only; real repository/executor backed by the abox gateway will land in a follow-up. --------- Co-authored-by: KremeCN <3265494849@qq.com> Co-authored-by: mxhsnd <2049295946@qq.com> Reviewed-on: #5 Co-authored-by: tzh <3091898014@qq.com>
This commit was merged in pull request #5.
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
||||
package com.flagship.abox.manager.ui.scene
|
||||
|
||||
import com.flagship.abox.manager.domain.AutomationType
|
||||
import com.flagship.abox.manager.domain.ComparisonOperator
|
||||
import com.flagship.abox.manager.domain.ConcurrencyMode
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.ExecutionStrategy
|
||||
import com.flagship.abox.manager.domain.ProviderProfile
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.ProviderType
|
||||
import com.flagship.abox.manager.domain.Scene
|
||||
import com.flagship.abox.manager.domain.SceneAction
|
||||
import com.flagship.abox.manager.domain.SceneActionId
|
||||
import com.flagship.abox.manager.domain.SceneColor
|
||||
import com.flagship.abox.manager.domain.SceneCondition
|
||||
import com.flagship.abox.manager.domain.SceneIcon
|
||||
import com.flagship.abox.manager.domain.SceneId
|
||||
import com.flagship.abox.manager.domain.SceneTrigger
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import com.flagship.abox.manager.testing.MainDispatcherRule
|
||||
import com.flagship.abox.manager.testing.fakes.FakeProviderProfileStore
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSceneRepository
|
||||
import com.flagship.abox.manager.testing.fakes.FakeVirtualRemoteRepository
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SceneEditorViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val profileStore = FakeProviderProfileStore()
|
||||
private val sceneRepository = FakeSceneRepository()
|
||||
private val virtualRemoteRepository = FakeVirtualRemoteRepository()
|
||||
private val profileId = ProviderProfileId("local-experience")
|
||||
|
||||
private val existingScene = Scene(
|
||||
id = SceneId("local-experience:scene-1"),
|
||||
profileId = profileId,
|
||||
name = "Existing Scene",
|
||||
icon = SceneIcon.HOME,
|
||||
color = SceneColor("#FF8A65"),
|
||||
actions = listOf(
|
||||
SceneAction.SocketPower(
|
||||
id = SceneActionId("local-experience:action-1"),
|
||||
profileId = profileId,
|
||||
deviceName = DeviceName("socket-1"),
|
||||
powerState = SocketPowerState.ON,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun setupProfile() {
|
||||
profileStore.saveProfile(ProviderProfile(profileId, ProviderType.LOCAL_EXPERIENCE, "Local"))
|
||||
profileStore.setActiveProfile(profileId)
|
||||
sceneRepository.setScenes(profileId, listOf(existingScene))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializeWithNullPreparesNewScene() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(null, AutomationType.MANUAL)
|
||||
|
||||
val state = viewModel.state.value
|
||||
assertFalse(state.isEditing)
|
||||
assertEquals("", state.name)
|
||||
assertTrue(state.actions.isEmpty())
|
||||
assertFalse(state.isValid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializeWithExistingSceneLoadsState() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(existingScene.id)
|
||||
|
||||
// Immediately binds sceneId and enters loading state
|
||||
assertTrue(viewModel.state.value.isEditing)
|
||||
assertEquals(existingScene.id, viewModel.state.value.sceneId)
|
||||
assertTrue(viewModel.state.value.isLoading)
|
||||
assertFalse(viewModel.state.value.isValid)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.state.value
|
||||
assertTrue(state.isEditing)
|
||||
assertFalse(state.isLoading)
|
||||
assertEquals("Existing Scene", state.name)
|
||||
assertEquals(1, state.actions.size)
|
||||
assertTrue(state.isValid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addingAndReorderingActionsUpdatesState() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(null, AutomationType.MANUAL)
|
||||
viewModel.updateName("My New Scene")
|
||||
|
||||
viewModel.addSocketAction("socket-1", SocketPowerState.ON)
|
||||
viewModel.addDelayAction(3)
|
||||
viewModel.addNotificationAction("Done", "Completed")
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(3, viewModel.state.value.actions.size)
|
||||
assertTrue(viewModel.state.value.actions[0] is SceneAction.SocketPower)
|
||||
assertTrue(viewModel.state.value.actions[1] is SceneAction.Delay)
|
||||
assertTrue(viewModel.state.value.actions[2] is SceneAction.Notification)
|
||||
|
||||
// Move second to first
|
||||
viewModel.moveAction(1, 0)
|
||||
assertTrue(viewModel.state.value.actions[0] is SceneAction.Delay)
|
||||
assertTrue(viewModel.state.value.actions[1] is SceneAction.SocketPower)
|
||||
|
||||
// Remove first action
|
||||
val firstId = viewModel.state.value.actions[0].id
|
||||
viewModel.removeAction(firstId)
|
||||
assertEquals(2, viewModel.state.value.actions.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addingTriggersAndConditionsForAutomaticRule() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(null, AutomationType.AUTOMATIC)
|
||||
viewModel.updateName("Auto Rule")
|
||||
|
||||
viewModel.addTemperatureTrigger("sensor-1", ComparisonOperator.GREATER_THAN, 28.0)
|
||||
viewModel.addSocketPowerCondition("socket-1", SocketPowerState.OFF)
|
||||
viewModel.addSocketAction("socket-1", SocketPowerState.ON)
|
||||
viewModel.updateConcurrencyMode(ConcurrencyMode.PARALLEL)
|
||||
viewModel.updateStrategy(ExecutionStrategy.ABORT_ON_ERROR)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.state.value
|
||||
assertEquals(1, state.triggers.size)
|
||||
assertTrue(state.triggers[0] is SceneTrigger.Temperature)
|
||||
assertEquals(1, state.conditions.size)
|
||||
assertTrue(state.conditions[0] is SceneCondition.SocketPower)
|
||||
assertEquals(ConcurrencyMode.PARALLEL, state.concurrencyMode)
|
||||
assertEquals(ExecutionStrategy.ABORT_ON_ERROR, state.strategy)
|
||||
assertTrue(state.isValid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveNewScenePersistsToRepository() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(null, AutomationType.MANUAL)
|
||||
viewModel.updateName("Brand New Scene")
|
||||
viewModel.addSocketAction("socket-main", SocketPowerState.OFF)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(viewModel.state.value.isValid)
|
||||
viewModel.save()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(viewModel.state.value.isSaved)
|
||||
assertFalse(viewModel.state.value.isSaving)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun automaticSceneWithoutTriggersIsInvalid() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(null, AutomationType.AUTOMATIC)
|
||||
viewModel.updateName("Auto Without Trigger")
|
||||
viewModel.addSocketAction("socket-1", SocketPowerState.ON)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Missing triggers -> invalid
|
||||
assertFalse(viewModel.state.value.isValid)
|
||||
|
||||
// Adding trigger makes it valid
|
||||
viewModel.addTemperatureTrigger("sensor-1", ComparisonOperator.GREATER_THAN, 28.0)
|
||||
advanceUntilIdle()
|
||||
assertTrue(viewModel.state.value.isValid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializeIsIdempotentAndPreservesDraftState() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneEditorViewModel(sceneRepository, virtualRemoteRepository, profileStore)
|
||||
viewModel.initialize(existingScene.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.updateName("Modified Unsaved Name")
|
||||
assertEquals("Modified Unsaved Name", viewModel.state.value.name)
|
||||
|
||||
// Re-invoking initialize with same sceneId should NOT overwrite draft
|
||||
viewModel.initialize(existingScene.id)
|
||||
advanceUntilIdle()
|
||||
assertEquals("Modified Unsaved Name", viewModel.state.value.name)
|
||||
}
|
||||
}
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package com.flagship.abox.manager.ui.scene
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.ProviderProfile
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.ProviderType
|
||||
import com.flagship.abox.manager.domain.Scene
|
||||
import com.flagship.abox.manager.domain.SceneAction
|
||||
import com.flagship.abox.manager.domain.SceneActionId
|
||||
import com.flagship.abox.manager.domain.SceneColor
|
||||
import com.flagship.abox.manager.domain.SceneIcon
|
||||
import com.flagship.abox.manager.domain.SceneId
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import com.flagship.abox.manager.testing.MainDispatcherRule
|
||||
import com.flagship.abox.manager.testing.fakes.FakeInfraredGateway
|
||||
import com.flagship.abox.manager.testing.fakes.FakeProviderProfileStore
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSceneExecutor
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSceneRepository
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSocketGateway
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SceneExecutionViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val profileStore = FakeProviderProfileStore()
|
||||
private val sceneRepository = FakeSceneRepository()
|
||||
private val fakeSocket = FakeSocketGateway()
|
||||
private val fakeInfrared = FakeInfraredGateway()
|
||||
private val sceneExecutor = FakeSceneExecutor(fakeSocket, fakeInfrared)
|
||||
|
||||
private val profileId = ProviderProfileId("local-experience")
|
||||
private val testScene = Scene(
|
||||
id = SceneId("local-experience:scene-1"),
|
||||
profileId = profileId,
|
||||
name = "Execution Test Scene",
|
||||
icon = SceneIcon.HOME,
|
||||
color = SceneColor("#FF8A65"),
|
||||
actions = listOf(
|
||||
SceneAction.SocketPower(
|
||||
id = SceneActionId("local-experience:action-1"),
|
||||
profileId = profileId,
|
||||
deviceName = DeviceName("socket-1"),
|
||||
powerState = SocketPowerState.ON,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun setupProfile() {
|
||||
profileStore.saveProfile(ProviderProfile(profileId, ProviderType.LOCAL_EXPERIENCE, "Local"))
|
||||
profileStore.setActiveProfile(profileId)
|
||||
sceneRepository.setScenes(profileId, listOf(testScene))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializeLoadsSceneStateReady() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneExecutionViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
viewModel.initialize(testScene.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(viewModel.uiState.value is SceneExecutionUiState.Ready)
|
||||
val ready = viewModel.uiState.value as SceneExecutionUiState.Ready
|
||||
assertEquals(testScene.id, ready.scene.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun executeRunsSceneAndTransitionsToCompleted() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneExecutionViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
viewModel.initialize(testScene.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.execute()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(viewModel.uiState.value is SceneExecutionUiState.Completed)
|
||||
val completed = viewModel.uiState.value as SceneExecutionUiState.Completed
|
||||
assertEquals(testScene.id, completed.result.scene.id)
|
||||
assertEquals(1, completed.result.results.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initializeIsIdempotentAndPreservesCompletedState() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneExecutionViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
viewModel.initialize(testScene.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.execute()
|
||||
advanceUntilIdle()
|
||||
assertTrue(viewModel.uiState.value is SceneExecutionUiState.Completed)
|
||||
|
||||
// Re-initializing with same sceneId should preserve Completed state
|
||||
viewModel.initialize(testScene.id)
|
||||
advanceUntilIdle()
|
||||
assertTrue(viewModel.uiState.value is SceneExecutionUiState.Completed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun executeFailsWhenActiveProfileMismatches() = runTest {
|
||||
setupProfile()
|
||||
val viewModel = SceneExecutionViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
viewModel.initialize(testScene.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Switch to a different profile
|
||||
profileStore.setActiveProfile(ProviderProfileId("different-profile"))
|
||||
|
||||
viewModel.execute()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(viewModel.uiState.value is SceneExecutionUiState.Failed)
|
||||
val failed = viewModel.uiState.value as SceneExecutionUiState.Failed
|
||||
assertEquals("Active profile mismatch", failed.message)
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package com.flagship.abox.manager.ui.scene
|
||||
|
||||
import com.flagship.abox.manager.domain.AutomationType
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.ProviderProfile
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.ProviderType
|
||||
import com.flagship.abox.manager.domain.Scene
|
||||
import com.flagship.abox.manager.domain.SceneAction
|
||||
import com.flagship.abox.manager.domain.SceneActionId
|
||||
import com.flagship.abox.manager.domain.SceneColor
|
||||
import com.flagship.abox.manager.domain.SceneIcon
|
||||
import com.flagship.abox.manager.domain.SceneId
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import com.flagship.abox.manager.testing.MainDispatcherRule
|
||||
import com.flagship.abox.manager.testing.fakes.FakeInfraredGateway
|
||||
import com.flagship.abox.manager.testing.fakes.FakeProviderProfileStore
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSceneExecutor
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSceneRepository
|
||||
import com.flagship.abox.manager.testing.fakes.FakeSocketGateway
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SceneListViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val profileStore = FakeProviderProfileStore()
|
||||
private val sceneRepository = FakeSceneRepository()
|
||||
private val socketGateway = FakeSocketGateway()
|
||||
private val infraredGateway = FakeInfraredGateway()
|
||||
private val sceneExecutor = FakeSceneExecutor(socketGateway, infraredGateway)
|
||||
|
||||
private val profileId = ProviderProfileId("local-experience")
|
||||
private val manualScene = Scene(
|
||||
id = SceneId("local-experience:scene-1"),
|
||||
profileId = profileId,
|
||||
name = "Manual Scene",
|
||||
type = AutomationType.MANUAL,
|
||||
icon = SceneIcon.HOME,
|
||||
color = SceneColor("#FF8A65"),
|
||||
actions = listOf(
|
||||
SceneAction.SocketPower(
|
||||
id = SceneActionId("local-experience:action-1"),
|
||||
profileId = profileId,
|
||||
deviceName = DeviceName("socket-1"),
|
||||
powerState = SocketPowerState.ON,
|
||||
),
|
||||
),
|
||||
)
|
||||
private val autoScene = Scene(
|
||||
id = SceneId("local-experience:scene-2"),
|
||||
profileId = profileId,
|
||||
name = "Auto Rule",
|
||||
type = AutomationType.AUTOMATIC,
|
||||
enabled = true,
|
||||
icon = SceneIcon.THERMOSTAT,
|
||||
color = SceneColor("#26A69A"),
|
||||
actions = listOf(
|
||||
SceneAction.Delay(
|
||||
id = SceneActionId("local-experience:action-2"),
|
||||
profileId = profileId,
|
||||
durationSeconds = 5,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun loadsScenesForActiveProfile() = runTest {
|
||||
profileStore.saveProfile(ProviderProfile(profileId, ProviderType.LOCAL_EXPERIENCE, "Local"))
|
||||
profileStore.setActiveProfile(profileId)
|
||||
sceneRepository.setScenes(profileId, listOf(manualScene, autoScene))
|
||||
|
||||
val viewModel = SceneListViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.scenes.collect {}
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(2, viewModel.scenes.value.size)
|
||||
assertEquals("Manual Scene", viewModel.scenes.value[0].name)
|
||||
assertEquals("Auto Rule", viewModel.scenes.value[1].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterTabFiltersScenes() = runTest {
|
||||
profileStore.saveProfile(ProviderProfile(profileId, ProviderType.LOCAL_EXPERIENCE, "Local"))
|
||||
profileStore.setActiveProfile(profileId)
|
||||
sceneRepository.setScenes(profileId, listOf(manualScene, autoScene))
|
||||
|
||||
val viewModel = SceneListViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.scenes.collect {}
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.selectTab(SceneFilterTab.MANUAL)
|
||||
advanceUntilIdle()
|
||||
assertEquals(1, viewModel.scenes.value.size)
|
||||
assertEquals(AutomationType.MANUAL, viewModel.scenes.value[0].type)
|
||||
|
||||
viewModel.selectTab(SceneFilterTab.AUTOMATIC)
|
||||
advanceUntilIdle()
|
||||
assertEquals(1, viewModel.scenes.value.size)
|
||||
assertEquals(AutomationType.AUTOMATIC, viewModel.scenes.value[0].type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toggleEnabledUpdatesSceneInRepository() = runTest {
|
||||
profileStore.saveProfile(ProviderProfile(profileId, ProviderType.LOCAL_EXPERIENCE, "Local"))
|
||||
profileStore.setActiveProfile(profileId)
|
||||
sceneRepository.setScenes(profileId, listOf(autoScene))
|
||||
|
||||
val viewModel = SceneListViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.scenes.collect {}
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.toggleEnabled(autoScene)
|
||||
advanceUntilIdle()
|
||||
|
||||
val updated = viewModel.scenes.value.first()
|
||||
assertFalse(updated.enabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSceneRemovesFromRepositoryAndClearsPending() = runTest {
|
||||
profileStore.saveProfile(ProviderProfile(profileId, ProviderType.LOCAL_EXPERIENCE, "Local"))
|
||||
profileStore.setActiveProfile(profileId)
|
||||
sceneRepository.setScenes(profileId, listOf(manualScene))
|
||||
|
||||
val viewModel = SceneListViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.scenes.collect {}
|
||||
}
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.markPendingDelete(manualScene.id)
|
||||
assertEquals(manualScene.id, viewModel.pendingDelete.value)
|
||||
|
||||
viewModel.deleteScene(manualScene.id)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertNull(viewModel.pendingDelete.value)
|
||||
assertTrue(viewModel.scenes.value.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelDeleteClearsPendingState() = runTest {
|
||||
val viewModel = SceneListViewModel(sceneRepository, sceneExecutor, profileStore)
|
||||
viewModel.markPendingDelete(manualScene.id)
|
||||
assertEquals(manualScene.id, viewModel.pendingDelete.value)
|
||||
|
||||
viewModel.cancelDelete()
|
||||
assertNull(viewModel.pendingDelete.value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user