mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
feat(press-controls): Add media and noise-control actions
Adds Stop, Fast Forward, Rewind, Mute, Cycle Noise Control, and Toggle Transparency as gesture targets. Migrates StemAction from enum to sealed interface with polymorphic serialization to unlock future parameterized actions. ANC actions are gated per-device capability and reuse the existing listening-mode cycle mask.
This commit is contained in:
+1
-1
@@ -346,7 +346,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
id = testAddress,
|
||||
label = "Test",
|
||||
address = testAddress,
|
||||
stemActions = StemActionsConfig(leftLong = StemAction.PLAY_PAUSE),
|
||||
stemActions = StemActionsConfig(leftLong = StemAction.PlayPause),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+20
-20
@@ -123,45 +123,45 @@ class PressControlsViewModelTest : BaseTest() {
|
||||
vm.initialize(testProfileId)
|
||||
vm.state.first()
|
||||
|
||||
vm.setLeftSingle(StemAction.PLAY_PAUSE)
|
||||
vm.setLeftSingle(StemAction.PlayPause)
|
||||
|
||||
coVerify { profilesRepo.updateAppleProfile(testProfileId, any()) }
|
||||
profilesFlow.value
|
||||
.filterIsInstance<AppleDeviceProfile>()
|
||||
.first().stemActions.leftSingle shouldBe StemAction.PLAY_PAUSE
|
||||
.first().stemActions.leftSingle shouldBe StemAction.PlayPause
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLeftSingle to non-NONE as free user navigates to Upgrade and does not mutate`() = runVmTest {
|
||||
fun `setLeftSingle to non-None as free user navigates to Upgrade and does not mutate`() = runVmTest {
|
||||
every { upgradeInfoFlow.value.isPro } returns false
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testProfileId)
|
||||
vm.state.first()
|
||||
|
||||
vm.setLeftSingle(StemAction.PLAY_PAUSE)
|
||||
vm.setLeftSingle(StemAction.PlayPause)
|
||||
|
||||
coVerify(exactly = 0) { profilesRepo.updateAppleProfile(testProfileId, any()) }
|
||||
profilesFlow.value
|
||||
.filterIsInstance<AppleDeviceProfile>()
|
||||
.first().stemActions.leftSingle shouldBe StemAction.NONE
|
||||
.first().stemActions.leftSingle shouldBe StemAction.None
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLeftSingle to NONE as free user is allowed and clears existing mapping`() = runVmTest {
|
||||
fun `setLeftSingle to None as free user is allowed and clears existing mapping`() = runVmTest {
|
||||
every { upgradeInfoFlow.value.isPro } returns false
|
||||
profilesFlow.value = listOf(makeProfile(StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE)))
|
||||
profilesFlow.value = listOf(makeProfile(StemActionsConfig(leftSingle = StemAction.PlayPause)))
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testProfileId)
|
||||
vm.state.first()
|
||||
|
||||
vm.setLeftSingle(StemAction.NONE)
|
||||
vm.setLeftSingle(StemAction.None)
|
||||
|
||||
coVerify { profilesRepo.updateAppleProfile(testProfileId, any()) }
|
||||
profilesFlow.value
|
||||
.filterIsInstance<AppleDeviceProfile>()
|
||||
.first().stemActions.leftSingle shouldBe StemAction.NONE
|
||||
.first().stemActions.leftSingle shouldBe StemAction.None
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -172,52 +172,52 @@ class PressControlsViewModelTest : BaseTest() {
|
||||
vm.initialize(testProfileId)
|
||||
vm.state.first()
|
||||
|
||||
vm.setLeftSingle(StemAction.NONE) // already NONE
|
||||
vm.setLeftSingle(StemAction.None) // already None
|
||||
|
||||
coVerify(exactly = 0) { profilesRepo.updateAppleProfile(testProfileId, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLeftSingle assigns NO_ACTION to right side when right was NONE`() = runVmTest {
|
||||
fun `setLeftSingle assigns NoAction to right side when right was None`() = runVmTest {
|
||||
every { upgradeInfoFlow.value.isPro } returns true
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testProfileId)
|
||||
vm.state.first()
|
||||
|
||||
vm.setLeftSingle(StemAction.PLAY_PAUSE)
|
||||
vm.setLeftSingle(StemAction.PlayPause)
|
||||
|
||||
val updated = profilesFlow.value
|
||||
.filterIsInstance<AppleDeviceProfile>()
|
||||
.first().stemActions
|
||||
updated.leftSingle shouldBe StemAction.PLAY_PAUSE
|
||||
updated.rightSingle shouldBe StemAction.NO_ACTION
|
||||
updated.leftSingle shouldBe StemAction.PlayPause
|
||||
updated.rightSingle shouldBe StemAction.NoAction
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLeftSingle to NONE clears the opposite side too`() = runVmTest {
|
||||
fun `setLeftSingle to None clears the opposite side too`() = runVmTest {
|
||||
every { upgradeInfoFlow.value.isPro } returns true
|
||||
profilesFlow.value = listOf(
|
||||
makeProfile(StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE, rightSingle = StemAction.NO_ACTION))
|
||||
makeProfile(StemActionsConfig(leftSingle = StemAction.PlayPause, rightSingle = StemAction.NoAction))
|
||||
)
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testProfileId)
|
||||
vm.state.first()
|
||||
|
||||
vm.setLeftSingle(StemAction.NONE)
|
||||
vm.setLeftSingle(StemAction.None)
|
||||
|
||||
val updated = profilesFlow.value
|
||||
.filterIsInstance<AppleDeviceProfile>()
|
||||
.first().stemActions
|
||||
updated.leftSingle shouldBe StemAction.NONE
|
||||
updated.rightSingle shouldBe StemAction.NONE
|
||||
updated.leftSingle shouldBe StemAction.None
|
||||
updated.rightSingle shouldBe StemAction.None
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resetAll wipes stemActions on the profile`() = runVmTest {
|
||||
profilesFlow.value = listOf(
|
||||
makeProfile(StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE, rightLong = StemAction.VOLUME_UP))
|
||||
makeProfile(StemActionsConfig(leftSingle = StemAction.PlayPause, rightLong = StemAction.VolumeUp))
|
||||
)
|
||||
|
||||
val vm = createViewModel()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package eu.darken.capod.monitor.core.aap
|
||||
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting.AncMode.Value
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class NextGestureAncModeTest : BaseTest() {
|
||||
|
||||
private val allFour = listOf(Value.OFF, Value.ON, Value.TRANSPARENCY, Value.ADAPTIVE)
|
||||
|
||||
private fun state(
|
||||
current: Value,
|
||||
supported: List<Value> = allFour,
|
||||
mask: Int? = 0x0F,
|
||||
allowOff: Boolean = true,
|
||||
) = EffectiveAncState(
|
||||
current = current,
|
||||
supported = supported,
|
||||
cycleMask = mask,
|
||||
allowOffEnabled = allowOff,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `null mask is a no-op`() {
|
||||
nextGestureAncMode(state(current = Value.ON, mask = null)).shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zero mask is a no-op`() {
|
||||
nextGestureAncMode(state(current = Value.ON, mask = 0x00)).shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `single-mode mask is a no-op`() {
|
||||
// Only ON enabled
|
||||
nextGestureAncMode(state(current = Value.ON, mask = 0x02)).shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two-mode mask cycles between them`() {
|
||||
// ON + TRANSPARENCY
|
||||
nextGestureAncMode(state(current = Value.ON, mask = 0x06)) shouldBe Value.TRANSPARENCY
|
||||
nextGestureAncMode(state(current = Value.TRANSPARENCY, mask = 0x06)) shouldBe Value.ON
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `four-mode mask walks the full cycle`() {
|
||||
nextGestureAncMode(state(current = Value.OFF)) shouldBe Value.ON
|
||||
nextGestureAncMode(state(current = Value.ON)) shouldBe Value.TRANSPARENCY
|
||||
nextGestureAncMode(state(current = Value.TRANSPARENCY)) shouldBe Value.ADAPTIVE
|
||||
nextGestureAncMode(state(current = Value.ADAPTIVE)) shouldBe Value.OFF
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `allowOff=false strips OFF from the cycle`() {
|
||||
nextGestureAncMode(state(current = Value.ADAPTIVE, allowOff = false)) shouldBe Value.ON
|
||||
// Current=OFF is out-of-cycle when allowOff=false → jumps to first cycle mode
|
||||
nextGestureAncMode(state(current = Value.OFF, allowOff = false)) shouldBe Value.ON
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `current out of cycle jumps to first cycle mode`() {
|
||||
// Cycle = TRANSPARENCY + ADAPTIVE only, but current is ON
|
||||
nextGestureAncMode(state(current = Value.ON, mask = 0x0C)) shouldBe Value.TRANSPARENCY
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unsupported modes are excluded even if mask enables them`() {
|
||||
// Device only supports OFF + ON, but mask says OFF+ON+TRANSPARENCY
|
||||
val supported = listOf(Value.OFF, Value.ON)
|
||||
nextGestureAncMode(state(current = Value.ON, supported = supported, mask = 0x07)) shouldBe Value.OFF
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ class StemConfigSenderTest : BaseTest() {
|
||||
// A has SINGLE only → mask 0x01. B has LONG only → mask 0x08.
|
||||
val profilesFlow = MutableStateFlow<List<DeviceProfile>>(
|
||||
listOf(
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE)),
|
||||
profile(addressB, StemActionsConfig(rightLong = StemAction.VOLUME_UP)),
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.PlayPause)),
|
||||
profile(addressB, StemActionsConfig(rightLong = StemAction.VolumeUp)),
|
||||
)
|
||||
)
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
@@ -77,8 +77,8 @@ class StemConfigSenderTest : BaseTest() {
|
||||
}
|
||||
val profilesFlow = MutableStateFlow<List<DeviceProfile>>(
|
||||
listOf(
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE)),
|
||||
profile(addressB, StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE)),
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.PlayPause)),
|
||||
profile(addressB, StemActionsConfig(leftSingle = StemAction.PlayPause)),
|
||||
)
|
||||
)
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
@@ -132,10 +132,10 @@ class StemConfigSenderTest : BaseTest() {
|
||||
profile(
|
||||
addressA,
|
||||
StemActionsConfig(
|
||||
leftSingle = StemAction.PLAY_PAUSE,
|
||||
rightDouble = StemAction.NEXT_TRACK,
|
||||
leftTriple = StemAction.VOLUME_UP,
|
||||
rightLong = StemAction.VOLUME_DOWN,
|
||||
leftSingle = StemAction.PlayPause,
|
||||
rightDouble = StemAction.NextTrack,
|
||||
leftTriple = StemAction.VolumeUp,
|
||||
rightLong = StemAction.VolumeDown,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.view.KeyEvent
|
||||
import eu.darken.capod.common.MediaControl
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
@@ -14,6 +17,7 @@ import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
@@ -41,8 +45,8 @@ class StemPressReactionTest : BaseTest() {
|
||||
every { stemPressEvents } returns events
|
||||
}
|
||||
val profiles = listOf<DeviceProfile>(
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE)),
|
||||
profile(addressB, StemActionsConfig(leftSingle = StemAction.NEXT_TRACK)),
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.PlayPause)),
|
||||
profile(addressB, StemActionsConfig(leftSingle = StemAction.NextTrack)),
|
||||
)
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
every { this@mockk.profiles } returns flowOf(profiles)
|
||||
@@ -71,7 +75,7 @@ class StemPressReactionTest : BaseTest() {
|
||||
}
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
every { this@mockk.profiles } returns flowOf(
|
||||
listOf<DeviceProfile>(profile(addressA, StemActionsConfig(leftSingle = StemAction.PLAY_PAUSE)))
|
||||
listOf<DeviceProfile>(profile(addressA, StemActionsConfig(leftSingle = StemAction.PlayPause)))
|
||||
)
|
||||
}
|
||||
val mediaControl = mockk<MediaControl>(relaxed = true)
|
||||
@@ -88,7 +92,7 @@ class StemPressReactionTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NO_ACTION and NONE do not execute anything`() = runTest(UnconfinedTestDispatcher()) {
|
||||
fun `NoAction and None do not execute anything`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val events = MutableSharedFlow<Pair<String, StemPressEvent>>(extraBufferCapacity = 8)
|
||||
val aapManager = mockk<AapConnectionManager>(relaxed = true) {
|
||||
every { stemPressEvents } returns events
|
||||
@@ -99,8 +103,8 @@ class StemPressReactionTest : BaseTest() {
|
||||
profile(
|
||||
addressA,
|
||||
StemActionsConfig(
|
||||
leftSingle = StemAction.NONE,
|
||||
rightSingle = StemAction.NO_ACTION,
|
||||
leftSingle = StemAction.None,
|
||||
rightSingle = StemAction.NoAction,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -120,4 +124,151 @@ class StemPressReactionTest : BaseTest() {
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `new media-key actions dispatch correct keycodes`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val events = MutableSharedFlow<Pair<String, StemPressEvent>>(extraBufferCapacity = 8)
|
||||
val aapManager = mockk<AapConnectionManager>(relaxed = true) {
|
||||
every { stemPressEvents } returns events
|
||||
}
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
every { this@mockk.profiles } returns flowOf(
|
||||
listOf<DeviceProfile>(
|
||||
profile(
|
||||
addressA,
|
||||
StemActionsConfig(
|
||||
leftSingle = StemAction.Stop,
|
||||
leftDouble = StemAction.FastForward,
|
||||
leftTriple = StemAction.Rewind,
|
||||
leftLong = StemAction.MuteToggle,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
val mediaControl = mockk<MediaControl>(relaxed = true)
|
||||
val reaction = StemPressReaction(aapManager, profilesRepo, mediaControl)
|
||||
val job = launch { reaction.monitor().collect {} }
|
||||
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.SINGLE, StemPressEvent.Bud.LEFT))
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.DOUBLE, StemPressEvent.Bud.LEFT))
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.TRIPLE, StemPressEvent.Bud.LEFT))
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.LONG, StemPressEvent.Bud.LEFT))
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_STOP) }
|
||||
coVerify(exactly = 1) { mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) }
|
||||
coVerify(exactly = 1) { mediaControl.sendKey(KeyEvent.KEYCODE_MEDIA_REWIND) }
|
||||
coVerify(exactly = 1) { mediaControl.toggleMuteMusic() }
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CycleAnc sends SetAncMode with the next cycle mode`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val events = MutableSharedFlow<Pair<String, StemPressEvent>>(extraBufferCapacity = 8)
|
||||
val aapState = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.AncMode::class to AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.TRANSPARENCY,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
),
|
||||
),
|
||||
AapSetting.ListeningModeCycle::class to AapSetting.ListeningModeCycle(modeMask = 0x0E),
|
||||
),
|
||||
)
|
||||
val aapManager = mockk<AapConnectionManager>(relaxed = true) {
|
||||
every { stemPressEvents } returns events
|
||||
every { allStates } returns MutableStateFlow(mapOf(addressA to aapState))
|
||||
}
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
every { this@mockk.profiles } returns flowOf(
|
||||
listOf<DeviceProfile>(
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.CycleAnc))
|
||||
)
|
||||
)
|
||||
}
|
||||
val mediaControl = mockk<MediaControl>(relaxed = true)
|
||||
val reaction = StemPressReaction(aapManager, profilesRepo, mediaControl)
|
||||
val job = launch { reaction.monitor().collect {} }
|
||||
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.SINGLE, StemPressEvent.Bud.LEFT))
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
aapManager.sendCommand(addressA, AapCommand.SetAncMode(AapSetting.AncMode.Value.TRANSPARENCY))
|
||||
}
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleAncTransparency flips to ON when leaving TRANSPARENCY`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val events = MutableSharedFlow<Pair<String, StemPressEvent>>(extraBufferCapacity = 8)
|
||||
val aapState = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.AncMode::class to AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.TRANSPARENCY,
|
||||
supported = listOf(AapSetting.AncMode.Value.ON, AapSetting.AncMode.Value.TRANSPARENCY),
|
||||
),
|
||||
),
|
||||
)
|
||||
val aapManager = mockk<AapConnectionManager>(relaxed = true) {
|
||||
every { stemPressEvents } returns events
|
||||
every { allStates } returns MutableStateFlow(mapOf(addressA to aapState))
|
||||
}
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
every { this@mockk.profiles } returns flowOf(
|
||||
listOf<DeviceProfile>(
|
||||
profile(addressA, StemActionsConfig(leftSingle = StemAction.ToggleAncTransparency))
|
||||
)
|
||||
)
|
||||
}
|
||||
val mediaControl = mockk<MediaControl>(relaxed = true)
|
||||
val reaction = StemPressReaction(aapManager, profilesRepo, mediaControl)
|
||||
val job = launch { reaction.monitor().collect {} }
|
||||
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.SINGLE, StemPressEvent.Bud.LEFT))
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
aapManager.sendCommand(addressA, AapCommand.SetAncMode(AapSetting.AncMode.Value.ON))
|
||||
}
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CycleAnc is a no-op when model lacks listening-mode-cycle`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val events = MutableSharedFlow<Pair<String, StemPressEvent>>(extraBufferCapacity = 8)
|
||||
val aapManager = mockk<AapConnectionManager>(relaxed = true) {
|
||||
every { stemPressEvents } returns events
|
||||
every { allStates } returns MutableStateFlow(emptyMap())
|
||||
}
|
||||
val noCycleProfile = AppleDeviceProfile(
|
||||
label = "Test",
|
||||
model = PodModel.AIRPODS_GEN3,
|
||||
address = addressA,
|
||||
stemActions = StemActionsConfig(leftSingle = StemAction.CycleAnc),
|
||||
)
|
||||
val profilesRepo = mockk<DeviceProfilesRepo>(relaxed = true) {
|
||||
every { this@mockk.profiles } returns flowOf(listOf<DeviceProfile>(noCycleProfile))
|
||||
}
|
||||
val mediaControl = mockk<MediaControl>(relaxed = true)
|
||||
val reaction = StemPressReaction(aapManager, profilesRepo, mediaControl)
|
||||
val job = launch { reaction.monitor().collect {} }
|
||||
|
||||
events.emit(addressA to StemPressEvent(StemPressEvent.PressType.SINGLE, StemPressEvent.Bud.LEFT))
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { aapManager.sendCommand(any(), any<AapCommand.SetAncMode>()) }
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.darken.capod.reaction.core.stem
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class StemActionSerializationTest : BaseTest() {
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
classDiscriminator = "type"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every action round-trips through Json`() {
|
||||
for (action in StemAction.all) {
|
||||
val encoded = json.encodeToString(StemAction.serializer(), action)
|
||||
val decoded = json.decodeFromString(StemAction.serializer(), encoded)
|
||||
decoded shouldBe action
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wire form uses type discriminator with legacy-style names`() {
|
||||
json.encodeToString(StemAction.serializer(), StemAction.PlayPause) shouldBe """{"type":"PLAY_PAUSE"}"""
|
||||
json.encodeToString(StemAction.serializer(), StemAction.CycleAnc) shouldBe """{"type":"CYCLE_ANC"}"""
|
||||
json.encodeToString(StemAction.serializer(), StemAction.ToggleAncTransparency) shouldBe """{"type":"TOGGLE_ANC_TRANSPARENCY"}"""
|
||||
json.encodeToString(StemAction.serializer(), StemAction.None) shouldBe """{"type":"NONE"}"""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `StemActionsConfig round-trips with new actions`() {
|
||||
val original = StemActionsConfig(
|
||||
leftSingle = StemAction.PlayPause,
|
||||
rightSingle = StemAction.NoAction,
|
||||
leftLong = StemAction.CycleAnc,
|
||||
rightLong = StemAction.ToggleAncTransparency,
|
||||
leftDouble = StemAction.Stop,
|
||||
rightDouble = StemAction.FastForward,
|
||||
leftTriple = StemAction.Rewind,
|
||||
rightTriple = StemAction.MuteToggle,
|
||||
)
|
||||
val encoded = json.encodeToString(StemActionsConfig.serializer(), original)
|
||||
val decoded = json.decodeFromString(StemActionsConfig.serializer(), encoded)
|
||||
decoded shouldBe original
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user