ui: cover the artist section split in tests

Assert the combined meta line while loading, cover the downloaded partition
and the plural album/song credit branches, share the album-credit id query
with its plan test, and put the meta line through a translatable string.
This commit is contained in:
austinried
2026-10-19 03:14:13 +00:00
parent 8cdbe64942
commit 3198a94df3
8 changed files with 116 additions and 23 deletions
@@ -118,6 +118,10 @@ internal const val ALBUMS_FOR_ARTIST =
"JOIN albums ON albums.sourceId = :sourceId AND albums.id = credits.albumId " + "JOIN albums ON albums.sourceId = :sourceId AND albums.id = credits.albumId " +
"ORDER BY year DESC, sortName COLLATE NOCASE, id" "ORDER BY year DESC, sortName COLLATE NOCASE, id"
internal const val ALBUM_CREDIT_ALBUM_IDS =
"SELECT DISTINCT albumId FROM ($CREDITED_ALBUM_ROWS) " +
"WHERE artistId = :artistId AND albumCredit = 1"
// Download visibility follows the credit path: an album credit includes the album when any song is // Download visibility follows the credit path: an album credit includes the album when any song is
// downloaded, while a track credit includes it only when the credited track is downloaded. // downloaded, while a track credit includes it only when the credited track is downloaded.
private const val DOWNLOADED_ARTIST_ALBUMS = private const val DOWNLOADED_ARTIST_ALBUMS =
@@ -1066,10 +1070,7 @@ interface LibraryDao {
artistId: String, artistId: String,
): Flow<List<Album>> ): Flow<List<Album>>
@Query( @Query(ALBUM_CREDIT_ALBUM_IDS)
"SELECT DISTINCT albumId FROM ($CREDITED_ALBUM_ROWS) " +
"WHERE artistId = :artistId AND albumCredit = 1",
)
fun albumCreditAlbumIds( fun albumCreditAlbumIds(
sourceId: Long, sourceId: Long,
artistId: String, artistId: String,
@@ -387,10 +387,8 @@ class LibraryRepository(
} }
return combine(albums, db.libraryDao().albumCreditAlbumIds(sourceId, artistId)) { list, creditIds -> return combine(albums, db.libraryDao().albumCreditAlbumIds(sourceId, artistId)) { list, creditIds ->
val credits = creditIds.toHashSet() val credits = creditIds.toHashSet()
ArtistAlbums( val (albumCredits, appearances) = list.partition { it.id in credits }
albumCredits = list.filter { it.id in credits }, ArtistAlbums(albumCredits = albumCredits, appearances = appearances)
appearances = list.filterNot { it.id in credits },
)
} }
} }
@@ -252,13 +252,8 @@ fun ArtistDetailScreen(
stringResource( stringResource(
if (albums.albumCredits.isNotEmpty()) R.string.artist_type_album_artist else R.string.artist_type_artist, if (albums.albumCredits.isNotEmpty()) R.string.artist_type_album_artist else R.string.artist_type_artist,
) )
val albumCountText = val albumCountText = pluralStringResource(R.plurals.resources_album_count, albumCount.toInt(), albumCount)
if (albumsLoaded) { val metaText = if (albumsLoaded) stringResource(R.string.artist_meta, artistType, albumCountText) else ""
pluralStringResource(R.plurals.resources_album_count, albumCount.toInt(), albumCount)
} else {
""
}
val metaText = if (albumsLoaded) "$artistType · $albumCountText" else ""
var titleHeightPx by remember { mutableFloatStateOf(0f) } var titleHeightPx by remember { mutableFloatStateOf(0f) }
val titleHeight = if (titleHeightPx > 0f) with(density) { titleHeightPx.toDp() } else lineHeight val titleHeight = if (titleHeightPx > 0f) with(density) { titleHeightPx.toDp() } else lineHeight
val titleMaxLines = val titleMaxLines =
+1
View File
@@ -37,6 +37,7 @@
<string name="artist_type_album_artist">Album artist</string> <string name="artist_type_album_artist">Album artist</string>
<string name="artist_section_albums">Albums (%1$d)</string> <string name="artist_section_albums">Albums (%1$d)</string>
<string name="artist_section_appearances">Appears on (%1$d)</string> <string name="artist_section_appearances">Appears on (%1$d)</string>
<string name="artist_meta">%1$s · %2$s</string>
<string name="resources_filter_genre">Genre</string> <string name="resources_filter_genre">Genre</string>
<string name="resources_filter_starred">Starred</string> <string name="resources_filter_starred">Starred</string>
<string name="resources_song_list_delete_all_content">%1$s\n\nDeleting will free %2$s</string> <string name="resources_song_list_delete_all_content">%1$s\n\nDeleting will free %2$s</string>
@@ -548,17 +548,18 @@ class LibraryDaoTest {
@Test @Test
fun albumCreditIdsFindCreditsThroughArtistIndexes() = fun albumCreditIdsFindCreditsThroughArtistIndexes() =
runTest { runTest {
val plan = val plan = plan(ALBUM_CREDIT_ALBUM_IDS)
plan(
"SELECT DISTINCT albumId FROM ($CREDITED_ALBUM_ROWS) " +
"WHERE artistId = :artistId AND albumCredit = 1",
)
assertTrue(plan, plan.contains("USING INDEX index_albums_sourceId_artistId (sourceId=? AND artistId=?)")) assertTrue(plan, plan.contains("USING INDEX index_albums_sourceId_artistId (sourceId=? AND artistId=?)"))
assertTrue( assertTrue(
plan, plan,
plan.contains("USING INDEX index_album_artists_sourceId_artistId (sourceId=? AND artistId=?)"), plan.contains("USING INDEX index_album_artists_sourceId_artistId (sourceId=? AND artistId=?)"),
) )
assertTrue(plan, plan.contains("USING INDEX index_songs_sourceId_artistId (sourceId=? AND artistId=?)"))
assertTrue(
plan,
plan.contains("USING INDEX index_song_artists_sourceId_artistId (sourceId=? AND artistId=?)"),
)
assertFalse(plan, plan.contains("SCAN albums")) assertFalse(plan, plan.contains("SCAN albums"))
assertFalse(plan, plan.contains("SCAN songs")) assertFalse(plan, plan.contains("SCAN songs"))
} }
@@ -12,7 +12,9 @@ import com.subtracks.cancelAndJoinBlocking
import com.subtracks.data.db.SubtracksDatabase import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.download.ArtworkStore import com.subtracks.data.download.ArtworkStore
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
import com.subtracks.data.model.DownloadStatus
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
import com.subtracks.data.model.SongDownload
import com.subtracks.data.model.Source import com.subtracks.data.model.Source
import com.subtracks.data.prefs.AlbumSort import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.StarredFilter import com.subtracks.data.prefs.StarredFilter
@@ -231,6 +233,37 @@ class LibraryRepositoryTest {
assertEquals(2, albums.count) assertEquals(2, albums.count)
} }
@Test
fun downloadedArtistAlbumsKeepPerCreditVisibility() =
runBlocking {
val sourceId = 1L
db.sourcesDao().upsertSource(
Source(id = sourceId, name = "test", address = "http://localhost", isActive = true, createdAt = 0),
)
db.libraryDao().upsertAlbums(
listOf(
seedAlbum(sourceId, 1),
seedAlbum(sourceId, 2).copy(artistId = "ar2"),
),
)
db.libraryDao().upsertSongs(
listOf(
seedSong(sourceId).copy(albumId = "al-0001"),
seedSong(sourceId).copy(id = "s2", albumId = "al-0002"),
),
)
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
val before = repository.artistAlbums(sourceId, "ar1", downloaded = true).first()
assertEquals(listOf("al-0001"), before.albumCredits.map { it.id })
assertEquals(emptyList<String>(), before.appearances.map { it.id })
db.downloadDao().upsert(SongDownload(sourceId, "s2", DownloadStatus.Completed))
val after = repository.artistAlbums(sourceId, "ar1", downloaded = true).first()
assertEquals(listOf("al-0001"), after.albumCredits.map { it.id })
assertEquals(listOf("al-0002"), after.appearances.map { it.id })
}
private suspend fun awaitRefreshes( private suspend fun awaitRefreshes(
presenter: RecordingPresenter, presenter: RecordingPresenter,
target: Int, target: Int,
@@ -1640,6 +1640,71 @@ class SyncServiceTest {
) )
} }
@Test
fun albumCreditIdsFollowPluralCredits() =
runTest {
insertSource()
val source =
FakeMusicSource(
artists = listOf(artist("a1", "A"), artist("a2", "B")),
albums =
listOf(
album("al1").copy(artistId = "a1", artists = listOf(ArtistRef("a1", "A"))),
album("al2").copy(artistId = "a2", artists = listOf(ArtistRef("a2", "B"))),
album("al3").copy(
artistId = "a2",
artists = listOf(ArtistRef("a1", "A"), ArtistRef("a2", "B")),
),
),
songs =
listOf(
song("s1").copy(
albumId = "al1",
artistId = "a1",
artists = listOf(ArtistRef("a1", "A")),
),
song("s2").copy(
albumId = "al2",
artistId = "a2",
artists = listOf(ArtistRef("a2", "B"), ArtistRef("a1", "A")),
),
song("s3").copy(
albumId = "al3",
artistId = "a2",
artists = listOf(ArtistRef("a1", "A"), ArtistRef("a2", "B")),
),
),
)
SyncService(db, source).sync()
assertEquals(
listOf("al1", "al3"),
db
.libraryDao()
.albumCreditAlbumIds(1, "a1")
.first()
.sorted(),
)
assertEquals(
listOf("al2", "al3"),
db
.libraryDao()
.albumCreditAlbumIds(1, "a2")
.first()
.sorted(),
)
assertEquals(
listOf("al1", "al2", "al3"),
db
.libraryDao()
.albumsForArtist(1, "a1")
.first()
.map { it.id }
.sorted(),
)
}
@Test @Test
fun everyArtistConsumerAgreesOnTheCreditedSet() = fun everyArtistConsumerAgreesOnTheCreditedSet() =
runTest { runTest {
@@ -104,9 +104,8 @@ class ArtistDetailScreenTest {
albumsLoaded = false, albumsLoaded = false,
) )
composeRule.onNodeWithText("Artist").assertDoesNotExist() composeRule.onNodeWithText("Artist · 0 albums").assertDoesNotExist()
composeRule.onNodeWithText("Album artist").assertDoesNotExist() composeRule.onNodeWithText("Album artist · 0 albums").assertDoesNotExist()
composeRule.onNodeWithText("0 albums").assertDoesNotExist()
} }
private fun album( private fun album(