Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,19 @@ class LoadingOrPromptTest {

composeTestRule.onNodeWithText("Finding your random movie").assertIsDisplayed()
}

@Test
fun animation_completes_full_cycle() {
composeTestRule.mainClock.autoAdvance = false
composeTestRule.setContent {
LoadingOrPrompt(isLoading = true)
}

composeTestRule.mainClock.advanceTimeBy(4000)

// Verify components are still displayed after animation
composeTestRule.onNodeWithTag("test-loading-indicator").assertIsDisplayed()
composeTestRule.onNodeWithText("Rolling the dice", substring = true).assertIsDisplayed()
composeTestRule.onNodeWithText("Finding your random movie").assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.nacchofer31.randomboxd.random_film.domain.model.FilmGenre
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 com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.After
import org.junit.Rule
Expand Down Expand Up @@ -267,8 +268,9 @@ class RandomFilmScreenTest {
releaseYear = 2000,
imageUrl = "test-image-url",
),
numberOfResults = 2,
) { action ->
if (action is com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction.OnRerollClicked) {
if (action is RandomFilmAction.OnRerollClicked) {
rerollClicked = true
}
}
Expand Down Expand Up @@ -302,7 +304,7 @@ class RandomFilmScreenTest {
imageUrl = "test-image-url",
),
) { action ->
if (action is com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction.OnFilmClicked) {
if (action is RandomFilmAction.OnFilmClicked) {
filmClicked = true
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.nacchofer31.randomboxd.core.domain

/**
* Returns a random element from the set, excluding the specified element if possible.
* Comparison is done by the provided key function instead of object equality.
* If [exclude] is null, returns a random element without filtering.
* If the set contains only the excluded element, returns a random element.
*
* @param exclude The element to exclude from the random selection, or null to not exclude anything
* @param keySelector Function to extract the comparison key from each element
* @return A random element from the set, preferably different from [exclude]
* @throws NoSuchElementException if the set is empty
*/
fun <T, K> Set<T>.randomExcluding(
exclude: T? = null,
keySelector: (T) -> K,
): T {
if (isEmpty()) throw NoSuchElementException("Set is empty.")
if (exclude == null) return random()

val excludeKey = keySelector(exclude)
val candidates = filter { keySelector(it) != excludeKey }
return if (candidates.isNotEmpty()) {
candidates.random()
} else {
// Only the excluded element exists, return it
random()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ fun FilmPoster(
fontSize = 17.sp,
textAlign = TextAlign.Center,
)
RerollButton(
onClick = onRerollClick,
)
if (numberOfResults > 1) {
RerollButton(
onClick = onRerollClick,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.core.domain.randomExcluding
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
Expand All @@ -26,6 +27,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

@Suppress("OPT_IN_USAGE")
class RandomFilmViewModel(
Expand Down Expand Up @@ -74,7 +76,9 @@ class RandomFilmViewModel(

else -> {
val userName = internalState.value.userName.trim()
userNameRepository.addUserName(userName)
withContext(dispatchers.io) {
userNameRepository.addUserName(userName)
}
repository.getRandomMovies(userName, internalState.value.selectedGenres)
}
}
Expand All @@ -92,7 +96,7 @@ class RandomFilmViewModel(
inAppReviewRepository.requestInAppReview()
}
cachedResultFilms = result.data
var filmResult = repository.extractResultMovie(result.data.random())
var filmResult = repository.extractResultMovie(result.data.randomExcluding(null) { it.name })
return@update when (filmResult) {
is ResultData.Success -> {
current.copy(
Expand Down Expand Up @@ -150,12 +154,14 @@ class RandomFilmViewModel(
if (internalState.value.userNameSearchList.contains(action.userName.username)) {
addOrRemoveUserNameToList(action.userName.username)
}
userNameRepository.deleteUserName(action.userName)
withContext(dispatchers.io) {
userNameRepository.deleteUserName(action.userName)
}
}
}

is RandomFilmAction.OnUserNameAdded -> {
viewModelScope.launch { userNameRepository.addUserName(action.username.trim()) }
viewModelScope.launch(dispatchers.io) { userNameRepository.addUserName(action.username.trim()) }
}

is RandomFilmAction.OnAddOrRemoveUserNameSearchList -> {
Expand Down Expand Up @@ -183,7 +189,12 @@ class RandomFilmViewModel(
}

is RandomFilmAction.OnGenreSelectionApplied -> {
internalState.update { it.copy(selectedGenres = action.genres, showGenreBottomSheet = false) }
internalState.update {
it.copy(
selectedGenres = action.genres,
showGenreBottomSheet = false,
)
}
}

is RandomFilmAction.OnRerollClicked -> {
Expand Down Expand Up @@ -240,22 +251,21 @@ class RandomFilmViewModel(
}
}

private fun rerollMovie() {
internalState.update {
it.copy(isLoading = true)
}

private fun rerollMovie() =
viewModelScope.launch {
internalState.update {
it.copy(isLoading = true)
}
val rerolledFilm = cachedResultFilms.randomExcluding(internalState.value.resultFilm) { it.name }
val filmResult =
repository.extractResultMovie(
cachedResultFilms.random(),
)
withContext(dispatchers.io) {
repository.extractResultMovie(rerolledFilm)
}
internalState.update {
when (filmResult) {
is ResultData.Success -> it.copy(isLoading = false, resultFilm = filmResult.data, resultError = null, numberOfResults = cachedResultFilms.size)
is ResultData.Error -> it.copy(isLoading = false, resultFilm = null, resultError = filmResult.error, numberOfResults = 0)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.nacchofer31.randomboxd.core.domain

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

class SetExtensionsTest {
data class TestItem(
val id: Int,
val name: String,
)

@Test
fun `randomExcluding throws NoSuchElementException when set is empty`() {
val emptySet = emptySet<TestItem>()

assertFailsWith<NoSuchElementException> {
emptySet.randomExcluding(keySelector = { it.id })
}
}

@Test
fun `randomExcluding returns random element when exclude is null`() {
val set = setOf(TestItem(1, "one"), TestItem(2, "two"), TestItem(3, "three"))

val result = set.randomExcluding(exclude = null, keySelector = { it.id })

assertTrue(result in set)
}

@Test
fun `randomExcluding returns element different from excluded using keySelector`() {
val excluded = TestItem(2, "two")
val set = setOf(TestItem(1, "one"), excluded, TestItem(3, "three"))

repeat(10) {
val result = set.randomExcluding(exclude = excluded, keySelector = { it.id })
assertTrue(result.id != excluded.id)
}
}

@Test
fun `randomExcluding returns excluded element when it is the only one`() {
val onlyItem = TestItem(1, "one")
val set = setOf(onlyItem)

val result = set.randomExcluding(exclude = onlyItem, keySelector = { it.id })

assertEquals(onlyItem, result)
}

@Test
fun `randomExcluding uses keySelector for comparison not object equality`() {
val excluded = TestItem(1, "original")
val differentObject = TestItem(1, "different")
val set = setOf(excluded, differentObject, TestItem(2, "two"))

repeat(10) {
val result = set.randomExcluding(exclude = excluded, keySelector = { it.id })
assertTrue(result.id != 1, "Should exclude all items with id=1")
}
}

@Test
fun `randomExcluding works with single element set and null exclude`() {
val item = TestItem(1, "one")
val set = setOf(item)

val result = set.randomExcluding(exclude = null, keySelector = { it.id })

assertEquals(item, result)
}
}
Loading