ui: show and expand the artist biography

Load the biography when the artist page is entered. A stored biography is
rendered immediately while it refreshes in the background; otherwise the
page starts with the loading message and falls back to the empty state.

Collapse a biography to two lines and append a bold More only when the
text does not fit. Tapping that line expands the biography, after which
the first lines keep clear of the star and overflow buttons and the rest
use the full width.
This commit is contained in:
austinried
2026-10-19 13:14:08 +00:00
parent 194787dfcc
commit 889bdb452b
5 changed files with 254 additions and 33 deletions
@@ -303,6 +303,25 @@ class AppScreenshotTest {
@Test
fun artistDetail() {
renderArtistDetail(bioExpanded = false)
awaitText("Kid A")
composeRule.waitUntil(timeoutMillis = 5_000) {
composeRule.onAllNodesWithTag(ARTIST_NAME_SCRIM_TAG).fetchSemanticsNodes().isNotEmpty()
}
composeRule.onRoot().captureRoboImage("src/test/screenshots/artist_detail.png")
}
@Test
fun artistDetailExpandedBio() {
renderArtistDetail(bioExpanded = true)
awaitText("Kid A")
composeRule.waitUntil(timeoutMillis = 5_000) {
composeRule.onAllNodesWithTag(ARTIST_NAME_SCRIM_TAG).fetchSemanticsNodes().isNotEmpty()
}
composeRule.onRoot().captureRoboImage("src/test/screenshots/artist_detail_bio_expanded.png")
}
private fun renderArtistDetail(bioExpanded: Boolean) {
composeRule.setContent {
SubtracksTheme {
ArtistDetailScreen(
@@ -311,6 +330,11 @@ class AppScreenshotTest {
ArtistAlbums(
albumCredits = Fixtures.albums.filter { it.artistId == Fixtures.artists.first().id },
),
bio =
"Radiohead are an English rock band formed in Abingdon, Oxfordshire, in 1985. " +
"The band consists of Thom Yorke, Jonny Greenwood, Colin Greenwood, Ed O'Brien, " +
"and Philip Selway.",
bioExpanded = bioExpanded,
art = CoverArtRef("art-ar-radiohead", "test:art-ar-radiohead"),
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
@@ -320,11 +344,6 @@ class AppScreenshotTest {
)
}
}
awaitText("Kid A")
composeRule.waitUntil(timeoutMillis = 5_000) {
composeRule.onAllNodesWithTag(ARTIST_NAME_SCRIM_TAG).fetchSemanticsNodes().isNotEmpty()
}
composeRule.onRoot().captureRoboImage("src/test/screenshots/artist_detail.png")
}
@Test
@@ -1,6 +1,10 @@
package com.subtracks.ui.library
import androidx.activity.ComponentActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.hasScrollAction
import androidx.compose.ui.test.hasText
@@ -8,13 +12,20 @@ import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performScrollToNode
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.ArtistAlbums
import com.subtracks.ui.theme.SubtracksTheme
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -31,18 +42,25 @@ class ArtistDetailScreenTest {
artistName: String,
albums: ArtistAlbums = ArtistAlbums(),
albumsLoaded: Boolean = true,
bio: String? = null,
bioLoading: Boolean = false,
) {
composeRule.setContent {
var expanded by remember { mutableStateOf(false) }
SubtracksTheme {
ArtistDetailScreen(
artist = artist,
artistName = artistName,
albums = albums,
albumsLoaded = albumsLoaded,
bio = bio,
bioLoading = bioLoading,
bioExpanded = expanded,
art = null,
coverArt = { _, _ -> null },
onBack = {},
onAlbumClick = {},
onExpandBio = { expanded = true },
)
}
}
@@ -108,6 +126,98 @@ class ArtistDetailScreenTest {
composeRule.onNodeWithText("Album artist · 0 albums").assertDoesNotExist()
}
@Test
fun aStoredBioShowsImmediatelyEvenWhileItRefreshes() {
render(
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 1, starred = null),
artistName = "Radiohead",
bio = "A stored biography.",
bioLoading = true,
)
composeRule.onNodeWithText("A stored biography.").assertExists()
composeRule.onNodeWithText("Loading artist bio…").assertDoesNotExist()
}
@Test
fun aMissingBioShowsTheLoadingStateFirst() {
render(
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 1, starred = null),
artistName = "Radiohead",
bioLoading = true,
)
composeRule.onNodeWithText("Loading artist bio…").assertExists()
composeRule.onNodeWithText("No artist bio available.").assertDoesNotExist()
}
@Test
fun aMissingBioShowsTheEmptyStateAfterTheRefresh() {
render(
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 1, starred = null),
artistName = "Radiohead",
)
composeRule.onNodeWithText("No artist bio available.").assertExists()
composeRule.onNodeWithText("Loading artist bio…").assertDoesNotExist()
}
@Test
fun aShortBioDoesNotOfferMore() {
render(
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 1, starred = null),
artistName = "Radiohead",
bio = "A short biography.",
)
composeRule.onNodeWithTag(ARTIST_BIO_MORE_TAG).assertDoesNotExist()
}
@Test
fun aLongBioExpandsWhenMoreIsTapped() {
render(
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 1, starred = null),
artistName = "Radiohead",
bio =
"Radiohead are an English rock band formed in Abingdon, Oxfordshire, in 1985. " +
"The band consists of Thom Yorke, Jonny Greenwood, Colin Greenwood, Ed O'Brien, " +
"and Philip Selway.",
)
composeRule.onNodeWithTag(ARTIST_BIO_MORE_TAG).performScrollTo().assertExists()
composeRule.onNodeWithTag(ARTIST_BIO_MORE_TAG).performClick()
composeRule.onNodeWithTag(ARTIST_BIO_MORE_TAG).assertDoesNotExist()
}
@Test
fun theCollapsedSecondLineContinuesWhereTheFirstLineEnds() {
val bio =
"Radiohead are an English rock band formed in Abingdon, Oxfordshire, in 1985. " +
"The band consists of Thom Yorke, Jonny Greenwood, Colin Greenwood, Ed O'Brien, " +
"and Philip Selway."
var result: BioLines? = null
composeRule.setContent {
val measurer = rememberTextMeasurer()
result = splitBioCollapsed(measurer, bio, TextStyle(fontSize = 14.sp), 400, 0, "More")
}
composeRule.waitForIdle()
val lines = requireNotNull(result)
assertTrue(lines.more)
val firstWords =
lines.lines[0]
.text
.split(' ')
.filter { it.isNotEmpty() }
val next = bio.split(' ').filter { it.isNotEmpty() }[firstWords.size]
assertTrue(
"expected the second line to continue with $next, was ${lines.lines[1].text}",
lines.lines[1].text.startsWith(next),
)
}
private fun album(
id: String,
name: String,