Align TV layouts and close recovery gaps
This commit is contained in:
parent
bfe3dbbb16
commit
04b1bdd6aa
22 changed files with 522 additions and 53 deletions
|
|
@ -91,6 +91,22 @@ class DiscoverKeysTest {
|
|||
assertNull(state.overlay)
|
||||
assertEquals("The Amazing Spider-Man", state.hero?.title)
|
||||
assertEquals(original, state.lanes.map { lane -> lane.id to lane.titles.map { it.key } })
|
||||
assertNull("Undo restores silently; only Show again announces the return", state.toast)
|
||||
}
|
||||
|
||||
@Test fun showAgainRestoresTheHiddenPickAndAnnouncesItsReturn() {
|
||||
replay("Round 3 · Not interested on a pick (Discover hero)")
|
||||
val title = (state.overlay as DiscoverOverlayState.Hidden).title
|
||||
press(Key.DirectionRight, Key.DirectionCenter)
|
||||
rule.runOnIdle {
|
||||
val lane = state.lanes.first { it.hidden }
|
||||
state.focus(state.lanes.indexOf(lane) + 2, lane.titles.indexOfFirst { it.key == title.key })
|
||||
}
|
||||
press(Key.DirectionCenter)
|
||||
assertTrue(state.overlay is DiscoverOverlayState.Restore)
|
||||
press(Key.DirectionCenter)
|
||||
assertFalse(state.isHidden(title))
|
||||
assertEquals("Back in your picks.", state.toast)
|
||||
}
|
||||
|
||||
@Test fun heldOkOpensSheetButNeverSubmits() {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,34 @@ class HomeRatingCacheTest {
|
|||
assertEquals(ratings, cache.read(title.copy(id = "last", tmdbId = 399)))
|
||||
}
|
||||
|
||||
@Test fun episodeScoresShareSeriesSeasonIdentityWithoutCrossingEpisodesOrSeasons() {
|
||||
var now = 0L
|
||||
val cache = HomeRatingCache { now }
|
||||
val episode = HomeTitle("episode-a", "Pilot", "episode", tmdbId = 42, season = 1, episode = 1)
|
||||
val pilot = listOf(HomeRating("tmdb", 8.1, episode = true))
|
||||
val second = listOf(HomeRating("tmdb", 7.2, episode = true))
|
||||
cache.save(episode, pilot)
|
||||
cache.save(episode.copy(id = "episode-b", episode = 2), second)
|
||||
assertEquals(pilot, cache.read(episode.copy(id = "another-jellyfin-copy")))
|
||||
assertEquals(second, cache.read(episode.copy(id = "another-second-copy", episode = 2)))
|
||||
assertNull(cache.read(episode.copy(season = 2)))
|
||||
assertNull(cache.read(episode.copy(tmdbId = 43)))
|
||||
now = 6L * 60 * 60 * 1000
|
||||
assertNull(cache.read(episode))
|
||||
}
|
||||
|
||||
@Test fun seasonCapacityCountsSeasonsNotEpisodesAndIsIndependentOfTitleCapacity() {
|
||||
val cache = HomeRatingCache { 0L }
|
||||
val episode = HomeTitle("episode-a", "Pilot", "episode", tmdbId = 42, season = 1, episode = 1)
|
||||
repeat(301) { index -> cache.save(episode.copy(id = "episode-$index", episode = index + 1), ratings) }
|
||||
repeat(300) { index -> cache.save(title.copy(tmdbId = index + 100), ratings) }
|
||||
assertEquals(ratings, cache.read(episode))
|
||||
repeat(300) { index -> cache.save(episode.copy(season = index + 2), ratings) }
|
||||
assertNull(cache.read(episode))
|
||||
assertEquals(ratings, cache.read(episode.copy(season = 301)))
|
||||
assertEquals(ratings, cache.read(title.copy(tmdbId = 100)))
|
||||
}
|
||||
|
||||
@Test fun decimalScoresFollowLocaleButPercentagesDoNot() {
|
||||
assertEquals("7,8", ratings.single().format(Locale.forLanguageTag("nl-NL")))
|
||||
assertEquals("7.8", ratings.single().format(Locale.US))
|
||||
|
|
|
|||
|
|
@ -172,6 +172,23 @@ class LibraryKeysTest {
|
|||
assertEquals("Title 18", grid.focusedTitle!!.title)
|
||||
}
|
||||
|
||||
@Test fun mixedCatalogueRowsKeepMovieAndSeriesWithTheSameNumericIdentityDistinct() {
|
||||
val titles = List(12) { index ->
|
||||
val type = if (index < 6) "movie" else "tv"
|
||||
LibraryTitle((index % 6).toString(), "$type title ${index % 6}", type, tmdbId = (index % 6).toString())
|
||||
}
|
||||
val grid = LibraryGridState("Trending", titles, catalogue = true, onTitle = { opened = it })
|
||||
shell.pickerVisible = false
|
||||
shell.enterDestination("discover")
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { LibraryScreen(shell, grid, it) } } }
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(6, grid.index)
|
||||
press(Key.DirectionCenter)
|
||||
assertEquals("tv", opened!!.type)
|
||||
assertEquals("0", opened!!.tmdbId)
|
||||
assertEquals("tv title 0", opened!!.title)
|
||||
}
|
||||
|
||||
@Test fun scoresFollowLocaleAndUnratedSortsLast() {
|
||||
assertEquals("7,8", LibraryRating("tmdb", 7.8).formatted(Locale.forLanguageTag("nl-NL")))
|
||||
assertEquals("7.8", LibraryRating("tmdb", 7.8).formatted(Locale.US))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import java.io.File
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/** The normal JVM gate rejects untokened styling, undersized typography and internal screen wording. */
|
||||
class OwnedSourceAuditTest {
|
||||
@Test fun ownedScreensUseTokensReadableTextAndViewerFacingNames() {
|
||||
val roots = listOf(File("src/main/java"), File("../app/src/wolfskin"))
|
||||
roots.forEach { assertTrue("Missing owned source root: $it", it.isDirectory) }
|
||||
val violations = roots.flatMap { root ->
|
||||
root.walkTopDown().filter { it.isFile && it.extension == "kt" }.flatMap { source ->
|
||||
audit(source.name, source.readText()).map { "$source:$it" }
|
||||
}.toList()
|
||||
}
|
||||
assertTrue(violations.joinToString("\n"), violations.isEmpty())
|
||||
}
|
||||
|
||||
@Test fun plantedViolationsFailEachDoneBarRule() {
|
||||
val cases = listOf(
|
||||
Triple("Screen.kt", "Box(Modifier.width(12.dp))", "unit"),
|
||||
Triple("Screen.kt", "Box(Modifier.width(12f.dp))", "unit"),
|
||||
Triple("Screen.kt", "Text(fontSize = 14.sp)", "unit"),
|
||||
Triple("Screen.kt", "Box(Modifier.width((width + gap).dp))", "unit"),
|
||||
Triple("Screen.kt", "Text(color = Color(0xFF123456))", "color"),
|
||||
Triple("Screen.kt", "Text(color = ComposeColor(red = 1f, green = 0f, blue = 0f))", "color"),
|
||||
Triple("WolfskinTokens.kt", "val tiny = TextStyle(fontSize = 11.5.sp)", "font-floor"),
|
||||
Triple("Screen.kt", "Text(\"Recs Movies\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"DGiC\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Justin Shows\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Next page\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Previous page\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Page 2\")", "wording"),
|
||||
)
|
||||
cases.forEach { (name, source, rule) ->
|
||||
assertTrue("$rule accepted $source", audit(name, source).any { it.endsWith(":$rule") })
|
||||
}
|
||||
assertTrue(audit("WolfskinTokens.kt", "val body = TextStyle(fontSize = 12.sp); val fill = Color(0xFF123456)").isEmpty())
|
||||
assertTrue(audit("Screen.kt", "import androidx.compose.ui.unit.dp\nText(\"This title can’t be read\", style = WolfskinTokens.Type.body)").isEmpty())
|
||||
}
|
||||
|
||||
private fun audit(name: String, source: String): List<String> {
|
||||
val generated = name == "WolfskinTokens.kt"
|
||||
return source.lineSequence().mapIndexedNotNull { index, line ->
|
||||
val code = line.trimStart()
|
||||
if (code.startsWith("import ") || code.startsWith("//") || code.startsWith("*") || code.startsWith("/*")) return@mapIndexedNotNull null
|
||||
val rules = buildList {
|
||||
if (!generated && units.containsMatchIn(line)) add("unit")
|
||||
if (!generated && colors.containsMatchIn(line)) add("color")
|
||||
if (fonts.findAll(line).any { it.groupValues[1].toDouble() < 12 }) add("font-floor")
|
||||
if (strings.findAll(line).any { forbidden.containsMatchIn(it.value) }) add("wording")
|
||||
}
|
||||
rules.map { "${index + 1}:$it" }
|
||||
}.flatten().toList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val units = Regex("\\.(?:dp|sp)\\b")
|
||||
private val colors = Regex("\\b(?:Compose)?Color\\s*\\(\\s*(?:0[xX]|[0-9.]|(?:red|green|blue|alpha|color)\\s*=)")
|
||||
private val fonts = Regex("\\bfontSize\\s*=\\s*([0-9]+(?:\\.[0-9]+)?)[fF]?\\.sp\\b")
|
||||
private val strings = Regex("\"(?:\\\\.|[^\"\\\\])*\"")
|
||||
private val forbidden = Regex("Recs (?:Movies|Shows)|DGiC|(?:Justin|Mindel|Shared) (?:Movies|Shows)|(?:Next|Previous) page|Page (?:[0-9]|\\$)|\\bpagination\\b", RegexOption.IGNORE_CASE)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue