mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
add screenshot goldens and the title collapse gesture test
This commit is contained in:
+28
-11
@@ -20,7 +20,8 @@ class SubsonicSourceIntegrationTest(
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
val client = SubsonicClient(
|
val client =
|
||||||
|
SubsonicClient(
|
||||||
baseUrl = server.baseUrl.toHttpUrl(),
|
baseUrl = server.baseUrl.toHttpUrl(),
|
||||||
username = server.username,
|
username = server.username,
|
||||||
password = server.password,
|
password = server.password,
|
||||||
@@ -31,12 +32,14 @@ class SubsonicSourceIntegrationTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun ping() = runBlocking {
|
fun ping() =
|
||||||
|
runBlocking {
|
||||||
source.ping()
|
source.ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun artists() = runBlocking {
|
fun artists() =
|
||||||
|
runBlocking {
|
||||||
val artists = source.getArtists()
|
val artists = source.getArtists()
|
||||||
|
|
||||||
assertEquals(2, artists.size)
|
assertEquals(2, artists.size)
|
||||||
@@ -45,7 +48,8 @@ class SubsonicSourceIntegrationTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun albums() = runBlocking {
|
fun albums() =
|
||||||
|
runBlocking {
|
||||||
val albums = source.getAlbums()
|
val albums = source.getAlbums()
|
||||||
|
|
||||||
assertEquals(3, albums.size)
|
assertEquals(3, albums.size)
|
||||||
@@ -66,12 +70,14 @@ class SubsonicSourceIntegrationTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun songs() = runBlocking {
|
fun songs() =
|
||||||
|
runBlocking {
|
||||||
assertEquals(20, source.getSongs().size)
|
assertEquals(20, source.getSongs().size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun playlists() = runBlocking {
|
fun playlists() =
|
||||||
|
runBlocking {
|
||||||
val playlists = source.getPlaylists()
|
val playlists = source.getPlaylists()
|
||||||
|
|
||||||
assertEquals(1, playlists.size)
|
assertEquals(1, playlists.size)
|
||||||
@@ -79,26 +85,37 @@ class SubsonicSourceIntegrationTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun albumArtistRelation() = runBlocking {
|
fun albumArtistRelation() =
|
||||||
|
runBlocking {
|
||||||
val artists = source.getArtists()
|
val artists = source.getArtists()
|
||||||
val albums = source.getAlbums()
|
val albums = source.getAlbums()
|
||||||
|
|
||||||
val ugressAlbums = albums.filter { it.artistId == artists.first { a -> a.name == "Ugress" }.id }
|
val ugressAlbums =
|
||||||
|
albums
|
||||||
|
.filter { it.artistId == artists.first { a -> a.name == "Ugress" }.id }
|
||||||
.map { it.name }
|
.map { it.name }
|
||||||
.sorted()
|
.sorted()
|
||||||
assertEquals(listOf("Kosmonaut", "Retroconnaissance EP"), ugressAlbums)
|
assertEquals(listOf("Kosmonaut", "Retroconnaissance EP"), ugressAlbums)
|
||||||
|
|
||||||
val bradAlbums = albums.filter { it.artistId == artists.first { a -> a.name == "Brad Sucks" }.id }
|
val bradAlbums =
|
||||||
|
albums
|
||||||
|
.filter { it.artistId == artists.first { a -> a.name == "Brad Sucks" }.id }
|
||||||
.map { it.name }
|
.map { it.name }
|
||||||
assertEquals(listOf("I Don't Know What I'm Doing"), bradAlbums)
|
assertEquals(listOf("I Don't Know What I'm Doing"), bradAlbums)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Server(val name: String, val baseUrl: String, val username: String, val password: String)
|
data class Server(
|
||||||
|
val name: String,
|
||||||
|
val baseUrl: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Parameterized.Parameters(name = "{0}")
|
@Parameterized.Parameters(name = "{0}")
|
||||||
fun servers(): List<Array<Any>> = listOf(
|
fun servers(): List<Array<Any>> =
|
||||||
|
listOf(
|
||||||
arrayOf(Server("navidrome", "http://localhost:4533/", "admin", "password")),
|
arrayOf(Server("navidrome", "http://localhost:4533/", "admin", "password")),
|
||||||
arrayOf(Server("gonic", "http://localhost:4747/", "admin", "admin")),
|
arrayOf(Server("gonic", "http://localhost:4747/", "admin", "admin")),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.subtracks.data.repo
|
||||||
|
|
||||||
|
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 kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
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 SourceRepositoryTest {
|
||||||
|
private lateinit var db: SubtracksDatabase
|
||||||
|
private lateinit var repository: SourceRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
db =
|
||||||
|
Room
|
||||||
|
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||||
|
.setDriver(BundledSQLiteDriver())
|
||||||
|
.build()
|
||||||
|
repository = SourceRepository(db, OkHttpClient())
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addingASourceMakesItTheSingleActiveOne() =
|
||||||
|
runTest {
|
||||||
|
repository.addSource("first", "http://a.example", "u", "p", true)
|
||||||
|
val firstId = repository.activeSourceId().first()
|
||||||
|
|
||||||
|
repository.addSource("second", "http://b.example", "u", "p", true)
|
||||||
|
|
||||||
|
assertEquals(1, repository.sources().first().count { it.isActive })
|
||||||
|
assertEquals("second", repository.activeConfig().first()?.name)
|
||||||
|
assertEquals(
|
||||||
|
"http://a.example/",
|
||||||
|
repository
|
||||||
|
.sources()
|
||||||
|
.first()
|
||||||
|
.first { it.id == firstId }
|
||||||
|
.address,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ class SubsonicClientTest {
|
|||||||
server.shutdown()
|
server.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun client(tokenAuth: Boolean) = SubsonicClient(
|
private fun client(tokenAuth: Boolean) =
|
||||||
|
SubsonicClient(
|
||||||
baseUrl = server.url("/"),
|
baseUrl = server.url("/"),
|
||||||
username = "guest",
|
username = "guest",
|
||||||
password = "secret",
|
password = "secret",
|
||||||
|
|||||||
@@ -26,19 +26,27 @@ class SubsonicSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getSongsFallsBackToAlbumsWhenEmptySearchUnsupported() = runBlocking {
|
fun getSongsFallsBackToAlbumsWhenEmptySearchUnsupported() =
|
||||||
server.dispatcher = object : Dispatcher() {
|
runBlocking {
|
||||||
override fun dispatch(request: RecordedRequest): MockResponse = when {
|
server.dispatcher =
|
||||||
request.path?.startsWith("/rest/search3.view") == true ->
|
object : Dispatcher() {
|
||||||
|
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||||
|
when {
|
||||||
|
request.path?.startsWith("/rest/search3.view") == true -> {
|
||||||
MockResponse().setBody(failed(10, "Required parameter is missing"))
|
MockResponse().setBody(failed(10, "Required parameter is missing"))
|
||||||
|
}
|
||||||
|
|
||||||
request.path?.startsWith("/rest/getAlbumList2.view") == true ->
|
request.path?.startsWith("/rest/getAlbumList2.view") == true -> {
|
||||||
MockResponse().setBody(ALBUM_LIST)
|
MockResponse().setBody(ALBUM_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
request.path?.startsWith("/rest/getAlbum.view") == true ->
|
request.path?.startsWith("/rest/getAlbum.view") == true -> {
|
||||||
MockResponse().setBody(ALBUM)
|
MockResponse().setBody(ALBUM)
|
||||||
|
}
|
||||||
|
|
||||||
else -> MockResponse().setResponseCode(404)
|
else -> {
|
||||||
|
MockResponse().setResponseCode(404)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val source = SubsonicSource(1, client())
|
val source = SubsonicSource(1, client())
|
||||||
@@ -46,7 +54,8 @@ class SubsonicSourceTest {
|
|||||||
assertEquals(listOf("s1", "s2"), source.getSongs().map { it.id })
|
assertEquals(listOf("s1", "s2"), source.getSongs().map { it.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun client() = SubsonicClient(
|
private fun client() =
|
||||||
|
SubsonicClient(
|
||||||
baseUrl = server.url("/"),
|
baseUrl = server.url("/"),
|
||||||
username = "u",
|
username = "u",
|
||||||
password = "p",
|
password = "p",
|
||||||
@@ -54,8 +63,10 @@ class SubsonicSourceTest {
|
|||||||
http = OkHttpClient(),
|
http = OkHttpClient(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun failed(code: Int, message: String) =
|
private fun failed(
|
||||||
"<subsonic-response status=\"failed\"><error code=\"$code\" message=\"$message\"/></subsonic-response>"
|
code: Int,
|
||||||
|
message: String,
|
||||||
|
) = "<subsonic-response status=\"failed\"><error code=\"$code\" message=\"$message\"/></subsonic-response>"
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val ALBUM_LIST =
|
const val ALBUM_LIST =
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package com.subtracks.data.source.subsonic
|
package com.subtracks.data.source.subsonic
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
class SubsonicXmlTest {
|
class SubsonicXmlTest {
|
||||||
private fun parse(xml: String): Document =
|
private fun parse(xml: String): Document =
|
||||||
DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
DocumentBuilderFactory
|
||||||
|
.newInstance()
|
||||||
|
.newDocumentBuilder()
|
||||||
.parse(xml.byteInputStream(Charsets.UTF_8))
|
.parse(xml.byteInputStream(Charsets.UTF_8))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsArtists() {
|
fun mapsArtists() {
|
||||||
val document = parse(
|
val document =
|
||||||
|
parse(
|
||||||
"""
|
"""
|
||||||
<subsonic-response status="ok">
|
<subsonic-response status="ok">
|
||||||
<artists>
|
<artists>
|
||||||
@@ -35,7 +38,8 @@ class SubsonicXmlTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsAlbumList() {
|
fun mapsAlbumList() {
|
||||||
val document = parse(
|
val document =
|
||||||
|
parse(
|
||||||
"""
|
"""
|
||||||
<subsonic-response status="ok">
|
<subsonic-response status="ok">
|
||||||
<albumList2>
|
<albumList2>
|
||||||
@@ -68,7 +72,8 @@ class SubsonicXmlTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mapsSongsAndPlaylistEntries() {
|
fun mapsSongsAndPlaylistEntries() {
|
||||||
val document = parse(
|
val document =
|
||||||
|
parse(
|
||||||
"""
|
"""
|
||||||
<subsonic-response status="ok">
|
<subsonic-response status="ok">
|
||||||
<searchResult3>
|
<searchResult3>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.subtracks.data.sync
|
package com.subtracks.data.sync
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.paging.PagingSource
|
||||||
import androidx.room3.Room
|
import androidx.room3.Room
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.subtracks.data.db.SubtracksDatabase
|
import com.subtracks.data.db.SubtracksDatabase
|
||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
@@ -13,7 +14,6 @@ import com.subtracks.data.model.PlaylistSong
|
|||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import com.subtracks.data.source.MusicSource
|
import com.subtracks.data.source.MusicSource
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -28,7 +28,9 @@ class SyncServiceTest {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
db = Room.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
db =
|
||||||
|
Room
|
||||||
|
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||||
.setDriver(BundledSQLiteDriver())
|
.setDriver(BundledSQLiteDriver())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -39,9 +41,11 @@ class SyncServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun syncInsertsAndPrunesRemovedRows() = runTest {
|
fun syncInsertsAndPrunesRemovedRows() =
|
||||||
|
runTest {
|
||||||
insertSource()
|
insertSource()
|
||||||
val source = FakeMusicSource(
|
val source =
|
||||||
|
FakeMusicSource(
|
||||||
artists = listOf(artist("a1"), artist("a2")),
|
artists = listOf(artist("a1"), artist("a2")),
|
||||||
albums = listOf(album("al1")),
|
albums = listOf(album("al1")),
|
||||||
songs = listOf(song("s1"), song("s2")),
|
songs = listOf(song("s1"), song("s2")),
|
||||||
@@ -51,23 +55,60 @@ class SyncServiceTest {
|
|||||||
|
|
||||||
SyncService(db, source).sync()
|
SyncService(db, source).sync()
|
||||||
|
|
||||||
assertEquals(2, db.libraryDao().artists(1).first().size)
|
assertEquals(
|
||||||
assertEquals(2, db.libraryDao().songs(1).first().size)
|
2,
|
||||||
assertEquals(1, db.libraryDao().songsByPlaylist(1, "p1").first().size)
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.artists(1)
|
||||||
|
.allRows()
|
||||||
|
.size,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
2,
|
||||||
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.songs(1)
|
||||||
|
.allRows()
|
||||||
|
.size,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.playlistSongs(1, "p1")
|
||||||
|
.allRows()
|
||||||
|
.size,
|
||||||
|
)
|
||||||
|
|
||||||
source.artists = listOf(artist("a1"))
|
source.artists = listOf(artist("a1"))
|
||||||
source.songs = listOf(song("s1"))
|
source.songs = listOf(song("s1"))
|
||||||
|
|
||||||
SyncService(db, source).sync()
|
SyncService(db, source).sync()
|
||||||
|
|
||||||
assertEquals(listOf("a1"), db.libraryDao().artists(1).first().map { it.id })
|
assertEquals(
|
||||||
assertEquals(listOf("s1"), db.libraryDao().songs(1).first().map { it.id })
|
listOf("a1"),
|
||||||
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.artists(1)
|
||||||
|
.allRows()
|
||||||
|
.map { it.id },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("s1"),
|
||||||
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.songs(1)
|
||||||
|
.allRows()
|
||||||
|
.map { it.song.id },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun searchIndexMatchesSubstrings() = runTest {
|
fun searchIndexMatchesSubstrings() =
|
||||||
|
runTest {
|
||||||
insertSource()
|
insertSource()
|
||||||
val source = FakeMusicSource(
|
val source =
|
||||||
|
FakeMusicSource(
|
||||||
artists = listOf(artist("a1", name = "Radiohead")),
|
artists = listOf(artist("a1", name = "Radiohead")),
|
||||||
songs = listOf(song("s1")),
|
songs = listOf(song("s1")),
|
||||||
)
|
)
|
||||||
@@ -81,7 +122,8 @@ class SyncServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun searchIgnoresTooShortQueries() = runTest {
|
fun searchIgnoresTooShortQueries() =
|
||||||
|
runTest {
|
||||||
insertSource()
|
insertSource()
|
||||||
SyncService(db, FakeMusicSource(artists = listOf(artist("a1", name = "Radiohead")))).sync()
|
SyncService(db, FakeMusicSource(artists = listOf(artist("a1", name = "Radiohead")))).sync()
|
||||||
|
|
||||||
@@ -91,17 +133,39 @@ class SyncServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun syncUpdatesExistingRows() = runTest {
|
fun syncUpdatesExistingRows() =
|
||||||
|
runTest {
|
||||||
insertSource()
|
insertSource()
|
||||||
val source = FakeMusicSource(artists = listOf(artist("a1", name = "Old")))
|
val source = FakeMusicSource(artists = listOf(artist("a1", name = "Old")))
|
||||||
|
|
||||||
SyncService(db, source).sync()
|
SyncService(db, source).sync()
|
||||||
assertEquals("Old", db.libraryDao().artists(1).first().single().name)
|
assertEquals(
|
||||||
|
"Old",
|
||||||
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.artists(1)
|
||||||
|
.allRows()
|
||||||
|
.single()
|
||||||
|
.name,
|
||||||
|
)
|
||||||
|
|
||||||
source.artists = listOf(artist("a1", name = "New"))
|
source.artists = listOf(artist("a1", name = "New"))
|
||||||
SyncService(db, source).sync()
|
SyncService(db, source).sync()
|
||||||
|
|
||||||
assertEquals("New", db.libraryDao().artists(1).first().single().name)
|
assertEquals(
|
||||||
|
"New",
|
||||||
|
db
|
||||||
|
.libraryDao()
|
||||||
|
.artists(1)
|
||||||
|
.allRows()
|
||||||
|
.single()
|
||||||
|
.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T : Any> PagingSource<Int, T>.allRows(): List<T> {
|
||||||
|
val page = load(PagingSource.LoadParams.Refresh(key = null, loadSize = 100, placeholdersEnabled = false))
|
||||||
|
return (page as PagingSource.LoadResult.Page).data
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertSource() {
|
private suspend fun insertSource() {
|
||||||
@@ -110,10 +174,13 @@ class SyncServiceTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun artist(id: String, name: String = "Artist $id") =
|
private fun artist(
|
||||||
Artist(sourceId = 1, id = id, name = name, albumCount = 1, starred = null)
|
id: String,
|
||||||
|
name: String = "Artist $id",
|
||||||
|
) = Artist(sourceId = 1, id = id, name = name, albumCount = 1, starred = null)
|
||||||
|
|
||||||
private fun album(id: String) = Album(
|
private fun album(id: String) =
|
||||||
|
Album(
|
||||||
sourceId = 1,
|
sourceId = 1,
|
||||||
id = id,
|
id = id,
|
||||||
artistId = "a1",
|
artistId = "a1",
|
||||||
@@ -129,7 +196,8 @@ class SyncServiceTest {
|
|||||||
recentRank = null,
|
recentRank = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun song(id: String) = Song(
|
private fun song(id: String) =
|
||||||
|
Song(
|
||||||
sourceId = 1,
|
sourceId = 1,
|
||||||
id = id,
|
id = id,
|
||||||
albumId = "al1",
|
albumId = "al1",
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
package com.subtracks.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import coil3.Image
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.annotation.DelicateCoilApi
|
||||||
|
import coil3.asImage
|
||||||
|
import coil3.test.FakeImageLoaderEngine
|
||||||
|
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import com.subtracks.data.model.Album
|
||||||
|
import com.subtracks.data.model.Artist
|
||||||
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.data.model.Playlist
|
||||||
|
import com.subtracks.data.model.Song
|
||||||
|
import com.subtracks.data.model.SongListItem
|
||||||
|
import com.subtracks.data.model.Source
|
||||||
|
import com.subtracks.ui.library.ALBUM_COVER_TAG
|
||||||
|
import com.subtracks.ui.library.AlbumDetailScreen
|
||||||
|
import com.subtracks.ui.library.LibraryScreen
|
||||||
|
import com.subtracks.ui.library.LibraryTab
|
||||||
|
import com.subtracks.ui.library.PlaylistDetailScreen
|
||||||
|
import com.subtracks.ui.settings.AddSourceScreen
|
||||||
|
import com.subtracks.ui.settings.AddSourceState
|
||||||
|
import com.subtracks.ui.settings.SettingsScreen
|
||||||
|
import com.subtracks.ui.theme.SubtracksTheme
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@OptIn(DelicateCoilApi::class)
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
|
||||||
|
class AppScreenshotTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun installImageLoader() {
|
||||||
|
SingletonImageLoader.reset()
|
||||||
|
SingletonImageLoader.setUnsafe(
|
||||||
|
ImageLoader
|
||||||
|
.Builder(ApplicationProvider.getApplicationContext<Context>())
|
||||||
|
.components { add(Fixtures.artEngine()) }
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun resetImageLoader() {
|
||||||
|
SingletonImageLoader.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun libraryAlbums() {
|
||||||
|
setLibraryContent(LibraryTab.Albums)
|
||||||
|
awaitTag(ALBUM_COVER_TAG)
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_albums.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun libraryArtists() {
|
||||||
|
setLibraryContent(LibraryTab.Artists)
|
||||||
|
awaitText("Radiohead")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_artists.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun librarySongs() {
|
||||||
|
setLibraryContent(LibraryTab.Songs)
|
||||||
|
awaitText("Everything In Its Right Place")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_songs.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun libraryPlaylists() {
|
||||||
|
setLibraryContent(LibraryTab.Playlists)
|
||||||
|
awaitText("Late Night")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_playlists.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun albumDetail() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
AlbumDetailScreen(
|
||||||
|
album = Fixtures.albums.first(),
|
||||||
|
songs = Fixtures.songs,
|
||||||
|
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||||
|
onBack = {},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
awaitText("Everything In Its Right Place")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/album_detail.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playlistDetail() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
PlaylistDetailScreen(
|
||||||
|
playlist = Fixtures.playlists.first(),
|
||||||
|
songs = remember { flowOf(PagingData.from(Fixtures.songItems)) }.collectAsLazyPagingItems(),
|
||||||
|
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||||
|
onBack = {},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
awaitText("Everything In Its Right Place")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/playlist_detail.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settings() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
SettingsScreen(
|
||||||
|
sources = Fixtures.sources,
|
||||||
|
activeSourceId = 1,
|
||||||
|
onSelectSource = {},
|
||||||
|
onDeleteSource = {},
|
||||||
|
onAddServer = {},
|
||||||
|
onBack = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
awaitText("Navidrome")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/settings.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addSource() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
AddSourceScreen(
|
||||||
|
state =
|
||||||
|
AddSourceState(
|
||||||
|
name = "Home",
|
||||||
|
address = "https://music.example.com",
|
||||||
|
username = "austin",
|
||||||
|
password = "hunter2",
|
||||||
|
),
|
||||||
|
onNameChange = {},
|
||||||
|
onAddressChange = {},
|
||||||
|
onUsernameChange = {},
|
||||||
|
onPasswordChange = {},
|
||||||
|
onTokenAuthChange = {},
|
||||||
|
onTest = {},
|
||||||
|
onSave = {},
|
||||||
|
onBack = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
awaitText("Add server")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/add_source.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLibraryContent(tab: LibraryTab) {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
LibraryScreen(
|
||||||
|
selectedTab = tab,
|
||||||
|
onTabSelected = {},
|
||||||
|
albums = remember { flowOf(PagingData.from(Fixtures.albums)) }.collectAsLazyPagingItems(),
|
||||||
|
artists = remember { flowOf(PagingData.from(Fixtures.artists)) }.collectAsLazyPagingItems(),
|
||||||
|
songs = remember { flowOf(PagingData.from(Fixtures.songItems)) }.collectAsLazyPagingItems(),
|
||||||
|
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) }.collectAsLazyPagingItems(),
|
||||||
|
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||||
|
onAlbumClick = {},
|
||||||
|
onPlaylistClick = {},
|
||||||
|
onSync = {},
|
||||||
|
onOpenSettings = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun awaitText(text: String) {
|
||||||
|
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||||
|
composeRule.onAllNodesWithText(text).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun awaitTag(tag: String) {
|
||||||
|
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||||
|
composeRule.onAllNodesWithTag(tag).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Fixtures {
|
||||||
|
val sources =
|
||||||
|
listOf(
|
||||||
|
Source(id = 1, name = "Navidrome", address = "https://navidrome.homelab.example.com:4533", isActive = true, createdAt = 0),
|
||||||
|
Source(id = 2, name = "gonic", address = "http://192.168.1.10:4747", isActive = false, createdAt = 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
val artists =
|
||||||
|
listOf(
|
||||||
|
artist("ar-radiohead", "Radiohead", 2),
|
||||||
|
artist("ar-portishead", "Portishead", 1),
|
||||||
|
artist("ar-massive", "Massive Attack", 1),
|
||||||
|
artist("ar-aphex", "Aphex Twin", 1),
|
||||||
|
artist("ar-boc", "Boards of Canada", 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
val albums =
|
||||||
|
listOf(
|
||||||
|
album("al-kid-a", "ar-radiohead", "Kid A", "Radiohead", 2000, 10),
|
||||||
|
album("al-amnesiac", "ar-radiohead", "Amnesiac", "Radiohead", 2001, 11),
|
||||||
|
album("al-dummy", "ar-portishead", "Dummy", "Portishead", 1994, 11),
|
||||||
|
album("al-mezzanine", "ar-massive", "Mezzanine", "Massive Attack", 1998, 11),
|
||||||
|
album("al-saw", "ar-aphex", "Selected Ambient Works 85-92", "Aphex Twin", 1992, 13),
|
||||||
|
album("al-mhtrtc", "ar-boc", "Music Has the Right to Children", "Boards of Canada", 1998, 17),
|
||||||
|
album("al-geogaddi", "ar-boc", "Geogaddi", "Boards of Canada", 2002, 23),
|
||||||
|
)
|
||||||
|
|
||||||
|
val songs =
|
||||||
|
listOf(
|
||||||
|
song("s-eiirp", "al-kid-a", "ar-radiohead", "Everything In Its Right Place", "Kid A", "Radiohead", 1, 251),
|
||||||
|
song("s-kid-a", "al-kid-a", "ar-radiohead", "Kid A", "Kid A", "Radiohead", 2, 274),
|
||||||
|
song("s-pyramid", "al-amnesiac", "ar-radiohead", "Pyramid Song", "Amnesiac", "Radiohead", 2, 289),
|
||||||
|
song("s-roads", "al-dummy", "ar-portishead", "Roads", "Dummy", "Portishead", 9, 302),
|
||||||
|
song("s-teardrop", "al-mezzanine", "ar-massive", "Teardrop", "Mezzanine", "Massive Attack", 2, 328),
|
||||||
|
song("s-xtal", "al-saw", "ar-aphex", "Xtal", "Selected Ambient Works 85-92", "Aphex Twin", 1, 293),
|
||||||
|
song("s-roygbiv", "al-mhtrtc", "ar-boc", "Roygbiv", "Music Has the Right to Children", "Boards of Canada", 3, 149),
|
||||||
|
song("s-gyroscope", "al-geogaddi", "ar-boc", "Gyroscope", "Geogaddi", "Boards of Canada", 5, 216),
|
||||||
|
)
|
||||||
|
|
||||||
|
val songItems =
|
||||||
|
songs.map { song ->
|
||||||
|
SongListItem(song = song, coverArt = albums.firstOrNull { it.id == song.albumId }?.coverArt)
|
||||||
|
}
|
||||||
|
|
||||||
|
val playlists =
|
||||||
|
listOf(
|
||||||
|
playlist("pl-late-night", "Late Night", 42),
|
||||||
|
playlist("pl-road-trip", "Road Trip", 63),
|
||||||
|
playlist("pl-rainy-day", "Rainy Day", 25),
|
||||||
|
playlist("pl-focus", "Focus", 18),
|
||||||
|
playlist("pl-morning", "Morning", 7),
|
||||||
|
playlist("pl-one-song", "One Song", 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun artist(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
albumCount: Long,
|
||||||
|
) = Artist(
|
||||||
|
sourceId = 1,
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
albumCount = albumCount,
|
||||||
|
starred = null,
|
||||||
|
coverArt = "art-$id",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun album(
|
||||||
|
id: String,
|
||||||
|
artistId: String,
|
||||||
|
name: String,
|
||||||
|
albumArtist: String,
|
||||||
|
year: Long,
|
||||||
|
songCount: Long,
|
||||||
|
) = Album(
|
||||||
|
sourceId = 1,
|
||||||
|
id = id,
|
||||||
|
artistId = artistId,
|
||||||
|
name = name,
|
||||||
|
albumArtist = albumArtist,
|
||||||
|
created = 0,
|
||||||
|
coverArt = "art-$id",
|
||||||
|
genre = null,
|
||||||
|
year = year,
|
||||||
|
starred = null,
|
||||||
|
songCount = songCount,
|
||||||
|
frequentRank = null,
|
||||||
|
recentRank = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun song(
|
||||||
|
id: String,
|
||||||
|
albumId: String,
|
||||||
|
artistId: String,
|
||||||
|
title: String,
|
||||||
|
album: String,
|
||||||
|
artist: String,
|
||||||
|
track: Long,
|
||||||
|
duration: Long,
|
||||||
|
) = Song(
|
||||||
|
sourceId = 1,
|
||||||
|
id = id,
|
||||||
|
albumId = albumId,
|
||||||
|
artistId = artistId,
|
||||||
|
title = title,
|
||||||
|
album = album,
|
||||||
|
artist = artist,
|
||||||
|
duration = duration,
|
||||||
|
track = track,
|
||||||
|
disc = 1,
|
||||||
|
starred = null,
|
||||||
|
genre = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun playlist(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
songCount: Long,
|
||||||
|
) = Playlist(
|
||||||
|
sourceId = 1,
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
comment = null,
|
||||||
|
coverArt = "art-$id",
|
||||||
|
songCount = songCount,
|
||||||
|
created = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun artEngine(): FakeImageLoaderEngine {
|
||||||
|
val builder = FakeImageLoaderEngine.Builder()
|
||||||
|
albums.mapNotNull { it.coverArt }.forEach { builder.intercept(it, art(it, ArtKind.Album)) }
|
||||||
|
playlists.mapNotNull { it.coverArt }.forEach { builder.intercept(it, art(it, ArtKind.Playlist)) }
|
||||||
|
artists.mapNotNull { it.coverArt }.forEach { builder.intercept(it, art(it, ArtKind.Artist)) }
|
||||||
|
return builder.default(art("art-default", ArtKind.Album)).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun art(
|
||||||
|
key: String,
|
||||||
|
kind: ArtKind,
|
||||||
|
): Image {
|
||||||
|
val size = 256
|
||||||
|
val f = size.toFloat()
|
||||||
|
val random = Random(key.hashCode())
|
||||||
|
val base = random.nextFloat() * 360f
|
||||||
|
|
||||||
|
fun color(offset: Float): Int =
|
||||||
|
Color.HSVToColor(
|
||||||
|
floatArrayOf(((base + offset) % 360f + 360f) % 360f, 0.88f, 0.92f),
|
||||||
|
)
|
||||||
|
|
||||||
|
val palette =
|
||||||
|
if (random.nextBoolean()) {
|
||||||
|
listOf(color(0f), color(120f + random.nextFloat() * 40f), color(230f + random.nextFloat() * 40f))
|
||||||
|
} else {
|
||||||
|
listOf(color(0f), color(170f + random.nextFloat() * 30f))
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
canvas.drawColor(palette[0])
|
||||||
|
|
||||||
|
when (kind) {
|
||||||
|
ArtKind.Album -> {
|
||||||
|
val stripe = f / 9f
|
||||||
|
canvas.save()
|
||||||
|
canvas.rotate(-30f, f / 2f, f / 2f)
|
||||||
|
var x = -f
|
||||||
|
var index = 1
|
||||||
|
while (x < f * 2f) {
|
||||||
|
paint.color = palette[index % palette.size]
|
||||||
|
canvas.drawRect(x, -f, x + stripe * 0.6f, f * 2f, paint)
|
||||||
|
x += stripe
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtKind.Playlist -> {
|
||||||
|
val step = f / 4f
|
||||||
|
var row = 1
|
||||||
|
var y = step / 2f
|
||||||
|
while (y < f) {
|
||||||
|
var x = step / 2f
|
||||||
|
while (x < f) {
|
||||||
|
paint.color = palette[row % palette.size]
|
||||||
|
canvas.drawCircle(x, y, step * 0.3f, paint)
|
||||||
|
x += step
|
||||||
|
row++
|
||||||
|
}
|
||||||
|
y += step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtKind.Artist -> {
|
||||||
|
paint.style = Paint.Style.STROKE
|
||||||
|
paint.strokeWidth = f / 13f
|
||||||
|
var radius = f * 0.5f
|
||||||
|
var index = 1
|
||||||
|
while (radius > 0f) {
|
||||||
|
paint.color = palette[index % palette.size]
|
||||||
|
canvas.drawCircle(f / 2f, f / 2f, radius, paint)
|
||||||
|
radius -= paint.strokeWidth
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap.asImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ArtKind { Album, Playlist, Artist }
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.subtracks.ui
|
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
|
||||||
import androidx.compose.ui.test.onRoot
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
|
||||||
import com.github.takahirom.roborazzi.captureRoboImage
|
|
||||||
import com.subtracks.ui.theme.SubtracksTheme
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
import org.robolectric.annotation.GraphicsMode
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
|
||||||
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
|
|
||||||
class HelloScreenshotTest {
|
|
||||||
@get:Rule
|
|
||||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun hello() {
|
|
||||||
composeRule.setContent {
|
|
||||||
SubtracksTheme {
|
|
||||||
HelloScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/hello.png")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.subtracks.ui
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import androidx.compose.ui.test.performTouchInput
|
||||||
|
import androidx.compose.ui.test.swipeDown
|
||||||
|
import androidx.compose.ui.test.swipeUp
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||||
|
import com.subtracks.data.model.Album
|
||||||
|
import com.subtracks.data.model.Artist
|
||||||
|
import com.subtracks.data.model.Playlist
|
||||||
|
import com.subtracks.data.model.SongListItem
|
||||||
|
import com.subtracks.ui.library.ALBUM_COVER_TAG
|
||||||
|
import com.subtracks.ui.library.LibraryScreen
|
||||||
|
import com.subtracks.ui.library.LibraryTab
|
||||||
|
import com.subtracks.ui.theme.SubtracksTheme
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
|
||||||
|
class LibraryScreenTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun titleCollapsesOnScrollDownAndExpandsOnScrollUp() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
LibraryScreen(
|
||||||
|
selectedTab = LibraryTab.Albums,
|
||||||
|
onTabSelected = {},
|
||||||
|
albums = remember { flowOf(PagingData.from(albums())) }.collectAsLazyPagingItems(),
|
||||||
|
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(),
|
||||||
|
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(),
|
||||||
|
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(),
|
||||||
|
coverArt = { _, _ -> null },
|
||||||
|
onAlbumClick = {},
|
||||||
|
onPlaylistClick = {},
|
||||||
|
onSync = {},
|
||||||
|
onOpenSettings = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||||
|
composeRule.onAllNodesWithTag(ALBUM_COVER_TAG).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
val expandedTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithContentDescription("Sync")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
|
||||||
|
composeRule.onRoot().performTouchInput { swipeUp(startY = centerY + 150f, endY = centerY - 150f, durationMillis = 600) }
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
val collapsedTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithContentDescription("Sync")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
|
||||||
|
composeRule.onRoot().performTouchInput { swipeDown(startY = centerY - 150f, endY = centerY + 150f, durationMillis = 600) }
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
val restoredTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithContentDescription("Sync")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
|
||||||
|
assertTrue("title should shrink while scrolling down ($expandedTop -> $collapsedTop)", collapsedTop < expandedTop)
|
||||||
|
assertTrue("title should come back on upward scroll ($collapsedTop -> $restoredTop)", restoredTop > collapsedTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun albums() =
|
||||||
|
(1..60).map { index ->
|
||||||
|
Album(
|
||||||
|
sourceId = 1,
|
||||||
|
id = "al-$index",
|
||||||
|
artistId = "ar-1",
|
||||||
|
name = "Album $index",
|
||||||
|
albumArtist = "Artist",
|
||||||
|
created = 0,
|
||||||
|
coverArt = null,
|
||||||
|
genre = null,
|
||||||
|
year = null,
|
||||||
|
starred = null,
|
||||||
|
songCount = 1,
|
||||||
|
frequentRank = null,
|
||||||
|
recentRank = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user