Show PWA source ratings on every surface through one shared rating row
All checks were successful
review/code-claude Claude code review: pass, no blocking findings

This commit is contained in:
justinzeus 2026-10-29 21:04:39 +01:00
parent 853a3989b2
commit 2ccce98dc1
26 changed files with 944 additions and 568 deletions

View file

@ -1,10 +1,8 @@
package com.github.damontecres.wholphin.data.model
import androidx.compose.ui.text.AnnotatedString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
@ -104,44 +102,59 @@ class SourceRatingsTest {
imdbVotes = 1,
)
assertEquals(listOf("85%", "40%"), ratings.criticEntries.map { it.text })
assertEquals(listOf(INLINE_RT_FRESH, INLINE_RT_SPILLED), ratings.criticEntries.map { it.inlineContentId })
assertEquals(listOf(RatingBadge.RT_FRESH, RatingBadge.RT_SPILLED), ratings.criticEntries.map { it.badge })
assertEquals(listOf("8.0"), ratings.communityEntries.map { it.text })
assertEquals(listOf(INLINE_IMDB), ratings.communityEntries.map { it.inlineContentId })
assertEquals(listOf(RatingBadge.IMDB), ratings.communityEntries.map { it.badge })
}
private val jellyfinDetails =
QuickDetailsData(
basic = AnnotatedString("2020"),
communityRating = AnnotatedString("7.8 jellyfin"),
criticRating = AnnotatedString("59% jellyfin"),
@Test
fun everyRtMarkHasItsOwnBadge() {
val badges =
listOf(
RtMark.FRESH to RatingBadge.RT_FRESH,
RtMark.ROTTEN to RatingBadge.RT_ROTTEN,
RtMark.UPRIGHT to RatingBadge.RT_UPRIGHT,
RtMark.SPILLED to RatingBadge.RT_SPILLED,
RtMark.UNKNOWN to RatingBadge.RT_UNKNOWN,
)
badges.forEach { (mark, badge) ->
assertEquals(badge, RatingEntry.RottenTomatoes(RtScore(50, mark), true).badge)
}
}
private val movie =
SourceRatings.of(
rtCriticsScore = 96,
rtCriticsLabel = "Certified Fresh",
rtAudienceScore = 59,
rtAudienceLabel = "Spilled",
imdbScore = 8.8,
imdbVotes = 10,
)
@Test
fun jellyfinRatingsAreKeptWhenThereAreNoSourceRatings() {
assertSame(jellyfinDetails, jellyfinDetails.withSourceRatings(null))
assertSame(jellyfinDetails, jellyfinDetails.withSourceRatings(SourceRatings()))
fun visibleEntriesAreInPwaOrderAndFollowTheDisplaySettings() {
// Rotten Tomatoes (critic rating setting) before IMDb (community rating setting)
assertEquals(
listOf("96%", "59%", "8.8"),
movie.visibleEntries(community = true, critic = true).map { it.text },
)
assertEquals(listOf("8.8"), movie.visibleEntries(community = true, critic = false).map { it.text })
assertEquals(listOf("96%", "59%"), movie.visibleEntries(community = false, critic = true).map { it.text })
assertTrue(movie.visibleEntries(community = false, critic = false).isEmpty())
}
@Test
fun sourceRatingsReplaceJellyfinRatingsAndMapOntoTheExistingToggles() {
val details =
jellyfinDetails.withSourceRatings(
SourceRatings.of(rtCriticsScore = 96, rtCriticsLabel = "Certified Fresh", imdbScore = 8.8, imdbVotes = 10),
)
// COMMUNITY_RATING toggle -> IMDb, CRITIC_RATING toggle -> Rotten Tomatoes
assertTrue(details.communityRating!!.text.endsWith("8.8"))
assertTrue(details.criticRating!!.text.endsWith("96%"))
assertEquals(jellyfinDetails.basic, details.basic)
// PWA order: Rotten Tomatoes before IMDb
assertTrue(details.criticRatingFirst)
assertFalse(jellyfinDetails.criticRatingFirst)
fun tmdbIsTheCommunityEntryWhenItIsTheOnlySource() {
val entries = SourceRatings.of(tmdbScore = 7.6, tmdbVotes = 100).visibleEntries(community = true, critic = true)
assertEquals(listOf(RatingBadge.TMDB), entries.map { it.badge })
assertEquals(listOf("7.6"), entries.map { it.text })
}
@Test
fun jellyfinValueForASourceThatSeerrDoesNotHaveIsNotMixedIn() {
// Seerr has only RT: the Jellyfin star is not shown next to it
val details = jellyfinDetails.withSourceRatings(SourceRatings.of(rtCriticsScore = 50, rtCriticsLabel = "Fresh"))
assertNull(details.communityRating)
assertTrue(details.criticRating!!.text.endsWith("50%"))
fun noEntriesWithoutRatings() {
assertTrue(SourceRatings().isEmpty)
assertTrue(SourceRatings().visibleEntries(community = true, critic = true).isEmpty())
assertFalse(movie.isEmpty)
}
}

View file

@ -0,0 +1,268 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.github.damontecres.wholphin.services
import com.github.damontecres.wholphin.api.seerr.MoviesApi
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
import com.github.damontecres.wholphin.api.seerr.TvApi
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingscombinedGet200Response
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response
import com.github.damontecres.wholphin.api.seerr.model.TvTvIdRatingsGet200Response
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.SeerrItemType
import com.github.damontecres.wholphin.ui.successResponse
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.operations.UserLibraryApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import java.io.IOException
import java.util.UUID
/**
* Which Jellyfin items have ratings, how their TMDB id is found, and when Seerr is asked
*/
class SeerrRatingsItemsTest {
private val moviesApi = mockk<MoviesApi>()
private val tvApi = mockk<TvApi>()
private val seerrApi =
mockk<SeerrApiClient> {
every { moviesApi } returns this@SeerrRatingsItemsTest.moviesApi
every { tvApi } returns this@SeerrRatingsItemsTest.tvApi
}
private val active = MutableStateFlow(true)
private val seerrService =
mockk<SeerrService> {
every { api } returns seerrApi
every { this@mockk.active } returns this@SeerrRatingsItemsTest.active
}
private val userLibraryApi = mockk<UserLibraryApi>()
private val jellyfin =
mockk<ApiClient> {
every { userLibraryApi } returns this@SeerrRatingsItemsTest.userLibraryApi
}
private val service = SeerrRatingsService(seerrService, jellyfin)
private val seriesId = UUID.randomUUID()
private val seriesTmdb = 1396
private val rtOnly = MovieMovieIdRatingscombinedGet200Response(rt = MovieMovieIdRatingsGet200Response(criticsScore = 90, criticsRating = "Fresh"))
private val tvRatings = TvTvIdRatingsGet200Response(criticsScore = 80, criticsRating = "Fresh")
private fun item(
type: BaseItemKind,
providerIds: Map<String, String>? = null,
seriesId: UUID? = null,
) = BaseItem(BaseItemDto(id = UUID.randomUUID(), type = type, providerIds = providerIds, seriesId = seriesId))
private fun jellyfinHasSeries(tmdb: Int? = seriesTmdb) {
coEvery { userLibraryApi.getItem(seriesId) } returns
successResponse(
BaseItemDto(
id = seriesId,
type = BaseItemKind.SERIES,
providerIds = tmdb?.let { mapOf("Tmdb" to it.toString()) } ?: mapOf("Tvdb" to "1"),
),
)
}
@Test
fun moviesAndSeriesUseTheirOwnProviderIdsWithoutAskingJellyfin() =
runTest {
assertEquals(
RatingsTarget(SeerrItemType.MOVIE, 11),
service.resolveTarget(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "11"))),
)
assertEquals(
RatingsTarget(SeerrItemType.TV, 22),
service.resolveTarget(item(BaseItemKind.SERIES, mapOf("Tmdb" to "22"))),
)
coVerify(exactly = 0) { userLibraryApi.getItem(any()) }
}
@Test
fun episodesAndSeasonsShowTheirSeriesRatingsNotTheirOwnTmdbId() =
runTest {
jellyfinHasSeries()
// The Tmdb id on an episode or season identifies the episode or season, never the series
val episode = item(BaseItemKind.EPISODE, mapOf("Tmdb" to "999999"), seriesId)
val season = item(BaseItemKind.SEASON, mapOf("Tmdb" to "888888"), seriesId)
assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(episode))
assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(season))
}
@Test
fun itemsThatArePartOfNoPwaTitleHaveNoRatingsAndNeverCallAnything() =
runTest {
val none =
listOf(
BaseItemKind.BOX_SET,
BaseItemKind.MUSIC_ALBUM,
BaseItemKind.MUSIC_ARTIST,
BaseItemKind.AUDIO,
BaseItemKind.PHOTO,
BaseItemKind.PHOTO_ALBUM,
BaseItemKind.PROGRAM,
BaseItemKind.PLAYLIST,
BaseItemKind.VIDEO,
)
none.forEach {
val item = item(it, mapOf("Tmdb" to "5"), seriesId)
assertNull(it.name, service.resolveTarget(item))
assertNull(it.name, service.forItem(item).first())
assertNull(it.name, service.cachedFor(item))
}
coVerify(exactly = 0) { userLibraryApi.getItem(any()) }
coVerify(exactly = 0) { moviesApi.movieMovieIdRatingscombinedGet(any()) }
coVerify(exactly = 0) { tvApi.tvTvIdRatingsGet(any()) }
}
@Test
fun episodeWithoutASeriesHasNoRatings() =
runTest {
assertNull(service.resolveTarget(item(BaseItemKind.EPISODE, mapOf("Tmdb" to "3"), null)))
}
@Test
fun homeItemsWithoutProviderIdsAreResolvedThroughJellyfinOnceAndCached() =
runTest {
jellyfinHasSeries()
val first = item(BaseItemKind.EPISODE, null, seriesId)
val second = item(BaseItemKind.EPISODE, null, seriesId)
assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(first))
assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(second))
// every other episode of the series is known without a request
coVerify(exactly = 1) { userLibraryApi.getItem(seriesId) }
}
@Test
fun homeMovieWithoutProviderIdsLooksItUp() =
runTest {
val movie = item(BaseItemKind.MOVIE, null)
coEvery { userLibraryApi.getItem(movie.id) } returns
successResponse(BaseItemDto(id = movie.id, type = BaseItemKind.MOVIE, providerIds = mapOf("Tmdb" to "77")))
assertEquals(RatingsTarget(SeerrItemType.MOVIE, 77), service.resolveTarget(movie))
}
@Test
fun seriesWithoutTmdbIdHasNoRatingsAndIsNotLookedUpAgain() =
runTest {
jellyfinHasSeries(tmdb = null)
val episode = item(BaseItemKind.EPISODE, null, seriesId)
assertNull(service.resolveTarget(episode))
assertNull(service.forItem(episode).first())
coVerify(exactly = 1) { userLibraryApi.getItem(seriesId) }
}
@Test
fun failedLookupIsRetriedLater() =
runTest {
coEvery { userLibraryApi.getItem(seriesId) } throws IOException("down")
val episode = item(BaseItemKind.EPISODE, null, seriesId)
assertNull(service.resolveTarget(episode))
jellyfinHasSeries()
assertEquals(RatingsTarget(SeerrItemType.TV, seriesTmdb), service.resolveTarget(episode))
}
@Test
fun episodeRatingsAreTheSeriesRatingsAndSeedLaterEpisodes() =
runTest {
jellyfinHasSeries()
coEvery { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } returns tvRatings
val episode = item(BaseItemKind.EPISODE, null, seriesId)
val other = item(BaseItemKind.EPISODE, null, seriesId)
assertNull(service.cachedFor(episode))
val ratings = service.forItem(episode).first()
assertEquals(80, ratings?.rtCritics?.percent)
// moving to another episode of the same series needs no wait and no request
assertEquals(ratings, service.cachedFor(other))
assertEquals(ratings, service.forItem(other).first())
coVerify(exactly = 1) { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) }
}
@Test
fun scrollingPastItemsNeverAsksSeerr() =
runTest {
coEvery { moviesApi.movieMovieIdRatingscombinedGet(any()) } returns rtOnly
val jobs =
(1..20).map { id ->
val job = launch { service.forItem(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "$id"))).collect { } }
runCurrent()
// focus moves on before the wait has passed
advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS - 50)
job.cancel()
job
}
jobs.forEach(Job::cancel)
runCurrent()
coVerify(exactly = 0) { moviesApi.movieMovieIdRatingscombinedGet(any()) }
}
@Test
fun restingOnAnItemAsksSeerrOnceAfterTheWait() =
runTest {
coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 7) } returns rtOnly
var result: Any? = "unset"
val job = launch { service.forItem(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "7"))).collect { result = it } }
advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS - 1)
runCurrent()
assertEquals("unset", result)
coVerify(exactly = 0) { moviesApi.movieMovieIdRatingscombinedGet(any()) }
advanceTimeBy(2)
runCurrent()
assertNotNull(result)
coVerify(exactly = 1) { moviesApi.movieMovieIdRatingscombinedGet(movieId = 7) }
job.cancel()
}
@Test
fun movingOnWhileTheRequestIsRunningCancelsIt() =
runTest {
var cancelled = false
coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 8) } coAnswers {
try {
awaitCancellation()
} finally {
cancelled = true
}
}
val job = launch { service.forItem(item(BaseItemKind.MOVIE, mapOf("Tmdb" to "8"))).collect { } }
advanceTimeBy(SeerrRatingsService.DEBOUNCE_MILLIS + 1)
runCurrent()
coVerify(exactly = 1) { moviesApi.movieMovieIdRatingscombinedGet(movieId = 8) }
job.cancel()
runCurrent()
assertEquals(true, cancelled)
assertNull(service.cached(SeerrItemType.MOVIE, 8))
}
}

