Play held-title themes only while their page is resumed
This commit is contained in:
parent
4af1898fd2
commit
2f3cdf2339
11 changed files with 276 additions and 14 deletions
|
|
@ -50,7 +50,7 @@ class ConductarrTitleSurfaceTest {
|
|||
runBlocking { session.authenticate() }
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.findHeld(any(), any()) } returns null
|
||||
val viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), mockk<NavigationManager>(relaxed = true), session, mockk<ServerRepository>(relaxed = true))
|
||||
val viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), mockk<NavigationManager>(relaxed = true), session, mockk<ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
val revokedLookup = CompletableDeferred<java.util.UUID?>()
|
||||
try {
|
||||
val route = titleRoute("movie:42")
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class DiscoverRefreshMotionTest {
|
|||
runBlocking { session.authenticate() }
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.findHeld(any(), any()) } returns null
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), NavigationManager(), session, mockk(relaxed = true))
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), NavigationManager(), session, mockk(relaxed = true), mockk(relaxed = true))
|
||||
val person = ShellPerson("shared", "Shared", "S", false)
|
||||
val route = titleRoute("tv:44")
|
||||
val shell = ShellState(listOf(person), person.key).apply { pickerVisible = false; enterDestination("discover"); enterDestination(route) }
|
||||
|
|
@ -94,7 +94,7 @@ class DiscoverRefreshMotionTest {
|
|||
@Test fun loadedFixtureRefreshKeepsTheSameReadyHeaderAndCatalogueIdentity() {
|
||||
ConductarrSession(MemoryCacheStore()).use { session ->
|
||||
val model = WolfskinTitleViewModel(rule.activity, mockk(relaxed = true), TitleFixtureProvider(rule.activity),
|
||||
NavigationManager(), session, mockk(relaxed = true))
|
||||
NavigationManager(), session, mockk(relaxed = true), mockk(relaxed = true))
|
||||
val person = ShellPerson("justin", "Justin", "J", true)
|
||||
val route = titleRoute("movie-the-amazing-spider-man-2012")
|
||||
val shell = ShellState(listOf(person), person.key).apply { pickerVisible = false; enterDestination("discover"); enterDestination(route) }
|
||||
|
|
@ -145,7 +145,7 @@ class DiscoverRefreshMotionTest {
|
|||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.readSaved(any(), any()) } returns null
|
||||
coEvery { facade.read(any(), any(), any()) } returns held
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), NavigationManager(), session, mockk(relaxed = true))
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), NavigationManager(), session, mockk(relaxed = true), mockk(relaxed = true))
|
||||
val person = ShellPerson("shared", "Shared", "S", false)
|
||||
val route = titleRoute(id.toString())
|
||||
val shell = ShellState(listOf(person), person.key).apply { pickerVisible = false; enterDestination(route) }
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class DiscoverTitleTimingTest {
|
|||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.findHeld(any(), any()) } returns null
|
||||
val navigation = NavigationManager().apply { replace(Destination.Wolfskin("discover")) }
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), navigation, session, mockk(relaxed = true))
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), navigation, session, mockk(relaxed = true), mockk(relaxed = true))
|
||||
val shell = ShellState(listOf(ShellPerson("shared", "Shared", "S", false)), "shared").apply {
|
||||
pickerVisible = false; pickerLaunch = false; enterDestination("discover")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ class PersonSwitchThreadingTest {
|
|||
val cache = mockk<org.justinvisser.wolfskin.tv.read.HomeLibraryCache>(relaxed = true)
|
||||
every { cache.read(any()) } returns null
|
||||
val homeModel = WolfskinHomeViewModel(context, home, cache, mockk(relaxed = true), WolfskinSeeAllLists(), conductarr)
|
||||
val titleModel = WolfskinTitleViewModel(context, mockk(), mockk(), mockk(relaxed = true), conductarr, repository)
|
||||
val titleModel = WolfskinTitleViewModel(context, mockk(), mockk(), mockk(relaxed = true), conductarr, repository, mockk(relaxed = true))
|
||||
try {
|
||||
conductarr.select("${server.id}:${user.id}", user.accessToken, fake.url)
|
||||
conductarr.authenticate()
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class TitleAsyncFocusTest {
|
|||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.readSaved(id, any()) } returns null
|
||||
coEvery { facade.read(id, any(), any()) } coAnswers { ready.await() }
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), navigation, ConductarrSession(org.justinvisser.wolfskin.conductarr.MemoryCacheStore()), mockk<com.github.damontecres.wholphin.data.ServerRepository>(relaxed = true))
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), navigation, ConductarrSession(org.justinvisser.wolfskin.conductarr.MemoryCacheStore()), mockk<com.github.damontecres.wholphin.data.ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
val person = ShellPerson("justin", "Justin", "J", false)
|
||||
val route = titleRoute(id.toString())
|
||||
navigation.navigateTo(Destination.Wolfskin(route))
|
||||
|
|
@ -103,7 +103,7 @@ class TitleAsyncFocusTest {
|
|||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.readPersonSaved(id) } returns null
|
||||
coEvery { facade.readPerson(id, 0) } coAnswers { reply.await() }
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), navigation, ConductarrSession(org.justinvisser.wolfskin.conductarr.MemoryCacheStore()), mockk<com.github.damontecres.wholphin.data.ServerRepository>(relaxed = true))
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), navigation, ConductarrSession(org.justinvisser.wolfskin.conductarr.MemoryCacheStore()), mockk<com.github.damontecres.wholphin.data.ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
val viewer = ShellPerson("justin", "Justin", "J", false)
|
||||
val route = personRoute(id.toString())
|
||||
navigation.navigateTo(Destination.Wolfskin(route))
|
||||
|
|
@ -150,7 +150,7 @@ class TitleAsyncFocusTest {
|
|||
reads.incrementAndGet()
|
||||
throw java.io.IOException("offline")
|
||||
}
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), navigation, ConductarrSession(org.justinvisser.wolfskin.conductarr.MemoryCacheStore()), mockk<com.github.damontecres.wholphin.data.ServerRepository>(relaxed = true))
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), navigation, ConductarrSession(org.justinvisser.wolfskin.conductarr.MemoryCacheStore()), mockk<com.github.damontecres.wholphin.data.ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
val viewer = ShellPerson("justin", "Justin", "J", false)
|
||||
val route = titleRoute(id.toString())
|
||||
navigation.navigateTo(Destination.Wolfskin(route))
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class TitleEntryBackTest {
|
|||
}
|
||||
conductarr = ConductarrSession(MemoryCacheStore())
|
||||
viewModel = WolfskinTitleViewModel(activity, facade, mockk<TitleFixtureProvider>(), navigation, conductarr,
|
||||
mockk<ServerRepository>(relaxed = true))
|
||||
mockk<ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
shell.enterDestination(route)
|
||||
navigation.navigateTo(destination)
|
||||
val home = HomeSnapshot(listOf(HomeRow("hero", HomeRowKind.Hero, cards = listOf(HomeTitle("home-film", "Home film", "movie")))))
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class TitlePlaybackNavigationTest {
|
|||
episodes = listOf(first, second), related = listOf(series), cast = listOf(TitlePerson("actor", "Actor")))
|
||||
}
|
||||
val session = ConductarrSession(MemoryCacheStore())
|
||||
val model = WolfskinTitleViewModel(RuntimeEnvironment.getApplication(), facade, mockk(), navigation, session, mockk<ServerRepository>(relaxed = true))
|
||||
val model = WolfskinTitleViewModel(RuntimeEnvironment.getApplication(), facade, mockk(), navigation, session, mockk<ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
fun open(id: UUID) = model.load(titleRoute(id.toString()), person, preferences, fixtures = false, force = true)
|
||||
try {
|
||||
navigation.navigateTo(Destination.Wolfskin(titleRoute(seriesId.toString())))
|
||||
|
|
@ -109,7 +109,7 @@ class TitlePlaybackNavigationTest {
|
|||
coEvery { facade.preparePlayback(firstId) } returns BaseItemDto(id = firstId, type = BaseItemKind.EPISODE,
|
||||
userData = playbackUserData(firstId, 960_780_000L, playCount = 1))
|
||||
val session = ConductarrSession(MemoryCacheStore())
|
||||
val model = WolfskinTitleViewModel(RuntimeEnvironment.getApplication(), facade, mockk(), navigation, session, mockk<ServerRepository>(relaxed = true))
|
||||
val model = WolfskinTitleViewModel(RuntimeEnvironment.getApplication(), facade, mockk(), navigation, session, mockk<ServerRepository>(relaxed = true), mockk(relaxed = true))
|
||||
try {
|
||||
model.load(titleRoute(seriesId.toString()), person, preferences, fixtures = false)
|
||||
advanceUntilIdle()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TitleRequestSafetyTest {
|
|||
runBlocking { session.authenticate() }
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.findHeld(any(), any()) } returns null
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), NavigationManager(), session, mockk(relaxed = true))
|
||||
val model = WolfskinTitleViewModel(rule.activity, facade, mockk(), NavigationManager(), session, mockk(relaxed = true), mockk(relaxed = true))
|
||||
val person = ShellPerson("shared", "Shared", "S", true)
|
||||
val route = titleRoute("movie:42")
|
||||
val shell = ShellState(listOf(person), person.key).apply { pickerVisible = false; enterDestination("discover"); enterDestination(route) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.InterfacePreferences
|
||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
|
||||
import org.justinvisser.wolfskin.design.ScreenEntry
|
||||
import org.justinvisser.wolfskin.design.ShellPerson
|
||||
import org.justinvisser.wolfskin.design.ShellState
|
||||
import org.justinvisser.wolfskin.design.TitleItem
|
||||
import org.justinvisser.wolfskin.design.TitlePageData
|
||||
import org.justinvisser.wolfskin.design.WolfskinTheme
|
||||
import org.justinvisser.wolfskin.tv.read.TitleReadFacade
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import java.util.UUID
|
||||
|
||||
/** Real Compose/lifecycle transitions must relinquish theme ownership before another page or video. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class TitleThemeMusicTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private val movieId = UUID.fromString("00000000-0000-0000-0000-000000000041")
|
||||
private val seriesId = UUID.fromString("00000000-0000-0000-0000-000000000042")
|
||||
private val episodeId = UUID.fromString("00000000-0000-0000-0000-000000000043")
|
||||
private val movie = TitleItem(movieId.toString(), "Held movie", "movie", held = true)
|
||||
private val playback = ThemePlayback()
|
||||
private val item = mutableStateOf<TitleItem?>(movie)
|
||||
private val preferences = mutableStateOf(preferences(ThemeSongVolume.HIGH))
|
||||
private val active = mutableStateOf(true)
|
||||
private val visible = mutableStateOf(true)
|
||||
private val viewer = mutableStateOf("server:justin")
|
||||
private val lifecycleOwner = ThemeLifecycleOwner()
|
||||
private var model: WolfskinTitleViewModel? = null
|
||||
private var session: ConductarrSession? = null
|
||||
|
||||
@After fun dispose() {
|
||||
model?.viewModelScope?.cancel()
|
||||
session?.close()
|
||||
}
|
||||
|
||||
@Test fun heldMovieStartsOnceAcrossRefreshAndRecreationNeverOverlaps() {
|
||||
mountEffect()
|
||||
assertEquals(listOf("play:$movieId"), playback.events)
|
||||
rule.runOnIdle { item.value = movie.copy(name = "Refreshed metadata") }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId"), playback.events)
|
||||
rule.runOnIdle { visible.value = false }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop"), playback.events)
|
||||
rule.runOnIdle { visible.value = true }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop", "play:$movieId"), playback.events)
|
||||
}
|
||||
|
||||
@Test fun episodeUsesItsSeriesAndMissingSeriesStaysSilent() {
|
||||
item.value = movie.copy(id = episodeId.toString(), kind = "episode", seriesId = seriesId.toString())
|
||||
mountEffect()
|
||||
assertEquals(listOf("play:$seriesId"), playback.events)
|
||||
rule.runOnIdle { item.value = item.value!!.copy(seriesId = null) }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$seriesId", "stop"), playback.events)
|
||||
}
|
||||
|
||||
@Test fun catalogueOnlyAndDisabledTitlesStaySilentAndDisablingStopsPlayingTheme() {
|
||||
item.value = movie.copy(id = "movie:42", held = false)
|
||||
mountEffect()
|
||||
assertEquals(emptyList<String>(), playback.events)
|
||||
rule.runOnIdle {
|
||||
item.value = movie
|
||||
preferences.value = preferences(ThemeSongVolume.DISABLED)
|
||||
}
|
||||
rule.waitForIdle()
|
||||
assertEquals(emptyList<String>(), playback.events)
|
||||
rule.runOnIdle { preferences.value = preferences(ThemeSongVolume.HIGH) }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId"), playback.events)
|
||||
rule.runOnIdle { preferences.value = preferences(ThemeSongVolume.DISABLED) }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop"), playback.events)
|
||||
}
|
||||
|
||||
@Test fun pausedPageStopsAndOnlyResumedPageRestarts() {
|
||||
rule.runOnIdle { lifecycleOwner.registry.currentState = Lifecycle.State.STARTED }
|
||||
mountEffect(resumed = false)
|
||||
assertEquals(emptyList<String>(), playback.events)
|
||||
rule.runOnIdle { lifecycleOwner.registry.currentState = Lifecycle.State.RESUMED }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId"), playback.events)
|
||||
rule.runOnIdle { lifecycleOwner.registry.currentState = Lifecycle.State.CREATED }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop"), playback.events)
|
||||
rule.runOnIdle { lifecycleOwner.registry.currentState = Lifecycle.State.RESUMED }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop", "play:$movieId"), playback.events)
|
||||
}
|
||||
|
||||
@Test fun outgoingEntryStopsEvenWhileStillComposedAndCanResumeOnBack() {
|
||||
mountEffect()
|
||||
rule.runOnIdle { active.value = false }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop"), playback.events)
|
||||
rule.runOnIdle { item.value = movie.copy(overview = "Exit animation still composed") }
|
||||
rule.waitForIdle()
|
||||
assertNull(playback.playing)
|
||||
rule.runOnIdle { active.value = true }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop", "play:$movieId"), playback.events)
|
||||
}
|
||||
|
||||
@Test fun personSwitchReleasesOldThemeAndSignOutDisposesIt() {
|
||||
mountEffect()
|
||||
rule.runOnIdle { viewer.value = "server:shared" }
|
||||
rule.waitForIdle()
|
||||
assertEquals(listOf("play:$movieId", "stop", "play:$movieId"), playback.events)
|
||||
rule.runOnIdle { visible.value = false }
|
||||
rule.waitForIdle()
|
||||
assertNull(playback.playing)
|
||||
assertEquals("stop", playback.events.last())
|
||||
}
|
||||
|
||||
@Test fun realEpisodePageStartsSeriesThemeAndStopsBeforePlaybackNavigation() {
|
||||
val series = movie.copy(id = seriesId.toString(), name = "Series", kind = "tv")
|
||||
val episode = movie.copy(id = episodeId.toString(), name = "Episode", kind = "episode", seriesId = seriesId.toString())
|
||||
val navigation = NavigationManager()
|
||||
val route = titleRoute(episodeId.toString())
|
||||
val destination = Destination.Wolfskin(route)
|
||||
val person = ShellPerson("justin", "Justin", "J", false)
|
||||
val shell = ShellState(listOf(person), person.key).apply {
|
||||
pickerVisible = false; pickerLaunch = false; enterDestination(route)
|
||||
}
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.readSaved(episodeId, any()) } returns null
|
||||
coEvery { facade.read(episodeId, any(), any()) } returns TitlePageData(series, playTarget = episode, watchTarget = episode)
|
||||
coEvery { facade.preparePlayback(episodeId) } returns BaseItemDto(id = episodeId, type = BaseItemKind.EPISODE)
|
||||
val conductarr = ConductarrSession(MemoryCacheStore()).also { session = it }
|
||||
val titleModel = WolfskinTitleViewModel(rule.activity, facade, mockk(), navigation, conductarr,
|
||||
mockk(relaxed = true), playback.player).also { model = it }
|
||||
rule.runOnIdle { navigation.navigateTo(destination) }
|
||||
rule.setContent {
|
||||
WolfskinTheme {
|
||||
ScreenEntry(shell, route, navigation.backStack.lastOrNull() == destination) {
|
||||
TitleDestination(route, shell, preferences.value, viewModel = titleModel, fixtures = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
rule.waitUntil(5_000) { playback.playing == seriesId }
|
||||
assertEquals(listOf("play:$seriesId"), playback.events)
|
||||
playback.onStop = { if (playback.playing != null) assertEquals(destination, navigation.backStack.last()) }
|
||||
rule.runOnIdle { titleModel.title!!.click("header", 0) }
|
||||
rule.waitUntil(5_000) { navigation.backStack.last() is Destination.Playback }
|
||||
playback.onStop = {}
|
||||
rule.waitForIdle()
|
||||
assertEquals(Destination.Playback(episodeId, 0L), navigation.backStack.last())
|
||||
assertNull(playback.playing)
|
||||
assertEquals(1, playback.events.count { it.startsWith("play:") })
|
||||
}
|
||||
|
||||
private fun mountEffect(resumed: Boolean = true) {
|
||||
if (resumed) rule.runOnIdle { lifecycleOwner.registry.currentState = Lifecycle.State.RESUMED }
|
||||
rule.setContent {
|
||||
CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
|
||||
if (visible.value) TitleThemeMusic(item.value, preferences.value, playback.player, active.value, viewer.value)
|
||||
}
|
||||
}
|
||||
rule.waitForIdle()
|
||||
}
|
||||
|
||||
private fun preferences(volume: ThemeSongVolume) = UserPreferences(
|
||||
AppPreferences.newBuilder().setInterfacePreferences(
|
||||
InterfacePreferences.newBuilder().setPlayThemeSongs(volume).build(),
|
||||
).build(), null,
|
||||
)
|
||||
|
||||
private class ThemeLifecycleOwner : LifecycleOwner {
|
||||
val registry = LifecycleRegistry(this)
|
||||
override val lifecycle: Lifecycle get() = registry
|
||||
}
|
||||
|
||||
private class ThemePlayback {
|
||||
val player = mockk<ThemeSongPlayer>()
|
||||
val events = mutableListOf<String>()
|
||||
var playing: UUID? = null
|
||||
var onStop: () -> Unit = {}
|
||||
init {
|
||||
every { player.playThemeFor(any()) } answers {
|
||||
assertNull("Two title entries must never own theme playback simultaneously", playing)
|
||||
playing = firstArg()
|
||||
events += "play:$playing"
|
||||
}
|
||||
every { player.stop() } answers {
|
||||
onStop()
|
||||
playing = null
|
||||
events += "stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue