mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
reorder the queue with the reorderable library
This commit is contained in:
@@ -1,159 +0,0 @@
|
||||
package com.subtracks.data.repo
|
||||
|
||||
import android.content.Context
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.SongListItem
|
||||
import com.subtracks.data.model.Source
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QueuePagingSourceTest {
|
||||
private lateinit var db: SubtracksDatabase
|
||||
private lateinit var repository: QueueRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db =
|
||||
Room
|
||||
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
repository = QueueRepository(db)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pagesAreContiguousAndDoNotOverlap() =
|
||||
runTest {
|
||||
seedSongs(130)
|
||||
repository.replace(listOf(repository.songsEntry(1)))
|
||||
val source = repository.pagingSource(initialPosition = 0)
|
||||
|
||||
val first = load(source, key = null)
|
||||
assertEquals((0L until 60L).toList(), first.data.map { it.position })
|
||||
assertEquals(1L, first.nextKey)
|
||||
assertEquals(null, first.prevKey)
|
||||
|
||||
val second = load(source, key = 1L)
|
||||
assertEquals((60L until 120L).toList(), second.data.map { it.position })
|
||||
assertEquals(2L, second.nextKey)
|
||||
assertEquals(0L, second.prevKey)
|
||||
|
||||
val third = load(source, key = 2L)
|
||||
assertEquals((120L until 130L).toList(), third.data.map { it.position })
|
||||
assertEquals(null, third.nextKey)
|
||||
assertEquals(1L, third.prevKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theFirstPageStartsAtTheInitialPosition() =
|
||||
runTest {
|
||||
seedSongs(130)
|
||||
repository.replace(listOf(repository.songsEntry(1)))
|
||||
val source = repository.pagingSource(initialPosition = 100)
|
||||
|
||||
val first = load(source, key = null)
|
||||
|
||||
assertEquals((60L until 120L).toList(), first.data.map { it.position })
|
||||
assertEquals(2L, first.nextKey)
|
||||
assertEquals(0L, first.prevKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theRefreshKeyUsesTheLoadedPositions() =
|
||||
runTest {
|
||||
val source = repository.pagingSource(initialPosition = 100)
|
||||
val state =
|
||||
PagingState(
|
||||
pages = listOf(PagingSource.LoadResult.Page(listOf(itemAt(60)), prevKey = 0L, nextKey = 2L)),
|
||||
anchorPosition = 5,
|
||||
config = PagingConfig(pageSize = QUEUE_PAGE_SIZE, enablePlaceholders = false),
|
||||
leadingPlaceholderCount = 0,
|
||||
)
|
||||
|
||||
assertEquals(1L, source.getRefreshKey(state))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEmptyQueueLoadsNothing() =
|
||||
runTest {
|
||||
val result = load(repository.pagingSource(0), key = null)
|
||||
|
||||
assertEquals(emptyList<QueueWindowItem>(), result.data)
|
||||
assertEquals(null, result.nextKey)
|
||||
assertEquals(null, result.prevKey)
|
||||
}
|
||||
|
||||
private fun itemAt(position: Long) =
|
||||
QueueWindowItem(
|
||||
position,
|
||||
SongListItem(
|
||||
song =
|
||||
Song(
|
||||
sourceId = 1,
|
||||
id = "s$position",
|
||||
albumId = "al1",
|
||||
artistId = "ar1",
|
||||
title = "Song $position",
|
||||
album = "Album",
|
||||
artist = "Artist",
|
||||
duration = 100,
|
||||
track = position,
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
),
|
||||
coverArt = null,
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun load(
|
||||
source: PagingSource<Long, QueueWindowItem>,
|
||||
key: Long?,
|
||||
): PagingSource.LoadResult.Page<Long, QueueWindowItem> {
|
||||
val params = PagingSource.LoadParams.Refresh<Long>(key, QUEUE_PAGE_SIZE, false)
|
||||
return source.load(params) as PagingSource.LoadResult.Page<Long, QueueWindowItem>
|
||||
}
|
||||
|
||||
private suspend fun seedSongs(count: Int) {
|
||||
db.sourcesDao().upsertSource(
|
||||
Source(id = 1, name = "test", address = "http://localhost", isActive = true, createdAt = 0),
|
||||
)
|
||||
db.libraryDao().upsertSongs(
|
||||
(1..count).map { track ->
|
||||
Song(
|
||||
sourceId = 1,
|
||||
id = "s$track",
|
||||
albumId = "al1",
|
||||
artistId = "ar1",
|
||||
title = "Song $track",
|
||||
album = "Album",
|
||||
artist = "Artist",
|
||||
duration = 100,
|
||||
track = track.toLong(),
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -223,20 +223,18 @@ class AppScreenshotTest {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
QueueScreen(
|
||||
items =
|
||||
remember {
|
||||
flowOf(
|
||||
PagingData.from(
|
||||
Fixtures.songItems.mapIndexed { index, item -> QueueWindowItem(index.toLong(), item) },
|
||||
),
|
||||
)
|
||||
}.collectAsLazyPagingItems(),
|
||||
rows =
|
||||
Fixtures.songItems.mapIndexed { index, item -> QueueWindowItem(index.toLong(), item) },
|
||||
ready = true,
|
||||
currentPosition = 1,
|
||||
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||
onBack = {},
|
||||
onPlay = {},
|
||||
onRemove = {},
|
||||
onReorder = { _, _ -> },
|
||||
onMove = { _, _ -> },
|
||||
onLoadOlder = {},
|
||||
onLoadNewer = {},
|
||||
onUndo = {},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.subtracks.ui.playback
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.prefs.fakeUserPreferences
|
||||
import com.subtracks.data.repo.QueueRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.playback.FakePlayerConnection
|
||||
import com.subtracks.playback.FakePlayerHandle
|
||||
import com.subtracks.playback.PlaybackController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QueueViewModelTest {
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
private lateinit var db: SubtracksDatabase
|
||||
private lateinit var queues: QueueRepository
|
||||
private lateinit var controller: PlaybackController
|
||||
private lateinit var viewModel: QueueViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db =
|
||||
Room
|
||||
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
val sources = SourceRepository(db, OkHttpClient(), fakeUserPreferences())
|
||||
queues = QueueRepository(db)
|
||||
controller = PlaybackController(sources, queues, FakePlayerConnection(FakePlayerHandle()), dispatcher)
|
||||
viewModel = QueueViewModel(queues, controller)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
controller.close()
|
||||
db.close()
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openingLoadsAWindowAroundTheCursorAndExtendingGrowsIt() {
|
||||
runBlocking { seedSongs(200) }
|
||||
controller.playSongs(1, 100)
|
||||
await { controller.state.value.position == 100L }
|
||||
|
||||
viewModel.open()
|
||||
await { viewModel.ready }
|
||||
|
||||
assertEquals((70L..129L).toList(), viewModel.rows.map { it.position })
|
||||
|
||||
viewModel.loadNewer()
|
||||
await { viewModel.rows.lastOrNull()?.position == 189L }
|
||||
assertEquals((70L..189L).toList(), viewModel.rows.map { it.position })
|
||||
|
||||
viewModel.loadOlder()
|
||||
await { viewModel.rows.firstOrNull()?.position == 10L }
|
||||
assertEquals((10L..189L).toList(), viewModel.rows.map { it.position })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reorderingMovesTheRowLocally() {
|
||||
runBlocking { seedSongs(200) }
|
||||
controller.playSongs(1, 100)
|
||||
await { controller.state.value.position == 100L }
|
||||
|
||||
viewModel.open()
|
||||
await { viewModel.ready }
|
||||
|
||||
viewModel.reorder(fromIndex = 0, toIndex = 2)
|
||||
|
||||
val positions = viewModel.rows.map { it.position }
|
||||
assertEquals(listOf(71L, 72L, 70L), positions.take(3))
|
||||
}
|
||||
|
||||
private suspend fun seedSongs(count: Int) {
|
||||
db.sourcesDao().upsertSource(
|
||||
Source(id = 1, name = "test", address = "http://localhost", isActive = true, createdAt = 0),
|
||||
)
|
||||
db.libraryDao().upsertSongs(
|
||||
(1..count).map { track ->
|
||||
Song(
|
||||
sourceId = 1,
|
||||
id = "s$track",
|
||||
albumId = "al1",
|
||||
artistId = "ar1",
|
||||
title = "Song $track",
|
||||
album = "Album",
|
||||
artist = "Artist",
|
||||
duration = 100,
|
||||
track = track.toLong(),
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun await(predicate: () -> Boolean) {
|
||||
val deadline = System.nanoTime() + 5_000_000_000L
|
||||
while (!predicate() && System.nanoTime() < deadline) Thread.sleep(10)
|
||||
assertTrue("Timed out waiting for the expected state", predicate())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user