View file

@ -21,6 +21,7 @@ import io.mockk.mockk
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.junit.Assert.assertEquals
@ -45,7 +46,7 @@ class SeerrRatingsServiceTest {
every { api } returns apiClient
every { active } answers { activeFlow }
}
private val service = SeerrRatingsService(seerrService)
private val service = SeerrRatingsService(seerrService, mockk<ApiClient>())
private fun movieDetails(
average: Double?,
@ -253,8 +254,7 @@ class SeerrRatingsServiceTest {
// no TMDB id
assertNull(service.forItem(jellyfinItem(BaseItemKind.MOVIE, null)).first())
assertNull(service.forItem(jellyfinItem(BaseItemKind.MOVIE, mapOf("Imdb" to "tt1"))).first())
// not a movie/series
assertNull(service.forItem(jellyfinItem(BaseItemKind.EPISODE, mapOf("Tmdb" to "1"))).first())
// not something that has ratings
assertNull(service.forItem(jellyfinItem(BaseItemKind.BOX_SET, mapOf("Tmdb" to "1"))).first())
// call fails
assertNull(service.forItem(jellyfinItem(BaseItemKind.SERIES, mapOf("Tmdb" to "2"))).first())

View file

@ -0,0 +1,239 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.github.damontecres.wholphin.api.seerr.MoviesApi
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
import com.github.damontecres.wholphin.api.seerr.TvApi
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingscombinedGet200Response
import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingscombinedGet200ResponseImdb
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
import com.github.damontecres.wholphin.api.seerr.model.TvTvIdRatingsGet200Response
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.SourceRatings
import com.github.damontecres.wholphin.preferences.DisplayToggle
import com.github.damontecres.wholphin.services.SeerrRatingsService
import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.successResponse
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.InterfaceCustomization
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.operations.UserLibraryApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.util.EnumSet
import java.util.UUID
/**
* What every surface shows: the rating row comes only from Seerr, per the PWA's rules for the kind of item, and
* the Jellyfin rating of an item is never drawn
*/
@Config(sdk = [34])
@RunWith(RobolectricTestRunner::class)
class SourceRatingsRowTest {
@get:Rule val rule = createComposeRule()
private val moviesApi = mockk<MoviesApi>()
private val tvApi = mockk<TvApi>()
private val seerrApi =
mockk<SeerrApiClient> {
every { moviesApi } returns this@SourceRatingsRowTest.moviesApi
every { tvApi } returns this@SourceRatingsRowTest.tvApi
}
private val seerrService =
mockk<SeerrService> {
every { api } returns seerrApi
every { active } returns flowOf(true)
}
private val userLibraryApi = mockk<UserLibraryApi>()
private val jellyfin = mockk<ApiClient> { every { userLibraryApi } returns this@SourceRatingsRowTest.userLibraryApi }
private val service = SeerrRatingsService(seerrService, jellyfin)
private val seriesId = UUID.randomUUID()
private val seriesTmdb = 1396
/** Every item carries Jellyfin ratings that must never be drawn */
private fun item(
type: BaseItemKind,
tmdb: Int? = null,
seriesId: UUID? = null,
) = BaseItem(
BaseItemDto(
id = UUID.randomUUID(),
type = type,
name = "Title",
providerIds = tmdb?.let { mapOf("Tmdb" to it.toString()) },
seriesId = seriesId,
communityRating = 7.8f,
criticRating = 59f,
),
)
private fun movieRatings() {
coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 603) } returns
MovieMovieIdRatingscombinedGet200Response(
rt = MovieMovieIdRatingsGet200Response(criticsScore = 96, criticsRating = "Certified Fresh", audienceScore = 90, audienceRating = "Upright"),
imdb = MovieMovieIdRatingscombinedGet200ResponseImdb(criticsScore = 8.8.toBigDecimal(), criticsScoreCount = 10),
)
}
private fun seriesRatings() {
coEvery { tvApi.tvTvIdRatingsGet(tvId = seriesTmdb) } returns
TvTvIdRatingsGet200Response(criticsScore = 81, criticsRating = "Fresh", audienceScore = 45, audienceRating = "Spilled")
coEvery { userLibraryApi.getItem(seriesId) } returns
successResponse(BaseItemDto(id = seriesId, type = BaseItemKind.SERIES, providerIds = mapOf("Tmdb" to "$seriesTmdb")))
}
/**
* Shows the quick details of the items, after what a screen's wait for Seerr would have loaded
*/
private fun show(
vararg items: BaseItem,
toggles: EnumSet<DisplayToggle> = EnumSet.allOf(DisplayToggle::class.java),
) {
runBlocking {
items.forEach { service.forItem(it, 0).collect { } }
}
rule.setContent {
WholphinTheme {
CompositionLocalProvider(
LocalSeerrRatingsService provides service,
LocalInterfaceCustomization provides InterfaceCustomization(toggles),
) {
Column {
items.forEach { QuickDetails(it, null) }
}
}
}
}
rule.waitForIdle()
}
private fun assertShown(text: String) = rule.onNodeWithText(text, substring = true).assertExists()
private fun assertNotShown(text: String) = rule.onNodeWithText(text, substring = true).assertDoesNotExist()
private fun assertJellyfinRatingNotShown() {
assertNotShown("7.8")
assertNotShown("59%")
}
@Test
fun movieShowsRottenTomatoesThenImdbAndNotItsJellyfinRating() {
movieRatings()
show(item(BaseItemKind.MOVIE, 603))
assertShown("96%")
assertShown("90%")
assertShown("8.8")
assertJellyfinRatingNotShown()
}
@Test
fun seriesShowsItsRatings() {
seriesRatings()
show(item(BaseItemKind.SERIES, seriesTmdb))
assertShown("81%")
assertShown("45%")
assertJellyfinRatingNotShown()
}
@Test
fun episodeAndSeasonShowTheirSeriesRatings() {
seriesRatings()
show(item(BaseItemKind.EPISODE, seriesId = seriesId))
assertShown("81%")
assertJellyfinRatingNotShown()
}
@Test
fun seasonShowsItsSeriesRatings() {
seriesRatings()
show(item(BaseItemKind.SEASON, seriesId = seriesId))
assertShown("45%")
assertJellyfinRatingNotShown()
}
@Test
fun surfacesWithoutAPwaEquivalentShowNoRatingNotTheJellyfinStar() {
movieRatings()
seriesRatings()
show(
*listOf(
BaseItemKind.BOX_SET,
BaseItemKind.MUSIC_ALBUM,
BaseItemKind.MUSIC_ARTIST,
BaseItemKind.PHOTO,
BaseItemKind.PROGRAM,
).map { item(it, 603, seriesId) }.toTypedArray(),
)
assertNotShown("96%")
assertNotShown("81%")
assertJellyfinRatingNotShown()
}
@Test
fun titleSeerrHasNoRatingsForShowsNoRatingNotTheJellyfinStar() {
coEvery { moviesApi.movieMovieIdRatingscombinedGet(movieId = 5) } returns MovieMovieIdRatingscombinedGet200Response()
coEvery { moviesApi.movieMovieIdGet(movieId = 5) } returns
mockk {
every { voteAverage } returns null
every { voteCount } returns null
}
show(item(BaseItemKind.MOVIE, 5))
assertJellyfinRatingNotShown()
}
@Test
fun theDisplaySettingsStillControlWhichSourcesAreDrawn() {
movieRatings()
show(item(BaseItemKind.MOVIE, 603), toggles = EnumSet.of(DisplayToggle.COMMUNITY_RATING))
assertShown("8.8")
assertNotShown("96%")
}
@Test
fun criticSettingHidesImdb() {
movieRatings()
show(item(BaseItemKind.MOVIE, 603), toggles = EnumSet.of(DisplayToggle.CRITIC_RATING))
assertShown("96%")
assertNotShown("8.8")
}
@Test
fun ratingsTextHasOneBadgeAndNumberPerEntryInOrder() {
val ratings =
SourceRatings.of(
rtCriticsScore = 96,
rtCriticsLabel = "Certified Fresh",
rtAudienceScore = 30,
rtAudienceLabel = "Spilled",
imdbScore = 8.8,
imdbVotes = 10,
)
val entries = ratings.visibleEntries(community = true, critic = true)
val text = ratingsText(entries, leadingDot = false)
assertTrue(text.text.indexOf("96%") < text.text.indexOf("30%"))
assertTrue(text.text.indexOf("30%") < text.text.indexOf("8.8"))
// dots only between entries, or also in front when something precedes the ratings
assertEquals(2, text.text.split("\u2022").size - 1)
assertEquals(3, ratingsText(entries, leadingDot = true).text.split("\u2022").size - 1)
}
}