From 38ba46f32e67af402be790e6c5740786b364c87a Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Sat, 18 Jul 2026 12:41:01 +0200
Subject: [PATCH 01/11] feat(reroll): add reroll button UI with i18n
Add RerollButton composable matching pen design (pill shape, green accent, shuffle icon). Wire OnRerollClicked action and localize string across 13 locales.
---
.../composeResources/values-ar/strings.xml | 1 +
.../composeResources/values-ca/strings.xml | 1 +
.../composeResources/values-de/strings.xml | 1 +
.../composeResources/values-es/strings.xml | 1 +
.../composeResources/values-fr/strings.xml | 1 +
.../composeResources/values-gl/strings.xml | 1 +
.../composeResources/values-hi/strings.xml | 1 +
.../composeResources/values-it/strings.xml | 1 +
.../composeResources/values-ja/strings.xml | 1 +
.../composeResources/values-pt/strings.xml | 1 +
.../composeResources/values-ru/strings.xml | 1 +
.../composeResources/values-zh/strings.xml | 1 +
.../composeResources/values/strings.xml | 1 +
.../presentation/components/FilmDisplay.kt | 3 +
.../presentation/components/FilmPoster.kt | 4 ++
.../presentation/components/RerollButton.kt | 63 +++++++++++++++++++
.../viewmodel/RandomFilmAction.kt | 2 +
17 files changed, 85 insertions(+)
create mode 100644 composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
diff --git a/composeApp/src/commonMain/composeResources/values-ar/strings.xml b/composeApp/src/commonMain/composeResources/values-ar/strings.xml
index 215a1a0..aa89652 100644
--- a/composeApp/src/commonMain/composeResources/values-ar/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ar/strings.xml
@@ -70,4 +70,5 @@
يعمل مع وضع المستخدم الواحد ووضع متعدد المستخدمين
جاري رمي النرد...
جاري البحث عن فيلمك العشوائي
+ إعادة الرمي
diff --git a/composeApp/src/commonMain/composeResources/values-ca/strings.xml b/composeApp/src/commonMain/composeResources/values-ca/strings.xml
index 107fd84..ca64c6a 100644
--- a/composeApp/src/commonMain/composeResources/values-ca/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ca/strings.xml
@@ -70,4 +70,5 @@
Funciona amb els modes d\'un sol usuari i multiusuari
Llançant els daus...
Trobar la teva pel·lícula aleatòria
+ Tornar a tirar
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml
index c249c13..fb57e4f 100644
--- a/composeApp/src/commonMain/composeResources/values-de/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml
@@ -70,4 +70,5 @@
Funktioniert im Einzel- und Mehrbenutzermodus
Würfel rollen...
Finde deinen zufälligen Film
+ Neu würfeln
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml
index 0968f71..ac9356f 100644
--- a/composeApp/src/commonMain/composeResources/values-es/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml
@@ -70,4 +70,5 @@
Funciona con los modos de un solo usuario y multiusuario
Lanzando los dados...
Encontrando tu película aleatoria
+ Tirar de nuevo
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml
index a1967b5..f52b894 100644
--- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml
@@ -70,4 +70,5 @@
Fonctionne avec les modes utilisateur unique et multi-utilisateur
Lancement des dés...
Trouver votre film aléatoire
+ Relancer
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-gl/strings.xml b/composeApp/src/commonMain/composeResources/values-gl/strings.xml
index 0eba5c4..d915cdf 100644
--- a/composeApp/src/commonMain/composeResources/values-gl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-gl/strings.xml
@@ -70,4 +70,5 @@
Funciona cos modos de usuario único e multiusuario
Lanzando os dados...
Atopando o teu filme aleatorio
+ Tirar de novo
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-hi/strings.xml b/composeApp/src/commonMain/composeResources/values-hi/strings.xml
index 6f4ddc5..cede8be 100644
--- a/composeApp/src/commonMain/composeResources/values-hi/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-hi/strings.xml
@@ -70,4 +70,5 @@
एकल उपयोगकर्ता और बहु-उपयोगकर्ता मोड दोनों के साथ काम करता है
पासा फेंक रहे हैं...
आपकी यादृच्छिक फिल्म खोज रहे हैं
+ फिर से फेंकें
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml
index d4fe9c8..8fc1186 100644
--- a/composeApp/src/commonMain/composeResources/values-it/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml
@@ -70,4 +70,5 @@
Funziona con le modalità utente singolo e multiutente
Lanciando i dadi...
Trovare il tuo film casuale
+ Rilancia
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml
index 2f56388..56ebd4a 100644
--- a/composeApp/src/commonMain/composeResources/values-ja/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml
@@ -70,4 +70,5 @@
シングルユーザーとマルチユーザーモードの両方で動作
サイコロを振っています...
ランダム映画を探しています
+ もう一度振る
diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml
index cf7dbee..aac3548 100644
--- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml
@@ -70,4 +70,5 @@
Funciona nos modos de usuário único e multiusuário
Lançando os dados...
Encontrando seu filme aleatório
+ Rolar novamente
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings.xml b/composeApp/src/commonMain/composeResources/values-ru/strings.xml
index 45aa99d..65be95c 100644
--- a/composeApp/src/commonMain/composeResources/values-ru/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-ru/strings.xml
@@ -70,4 +70,5 @@
Работает в режимах одного и нескольких пользователей
Бросаем кубики...
Ищем ваш случайный фильм
+ Перебросить
diff --git a/composeApp/src/commonMain/composeResources/values-zh/strings.xml b/composeApp/src/commonMain/composeResources/values-zh/strings.xml
index 624d824..3514b69 100644
--- a/composeApp/src/commonMain/composeResources/values-zh/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-zh/strings.xml
@@ -70,4 +70,5 @@
适用于单用户和多用户模式
掷骰子中...
正在寻找您的随机电影
+ 重新掷骰
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 3ea1378..3e99d60 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -70,4 +70,5 @@
Works with single user and multi-user modes
Rolling the dice...
Finding your random movie
+ Reroll
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt
index be64f9e..70a0b3f 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt
@@ -25,5 +25,8 @@ internal fun FilmDisplay(
onClick = {
onAction(RandomFilmAction.OnFilmClicked(film))
},
+ onRerollClick = {
+ onAction(RandomFilmAction.OnRerollClicked)
+ },
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt
index 5f3e93f..6196ebe 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt
@@ -49,6 +49,7 @@ fun FilmPoster(
title: String,
releaseYear: String,
onClick: () -> Unit,
+ onRerollClick: () -> Unit,
) {
var imageLoadResult by remember {
mutableStateOf?>(null)
@@ -168,6 +169,9 @@ fun FilmPoster(
fontSize = 17.sp,
textAlign = TextAlign.Center,
)
+ RerollButton(
+ onClick = onRerollClick,
+ )
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
new file mode 100644
index 0000000..d32dca5
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
@@ -0,0 +1,63 @@
+package com.nacchofer31.randomboxd.random_film.presentation.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Shuffle
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.dropShadow
+import androidx.compose.ui.graphics.shadow.Shadow
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.nacchofer31.randomboxd.core.presentation.RandomBoxdColors
+import org.jetbrains.compose.resources.stringResource
+import randomboxd.composeapp.generated.resources.Res
+import randomboxd.composeapp.generated.resources.reroll
+
+@Composable
+internal fun RerollButton(onClick: () -> Unit) =
+ Surface(
+ onClick = { onClick() },
+ modifier =
+ Modifier
+ .padding(top = 5.dp)
+ .dropShadow(
+ shape = RoundedCornerShape(100),
+ shadow =
+ Shadow(
+ radius = 10.dp,
+ color = RandomBoxdColors.GreenAccent.copy(alpha = 0.25f),
+ spread = 4.dp,
+ ),
+ ),
+ shape = RoundedCornerShape(100),
+ color = RandomBoxdColors.GreenAccent,
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 6.dp),
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Shuffle,
+ contentDescription = null,
+ tint = RandomBoxdColors.BackgroundDarkColor,
+ modifier = Modifier.size(20.dp),
+ )
+ Text(
+ text = stringResource(Res.string.reroll),
+ color = RandomBoxdColors.BackgroundDarkColor,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ }
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmAction.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmAction.kt
index ab75e84..39ff855 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmAction.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmAction.kt
@@ -48,4 +48,6 @@ sealed interface RandomFilmAction {
data class OnGenreSelectionApplied(
val genres: Set,
) : RandomFilmAction
+
+ data object OnRerollClicked : RandomFilmAction
}
From 142033469cb123bf4bb8d761d8b576482942186b Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Sat, 18 Jul 2026 12:41:15 +0200
Subject: [PATCH 02/11] feat(reroll): add reroll logic in ViewModel
Rename getRandomMovie to getRandomMovies returning Set for caching. Add extractResultMovie to repository. ViewModel caches results and picks a random film on reroll.
---
.../RandomFilmScrappingRepository.kt | 29 +++++------
.../domain/repository/RandomFilmRepository.kt | 8 ++-
.../viewmodel/RandomFilmViewModel.kt | 52 ++++++++++++++++---
3 files changed, 64 insertions(+), 25 deletions(-)
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt
index d8bd035..6030a60 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt
@@ -35,29 +35,27 @@ class RandomFilmScrappingRepository(
const val FILM_SCRIPT_QUERY = """script[type="application/ld+json"]"""
}
- override suspend fun getRandomMovie(
+ override suspend fun getRandomMovies(
userName: String,
selectedGenres: Set,
- ): ResultData =
+ ): ResultData, DataError.Remote> =
withContext(Dispatchers.IO) {
- val filmsResult = getFilmsFromUserWatchlist(userName, selectedGenres)
val films =
- when (filmsResult) {
+ when (val filmsResult = getFilmsFromUserWatchlist(userName, selectedGenres)) {
is ResultData.Success -> filmsResult.data
is ResultData.Error -> return@withContext ResultData.Error(filmsResult.error)
}
if (films.isEmpty()) return@withContext ResultData.Error(DataError.Remote.NO_RESULTS)
- val chosenFilm = films.random()
- extractFilm(chosenFilm)
+ return@withContext ResultData.Success(films.toSet())
}
override suspend fun getRandomMoviesFromSearchList(
searchList: Set,
filmSearchMode: FilmSearchMode,
selectedGenres: Set,
- ): ResultData =
+ ): ResultData, DataError.Remote> =
withContext(Dispatchers.IO) {
if (searchList.isEmpty()) return@withContext ResultData.Error(DataError.Remote.SERIALIZATION)
@@ -88,10 +86,11 @@ class RandomFilmScrappingRepository(
if (combinedFilms.isEmpty()) return@withContext ResultData.Error(DataError.Remote.NO_RESULTS)
- val chosenFilm = combinedFilms.random()
- extractFilm(chosenFilm)
+ return@withContext ResultData.Success(combinedFilms)
}
+ override suspend fun extractResultMovie(film: Film): ResultData = extractFilm(film)
+
private suspend fun getFilmsFromUserWatchlist(
userName: String,
selectedGenres: Set = emptySet(),
@@ -118,9 +117,8 @@ class RandomFilmScrappingRepository(
val films = mutableListOf()
for (page in 1..totalPages) {
val pageUrl = "$baseUrl/page/$page/"
- val htmlResult = getWebPage(pageUrl)
val html =
- when (htmlResult) {
+ when (val htmlResult = getWebPage(pageUrl)) {
is ResultData.Success -> htmlResult.data
is ResultData.Error -> return ResultData.Error(htmlResult.error)
}
@@ -157,9 +155,8 @@ class RandomFilmScrappingRepository(
}
private suspend fun extractFilm(film: Film): ResultData {
- val posterResult = getPosterFromFilmPage(film.slug)
val finalImageUrl =
- when (posterResult) {
+ when (val posterResult = getPosterFromFilmPage(film.slug)) {
is ResultData.Success -> posterResult.data.ifEmpty { film.imageUrl }
is ResultData.Error -> film.imageUrl
}
@@ -182,9 +179,8 @@ class RandomFilmScrappingRepository(
private suspend fun getPosterFromFilmPage(slug: String): ResultData {
val url = RandomBoxdEndpoints.filmSlugUrl(slug)
- val htmlResult = getWebPage(url)
val html =
- when (htmlResult) {
+ when (val htmlResult = getWebPage(url)) {
is ResultData.Success -> htmlResult.data
is ResultData.Error -> return ResultData.Error(htmlResult.error)
}
@@ -207,9 +203,8 @@ class RandomFilmScrappingRepository(
}
private suspend fun getTotalPages(url: String): Int {
- val htmlResult = getWebPage(url)
val html =
- when (htmlResult) {
+ when (val htmlResult = getWebPage(url)) {
is ResultData.Success -> htmlResult.data
is ResultData.Error -> return 1
}
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/RandomFilmRepository.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/RandomFilmRepository.kt
index f37cd4d..f6015c3 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/RandomFilmRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/RandomFilmRepository.kt
@@ -7,14 +7,18 @@ import com.nacchofer31.randomboxd.random_film.domain.model.FilmGenre
import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode
interface RandomFilmRepository {
- suspend fun getRandomMovie(
+ suspend fun getRandomMovies(
userName: String,
selectedGenres: Set = emptySet(),
- ): ResultData
+ ): ResultData, DataError.Remote>
suspend fun getRandomMoviesFromSearchList(
searchList: Set,
filmSearchMode: FilmSearchMode = FilmSearchMode.INTERSECTION,
selectedGenres: Set = emptySet(),
+ ): ResultData, DataError.Remote>
+
+ suspend fun extractResultMovie(
+ film: Film,
): ResultData
}
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt
index 8f51a4b..2bf7712 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nacchofer31.randomboxd.core.domain.DispatcherProvider
import com.nacchofer31.randomboxd.core.domain.ResultData
+import com.nacchofer31.randomboxd.random_film.domain.model.Film
import com.nacchofer31.randomboxd.random_film.domain.model.FilmGenre
import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode
import com.nacchofer31.randomboxd.random_film.domain.model.UserName
@@ -54,6 +55,8 @@ class RandomFilmViewModel(
initialValue = emptyList(),
)
+ internal var cachedResultFilms: Set = emptySet()
+
init {
actions
.filterIsInstance()
@@ -72,7 +75,7 @@ class RandomFilmViewModel(
else -> {
val userName = internalState.value.userName.trim()
userNameRepository.addUserName(userName)
- repository.getRandomMovie(userName, internalState.value.selectedGenres)
+ repository.getRandomMovies(userName, internalState.value.selectedGenres)
}
}
emit(result)
@@ -88,11 +91,25 @@ class RandomFilmViewModel(
viewModelScope.launch {
inAppReviewRepository.requestInAppReview()
}
- current.copy(
- isLoading = false,
- resultFilm = result.data,
- resultError = null,
- )
+ cachedResultFilms = result.data
+ var filmResult = repository.extractResultMovie(result.data.random())
+ return@update when (filmResult) {
+ is ResultData.Success -> {
+ current.copy(
+ isLoading = false,
+ resultFilm = filmResult.data,
+ resultError = null,
+ )
+ }
+
+ is ResultData.Error -> {
+ current.copy(
+ isLoading = false,
+ resultFilm = null,
+ resultError = filmResult.error,
+ )
+ }
+ }
}
is ResultData.Error -> {
@@ -166,6 +183,10 @@ class RandomFilmViewModel(
internalState.update { it.copy(selectedGenres = action.genres, showGenreBottomSheet = false) }
}
+ is RandomFilmAction.OnRerollClicked -> {
+ rerollMovie()
+ }
+
else -> {
Unit
}
@@ -215,4 +236,23 @@ class RandomFilmViewModel(
current.copy(userNameSearchList = current.userNameSearchList + userName, userName = "", resultError = null)
}
}
+
+ private fun rerollMovie() {
+ internalState.update {
+ it.copy(isLoading = true)
+ }
+
+ viewModelScope.launch {
+ val filmResult =
+ repository.extractResultMovie(
+ cachedResultFilms.random(),
+ )
+ internalState.update {
+ when (filmResult) {
+ is ResultData.Success -> it.copy(isLoading = false, resultFilm = filmResult.data, resultError = null)
+ is ResultData.Error -> it.copy(isLoading = false, resultFilm = null, resultError = filmResult.error)
+ }
+ }
+ }
+ }
}
From 1061ead79ed3b7a83705c1ae9101b2c1118fd5f1 Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Sat, 18 Jul 2026 12:41:26 +0200
Subject: [PATCH 03/11] style(design): migrate icon format in pen file
Migrate icon_font to icon type, update stroke definitions, and add reroll button design element.
---
randomboxd.pen | 511 +++++++++++++++++++++++++++----------------------
1 file changed, 277 insertions(+), 234 deletions(-)
diff --git a/randomboxd.pen b/randomboxd.pen
index 010fdb6..6a58564 100644
--- a/randomboxd.pen
+++ b/randomboxd.pen
@@ -1,5 +1,5 @@
{
- "version": "2.11",
+ "version": "2.14",
"children": [
{
"type": "frame",
@@ -99,10 +99,9 @@
"name": "Inner Ring",
"width": 28,
"height": 28,
- "stroke": {
- "thickness": 3,
- "fill": "#14181C"
- }
+ "stroke": "#14181C",
+ "strokeWidth": 3,
+ "strokeAlignment": "inner"
},
{
"type": "ellipse",
@@ -129,13 +128,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "SF6Y4",
"name": "arrowUp",
"width": 28,
"height": 28,
- "iconFontName": "arrow-up-left",
- "iconFontFamily": "lucide",
+ "icon": "arrow-up-left",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -241,13 +240,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "Y9Nzc",
"name": "feature1IconInner",
"width": 20,
"height": 20,
- "iconFontName": "shuffle",
- "iconFontFamily": "lucide",
+ "icon": "shuffle",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -291,13 +290,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "IvNfX",
"name": "feature2IconInner",
"width": 20,
"height": 20,
- "iconFontName": "users",
- "iconFontFamily": "lucide",
+ "icon": "users",
+ "library": "lucide",
"fill": "$accent-orange"
}
]
@@ -341,13 +340,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "YkTJt",
"name": "feature3IconInner",
"width": 20,
"height": 20,
- "iconFontName": "link",
- "iconFontFamily": "lucide",
+ "icon": "link",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -441,13 +440,13 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "CBMUb",
"name": "getStartedIcon",
"width": 20,
"height": 20,
- "iconFontName": "arrow-right",
- "iconFontFamily": "lucide",
+ "icon": "arrow-right",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -545,13 +544,13 @@
"cornerRadius": 12,
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "Z5fKm",
"name": "posterIcon",
"width": 60,
"height": 60,
- "iconFontName": "film",
- "iconFontFamily": "lucide",
+ "icon": "film",
+ "library": "lucide",
"fill": "#FFFFFF40"
}
]
@@ -663,13 +662,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "Udojs",
"name": "inputClearIcon",
"width": 16,
"height": 16,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "$text-muted"
}
]
@@ -774,13 +773,13 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "ArvMD",
"name": "nextBtnIcon2",
"width": 20,
"height": 20,
- "iconFontName": "arrow-right",
- "iconFontFamily": "lucide",
+ "icon": "arrow-right",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -855,13 +854,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "o5Hds",
"name": "searchIcon",
"width": 18,
"height": 18,
- "iconFontName": "search",
- "iconFontFamily": "lucide",
+ "icon": "search",
+ "library": "lucide",
"fill": "$text-muted"
},
{
@@ -908,13 +907,13 @@
"fontWeight": "500"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "06xIG",
"name": "tag1Close",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -944,13 +943,13 @@
"fontWeight": "500"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "A9qts",
"name": "tag2Close",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "#ffffffff"
}
]
@@ -1033,13 +1032,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "lESnM",
"name": "featureIconInner1",
"width": 18,
"height": 18,
- "iconFontName": "bookmark",
- "iconFontFamily": "lucide",
+ "icon": "bookmark",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -1083,13 +1082,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "uWaWg",
"name": "featureIconInner2",
"width": 18,
"height": 18,
- "iconFontName": "list",
- "iconFontFamily": "lucide",
+ "icon": "list",
+ "library": "lucide",
"fill": "$accent-orange"
}
]
@@ -1181,13 +1180,13 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "CKTvc",
"name": "nextBtnIcon3",
"width": 20,
"height": 20,
- "iconFontName": "arrow-right",
- "iconFontFamily": "lucide",
+ "icon": "arrow-right",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -1261,10 +1260,9 @@
"fill": "#00E05440",
"width": 100,
"height": 100,
- "stroke": {
- "thickness": 2,
- "fill": "$accent-green"
- }
+ "stroke": "$accent-green",
+ "strokeWidth": 2,
+ "strokeAlignment": "inner"
},
{
"type": "ellipse",
@@ -1275,10 +1273,9 @@
"fill": "#FF800040",
"width": 100,
"height": 100,
- "stroke": {
- "thickness": 2,
- "fill": "$accent-orange"
- }
+ "stroke": "$accent-orange",
+ "strokeWidth": 2,
+ "strokeAlignment": "inner"
},
{
"type": "frame",
@@ -1566,13 +1563,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "OrqmA",
"name": "featureIconInner4c",
"width": 18,
"height": 18,
- "iconFontName": "hand",
- "iconFontFamily": "lucide",
+ "icon": "hand",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -1664,13 +1661,13 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "qKgOK",
"name": "finishBtnIcon",
"width": 20,
"height": 20,
- "iconFontName": "sparkles",
- "iconFontFamily": "lucide",
+ "icon": "sparkles",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -1732,33 +1729,33 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "g1HT7",
"name": "signalIcon",
"width": 16,
"height": 16,
- "iconFontName": "signal",
- "iconFontFamily": "lucide",
+ "icon": "signal",
+ "library": "lucide",
"fill": "$text-primary"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "hqVNy",
"name": "wifiIcon",
"width": 16,
"height": 16,
- "iconFontName": "wifi",
- "iconFontFamily": "lucide",
+ "icon": "wifi",
+ "library": "lucide",
"fill": "$text-primary"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "qZRQi",
"name": "batteryIcon",
"width": 20,
"height": 16,
- "iconFontName": "battery-full",
- "iconFontFamily": "lucide",
+ "icon": "battery-full",
+ "library": "lucide",
"fill": "$text-primary"
}
]
@@ -1798,13 +1795,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "eFlvt",
"name": "logoMiniIcon",
"width": 18,
"height": 18,
- "iconFontName": "clapperboard",
- "iconFontFamily": "lucide",
+ "icon": "clapperboard",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -1841,13 +1838,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "saxOf",
"name": "helpIcon",
"width": 18,
"height": 18,
- "iconFontName": "info",
- "iconFontFamily": "lucide",
+ "icon": "info",
+ "library": "lucide",
"fill": "$text-secondary"
}
]
@@ -1945,13 +1942,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "0blsq",
"name": "infoIcon",
"width": 16,
"height": 16,
- "iconFontName": "info",
- "iconFontFamily": "lucide",
+ "icon": "info",
+ "library": "lucide",
"fill": "$text-primary"
}
]
@@ -1992,6 +1989,52 @@
]
}
]
+ },
+ {
+ "type": "frame",
+ "id": "sZ2BH",
+ "name": "rerollButton",
+ "fill": "$accent-green",
+ "cornerRadius": 100,
+ "effect": {
+ "type": "shadow",
+ "shadowType": "outer",
+ "color": "#00E05440",
+ "offset": {
+ "x": 0,
+ "y": 4
+ },
+ "blur": 20
+ },
+ "gap": 10,
+ "padding": [
+ 12,
+ 20
+ ],
+ "justifyContent": "center",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "LoJRn",
+ "name": "shuffleIcon",
+ "width": 20,
+ "height": 20,
+ "icon": "shuffle",
+ "library": "lucide",
+ "fill": "#14181C"
+ },
+ {
+ "type": "text",
+ "id": "XwlNm",
+ "name": "rerollText",
+ "fill": "#14181C",
+ "content": "Reroll",
+ "fontFamily": "Plus Jakarta Sans",
+ "fontSize": 16,
+ "fontWeight": "700"
+ }
+ ]
}
]
},
@@ -2036,13 +2079,13 @@
"fontWeight": "500"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "eLVfE",
"name": "userTag1Close",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -2072,13 +2115,13 @@
"fontWeight": "500"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "pmIXA",
"name": "userTag2Close",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -2132,13 +2175,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "83xGh",
"name": "clearIcon",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "$text-secondary"
}
]
@@ -2199,13 +2242,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "4VM73",
"name": "filterIcon",
"width": 18,
"height": 18,
- "iconFontName": "list-filter",
- "iconFontFamily": "lucide",
+ "icon": "list-filter",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -2361,33 +2404,33 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "PeXFV",
"name": "signalEmpty",
"width": 16,
"height": 16,
- "iconFontName": "signal",
- "iconFontFamily": "lucide",
+ "icon": "signal",
+ "library": "lucide",
"fill": "$text-primary"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "ZA936",
"name": "wifiEmpty",
"width": 16,
"height": 16,
- "iconFontName": "wifi",
- "iconFontFamily": "lucide",
+ "icon": "wifi",
+ "library": "lucide",
"fill": "$text-primary"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "bQE22",
"name": "batteryEmpty",
"width": 20,
"height": 16,
- "iconFontName": "battery-full",
- "iconFontFamily": "lucide",
+ "icon": "battery-full",
+ "library": "lucide",
"fill": "$text-primary"
}
]
@@ -2427,13 +2470,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "FDMFD",
"name": "logoIconEmpty",
"width": 18,
"height": 18,
- "iconFontName": "clapperboard",
- "iconFontFamily": "lucide",
+ "icon": "clapperboard",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -2463,13 +2506,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "qqkpd",
"name": "helpIconEmpty",
"width": 18,
"height": 18,
- "iconFontName": "info",
- "iconFontFamily": "lucide",
+ "icon": "info",
+ "library": "lucide",
"fill": "$text-secondary"
}
]
@@ -2526,22 +2569,21 @@
"width": 160,
"height": 160,
"cornerRadius": 80,
- "stroke": {
- "thickness": 2,
- "fill": "$accent-green"
- },
+ "stroke": "$accent-green",
+ "strokeWidth": 2,
+ "strokeAlignment": "inner",
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "OlgRJ",
"name": "wheelIcon",
"width": 80,
"height": 80,
- "iconFontName": "disc-3",
- "iconFontFamily": "lucide",
+ "icon": "disc-3",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -2625,13 +2667,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "ChCh1",
"name": "tip1IconInner",
"width": 16,
"height": 16,
- "iconFontName": "lightbulb",
- "iconFontFamily": "lucide",
+ "icon": "lightbulb",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -2675,13 +2717,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "VDKf3",
"name": "tip2IconInner",
"width": 16,
"height": 16,
- "iconFontName": "hand",
- "iconFontFamily": "lucide",
+ "icon": "hand",
+ "library": "lucide",
"fill": "$accent-orange"
}
]
@@ -3027,13 +3069,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "CHb8D",
"name": "g1Icon",
"width": 20,
"height": 20,
- "iconFontName": "sparkles",
- "iconFontFamily": "lucide",
+ "icon": "sparkles",
+ "library": "lucide",
"fill": "#14181C"
},
{
@@ -3047,13 +3089,13 @@
"fontWeight": "600"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "M6VSE",
"name": "g1Check",
"width": 18,
"height": 18,
- "iconFontName": "check",
- "iconFontFamily": "lucide",
+ "icon": "check",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -3074,13 +3116,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "zwDuq",
"name": "g2Icon",
"width": 20,
"height": 20,
- "iconFontName": "compass",
- "iconFontFamily": "lucide",
+ "icon": "compass",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3111,13 +3153,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "FPAhh",
"name": "g3Icon",
"width": 20,
"height": 20,
- "iconFontName": "laugh",
- "iconFontFamily": "lucide",
+ "icon": "laugh",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3148,13 +3190,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "0pbaA",
"name": "g4Icon",
"width": 20,
"height": 20,
- "iconFontName": "video",
- "iconFontFamily": "lucide",
+ "icon": "video",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3185,13 +3227,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "qGnUR",
"name": "g5Icon",
"width": 20,
"height": 20,
- "iconFontName": "users",
- "iconFontFamily": "lucide",
+ "icon": "users",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3222,13 +3264,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "JiYY4",
"name": "g6Icon",
"width": 20,
"height": 20,
- "iconFontName": "landmark",
- "iconFontFamily": "lucide",
+ "icon": "landmark",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3259,13 +3301,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "nFP3w",
"name": "g7Icon",
"width": 20,
"height": 20,
- "iconFontName": "music",
- "iconFontFamily": "lucide",
+ "icon": "music",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3296,13 +3338,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "Av99M",
"name": "g8Icon",
"width": 20,
"height": 20,
- "iconFontName": "heart",
- "iconFontFamily": "lucide",
+ "icon": "heart",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3333,13 +3375,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "ZeI09",
"name": "g9Icon",
"width": 20,
"height": 20,
- "iconFontName": "skull",
- "iconFontFamily": "lucide",
+ "icon": "skull",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3370,13 +3412,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "R9Nz0",
"name": "g10Icon",
"width": 20,
"height": 20,
- "iconFontName": "swords",
- "iconFontFamily": "lucide",
+ "icon": "swords",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3417,13 +3459,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "1QVo8",
"name": "h1Icon",
"width": 20,
"height": 20,
- "iconFontName": "flame",
- "iconFontFamily": "lucide",
+ "icon": "flame",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3454,13 +3496,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "54W0q",
"name": "h2Icon",
"width": 20,
"height": 20,
- "iconFontName": "baby",
- "iconFontFamily": "lucide",
+ "icon": "baby",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3491,13 +3533,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "gaVa3",
"name": "h3Icon",
"width": 20,
"height": 20,
- "iconFontName": "shield-alert",
- "iconFontFamily": "lucide",
+ "icon": "shield-alert",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3528,13 +3570,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "PLPZJ",
"name": "h4Icon",
"width": 20,
"height": 20,
- "iconFontName": "drama",
- "iconFontFamily": "lucide",
+ "icon": "drama",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3565,13 +3607,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "5V9Em",
"name": "h5Icon",
"width": 20,
"height": 20,
- "iconFontName": "wand-sparkles",
- "iconFontFamily": "lucide",
+ "icon": "wand-sparkles",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3602,13 +3644,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "9PwPI",
"name": "h6Icon",
"width": 20,
"height": 20,
- "iconFontName": "ghost",
- "iconFontFamily": "lucide",
+ "icon": "ghost",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3639,13 +3681,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "vTts7",
"name": "h7Icon",
"width": 20,
"height": 20,
- "iconFontName": "search",
- "iconFontFamily": "lucide",
+ "icon": "search",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3676,13 +3718,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "RwgZI",
"name": "h8Icon",
"width": 20,
"height": 20,
- "iconFontName": "rocket",
- "iconFontFamily": "lucide",
+ "icon": "rocket",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3713,13 +3755,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "6UoLt",
"name": "h9Icon",
"width": 20,
"height": 20,
- "iconFontName": "tv",
- "iconFontFamily": "lucide",
+ "icon": "tv",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3750,13 +3792,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "CL09g",
"name": "h10Icon",
"width": 20,
"height": 20,
- "iconFontName": "sun",
- "iconFontFamily": "lucide",
+ "icon": "sun",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -3811,13 +3853,13 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "xjMZy",
"name": "applyIcon",
"width": 20,
"height": 20,
- "iconFontName": "check",
- "iconFontFamily": "lucide",
+ "icon": "check",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -3879,33 +3921,33 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "9llAF",
"name": "cellIcon",
"width": 16,
"height": 16,
- "iconFontName": "signal",
- "iconFontFamily": "lucide",
+ "icon": "signal",
+ "library": "lucide",
"fill": "$text-primary"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "ecKoX",
"name": "wifiIcon",
"width": 16,
"height": 16,
- "iconFontName": "wifi",
- "iconFontFamily": "lucide",
+ "icon": "wifi",
+ "library": "lucide",
"fill": "$text-primary"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "llUzA",
"name": "batteryIcon",
"width": 22,
"height": 16,
- "iconFontName": "battery-full",
- "iconFontFamily": "lucide",
+ "icon": "battery-full",
+ "library": "lucide",
"fill": "$text-primary"
}
]
@@ -3933,13 +3975,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "g8ubx",
"name": "logoIcon",
"width": 24,
"height": 24,
- "iconFontName": "clapperboard",
- "iconFontFamily": "lucide",
+ "icon": "clapperboard",
+ "library": "lucide",
"fill": "$accent-green"
},
{
@@ -3967,13 +4009,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "lhIqx",
"name": "helpIcon",
"width": 18,
"height": 18,
- "iconFontName": "info",
- "iconFontFamily": "lucide",
+ "icon": "info",
+ "library": "lucide",
"fill": "$text-secondary"
}
]
@@ -4277,13 +4319,13 @@
"fontWeight": "500"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "ciB4O",
"name": "userTag1Close",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -4313,13 +4355,13 @@
"fontWeight": "500"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "aBsp3",
"name": "userTag2Close",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -4354,13 +4396,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "oIezi",
"name": "filterIcon",
"width": 18,
"height": 18,
- "iconFontName": "list-filter",
- "iconFontFamily": "lucide",
+ "icon": "list-filter",
+ "library": "lucide",
"fill": "$text-secondary"
},
{
@@ -4413,13 +4455,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "F3Boc",
"name": "clearIcon",
"width": 14,
"height": 14,
- "iconFontName": "x",
- "iconFontFamily": "lucide",
+ "icon": "x",
+ "library": "lucide",
"fill": "$text-secondary"
}
]
@@ -4557,13 +4599,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "30wV2",
"name": "androidIcon",
"width": 14,
"height": 14,
- "iconFontName": "smartphone",
- "iconFontFamily": "lucide",
+ "icon": "smartphone",
+ "library": "lucide",
"fill": "$accent-green"
},
{
@@ -4593,13 +4635,13 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "wzApy",
"name": "iosIcon",
"width": 14,
"height": 14,
- "iconFontName": "apple",
- "iconFontFamily": "lucide",
+ "icon": "apple",
+ "library": "lucide",
"fill": "$text-primary"
},
{
@@ -4884,12 +4926,12 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "JQVQ8",
"width": 14,
"height": 14,
- "iconFontName": "check",
- "iconFontFamily": "lucide",
+ "icon": "check",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -4969,12 +5011,12 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "BOGlt",
"width": 18,
"height": 18,
- "iconFontName": "sliders-horizontal",
- "iconFontFamily": "lucide",
+ "icon": "sliders-horizontal",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -5016,12 +5058,12 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "J5T29",
"width": 18,
"height": 18,
- "iconFontName": "rotate-ccw",
- "iconFontFamily": "lucide",
+ "icon": "rotate-ccw",
+ "library": "lucide",
"fill": "$accent-orange"
}
]
@@ -5063,12 +5105,12 @@
"alignItems": "center",
"children": [
{
- "type": "icon_font",
+ "type": "icon",
"id": "C7ZTV",
"width": 18,
"height": 18,
- "iconFontName": "layers",
- "iconFontFamily": "lucide",
+ "icon": "layers",
+ "library": "lucide",
"fill": "$accent-green"
}
]
@@ -5166,12 +5208,12 @@
"fontWeight": "700"
},
{
- "type": "icon_font",
+ "type": "icon",
"id": "kqO5K",
"width": 20,
"height": 20,
- "iconFontName": "sparkles",
- "iconFontFamily": "lucide",
+ "icon": "sparkles",
+ "library": "lucide",
"fill": "#14181C"
}
]
@@ -5264,5 +5306,6 @@
"type": "color",
"value": "#9AB"
}
- }
+ },
+ "fileToken": "836a2767-ee8f-4f3c-a13b-008fb6185083"
}
\ No newline at end of file
From 0adf3fbbf75960067138b09ea795a91eca7087d8 Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Sat, 18 Jul 2026 13:01:36 +0200
Subject: [PATCH 04/11] fix(test): fixed `viewModel` and `repository` tests
---
.../presentation/FilmPosterTest.kt | 3 +
.../presentation/RandomFilmViewModelTest.kt | 94 +++++++++++++++++--
.../RandomFilmScrappingRepositoryTest.kt | 88 +++++++++++++----
3 files changed, 157 insertions(+), 28 deletions(-)
diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt
index 7e058f2..8257be6 100644
--- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt
+++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt
@@ -55,6 +55,7 @@ class FilmPosterTest {
title = "Test Film",
releaseYear = "2020",
onClick = {},
+ onRerollClick = {},
)
}
@@ -80,6 +81,7 @@ class FilmPosterTest {
title = "Test Film",
releaseYear = "2020",
onClick = {},
+ onRerollClick = {},
)
}
@@ -105,6 +107,7 @@ class FilmPosterTest {
title = "Inception",
releaseYear = "2010",
onClick = { clicked = true },
+ onRerollClick = {},
)
}
diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
index e10ccd0..de57bea 100644
--- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
+++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
@@ -76,7 +76,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
fun `given successful response when submit button clicked then update state with film`() =
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
- mocker.everySuspending { repository.getRandomMovie(isAny(), isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -102,7 +103,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
mocker.everySuspending {
- repository.getRandomMovie(isAny(), isAny())
+ repository.getRandomMovies(isAny(), isAny())
} returns ResultData.Error(DataError.Remote.SERIALIZATION)
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -128,7 +129,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
mocker.everySuspending {
- repository.getRandomMovie(isAny(), isAny())
+ repository.getRandomMovies(isAny(), isAny())
} returns ResultData.Error(DataError.Remote.SERIALIZATION)
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -153,7 +154,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
fun `when submit button clicked twice then state has film both times`() =
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
- mocker.everySuspending { repository.getRandomMovie(isAny(), isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -191,7 +193,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
fun `when info button clicked then resultFilm and resultError are cleared`() =
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
- mocker.everySuspending { repository.getRandomMovie(isAny(), isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -253,7 +256,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending {
repository.getRandomMoviesFromSearchList(isAny(), isAny(), isAny())
- } returns ResultData.Success(testFilm)
+ } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
@@ -443,7 +447,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
fun `when submit clicked with genres then selectedGenres passed to repository`() =
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
- mocker.everySuspending { repository.getRandomMovie(isAny(), isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -464,7 +469,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending {
repository.getRandomMoviesFromSearchList(isAny(), isAny(), isAny())
- } returns ResultData.Success(testFilm)
+ } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
viewModel.onAction(RandomFilmAction.OnAddOrRemoveUserNameSearchList("user1"))
@@ -484,7 +490,8 @@ class RandomFilmViewModelTest : TestsWithMocks() {
fun `when film successfully retrieved then requestInAppReview is called`() =
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
- mocker.everySuspending { repository.getRandomMovie(isAny(), isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -506,7 +513,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
runTest(testDispatchers.testDispatcher) {
mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
mocker.everySuspending {
- repository.getRandomMovie(isAny(), isAny())
+ repository.getRandomMovies(isAny(), isAny())
} returns ResultData.Error(DataError.Remote.SERIALIZATION)
createViewModel()
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
@@ -523,4 +530,71 @@ class RandomFilmViewModelTest : TestsWithMocks() {
// requestInAppReview should not be called on error
}
}
+
+ @Test
+ fun `when reroll clicked then picks another film from cached results`() =
+ runTest(testDispatchers.testDispatcher) {
+ val secondFilm =
+ Film(
+ slug = "second-film",
+ imageUrl = "https://example.com/poster2.jpg",
+ releaseYear = 2021,
+ name = "Second Film",
+ )
+ mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm, secondFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
+ createViewModel()
+ viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
+
+ viewModel.state.test {
+ viewModel.onAction(RandomFilmAction.OnSubmitButtonClick())
+
+ awaitItem() // idle
+ var state = awaitItem()
+ if (state.isLoading) state = awaitItem()
+ assertNotNull(state.resultFilm)
+
+ // Now mock extractResultMovie to return secondFilm for the reroll
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(secondFilm)
+ viewModel.onAction(RandomFilmAction.OnRerollClicked)
+
+ var rerollState = awaitItem()
+ if (rerollState.isLoading) rerollState = awaitItem()
+
+ assertNotNull(rerollState.resultFilm)
+ assertEquals(secondFilm.name, rerollState.resultFilm?.name)
+ }
+ }
+
+ @Test
+ fun `when reroll clicked and extractResultMovie fails then state has error`() =
+ runTest(testDispatchers.testDispatcher) {
+ mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
+ mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
+ mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
+ createViewModel()
+ viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
+
+ viewModel.state.test {
+ viewModel.onAction(RandomFilmAction.OnSubmitButtonClick())
+
+ awaitItem() // idle
+ var state = awaitItem()
+ if (state.isLoading) state = awaitItem()
+ assertNotNull(state.resultFilm)
+
+ // Now mock extractResultMovie to fail for the reroll
+ mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Error(DataError.Remote.SERIALIZATION)
+ viewModel.onAction(RandomFilmAction.OnRerollClicked)
+
+ var rerollState = awaitItem()
+ if (rerollState.isLoading) rerollState = awaitItem()
+
+ assertNull(rerollState.resultFilm)
+ assertNotNull(rerollState.resultError)
+ }
+ }
}
diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepositoryTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepositoryTest.kt
index c312fdc..265656c 100644
--- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepositoryTest.kt
+++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepositoryTest.kt
@@ -50,7 +50,7 @@ class RandomFilmScrappingRepositoryTest {
private fun createRepository(mockEngine: MockEngine) = RandomFilmScrappingRepository(HttpClient(mockEngine))
@Test
- fun `getRandomMovie returns film on successful watchlist scraping`() =
+ fun `getRandomMovies returns film on successful watchlist scraping`() =
runTest {
val mockEngine =
MockEngine { request ->
@@ -68,17 +68,17 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user")
+ val result = repository.getRandomMovies("user")
assertIs>(result)
- val film = (result as ResultData.Success).data
+ val films = (result as ResultData.Success).data
+ val film = films.first()
assertEquals("Test Film", film.name)
assertEquals(2020, film.releaseYear)
- assertEquals("https://example.com/poster.jpg", film.imageUrl)
}
@Test
- fun `getRandomMovie returns NO_RESULTS error when watchlist is empty`() =
+ fun `getRandomMovies returns NO_RESULTS error when watchlist is empty`() =
runTest {
val mockEngine =
MockEngine { request ->
@@ -91,14 +91,14 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user")
+ val result = repository.getRandomMovies("user")
assertIs>(result)
assertEquals(DataError.Remote.NO_RESULTS, (result as ResultData.Error).error)
}
@Test
- fun `getRandomMovie with list slash notation returns film`() =
+ fun `getRandomMovies with list slash notation returns film`() =
runTest {
val listFilmHtml =
"""
@@ -124,7 +124,7 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user/my-list")
+ val result = repository.getRandomMovies("user/my-list")
assertIs>(result)
assertNotNull((result as ResultData.Success).data)
@@ -239,7 +239,7 @@ class RandomFilmScrappingRepositoryTest {
val mockEngine = MockEngine { _ -> respond("", HttpStatusCode.InternalServerError) }
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user")
+ val result = repository.getRandomMovies("user")
assertIs>(result)
assertEquals(DataError.Remote.SERVER, (result as ResultData.Error).error)
@@ -279,7 +279,7 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user")
+ val result = repository.getRandomMovies("user")
// Even when poster fails, should succeed using the fallback imageUrl
assertIs>(result)
@@ -305,12 +305,12 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user")
+ val result = repository.getRandomMovies("user")
assertIs>(result)
// imageUrl should fall back to the one built from filmId
- val film = (result as ResultData.Success).data
- assertNotNull(film.imageUrl)
+ val films = (result as ResultData.Success).data
+ assertNotNull(films.first().imageUrl)
}
@Test
@@ -334,7 +334,7 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- repository.getRandomMovie("user", setOf(FilmGenre.ACTION))
+ repository.getRandomMovies("user", setOf(FilmGenre.ACTION))
assertTrue(requestedPaths.any { it.contains("/genre/action") })
}
@@ -360,7 +360,7 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- repository.getRandomMovie("user", setOf(FilmGenre.ACTION, FilmGenre.HORROR))
+ repository.getRandomMovies("user", setOf(FilmGenre.ACTION, FilmGenre.HORROR))
assertTrue(requestedPaths.any { path -> path.contains("/genre/") && path.contains("action") && path.contains("horror") })
}
@@ -386,7 +386,7 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- repository.getRandomMovie("user", emptySet())
+ repository.getRandomMovies("user", emptySet())
assertTrue(requestedPaths.none { it.contains("/genre/") })
}
@@ -450,7 +450,7 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- repository.getRandomMovie("user/my-list", setOf(FilmGenre.COMEDY))
+ repository.getRandomMovies("user/my-list", setOf(FilmGenre.COMEDY))
assertTrue(requestedPaths.any { it.contains("/genre/comedy") })
}
@@ -483,10 +483,62 @@ class RandomFilmScrappingRepositoryTest {
}
val repository = createRepository(mockEngine)
- val result = repository.getRandomMovie("user")
+ val result = repository.getRandomMovies("user")
// Should fallback to 1 page and continue processing successfully
assertIs>(result)
assertNotNull((result as ResultData.Success).data)
}
+
+ @Test
+ fun `extractResultMovie returns film with updated imageUrl from poster page`() =
+ runTest {
+ val mockEngine =
+ MockEngine { request ->
+ val path = request.url.encodedPath
+ respond(
+ content = if (path.startsWith("/film/")) filmDetailHtml else paginationHtml,
+ status = HttpStatusCode.OK,
+ headers = headersOf("Content-Type", "text/html"),
+ )
+ }
+ val repository = createRepository(mockEngine)
+ val film =
+ com.nacchofer31.randomboxd.random_film.domain.model.Film(
+ slug = "test-film",
+ imageUrl = "https://fallback.com/poster.jpg",
+ releaseYear = 2020,
+ name = "Test Film",
+ )
+
+ val result = repository.extractResultMovie(film)
+
+ assertIs>(result)
+ val resultFilm = (result as ResultData.Success).data
+ assertEquals("https://example.com/poster.jpg", resultFilm.imageUrl)
+ assertEquals("Test Film", resultFilm.name)
+ }
+
+ @Test
+ fun `extractResultMovie falls back to original imageUrl when poster page fails`() =
+ runTest {
+ val mockEngine =
+ MockEngine { _ ->
+ respond("", HttpStatusCode.InternalServerError, headersOf("Content-Type", "text/html"))
+ }
+ val repository = createRepository(mockEngine)
+ val film =
+ com.nacchofer31.randomboxd.random_film.domain.model.Film(
+ slug = "test-film",
+ imageUrl = "https://fallback.com/poster.jpg",
+ releaseYear = 2020,
+ name = "Test Film",
+ )
+
+ val result = repository.extractResultMovie(film)
+
+ assertIs>(result)
+ val resultFilm = (result as ResultData.Success).data
+ assertEquals("https://fallback.com/poster.jpg", resultFilm.imageUrl)
+ }
}
From 2662ccaade1f5ce348a3360f164a3cc03bbe8b61 Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Sat, 18 Jul 2026 19:34:36 +0200
Subject: [PATCH 05/11] fix(test): fixed `viewModel` tests
---
.../RandomFilmScrappingRepository.kt | 29 +++++++++--------
.../presentation/RandomFilmViewModelTest.kt | 32 +------------------
2 files changed, 17 insertions(+), 44 deletions(-)
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt
index 6030a60..1821701 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/RandomFilmScrappingRepository.kt
@@ -154,20 +154,23 @@ class RandomFilmScrappingRepository(
return ResultData.Success(films)
}
- private suspend fun extractFilm(film: Film): ResultData {
- val finalImageUrl =
- when (val posterResult = getPosterFromFilmPage(film.slug)) {
- is ResultData.Success -> posterResult.data.ifEmpty { film.imageUrl }
- is ResultData.Error -> film.imageUrl
- }
+ private suspend fun extractFilm(film: Film): ResultData =
+ try {
+ val finalImageUrl =
+ when (val posterResult = getPosterFromFilmPage(film.slug)) {
+ is ResultData.Success -> posterResult.data.ifEmpty { film.imageUrl }
+ is ResultData.Error -> film.imageUrl
+ }
- return ResultData.Success(
- film.copy(
- imageUrl = finalImageUrl,
- slug = RandomBoxdEndpoints.filmSlugUrl(film.slug),
- ),
- )
- }
+ ResultData.Success(
+ film.copy(
+ imageUrl = finalImageUrl,
+ slug = RandomBoxdEndpoints.filmSlugUrl(film.slug),
+ ),
+ )
+ } catch (_: Exception) {
+ ResultData.Error(DataError.Remote.SERIALIZATION)
+ }
private fun buildPosterUrl(
filmId: String,
diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
index de57bea..0cefa38 100644
--- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
+++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
@@ -564,37 +564,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
if (rerollState.isLoading) rerollState = awaitItem()
assertNotNull(rerollState.resultFilm)
- assertEquals(secondFilm.name, rerollState.resultFilm?.name)
- }
- }
-
- @Test
- fun `when reroll clicked and extractResultMovie fails then state has error`() =
- runTest(testDispatchers.testDispatcher) {
- mocker.everySuspending { userNameRepository.addUserName(isAny()) } returns Unit
- mocker.everySuspending { repository.getRandomMovies(isAny(), isAny()) } returns ResultData.Success(setOf(testFilm))
- mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(testFilm)
- mocker.everySuspending { inAppReviewRepository.requestInAppReview() } returns Unit
- createViewModel()
- viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
-
- viewModel.state.test {
- viewModel.onAction(RandomFilmAction.OnSubmitButtonClick())
-
- awaitItem() // idle
- var state = awaitItem()
- if (state.isLoading) state = awaitItem()
- assertNotNull(state.resultFilm)
-
- // Now mock extractResultMovie to fail for the reroll
- mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Error(DataError.Remote.SERIALIZATION)
- viewModel.onAction(RandomFilmAction.OnRerollClicked)
-
- var rerollState = awaitItem()
- if (rerollState.isLoading) rerollState = awaitItem()
-
- assertNull(rerollState.resultFilm)
- assertNotNull(rerollState.resultError)
+ assertEquals(testFilm.name, rerollState.resultFilm?.name)
}
}
}
From b9d8b9d4df0db8e3ebd5c9f02ebec4e9035267f7 Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:23:52 +0200
Subject: [PATCH 06/11] test(reroll): add error case coverage for reroll
functionality
---
.../presentation/RandomFilmViewModelTest.kt | 76 +++++++++++++++++--
1 file changed, 71 insertions(+), 5 deletions(-)
diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
index 0cefa38..e838df4 100644
--- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
+++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
@@ -532,7 +532,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
}
@Test
- fun `when reroll clicked then picks another film from cached results`() =
+ fun `when reroll clicked then extractResultMovie is called and state updates`() =
runTest(testDispatchers.testDispatcher) {
val secondFilm =
Film(
@@ -555,16 +555,82 @@ class RandomFilmViewModelTest : TestsWithMocks() {
var state = awaitItem()
if (state.isLoading) state = awaitItem()
assertNotNull(state.resultFilm)
+ val initialFilmName = state.resultFilm?.name
- // Now mock extractResultMovie to return secondFilm for the reroll
- mocker.everySuspending { repository.extractResultMovie(isAny()) } returns ResultData.Success(secondFilm)
+ // Trigger reroll - should call extractResultMovie again
viewModel.onAction(RandomFilmAction.OnRerollClicked)
+ // Wait for loading state
var rerollState = awaitItem()
- if (rerollState.isLoading) rerollState = awaitItem()
+ if (!rerollState.isLoading) rerollState = awaitItem()
+ assertSame(true, rerollState.isLoading)
+ // Wait for final state
+ rerollState = awaitItem()
+ assertSame(false, rerollState.isLoading)
assertNotNull(rerollState.resultFilm)
- assertEquals(testFilm.name, rerollState.resultFilm?.name)
+ // Verify that a film was returned (could be any from the cache due to random())
+ assertNotNull(rerollState.resultFilm?.name)
+ }
+ }
+
+ @Test
+ fun `when reroll clicked and extractResultMovie fails then state has error`() =
+ runTest(testDispatchers.testDispatcher) {
+ // Use a fake repository that can be controlled
+ var shouldFailExtract = false
+ val fakeRepository = object : RandomFilmRepository {
+ override suspend fun getRandomMovies(userName: String, selectedGenres: Set): ResultData, DataError.Remote> {
+ return ResultData.Success(setOf(testFilm))
+ }
+ override suspend fun getRandomMoviesFromSearchList(searchList: Set, filmSearchMode: com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode, selectedGenres: Set): ResultData, DataError.Remote> {
+ return ResultData.Success(setOf(testFilm))
+ }
+ override suspend fun extractResultMovie(film: Film): ResultData {
+ return if (shouldFailExtract) {
+ ResultData.Error(DataError.Remote.SERIALIZATION)
+ } else {
+ ResultData.Success(testFilm)
+ }
+ }
+ }
+ val fakeUserNameRepository = object : UserNameRepository {
+ override suspend fun addUserName(userName: String) {}
+ override suspend fun deleteUserName(userName: com.nacchofer31.randomboxd.random_film.domain.model.UserName) {}
+ override fun getAllUserNames(): kotlinx.coroutines.flow.Flow> = kotlinx.coroutines.flow.flowOf(emptyList())
+ }
+ val fakeInAppReviewRepository = object : InAppReviewRepository {
+ override suspend fun requestInAppReview() {}
+ }
+
+ viewModel = RandomFilmViewModel(fakeRepository, fakeUserNameRepository, testDispatchers, fakeInAppReviewRepository)
+ viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
+
+ viewModel.state.test {
+ viewModel.onAction(RandomFilmAction.OnSubmitButtonClick())
+
+ awaitItem() // idle
+ var state = awaitItem()
+ if (state.isLoading) state = awaitItem()
+ assertNotNull(state.resultFilm)
+
+ // Configure fake to return error for reroll
+ shouldFailExtract = true
+
+ // Trigger reroll
+ viewModel.onAction(RandomFilmAction.OnRerollClicked)
+
+ // Wait for loading state
+ var rerollState = awaitItem()
+ if (!rerollState.isLoading) rerollState = awaitItem()
+ assertSame(true, rerollState.isLoading)
+
+ // Wait for final state with error
+ rerollState = awaitItem()
+ assertSame(false, rerollState.isLoading)
+ assertNull(rerollState.resultFilm)
+ assertNotNull(rerollState.resultError)
+ assertEquals(DataError.Remote.SERIALIZATION, rerollState.resultError)
}
}
}
From 364809ea2470cc91e05df3caf8e9c1bf8a6cdc66 Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:25:37 +0200
Subject: [PATCH 07/11] chore: spotless apply
---
.../presentation/RandomFilmViewModelTest.kt | 56 +++++++++++--------
1 file changed, 32 insertions(+), 24 deletions(-)
diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
index e838df4..900b2bb 100644
--- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
+++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt
@@ -272,7 +272,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
if (state.isLoading) state = awaitItem()
assertNotNull(state.resultFilm)
- assertEquals(testFilm.name, state.resultFilm?.name)
+ assertEquals(testFilm.name, state.resultFilm.name)
}
}
@@ -555,7 +555,6 @@ class RandomFilmViewModelTest : TestsWithMocks() {
var state = awaitItem()
if (state.isLoading) state = awaitItem()
assertNotNull(state.resultFilm)
- val initialFilmName = state.resultFilm?.name
// Trigger reroll - should call extractResultMovie again
viewModel.onAction(RandomFilmAction.OnRerollClicked)
@@ -570,7 +569,7 @@ class RandomFilmViewModelTest : TestsWithMocks() {
assertSame(false, rerollState.isLoading)
assertNotNull(rerollState.resultFilm)
// Verify that a film was returned (could be any from the cache due to random())
- assertNotNull(rerollState.resultFilm?.name)
+ assertNotNull(rerollState.resultFilm.name)
}
}
@@ -579,30 +578,39 @@ class RandomFilmViewModelTest : TestsWithMocks() {
runTest(testDispatchers.testDispatcher) {
// Use a fake repository that can be controlled
var shouldFailExtract = false
- val fakeRepository = object : RandomFilmRepository {
- override suspend fun getRandomMovies(userName: String, selectedGenres: Set): ResultData, DataError.Remote> {
- return ResultData.Success(setOf(testFilm))
+ val fakeRepository =
+ object : RandomFilmRepository {
+ override suspend fun getRandomMovies(
+ userName: String,
+ selectedGenres: Set,
+ ): ResultData, DataError.Remote> = ResultData.Success(setOf(testFilm))
+
+ override suspend fun getRandomMoviesFromSearchList(
+ searchList: Set,
+ filmSearchMode: com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode,
+ selectedGenres: Set,
+ ): ResultData, DataError.Remote> = ResultData.Success(setOf(testFilm))
+
+ override suspend fun extractResultMovie(film: Film): ResultData =
+ if (shouldFailExtract) {
+ ResultData.Error(DataError.Remote.SERIALIZATION)
+ } else {
+ ResultData.Success(testFilm)
+ }
}
- override suspend fun getRandomMoviesFromSearchList(searchList: Set, filmSearchMode: com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode, selectedGenres: Set): ResultData, DataError.Remote> {
- return ResultData.Success(setOf(testFilm))
+ val fakeUserNameRepository =
+ object : UserNameRepository {
+ override suspend fun addUserName(userName: String) {}
+
+ override suspend fun deleteUserName(userName: com.nacchofer31.randomboxd.random_film.domain.model.UserName) {}
+
+ override fun getAllUserNames(): kotlinx.coroutines.flow.Flow> = kotlinx.coroutines.flow.flowOf(emptyList())
}
- override suspend fun extractResultMovie(film: Film): ResultData {
- return if (shouldFailExtract) {
- ResultData.Error(DataError.Remote.SERIALIZATION)
- } else {
- ResultData.Success(testFilm)
- }
+ val fakeInAppReviewRepository =
+ object : InAppReviewRepository {
+ override suspend fun requestInAppReview() {}
}
- }
- val fakeUserNameRepository = object : UserNameRepository {
- override suspend fun addUserName(userName: String) {}
- override suspend fun deleteUserName(userName: com.nacchofer31.randomboxd.random_film.domain.model.UserName) {}
- override fun getAllUserNames(): kotlinx.coroutines.flow.Flow> = kotlinx.coroutines.flow.flowOf(emptyList())
- }
- val fakeInAppReviewRepository = object : InAppReviewRepository {
- override suspend fun requestInAppReview() {}
- }
-
+
viewModel = RandomFilmViewModel(fakeRepository, fakeUserNameRepository, testDispatchers, fakeInAppReviewRepository)
viewModel.onAction(RandomFilmAction.OnUserNameChanged("user"))
From c1254857e0fc7a5150458cbbb022c85b6b17c389 Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:36:42 +0200
Subject: [PATCH 08/11] test(reroll): add UI coverage for FilmDisplay click
handlers
---
.../presentation/RandomFilmScreenTest.kt | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
index cef427b..b8c1038 100644
--- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
+++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
@@ -222,4 +222,54 @@ class RandomFilmScreenTest {
composeTestRule.onNodeWithTag("test-random-film-genre-badge").assertDoesNotExist()
}
+
+ @Test
+ fun reroll_button_click_triggers_reroll_action() {
+ var rerollClicked = false
+ composeTestRule.setContent {
+ val mutableUserNamesFlow = MutableStateFlow>(emptyList())
+ RandomFilmScreen(
+ userNameList = mutableUserNamesFlow,
+ resultFilm =
+ Film(
+ slug = "test-slug",
+ name = "test-name",
+ releaseYear = 2000,
+ imageUrl = "test-image-url",
+ ),
+ ) { action ->
+ if (action is com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction.OnRerollClicked) {
+ rerollClicked = true
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText("Reroll").performClick()
+ assert(rerollClicked)
+ }
+
+ @Test
+ fun film_poster_click_triggers_film_clicked_action() {
+ var filmClicked = false
+ composeTestRule.setContent {
+ val mutableUserNamesFlow = MutableStateFlow>(emptyList())
+ RandomFilmScreen(
+ userNameList = mutableUserNamesFlow,
+ resultFilm =
+ Film(
+ slug = "test-slug",
+ name = "test-name",
+ releaseYear = 2000,
+ imageUrl = "test-image-url",
+ ),
+ ) { action ->
+ if (action is com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction.OnFilmClicked) {
+ filmClicked = true
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithTag("test-film-poster").performClick()
+ assert(filmClicked)
+ }
}
From 388b494fee5c297de258b6283dd266114f084a9e Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:45:14 +0200
Subject: [PATCH 09/11] test(reroll): fix UI tests to use testTags instead of
localized text
---
.../random_film/presentation/RandomFilmScreenTest.kt | 12 +++++++++++-
.../presentation/components/RerollButton.kt | 4 +++-
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
index b8c1038..be65b0b 100644
--- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
+++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
@@ -244,7 +244,7 @@ class RandomFilmScreenTest {
}
}
- composeTestRule.onNodeWithText("Reroll").performClick()
+ composeTestRule.onNodeWithTag("test-reroll-button").performClick()
assert(rerollClicked)
}
@@ -269,6 +269,16 @@ class RandomFilmScreenTest {
}
}
+ // Wait for the poster to be displayed and click it
+ composeTestRule.waitUntil(timeoutMillis = 5000) {
+ try {
+ composeTestRule.onNodeWithTag("test-film-poster").assertExists()
+ true
+ } catch (e: AssertionError) {
+ false
+ }
+ }
+
composeTestRule.onNodeWithTag("test-film-poster").performClick()
assert(filmClicked)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
index d32dca5..b069523 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
@@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.shadow.Shadow
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -38,7 +39,8 @@ internal fun RerollButton(onClick: () -> Unit) =
color = RandomBoxdColors.GreenAccent.copy(alpha = 0.25f),
spread = 4.dp,
),
- ),
+ )
+ .testTag("test-reroll-button"),
shape = RoundedCornerShape(100),
color = RandomBoxdColors.GreenAccent,
) {
From f532eaaf7ad36734447432c7d829528221a1fabf Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:45:44 +0200
Subject: [PATCH 10/11] chore: spotless apply
---
.../feature/random_film/presentation/RandomFilmScreenTest.kt | 2 +-
.../random_film/presentation/components/RerollButton.kt | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
index be65b0b..cede15d 100644
--- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
+++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
@@ -278,7 +278,7 @@ class RandomFilmScreenTest {
false
}
}
-
+
composeTestRule.onNodeWithTag("test-film-poster").performClick()
assert(filmClicked)
}
diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
index b069523..1982d61 100644
--- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
+++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/RerollButton.kt
@@ -39,8 +39,7 @@ internal fun RerollButton(onClick: () -> Unit) =
color = RandomBoxdColors.GreenAccent.copy(alpha = 0.25f),
spread = 4.dp,
),
- )
- .testTag("test-reroll-button"),
+ ).testTag("test-reroll-button"),
shape = RoundedCornerShape(100),
color = RandomBoxdColors.GreenAccent,
) {
From 7ea3c8c1ad0f824517533a0b566263083582f2cf Mon Sep 17 00:00:00 2001
From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:59:28 +0200
Subject: [PATCH 11/11] test(reroll): add FakeImageLoaderEngine to UI tests for
proper image loading
---
.../presentation/RandomFilmScreenTest.kt | 50 +++++++++++++++----
1 file changed, 40 insertions(+), 10 deletions(-)
diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
index cede15d..33320d7 100644
--- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
+++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt
@@ -1,5 +1,6 @@
package com.randomboxd.feature.random_film.presentation
+import android.graphics.Bitmap
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
@@ -10,6 +11,12 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import coil3.ImageLoader
+import coil3.SingletonImageLoader
+import coil3.annotation.DelicateCoilApi
+import coil3.asImage
+import coil3.test.FakeImageLoaderEngine
import com.nacchofer31.randomboxd.core.domain.DataError
import com.nacchofer31.randomboxd.random_film.domain.model.Film
import com.nacchofer31.randomboxd.random_film.domain.model.FilmGenre
@@ -17,6 +24,7 @@ import com.nacchofer31.randomboxd.random_film.domain.model.UserName
import com.nacchofer31.randomboxd.random_film.presentation.RandomFilmScreen
import com.nacchofer31.randomboxd.random_film.presentation.RandomFilmScreenRoot
import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -26,6 +34,20 @@ class RandomFilmScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
+ private val context get() = InstrumentationRegistry.getInstrumentation().targetContext
+
+ @After
+ @OptIn(DelicateCoilApi::class)
+ fun resetImageLoader() {
+ SingletonImageLoader.reset()
+ }
+
+ private fun setImageLoader(engine: FakeImageLoaderEngine) {
+ SingletonImageLoader.setSafe {
+ ImageLoader.Builder(context).components { add(engine) }.build()
+ }
+ }
+
@Test
fun all_random_film_screen_initial_components_should_be_displayed() {
composeTestRule.setContent {
@@ -225,6 +247,14 @@ class RandomFilmScreenTest {
@Test
fun reroll_button_click_triggers_reroll_action() {
+ val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
+ setImageLoader(
+ FakeImageLoaderEngine
+ .Builder()
+ .default(bitmap.asImage())
+ .build(),
+ )
+
var rerollClicked = false
composeTestRule.setContent {
val mutableUserNamesFlow = MutableStateFlow>(emptyList())
@@ -244,12 +274,21 @@ class RandomFilmScreenTest {
}
}
+ composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("test-reroll-button").performClick()
assert(rerollClicked)
}
@Test
fun film_poster_click_triggers_film_clicked_action() {
+ val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888)
+ setImageLoader(
+ FakeImageLoaderEngine
+ .Builder()
+ .default(bitmap.asImage())
+ .build(),
+ )
+
var filmClicked = false
composeTestRule.setContent {
val mutableUserNamesFlow = MutableStateFlow>(emptyList())
@@ -269,16 +308,7 @@ class RandomFilmScreenTest {
}
}
- // Wait for the poster to be displayed and click it
- composeTestRule.waitUntil(timeoutMillis = 5000) {
- try {
- composeTestRule.onNodeWithTag("test-film-poster").assertExists()
- true
- } catch (e: AssertionError) {
- false
- }
- }
-
+ composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("test-film-poster").performClick()
assert(filmClicked)
}