mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
add screenshot goldens and the title collapse gesture test
This commit is contained in:
+73
-56
@@ -20,87 +20,104 @@ class SubsonicSourceIntegrationTest(
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val client = SubsonicClient(
|
||||
baseUrl = server.baseUrl.toHttpUrl(),
|
||||
username = server.username,
|
||||
password = server.password,
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
val client =
|
||||
SubsonicClient(
|
||||
baseUrl = server.baseUrl.toHttpUrl(),
|
||||
username = server.username,
|
||||
password = server.password,
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
source = SubsonicSource(1, client)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ping() = runBlocking {
|
||||
source.ping()
|
||||
}
|
||||
fun ping() =
|
||||
runBlocking {
|
||||
source.ping()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun artists() = runBlocking {
|
||||
val artists = source.getArtists()
|
||||
fun artists() =
|
||||
runBlocking {
|
||||
val artists = source.getArtists()
|
||||
|
||||
assertEquals(2, artists.size)
|
||||
assertNotNull(artists.first { it.name == "Ugress" }.starred)
|
||||
assertNull(artists.first { it.name == "Brad Sucks" }.starred)
|
||||
}
|
||||
assertEquals(2, artists.size)
|
||||
assertNotNull(artists.first { it.name == "Ugress" }.starred)
|
||||
assertNull(artists.first { it.name == "Brad Sucks" }.starred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albums() = runBlocking {
|
||||
val albums = source.getAlbums()
|
||||
fun albums() =
|
||||
runBlocking {
|
||||
val albums = source.getAlbums()
|
||||
|
||||
assertEquals(3, albums.size)
|
||||
assertEquals(3, albums.size)
|
||||
|
||||
val kosmo = albums.first { it.name == "Kosmonaut" }
|
||||
assertTrue(kosmo.id.isNotEmpty())
|
||||
assertTrue(kosmo.artistId!!.isNotEmpty())
|
||||
assertEquals("Ugress", kosmo.albumArtist)
|
||||
assertEquals(2006L, kosmo.year)
|
||||
assertEquals("Electronic", kosmo.genre)
|
||||
assertNotNull(kosmo.coverArt)
|
||||
assertNotNull(kosmo.starred)
|
||||
assertTrue(kosmo.created <= System.currentTimeMillis() / 1000)
|
||||
assertTrue(kosmo.frequentRank != null || kosmo.recentRank != null)
|
||||
val kosmo = albums.first { it.name == "Kosmonaut" }
|
||||
assertTrue(kosmo.id.isNotEmpty())
|
||||
assertTrue(kosmo.artistId!!.isNotEmpty())
|
||||
assertEquals("Ugress", kosmo.albumArtist)
|
||||
assertEquals(2006L, kosmo.year)
|
||||
assertEquals("Electronic", kosmo.genre)
|
||||
assertNotNull(kosmo.coverArt)
|
||||
assertNotNull(kosmo.starred)
|
||||
assertTrue(kosmo.created <= System.currentTimeMillis() / 1000)
|
||||
assertTrue(kosmo.frequentRank != null || kosmo.recentRank != null)
|
||||
|
||||
assertNull(albums.first { it.name == "Retroconnaissance EP" }.starred)
|
||||
assertNull(albums.first { it.name == "I Don't Know What I'm Doing" }.starred)
|
||||
}
|
||||
assertNull(albums.first { it.name == "Retroconnaissance EP" }.starred)
|
||||
assertNull(albums.first { it.name == "I Don't Know What I'm Doing" }.starred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun songs() = runBlocking {
|
||||
assertEquals(20, source.getSongs().size)
|
||||
}
|
||||
fun songs() =
|
||||
runBlocking {
|
||||
assertEquals(20, source.getSongs().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlists() = runBlocking {
|
||||
val playlists = source.getPlaylists()
|
||||
fun playlists() =
|
||||
runBlocking {
|
||||
val playlists = source.getPlaylists()
|
||||
|
||||
assertEquals(1, playlists.size)
|
||||
assertEquals(7, source.getPlaylistSongs(playlists).size)
|
||||
}
|
||||
assertEquals(1, playlists.size)
|
||||
assertEquals(7, source.getPlaylistSongs(playlists).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumArtistRelation() = runBlocking {
|
||||
val artists = source.getArtists()
|
||||
val albums = source.getAlbums()
|
||||
fun albumArtistRelation() =
|
||||
runBlocking {
|
||||
val artists = source.getArtists()
|
||||
val albums = source.getAlbums()
|
||||
|
||||
val ugressAlbums = albums.filter { it.artistId == artists.first { a -> a.name == "Ugress" }.id }
|
||||
.map { it.name }
|
||||
.sorted()
|
||||
assertEquals(listOf("Kosmonaut", "Retroconnaissance EP"), ugressAlbums)
|
||||
val ugressAlbums =
|
||||
albums
|
||||
.filter { it.artistId == artists.first { a -> a.name == "Ugress" }.id }
|
||||
.map { it.name }
|
||||
.sorted()
|
||||
assertEquals(listOf("Kosmonaut", "Retroconnaissance EP"), ugressAlbums)
|
||||
|
||||
val bradAlbums = albums.filter { it.artistId == artists.first { a -> a.name == "Brad Sucks" }.id }
|
||||
.map { it.name }
|
||||
assertEquals(listOf("I Don't Know What I'm Doing"), bradAlbums)
|
||||
}
|
||||
val bradAlbums =
|
||||
albums
|
||||
.filter { it.artistId == artists.first { a -> a.name == "Brad Sucks" }.id }
|
||||
.map { it.name }
|
||||
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 {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun servers(): List<Array<Any>> = listOf(
|
||||
arrayOf(Server("navidrome", "http://localhost:4533/", "admin", "password")),
|
||||
arrayOf(Server("gonic", "http://localhost:4747/", "admin", "admin")),
|
||||
)
|
||||
fun servers(): List<Array<Any>> =
|
||||
listOf(
|
||||
arrayOf(Server("navidrome", "http://localhost:4533/", "admin", "password")),
|
||||
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,13 +25,14 @@ class SubsonicClientTest {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
private fun client(tokenAuth: Boolean) = SubsonicClient(
|
||||
baseUrl = server.url("/"),
|
||||
username = "guest",
|
||||
password = "secret",
|
||||
useTokenAuth = tokenAuth,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
private fun client(tokenAuth: Boolean) =
|
||||
SubsonicClient(
|
||||
baseUrl = server.url("/"),
|
||||
username = "guest",
|
||||
password = "secret",
|
||||
useTokenAuth = tokenAuth,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun tokenAuthSendsSaltAndHash() {
|
||||
|
||||
@@ -26,36 +26,47 @@ class SubsonicSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getSongsFallsBackToAlbumsWhenEmptySearchUnsupported() = runBlocking {
|
||||
server.dispatcher = object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse = when {
|
||||
request.path?.startsWith("/rest/search3.view") == true ->
|
||||
MockResponse().setBody(failed(10, "Required parameter is missing"))
|
||||
fun getSongsFallsBackToAlbumsWhenEmptySearchUnsupported() =
|
||||
runBlocking {
|
||||
server.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse =
|
||||
when {
|
||||
request.path?.startsWith("/rest/search3.view") == true -> {
|
||||
MockResponse().setBody(failed(10, "Required parameter is missing"))
|
||||
}
|
||||
|
||||
request.path?.startsWith("/rest/getAlbumList2.view") == true ->
|
||||
MockResponse().setBody(ALBUM_LIST)
|
||||
request.path?.startsWith("/rest/getAlbumList2.view") == true -> {
|
||||
MockResponse().setBody(ALBUM_LIST)
|
||||
}
|
||||
|
||||
request.path?.startsWith("/rest/getAlbum.view") == true ->
|
||||
MockResponse().setBody(ALBUM)
|
||||
request.path?.startsWith("/rest/getAlbum.view") == true -> {
|
||||
MockResponse().setBody(ALBUM)
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
else -> {
|
||||
MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
val source = SubsonicSource(1, client())
|
||||
|
||||
assertEquals(listOf("s1", "s2"), source.getSongs().map { it.id })
|
||||
}
|
||||
val source = SubsonicSource(1, client())
|
||||
|
||||
assertEquals(listOf("s1", "s2"), source.getSongs().map { it.id })
|
||||
}
|
||||
private fun client() =
|
||||
SubsonicClient(
|
||||
baseUrl = server.url("/"),
|
||||
username = "u",
|
||||
password = "p",
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
|
||||
private fun client() = SubsonicClient(
|
||||
baseUrl = server.url("/"),
|
||||
username = "u",
|
||||
password = "p",
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
|
||||
private fun failed(code: Int, message: String) =
|
||||
"<subsonic-response status=\"failed\"><error code=\"$code\" message=\"$message\"/></subsonic-response>"
|
||||
private fun failed(
|
||||
code: Int,
|
||||
message: String,
|
||||
) = "<subsonic-response status=\"failed\"><error code=\"$code\" message=\"$message\"/></subsonic-response>"
|
||||
|
||||
private companion object {
|
||||
const val ALBUM_LIST =
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
package com.subtracks.data.source.subsonic
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.w3c.dom.Document
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class SubsonicXmlTest {
|
||||
private fun parse(xml: String): Document =
|
||||
DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
DocumentBuilderFactory
|
||||
.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.parse(xml.byteInputStream(Charsets.UTF_8))
|
||||
|
||||
@Test
|
||||
fun mapsArtists() {
|
||||
val document = parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<artists>
|
||||
<artist id="ar1" name="Radiohead" albumCount="9" starred="2020-01-02T03:04:05.000Z"/>
|
||||
<artist id="ar2" name="Portishead" albumCount="3"/>
|
||||
</artists>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
val document =
|
||||
parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<artists>
|
||||
<artist id="ar1" name="Radiohead" albumCount="9" starred="2020-01-02T03:04:05.000Z"/>
|
||||
<artist id="ar2" name="Portishead" albumCount="3"/>
|
||||
</artists>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val artists = SubsonicXml.artists(7, document)
|
||||
|
||||
@@ -35,16 +38,17 @@ class SubsonicXmlTest {
|
||||
|
||||
@Test
|
||||
fun mapsAlbumList() {
|
||||
val document = parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<albumList2>
|
||||
<album id="al1" name="OK Computer" artist="Radiohead" artistId="ar1" created="1997-05-21T00:00:00.000Z" songCount="12" coverArt="al1"/>
|
||||
<album id="al2" name="Kid A" artist="Radiohead" artistId="ar1" created="2000-10-02T00:00:00.000Z" songCount="10" coverArt="al2"/>
|
||||
</albumList2>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
val document =
|
||||
parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<albumList2>
|
||||
<album id="al1" name="OK Computer" artist="Radiohead" artistId="ar1" created="1997-05-21T00:00:00.000Z" songCount="12" coverArt="al1"/>
|
||||
<album id="al2" name="Kid A" artist="Radiohead" artistId="ar1" created="2000-10-02T00:00:00.000Z" songCount="10" coverArt="al2"/>
|
||||
</albumList2>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val albums = SubsonicXml.albums(1, document)
|
||||
|
||||
@@ -68,18 +72,19 @@ class SubsonicXmlTest {
|
||||
|
||||
@Test
|
||||
fun mapsSongsAndPlaylistEntries() {
|
||||
val document = parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<searchResult3>
|
||||
<song id="sg2" title="Idioteque" artist="Radiohead" album="Kid A" albumId="al2" artistId="ar1" duration="300" track="8" discNumber="1"/>
|
||||
</searchResult3>
|
||||
<playlist id="pl1" name="Favourites" songCount="1" created="2021-02-03T04:05:06.000Z">
|
||||
<entry id="sg1" title="Everything In Its Right Place" artist="Radiohead" album="Kid A" albumId="al2" artistId="ar1" duration="251" track="1" discNumber="1"/>
|
||||
</playlist>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
val document =
|
||||
parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<searchResult3>
|
||||
<song id="sg2" title="Idioteque" artist="Radiohead" album="Kid A" albumId="al2" artistId="ar1" duration="300" track="8" discNumber="1"/>
|
||||
</searchResult3>
|
||||
<playlist id="pl1" name="Favourites" songCount="1" created="2021-02-03T04:05:06.000Z">
|
||||
<entry id="sg1" title="Everything In Its Right Place" artist="Radiohead" album="Kid A" albumId="al2" artistId="ar1" duration="251" track="1" discNumber="1"/>
|
||||
</playlist>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val songs = SubsonicXml.songs(1, document)
|
||||
val entries = SubsonicXml.playlistSongs(1, "pl1", document)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.subtracks.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room3.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
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.Album
|
||||
@@ -13,7 +14,6 @@ import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.source.MusicSource
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -28,9 +28,11 @@ class SyncServiceTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = Room.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
db =
|
||||
Room
|
||||
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -39,69 +41,131 @@ class SyncServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncInsertsAndPrunesRemovedRows() = runTest {
|
||||
insertSource()
|
||||
val source = FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2")),
|
||||
albums = listOf(album("al1")),
|
||||
songs = listOf(song("s1"), song("s2")),
|
||||
playlists = listOf(playlist("p1")),
|
||||
playlistSongs = listOf(PlaylistSong(1, "p1", "s1", 0)),
|
||||
)
|
||||
fun syncInsertsAndPrunesRemovedRows() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1"), artist("a2")),
|
||||
albums = listOf(album("al1")),
|
||||
songs = listOf(song("s1"), song("s2")),
|
||||
playlists = listOf(playlist("p1")),
|
||||
playlistSongs = listOf(PlaylistSong(1, "p1", "s1", 0)),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
SyncService(db, source).sync()
|
||||
|
||||
assertEquals(2, db.libraryDao().artists(1).first().size)
|
||||
assertEquals(2, db.libraryDao().songs(1).first().size)
|
||||
assertEquals(1, db.libraryDao().songsByPlaylist(1, "p1").first().size)
|
||||
assertEquals(
|
||||
2,
|
||||
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.songs = listOf(song("s1"))
|
||||
source.artists = listOf(artist("a1"))
|
||||
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(listOf("s1"), db.libraryDao().songs(1).first().map { it.id })
|
||||
}
|
||||
assertEquals(
|
||||
listOf("a1"),
|
||||
db
|
||||
.libraryDao()
|
||||
.artists(1)
|
||||
.allRows()
|
||||
.map { it.id },
|
||||
)
|
||||
assertEquals(
|
||||
listOf("s1"),
|
||||
db
|
||||
.libraryDao()
|
||||
.songs(1)
|
||||
.allRows()
|
||||
.map { it.song.id },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchIndexMatchesSubstrings() = runTest {
|
||||
insertSource()
|
||||
val source = FakeMusicSource(
|
||||
artists = listOf(artist("a1", name = "Radiohead")),
|
||||
songs = listOf(song("s1")),
|
||||
)
|
||||
fun searchIndexMatchesSubstrings() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1", name = "Radiohead")),
|
||||
songs = listOf(song("s1")),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
SyncService(db, source).sync()
|
||||
|
||||
val results = db.searchDao().search("1", "adio", 10)
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("a1", results.single().itemId)
|
||||
assertEquals("artist", results.single().type)
|
||||
}
|
||||
val results = db.searchDao().search("1", "adio", 10)
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("a1", results.single().itemId)
|
||||
assertEquals("artist", results.single().type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchIgnoresTooShortQueries() = runTest {
|
||||
insertSource()
|
||||
SyncService(db, FakeMusicSource(artists = listOf(artist("a1", name = "Radiohead")))).sync()
|
||||
fun searchIgnoresTooShortQueries() =
|
||||
runTest {
|
||||
insertSource()
|
||||
SyncService(db, FakeMusicSource(artists = listOf(artist("a1", name = "Radiohead")))).sync()
|
||||
|
||||
assertEquals(0, db.searchDao().search("1", "ad", 10).size)
|
||||
assertEquals(0, db.searchDao().search("1", "", 10).size)
|
||||
assertEquals(1, db.searchDao().search("1", "adi", 10).size)
|
||||
}
|
||||
assertEquals(0, db.searchDao().search("1", "ad", 10).size)
|
||||
assertEquals(0, db.searchDao().search("1", "", 10).size)
|
||||
assertEquals(1, db.searchDao().search("1", "adi", 10).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncUpdatesExistingRows() = runTest {
|
||||
insertSource()
|
||||
val source = FakeMusicSource(artists = listOf(artist("a1", name = "Old")))
|
||||
fun syncUpdatesExistingRows() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source = FakeMusicSource(artists = listOf(artist("a1", name = "Old")))
|
||||
|
||||
SyncService(db, source).sync()
|
||||
assertEquals("Old", db.libraryDao().artists(1).first().single().name)
|
||||
SyncService(db, source).sync()
|
||||
assertEquals(
|
||||
"Old",
|
||||
db
|
||||
.libraryDao()
|
||||
.artists(1)
|
||||
.allRows()
|
||||
.single()
|
||||
.name,
|
||||
)
|
||||
|
||||
source.artists = listOf(artist("a1", name = "New"))
|
||||
SyncService(db, source).sync()
|
||||
source.artists = listOf(artist("a1", name = "New"))
|
||||
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() {
|
||||
@@ -110,39 +174,43 @@ class SyncServiceTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun artist(id: String, name: String = "Artist $id") =
|
||||
Artist(sourceId = 1, id = id, name = name, albumCount = 1, starred = null)
|
||||
private fun artist(
|
||||
id: String,
|
||||
name: String = "Artist $id",
|
||||
) = Artist(sourceId = 1, id = id, name = name, albumCount = 1, starred = null)
|
||||
|
||||
private fun album(id: String) = Album(
|
||||
sourceId = 1,
|
||||
id = id,
|
||||
artistId = "a1",
|
||||
name = "Album $id",
|
||||
albumArtist = "Artist",
|
||||
created = 0,
|
||||
coverArt = null,
|
||||
genre = null,
|
||||
year = null,
|
||||
starred = null,
|
||||
songCount = 1,
|
||||
frequentRank = null,
|
||||
recentRank = null,
|
||||
)
|
||||
private fun album(id: String) =
|
||||
Album(
|
||||
sourceId = 1,
|
||||
id = id,
|
||||
artistId = "a1",
|
||||
name = "Album $id",
|
||||
albumArtist = "Artist",
|
||||
created = 0,
|
||||
coverArt = null,
|
||||
genre = null,
|
||||
year = null,
|
||||
starred = null,
|
||||
songCount = 1,
|
||||
frequentRank = null,
|
||||
recentRank = null,
|
||||
)
|
||||
|
||||
private fun song(id: String) = Song(
|
||||
sourceId = 1,
|
||||
id = id,
|
||||
albumId = "al1",
|
||||
artistId = "a1",
|
||||
title = "Song $id",
|
||||
album = "Album",
|
||||
artist = "Artist",
|
||||
duration = 100,
|
||||
track = 1,
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
)
|
||||
private fun song(id: String) =
|
||||
Song(
|
||||
sourceId = 1,
|
||||
id = id,
|
||||
albumId = "al1",
|
||||
artistId = "a1",
|
||||
title = "Song $id",
|
||||
album = "Album",
|
||||
artist = "Artist",
|
||||
duration = 100,
|
||||
track = 1,
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
)
|
||||
|
||||
private fun playlist(id: String) =
|
||||
Playlist(sourceId = 1, id = id, name = "Playlist $id", comment = null, coverArt = null, songCount = 1, created = 0)
|
||||
|
||||
@@ -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