From 625826887ab3d70160593bbc104ac32f2d887b3b Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:29:26 +0100 Subject: [PATCH 01/10] test: add unit and UI tests for error handling, pagination fallback, and null release years --- .../presentation/RandomFilmScreenTest.kt | 47 +++++++++++++++++++ .../data/mapper/RandomFilmMappersTest.kt | 15 ++++++ .../RandomFilmScrappingRepositoryTest.kt | 35 ++++++++++++++ 3 files changed, 97 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 97dbd0f..f21e6b8 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 @@ -3,9 +3,11 @@ package com.randomboxd.feature.random_film.presentation import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onNodeWithTag 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 com.nacchofer31.randomboxd.random_film.domain.model.Film import com.nacchofer31.randomboxd.random_film.domain.model.UserName @@ -127,4 +129,49 @@ class RandomFilmScreenTest { composeTestRule.onNodeWithTag("test-random-film-submit-button").assertIsDisplayed() } + + @Test + fun submit_button_long_click_adds_username_to_search_list() { + composeTestRule.setContent { + RandomFilmScreenRoot(onFilmClicked = {}) + } + + composeTestRule.onNodeWithTag("test-random-film-user-name-text-field").performTextInput("testuser") + composeTestRule.onNodeWithTag("test-random-film-submit-button").performTouchInput { longClick() } + } + + @Test + fun film_display_shows_with_null_release_year() { + composeTestRule.setContent { + val mutableUserNamesFlow = MutableStateFlow>(emptyList()) + RandomFilmScreen( + userNameList = mutableUserNamesFlow, + state = + RandomFilmState( + resultFilm = + Film( + slug = "test-slug", + name = "test-name", + releaseYear = null, + imageUrl = "test-image-url", + ), + ), + ) { } + } + + composeTestRule.onNodeWithTag("test-film-display").assertExists() + } + + @Test + fun error_view_shows_for_generic_error() { + composeTestRule.setContent { + val mutableUserNamesFlow = MutableStateFlow>(emptyList()) + RandomFilmScreen( + userNameList = mutableUserNamesFlow, + state = RandomFilmState(resultError = com.nacchofer31.randomboxd.core.domain.DataError.Remote.UNKNOWN), + ) { } + } + + composeTestRule.onNodeWithTag("test-film-error").assertIsDisplayed() + } } diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/mapper/RandomFilmMappersTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/mapper/RandomFilmMappersTest.kt index 5ef4c57..3282ed0 100644 --- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/mapper/RandomFilmMappersTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/data/mapper/RandomFilmMappersTest.kt @@ -1,6 +1,7 @@ package com.nacchofer31.randomboxd.random_film.data.mapper import com.nacchofer31.randomboxd.random_film.data.dto.FilmDto +import com.nacchofer31.randomboxd.random_film.domain.model.Film import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -54,4 +55,18 @@ class RandomFilmMappersTest { assertEquals("my-special-film-2024", film.slug) assertEquals(1994, film.releaseYear) } + + @Test + fun `Film uses default null for releaseYear`() { + val film = + Film( + slug = "test-film", + imageUrl = "https://example.com/poster.jpg", + name = "Test Film", + ) + + assertNull(film.releaseYear) + assertEquals("test-film", film.slug) + assertEquals("Test Film", film.name) + } } 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 dbb56ec..cd63777 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 @@ -310,4 +310,39 @@ class RandomFilmScrappingRepositoryTest { val film = (result as ResultData.Success).data assertNotNull(film.imageUrl) } + + @Test + fun `getRandomMovie handles total pages request failure`() = + runTest { + val mockEngine = + MockEngine { request -> + val path = request.url.encodedPath + if (path.contains("/watchlist") && !path.contains("/page/")) { + // Initial request for pagination fails - fallback to 1 page + respond("", HttpStatusCode.InternalServerError) + } else if (path.contains("/page/")) { + // Page request succeeds + respond( + content = filmListHtml, + status = HttpStatusCode.OK, + headers = headersOf("Content-Type", "text/html"), + ) + } else if (path.startsWith("/film/")) { + respond( + content = filmDetailHtml, + status = HttpStatusCode.OK, + headers = headersOf("Content-Type", "text/html"), + ) + } else { + respond("", HttpStatusCode.InternalServerError) + } + } + val repository = createRepository(mockEngine) + + val result = repository.getRandomMovie("user") + + // Should fallback to 1 page and continue processing successfully + assertIs>(result) + assertNotNull((result as ResultData.Success).data) + } } From e112b62fcf1c934cd1827969c20b39ea935404db Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:29:43 +0100 Subject: [PATCH 02/10] test: added `FilmErrorViewTest` instrumented tests --- .../presentation/FilmErrorViewTest.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmErrorViewTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmErrorViewTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmErrorViewTest.kt new file mode 100644 index 0000000..fa8e504 --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmErrorViewTest.kt @@ -0,0 +1,72 @@ +package com.randomboxd.feature.random_film.presentation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nacchofer31.randomboxd.core.domain.DataError +import com.nacchofer31.randomboxd.random_film.presentation.components.FilmErrorView +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FilmErrorViewTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun error_view_shows_no_results_title_for_no_results_error() { + composeTestRule.setContent { + FilmErrorView(error = DataError.Remote.NO_RESULTS) + } + + composeTestRule.onNodeWithText("NO RESULTS FOUND").assertIsDisplayed() + } + + @Test + fun error_view_shows_no_results_subtitle_for_no_results_error() { + composeTestRule.setContent { + FilmErrorView(error = DataError.Remote.NO_RESULTS) + } + + composeTestRule.onNodeWithText("No movie matches the search. Please try again with another one.").assertIsDisplayed() + } + + @Test + fun error_view_shows_connection_title_for_no_internet_error() { + composeTestRule.setContent { + FilmErrorView(error = DataError.Remote.NO_INTERNET) + } + + composeTestRule.onNodeWithText("CONNECTION LOST").assertIsDisplayed() + } + + @Test + fun error_view_shows_generic_title_for_unknown_error() { + composeTestRule.setContent { + FilmErrorView(error = DataError.Remote.UNKNOWN) + } + + composeTestRule.onNodeWithText("SOMETHING WENT WRONG!").assertIsDisplayed() + } + + @Test + fun error_view_shows_generic_title_for_server_error() { + composeTestRule.setContent { + FilmErrorView(error = DataError.Remote.SERVER) + } + + composeTestRule.onNodeWithText("SOMETHING WENT WRONG!").assertIsDisplayed() + } + + @Test + fun error_view_has_test_tag() { + composeTestRule.setContent { + FilmErrorView(error = DataError.Remote.NO_RESULTS) + } + + composeTestRule.onNodeWithTag("test-film-error").assertIsDisplayed() + } +} From 4669493e3e53de03386e949fe9db21173c24e96d Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:29:52 +0100 Subject: [PATCH 03/10] test: add instrumented tests for `FilmHeader` --- .../presentation/FilmHeaderTest.kt | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt new file mode 100644 index 0000000..20d7584 --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt @@ -0,0 +1,77 @@ +package com.randomboxd.feature.random_film.presentation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nacchofer31.randomboxd.random_film.presentation.components.FilmHeader +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FilmHeaderTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun film_header_shows_app_name() { + composeTestRule.setContent { + FilmHeader(onInfoClick = {}) + } + + composeTestRule.onNodeWithText("RandomBoxd").assertIsDisplayed() + } + + @Test + fun film_header_shows_info_button_when_showInfoButton_is_true() { + composeTestRule.setContent { + FilmHeader(onInfoClick = {}, showInfoButton = true) + } + + composeTestRule.onNodeWithContentDescription("Info").assertIsDisplayed() + } + + @Test + fun film_header_hides_info_button_when_showInfoButton_is_false() { + composeTestRule.setContent { + FilmHeader(onInfoClick = {}, showInfoButton = false) + } + + composeTestRule.onNodeWithContentDescription("Info").assertDoesNotExist() + } + + @Test + fun film_header_info_button_triggers_callback() { + var clicked = false + composeTestRule.setContent { + FilmHeader(onInfoClick = { clicked = true }) + } + + composeTestRule.onNodeWithContentDescription("Info").performClick() + + assertTrue(clicked) + } + + @Test + fun film_header_shows_logo_icon() { + composeTestRule.setContent { + FilmHeader(onInfoClick = {}) + } + + composeTestRule.onNodeWithContentDescription("Logo").assertIsDisplayed() + } + + @Test + fun film_header_hides_info_button_when_showInfoButton_is_null() { + composeTestRule.setContent { + FilmHeader(onInfoClick = {}, showInfoButton = null) + } + + composeTestRule.onNodeWithContentDescription("Info").assertDoesNotExist() + } +} From 85fe66809140c4868801ef8c69130371a7897f13 Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:29:59 +0100 Subject: [PATCH 04/10] test: add instrumented tests for `UnionIntersectionSwitch` --- .../presentation/FilmSearchModeSwitchTest.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt new file mode 100644 index 0000000..15c1ae5 --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt @@ -0,0 +1,105 @@ +package com.randomboxd.feature.random_film.presentation + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode +import com.nacchofer31.randomboxd.random_film.presentation.components.UnionIntersectionSwitch +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FilmSearchModeSwitchTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun switch_shows_intersection_and_union_labels() { + composeTestRule.setContent { + UnionIntersectionSwitch( + searchMode = FilmSearchMode.INTERSECTION, + onModeChange = {}, + ) + } + + composeTestRule.onNodeWithText("INTERSECTION").assertIsDisplayed() + composeTestRule.onNodeWithText("UNION").assertIsDisplayed() + } + + @Test + fun click_union_when_in_intersection_mode_calls_on_mode_change() { + var toggled = false + composeTestRule.setContent { + UnionIntersectionSwitch( + searchMode = FilmSearchMode.INTERSECTION, + onModeChange = { toggled = true }, + ) + } + + composeTestRule.onNodeWithText("UNION").performClick() + + assertEquals(true, toggled) + } + + @Test + fun click_intersection_when_in_union_mode_calls_on_mode_change() { + var toggled = false + composeTestRule.setContent { + UnionIntersectionSwitch( + searchMode = FilmSearchMode.UNION, + onModeChange = { toggled = true }, + ) + } + + composeTestRule.onNodeWithText("INTERSECTION").performClick() + + assertEquals(true, toggled) + } + + @Test + fun click_active_mode_does_not_call_on_mode_change() { + var toggleCount = 0 + composeTestRule.setContent { + UnionIntersectionSwitch( + searchMode = FilmSearchMode.INTERSECTION, + onModeChange = { toggleCount++ }, + ) + } + + composeTestRule.onNodeWithText("INTERSECTION").performClick() + + assertEquals(0, toggleCount) + } + + @Test + fun toggle_switches_between_modes() { + composeTestRule.setContent { + var mode by remember { mutableStateOf(FilmSearchMode.INTERSECTION) } + UnionIntersectionSwitch( + searchMode = mode, + onModeChange = { + mode = if (mode == FilmSearchMode.INTERSECTION) { + FilmSearchMode.UNION + } else { + FilmSearchMode.INTERSECTION + } + }, + ) + } + + composeTestRule.onNodeWithText("INTERSECTION").assertIsDisplayed() + + composeTestRule.onNodeWithText("UNION").performClick() + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText("UNION").assertIsDisplayed() + } +} From 7574cd1c2c40107755140769a81af1ea7eec0a96 Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:30:05 +0100 Subject: [PATCH 05/10] test: add UI tests for `LoadingOrPrompt` component --- .../presentation/LoadingOrPromptTest.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/LoadingOrPromptTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/LoadingOrPromptTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/LoadingOrPromptTest.kt new file mode 100644 index 0000000..37e90ea --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/LoadingOrPromptTest.kt @@ -0,0 +1,54 @@ +package com.randomboxd.feature.random_film.presentation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nacchofer31.randomboxd.random_film.presentation.components.LoadingOrPrompt +import com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LoadingOrPromptTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun loading_indicator_is_displayed_when_isLoading_is_true() { + composeTestRule.setContent { + LoadingOrPrompt(state = RandomFilmState(isLoading = true)) + } + + composeTestRule.onNodeWithTag("test-loading-indicator").assertIsDisplayed() + } + + @Test + fun loading_indicator_does_not_exist_when_isLoading_is_false() { + composeTestRule.setContent { + LoadingOrPrompt(state = RandomFilmState(isLoading = false)) + } + + composeTestRule.onNodeWithTag("test-loading-indicator").assertDoesNotExist() + } + + @Test + fun rolling_dice_text_is_shown_when_loading() { + composeTestRule.setContent { + LoadingOrPrompt(state = RandomFilmState(isLoading = true)) + } + + composeTestRule.onNodeWithText("Rolling the dice...").assertIsDisplayed() + } + + @Test + fun finding_random_movie_text_is_shown_when_loading() { + composeTestRule.setContent { + LoadingOrPrompt(state = RandomFilmState(isLoading = true)) + } + + composeTestRule.onNodeWithText("Finding your random movie").assertIsDisplayed() + } +} From 2b32154b22c579b9da706632c7f16dd63acc07fc Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:30:15 +0100 Subject: [PATCH 06/10] test: add UI tests for `RandomFilmInfoView` --- .../presentation/RandomFilmInfoViewTest.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmInfoViewTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmInfoViewTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmInfoViewTest.kt new file mode 100644 index 0000000..a97390a --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmInfoViewTest.kt @@ -0,0 +1,53 @@ +package com.randomboxd.feature.random_film.presentation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nacchofer31.randomboxd.random_film.presentation.components.RandomFilmInfoView +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RandomFilmInfoViewTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun info_view_shows_ready_to_spin_text() { + composeTestRule.setContent { + RandomFilmInfoView() + } + + composeTestRule.onNodeWithText("Ready to spin?").assertIsDisplayed() + } + + @Test + fun info_view_shows_roulette_icon() { + composeTestRule.setContent { + RandomFilmInfoView() + } + + composeTestRule.onNodeWithContentDescription("Roulette").assertIsDisplayed() + } + + @Test + fun info_view_shows_tip_username_format() { + composeTestRule.setContent { + RandomFilmInfoView() + } + + composeTestRule.onNodeWithText("Try: username or username/list-name").assertIsDisplayed() + } + + @Test + fun info_view_shows_tip_hold_submit() { + composeTestRule.setContent { + RandomFilmInfoView() + } + + composeTestRule.onNodeWithText("Long-press Submit or tags for multi-user mode").assertIsDisplayed() + } +} From 27adb15c72e81b57d475e2fd3d11715a3814da3d Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:30:20 +0100 Subject: [PATCH 07/10] test: add `RandomFilmStateTest` for `RandomFilmState` validation --- .../viewmodel/RandomFilmStateTest.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmStateTest.kt diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmStateTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmStateTest.kt new file mode 100644 index 0000000..6bed006 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmStateTest.kt @@ -0,0 +1,88 @@ +package com.nacchofer31.randomboxd.random_film.presentation.viewmodel + +import com.nacchofer31.randomboxd.core.domain.DataError +import com.nacchofer31.randomboxd.random_film.domain.model.Film +import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class RandomFilmStateTest { + @Test + fun `default state has empty userName`() { + val state = RandomFilmState() + + assertEquals("", state.userName) + } + + @Test + fun `default state has null resultFilm`() { + val state = RandomFilmState() + + assertNull(state.resultFilm) + } + + @Test + fun `default state has null resultError`() { + val state = RandomFilmState() + + assertNull(state.resultError) + } + + @Test + fun `default state is not loading`() { + val state = RandomFilmState() + + assertEquals(false, state.isLoading) + } + + @Test + fun `default state has empty userNameSearchList`() { + val state = RandomFilmState() + + assertEquals(emptySet(), state.userNameSearchList) + } + + @Test + fun `default state has INTERSECTION filmSearchMode`() { + val state = RandomFilmState() + + assertEquals(FilmSearchMode.INTERSECTION, state.filmSearchMode) + } + + @Test + fun `copy with resultFilm updates film`() { + val film = Film(slug = "test", imageUrl = "url", name = "Test Film") + val state = RandomFilmState().copy(resultFilm = film) + + assertEquals(film, state.resultFilm) + } + + @Test + fun `copy with isLoading true sets loading state`() { + val state = RandomFilmState().copy(isLoading = true) + + assertEquals(true, state.isLoading) + } + + @Test + fun `copy with resultError updates error`() { + val state = RandomFilmState().copy(resultError = DataError.Remote.NO_RESULTS) + + assertEquals(DataError.Remote.NO_RESULTS, state.resultError) + } + + @Test + fun `copy with userNameSearchList updates list`() { + val state = RandomFilmState().copy(userNameSearchList = setOf("user1", "user2")) + + assertEquals(setOf("user1", "user2"), state.userNameSearchList) + } + + @Test + fun `copy with UNION filmSearchMode updates mode`() { + val state = RandomFilmState().copy(filmSearchMode = FilmSearchMode.UNION) + + assertEquals(FilmSearchMode.UNION, state.filmSearchMode) + } +} From d4b95d89aee49dd69880096f5115ef4b530faf99 Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:30:27 +0100 Subject: [PATCH 08/10] test: add instrumentation tests for `UserNameRepositoryImpl` --- .../data/UserNameRepositoryImplTest.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/data/UserNameRepositoryImplTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/data/UserNameRepositoryImplTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/data/UserNameRepositoryImplTest.kt new file mode 100644 index 0000000..4340df4 --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/data/UserNameRepositoryImplTest.kt @@ -0,0 +1,105 @@ +package com.randomboxd.feature.random_film.data + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.nacchofer31.randomboxd.core.data.UsernameDatabase +import com.nacchofer31.randomboxd.random_film.data.repository_impl.UserNameRepositoryImpl +import com.nacchofer31.randomboxd.random_film.domain.model.UserName +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class UserNameRepositoryImplTest { + private lateinit var database: UsernameDatabase + private lateinit var repository: UserNameRepositoryImpl + + @Before + fun setUp() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + database = + Room + .inMemoryDatabaseBuilder(context, UsernameDatabase::class.java) + .allowMainThreadQueries() + .build() + repository = UserNameRepositoryImpl(database) + } + + @After + fun tearDown() { + database.close() + } + + @Test + fun getAllUserNames_returns_empty_list_initially() = + runBlocking { + val userNames = repository.getAllUserNames().first() + assertEquals(emptyList(), userNames) + } + + @Test + fun addUserName_adds_new_username() = + runBlocking { + repository.addUserName("newuser") + + val userNames = repository.getAllUserNames().first() + + assertEquals(1, userNames.size) + assertEquals("newuser", userNames.first().username) + } + + @Test + fun addUserName_does_not_add_duplicate_username() = + runBlocking { + repository.addUserName("existinguser") + repository.addUserName("existinguser") + + val userNames = repository.getAllUserNames().first() + + assertEquals(1, userNames.size) + } + + @Test + fun addUserName_adds_multiple_different_usernames() = + runBlocking { + repository.addUserName("user1") + repository.addUserName("user2") + repository.addUserName("user3") + + val userNames = repository.getAllUserNames().first() + + assertEquals(3, userNames.size) + } + + @Test + fun deleteUserName_removes_existing_username() = + runBlocking { + repository.addUserName("userToDelete") + val userNameToDelete = + database.userNameDao().getUserNameByValue("userToDelete") + ?: UserName(username = "userToDelete") + + repository.deleteUserName(userNameToDelete) + + val userNames = repository.getAllUserNames().first() + assertTrue(userNames.isEmpty()) + } + + @Test + fun addUserName_does_not_add_same_user_after_delete_duplicate_check() = + runBlocking { + repository.addUserName("user1") + repository.addUserName("user2") + repository.addUserName("user1") + + val userNames = repository.getAllUserNames().first() + + assertEquals(2, userNames.size) + } +} From 38e2a6d15199252f838382e0751eaedb1b1662e8 Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:30:35 +0100 Subject: [PATCH 09/10] test: add instrumented tests for `UserNameTag` --- .../presentation/UserNameTagTest.kt | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/UserNameTagTest.kt diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/UserNameTagTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/UserNameTagTest.kt new file mode 100644 index 0000000..6209b0b --- /dev/null +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/UserNameTagTest.kt @@ -0,0 +1,122 @@ +package com.randomboxd.feature.random_film.presentation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode +import com.nacchofer31.randomboxd.random_film.domain.model.UserName +import com.nacchofer31.randomboxd.random_film.presentation.components.UserNameTag +import com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class UserNameTagTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val testUserName = UserName(id = 1, username = "testuser") + + @Test + fun tag_displays_username() { + composeTestRule.setContent { + UserNameTag( + userName = testUserName, + isIncludedInSearchList = false, + onAction = {}, + fimSearchMode = FilmSearchMode.INTERSECTION, + ) + } + + composeTestRule.onNodeWithText("testuser").assertIsDisplayed() + } + + @Test + fun tag_click_triggers_on_user_name_changed_action() { + var capturedAction: RandomFilmAction? = null + composeTestRule.setContent { + UserNameTag( + userName = testUserName, + isIncludedInSearchList = false, + onAction = { capturedAction = it }, + fimSearchMode = FilmSearchMode.INTERSECTION, + ) + } + + composeTestRule.onNodeWithText("testuser").performClick() + + assertEquals(RandomFilmAction.OnUserNameChanged("testuser"), capturedAction) + } + + @Test + fun tag_long_click_triggers_add_or_remove_search_list_action() { + var capturedAction: RandomFilmAction? = null + composeTestRule.setContent { + UserNameTag( + userName = testUserName, + isIncludedInSearchList = false, + onAction = { capturedAction = it }, + fimSearchMode = FilmSearchMode.INTERSECTION, + ) + } + + composeTestRule.onNodeWithText("testuser").performTouchInput { longClick() } + + assertEquals(RandomFilmAction.OnAddOrRemoveUserNameSearchList("testuser"), capturedAction) + } + + @Test + fun tag_shows_when_included_in_intersection_search_list() { + composeTestRule.setContent { + UserNameTag( + userName = testUserName, + isIncludedInSearchList = true, + onAction = {}, + fimSearchMode = FilmSearchMode.INTERSECTION, + ) + } + + composeTestRule.onNodeWithText("testuser").assertIsDisplayed() + } + + @Test + fun tag_shows_when_included_in_union_search_list() { + composeTestRule.setContent { + UserNameTag( + userName = testUserName, + isIncludedInSearchList = true, + onAction = {}, + fimSearchMode = FilmSearchMode.UNION, + ) + } + + composeTestRule.onNodeWithText("testuser").assertIsDisplayed() + } + + @Test + fun remove_icon_click_triggers_remove_username_action() { + var removeCalled = false + composeTestRule.setContent { + UserNameTag( + userName = testUserName, + isIncludedInSearchList = false, + onAction = { action -> + if (action is RandomFilmAction.OnRemoveUserName) removeCalled = true + }, + fimSearchMode = FilmSearchMode.INTERSECTION, + ) + } + + composeTestRule.onNodeWithContentDescription("Remove").performClick() + + assertTrue(removeCalled) + } +} From 28eb38e39a04ae0746d597e512e61f7440605382 Mon Sep 17 00:00:00 2001 From: Nacchofer31 <10453558+Nacchofer31@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:33:12 +0000 Subject: [PATCH 10/10] style: apply spotless formatting --- .../random_film/presentation/FilmHeaderTest.kt | 1 - .../presentation/FilmSearchModeSwitchTest.kt | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt index 20d7584..5464f76 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmHeaderTest.kt @@ -1,7 +1,6 @@ package com.randomboxd.feature.random_film.presentation import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt index 15c1ae5..cc843c2 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmSearchModeSwitchTest.kt @@ -86,11 +86,12 @@ class FilmSearchModeSwitchTest { UnionIntersectionSwitch( searchMode = mode, onModeChange = { - mode = if (mode == FilmSearchMode.INTERSECTION) { - FilmSearchMode.UNION - } else { - FilmSearchMode.INTERSECTION - } + mode = + if (mode == FilmSearchMode.INTERSECTION) { + FilmSearchMode.UNION + } else { + FilmSearchMode.INTERSECTION + } }, ) }