Add Android unit and UI tests with test dependencies
This commit is contained in:
@@ -96,6 +96,16 @@ dependencies {
|
||||
|
||||
// Testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.13.12")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
testImplementation("org.robolectric:robolectric:4.13")
|
||||
testImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
|
||||
testImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
testImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
testImplementation("io.insert-koin:koin-test:3.5.6")
|
||||
testImplementation("io.insert-koin:koin-test-junit4:3.5.6")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bikeloam.app
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.bikeloam.app", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.bikeloam.app
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainDispatcherRule(
|
||||
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
|
||||
) : TestWatcher() {
|
||||
|
||||
override fun starting(description: Description?) {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
override fun finished(description: Description?) {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bikeloam.app.data.network
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
|
||||
class NetworkClientTest {
|
||||
|
||||
@Test
|
||||
fun `create returns configured client`() {
|
||||
val client = NetworkClient.create("http://10.0.2.2:3000")
|
||||
|
||||
assertNotNull(client)
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns client with default base url`() {
|
||||
val client = NetworkClient.create("https://api.example.com")
|
||||
|
||||
assertNotNull(client)
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.bikeloam.app.data.repository
|
||||
|
||||
import com.bikeloam.app.data.model.*
|
||||
import com.bikeloam.app.data.network.BikeApiClient
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BikeRepositoryTest {
|
||||
|
||||
private val apiClient: BikeApiClient = mockk()
|
||||
private val repository = BikeRepository(apiClient)
|
||||
|
||||
@Test
|
||||
fun `login returns user on success`() = runTest {
|
||||
val user = User("1", "test@example.com", "Test User")
|
||||
coEvery { apiClient.login("test@example.com", "password123") } returns AuthResponse(user)
|
||||
|
||||
val result = repository.login("test@example.com", "password123")
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(user, result.getOrNull())
|
||||
coVerify(exactly = 1) { apiClient.login("test@example.com", "password123") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login returns failure on exception`() = runTest {
|
||||
coEvery { apiClient.login(any(), any()) } throws RuntimeException("Network error")
|
||||
|
||||
val result = repository.login("test@example.com", "password123")
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertEquals("Network error", result.exceptionOrNull()?.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register returns user on success`() = runTest {
|
||||
val user = User("1", "new@example.com", "New User")
|
||||
coEvery { apiClient.register("new@example.com", "password123", "New User") } returns AuthResponse(user)
|
||||
|
||||
val result = repository.register("new@example.com", "password123", "New User")
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(user, result.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register returns failure when email already exists`() = runTest {
|
||||
coEvery { apiClient.register(any(), any(), any()) } throws RuntimeException("Email already registered")
|
||||
|
||||
val result = repository.register("dup@example.com", "password123", null)
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertEquals("Email already registered", result.exceptionOrNull()?.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUser returns user when authenticated`() = runTest {
|
||||
val user = User("1", "test@example.com", "Test User")
|
||||
coEvery { apiClient.getCurrentUser() } returns AuthResponse(user)
|
||||
|
||||
val result = repository.getCurrentUser()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(user, result.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUser returns null when not authenticated`() = runTest {
|
||||
coEvery { apiClient.getCurrentUser() } returns AuthResponse(null)
|
||||
|
||||
val result = repository.getCurrentUser()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(null, result.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout returns success`() = runTest {
|
||||
coEvery { apiClient.logout() } returns Unit
|
||||
|
||||
val result = repository.logout()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
coVerify(exactly = 1) { apiClient.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchCatalog returns catalog on success`() = runTest {
|
||||
val catalog = CatalogResponse(
|
||||
frames = listOf(Product("f1", "frame", "Brand", "Model")),
|
||||
forks = emptyList(),
|
||||
shocks = emptyList(),
|
||||
tires = emptyList()
|
||||
)
|
||||
coEvery { apiClient.getCatalog() } returns catalog
|
||||
|
||||
val result = repository.fetchCatalog()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(1, result.getOrNull()?.frames?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchBuilds returns list on success`() = runTest {
|
||||
val builds = listOf(
|
||||
BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
)
|
||||
coEvery { apiClient.getBuilds() } returns builds
|
||||
|
||||
val result = repository.fetchBuilds()
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(1, result.getOrNull()?.size)
|
||||
assertEquals("Trail Beast", result.getOrNull()?.first()?.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createBuild returns build on success`() = runTest {
|
||||
val rider = RiderProfile(75, "trail", "intermediate", "aggressive")
|
||||
val request = CreateBuildRequest("New Build", rider)
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "New Build", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
coEvery { apiClient.createBuild(request) } returns build
|
||||
|
||||
val result = repository.createBuild(request)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals("New Build", result.getOrNull()?.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createReview returns review on success`() = runTest {
|
||||
val request = CreateReviewRequest(
|
||||
buildId = "b1", suspFeel = 7, frontGrip = 8, rearGrip = 6,
|
||||
rollSpeed = 7, confidence = 9
|
||||
)
|
||||
val review = RideReview(
|
||||
id = "r1", buildId = "b1", suspFeel = 7, frontGrip = 8,
|
||||
rearGrip = 6, rollSpeed = 7, confidence = 9
|
||||
)
|
||||
coEvery { apiClient.createReview(request) } returns review
|
||||
|
||||
val result = repository.createReview(request)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(7, result.getOrNull()?.suspFeel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRecommendation returns response on success`() = runTest {
|
||||
val rider = RiderProfile(75, "trail", "intermediate", "aggressive")
|
||||
val selection = BuildSelection("Trail Beast")
|
||||
val request = RecommendRequest(rider, selection)
|
||||
val response = RecommendResponse("llama3.1:8b", "Fork: 85 PSI, Shock: 200 PSI")
|
||||
coEvery { apiClient.getRecommendation(request) } returns response
|
||||
|
||||
val result = repository.getRecommendation(request)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals("llama3.1:8b", result.getOrNull()?.model)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.bikeloam.app.ui.screen
|
||||
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import com.bikeloam.app.data.model.User
|
||||
import com.bikeloam.app.ui.theme.BikeSetupTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class HomeScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun `home screen shows welcome with user name`() {
|
||||
composeTestRule.setContent {
|
||||
BikeSetupTheme {
|
||||
HomeScreen(
|
||||
onNavigateToBuildList = {},
|
||||
onNavigateToCreateBuild = {},
|
||||
onNavigateToSettings = {},
|
||||
onLogout = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.waitForIdle()
|
||||
composeTestRule.onNodeWithText("BikeSetup", useUnmergedTree = true).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("My Builds").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("New Build").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking my builds navigates to build list`() {
|
||||
var navigated = false
|
||||
composeTestRule.setContent {
|
||||
BikeSetupTheme {
|
||||
HomeScreen(
|
||||
onNavigateToBuildList = { navigated = true },
|
||||
onNavigateToCreateBuild = {},
|
||||
onNavigateToSettings = {},
|
||||
onLogout = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("My Builds").performClick()
|
||||
assert(navigated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking new build navigates to create build`() {
|
||||
var navigated = false
|
||||
composeTestRule.setContent {
|
||||
BikeSetupTheme {
|
||||
HomeScreen(
|
||||
onNavigateToBuildList = {},
|
||||
onNavigateToCreateBuild = { navigated = true },
|
||||
onNavigateToSettings = {},
|
||||
onLogout = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("New Build").performClick()
|
||||
assert(navigated)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.bikeloam.app.ui.screen
|
||||
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import com.bikeloam.app.ui.theme.BikeSetupTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class LoginScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun `login screen shows title and fields`() {
|
||||
composeTestRule.setContent {
|
||||
BikeSetupTheme {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {},
|
||||
onNavigateToRegister = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("BikeSetup").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Sign in to your account").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Email").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Password").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Sign In").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register button navigates to register`() {
|
||||
var navigated = false
|
||||
composeTestRule.setContent {
|
||||
BikeSetupTheme {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {},
|
||||
onNavigateToRegister = { navigated = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Don't have an account? Register").performClick()
|
||||
assert(navigated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty fields show error on login attempt`() {
|
||||
composeTestRule.setContent {
|
||||
BikeSetupTheme {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {},
|
||||
onNavigateToRegister = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Sign In").performClick()
|
||||
composeTestRule.onNodeWithText("Email and password are required").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.*
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class BuildDetailViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: BuildDetailViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is empty`() {
|
||||
viewModel = BuildDetailViewModel(repository)
|
||||
|
||||
assertNull(viewModel.uiState.build)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
assertNull(viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuild finds build and resolves names`() = runTest {
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
frameId = "f1", forkId = "fk1", userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
val catalog = CatalogResponse(
|
||||
frames = listOf(Product("f1", "frame", "Santa Cruz", "Nomad")),
|
||||
forks = listOf(Product("fk1", "fork", "RockShox", "Lyrik")),
|
||||
shocks = emptyList(),
|
||||
tires = emptyList()
|
||||
)
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(listOf(build))
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(catalog)
|
||||
|
||||
viewModel = BuildDetailViewModel(repository)
|
||||
viewModel.loadBuild("b1")
|
||||
|
||||
assertEquals("Trail Beast", viewModel.uiState.build?.name)
|
||||
assertEquals("Santa Cruz Nomad", viewModel.resolveProductName("f1"))
|
||||
assertEquals("RockShox Lyrik", viewModel.resolveProductName("fk1"))
|
||||
assertNull(viewModel.resolveProductName("unknown"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuild shows error when build not found`() = runTest {
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(emptyList())
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
|
||||
viewModel = BuildDetailViewModel(repository)
|
||||
viewModel.loadBuild("missing")
|
||||
|
||||
assertNull(viewModel.uiState.build)
|
||||
assertEquals("Build not found", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuild handles network error`() = runTest {
|
||||
coEvery { repository.fetchBuilds() } returns Result.failure(RuntimeException("Network down"))
|
||||
|
||||
viewModel = BuildDetailViewModel(repository)
|
||||
viewModel.loadBuild("b1")
|
||||
|
||||
assertNull(viewModel.uiState.build)
|
||||
assertEquals("Network down", viewModel.uiState.error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.BikeBuild
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class BuildListViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: BuildListViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is loading`() {
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(emptyList())
|
||||
viewModel = BuildListViewModel(repository)
|
||||
|
||||
assertTrue(viewModel.uiState.builds.isEmpty())
|
||||
assertNull(viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuilds populates list on success`() = runTest {
|
||||
val builds = listOf(
|
||||
BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
),
|
||||
BikeBuild(
|
||||
id = "b2", name = "XC Racer", riderWeightKg = 65,
|
||||
discipline = "xc", riderLevel = "advanced", rideStyle = "smooth",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
)
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(builds)
|
||||
|
||||
viewModel = BuildListViewModel(repository)
|
||||
|
||||
assertEquals(2, viewModel.uiState.builds.size)
|
||||
assertEquals("Trail Beast", viewModel.uiState.builds[0].name)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuilds shows error on failure`() = runTest {
|
||||
coEvery { repository.fetchBuilds() } returns Result.failure(RuntimeException("Network error"))
|
||||
|
||||
viewModel = BuildListViewModel(repository)
|
||||
|
||||
assertTrue(viewModel.uiState.builds.isEmpty())
|
||||
assertEquals("Network error", viewModel.uiState.error)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh reloads builds`() = runTest {
|
||||
val initialBuilds = listOf(
|
||||
BikeBuild(
|
||||
id = "b1", name = "First", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
)
|
||||
val updatedBuilds = listOf(
|
||||
BikeBuild(
|
||||
id = "b1", name = "First", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
),
|
||||
BikeBuild(
|
||||
id = "b2", name = "Second", riderWeightKg = 65,
|
||||
discipline = "xc", riderLevel = "advanced", rideStyle = "smooth",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
)
|
||||
coEvery { repository.fetchBuilds() } returnsMany listOf(
|
||||
Result.success(initialBuilds),
|
||||
Result.success(updatedBuilds)
|
||||
)
|
||||
|
||||
viewModel = BuildListViewModel(repository)
|
||||
assertEquals(1, viewModel.uiState.builds.size)
|
||||
|
||||
viewModel.loadBuilds()
|
||||
assertEquals(2, viewModel.uiState.builds.size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.*
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class CreateBuildViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: CreateBuildViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is empty`() {
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
|
||||
assertEquals("", viewModel.uiState.buildName)
|
||||
assertEquals("", viewModel.uiState.riderWeight)
|
||||
assertEquals("", viewModel.uiState.discipline)
|
||||
assertEquals("", viewModel.uiState.level)
|
||||
assertEquals("", viewModel.uiState.style)
|
||||
assertNull(viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `catalog loads on init`() = runTest {
|
||||
val catalog = CatalogResponse(
|
||||
frames = listOf(Product("f1", "frame", "Santa Cruz", "Nomad")),
|
||||
forks = listOf(Product("fk1", "fork", "RockShox", "Lyrik")),
|
||||
shocks = listOf(Product("s1", "shock", "Fox", "Float X")),
|
||||
tires = listOf(Product("t1", "tire", "Maxxis", "Minion DHF", position = "front"))
|
||||
)
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(catalog)
|
||||
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
|
||||
assertEquals(1, viewModel.uiState.frames.size)
|
||||
assertEquals(1, viewModel.uiState.forks.size)
|
||||
assertEquals(1, viewModel.uiState.shocks.size)
|
||||
assertEquals(1, viewModel.uiState.tires.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createBuild with blank name shows error`() = runTest {
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
viewModel.updateBuildName("")
|
||||
viewModel.updateRiderWeight("75")
|
||||
viewModel.updateDiscipline("trail")
|
||||
viewModel.updateLevel("intermediate")
|
||||
viewModel.updateStyle("aggressive")
|
||||
|
||||
var called = false
|
||||
viewModel.createBuild { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Build name is required", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createBuild with invalid weight shows error`() = runTest {
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
viewModel.updateBuildName("Test")
|
||||
viewModel.updateRiderWeight("20")
|
||||
viewModel.updateDiscipline("trail")
|
||||
viewModel.updateLevel("intermediate")
|
||||
viewModel.updateStyle("aggressive")
|
||||
|
||||
var called = false
|
||||
viewModel.createBuild { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Valid rider weight (35-160 kg) is required", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createBuild success navigates back`() = runTest {
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
val rider = RiderProfile(75, "trail", "intermediate", "aggressive")
|
||||
val request = CreateBuildRequest("Trail Beast", rider)
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
coEvery { repository.createBuild(any()) } returns Result.success(build)
|
||||
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
viewModel.updateBuildName("Trail Beast")
|
||||
viewModel.updateRiderWeight("75")
|
||||
viewModel.updateDiscipline("trail")
|
||||
viewModel.updateLevel("intermediate")
|
||||
viewModel.updateStyle("aggressive")
|
||||
|
||||
var called = false
|
||||
viewModel.createBuild { called = true }
|
||||
|
||||
assertTrue(called)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `discipline presets are correct`() {
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
|
||||
assertEquals(listOf("xc", "trail", "enduro"), viewModel.disciplines)
|
||||
assertEquals(listOf("beginner", "intermediate", "advanced", "expert"), viewModel.levels)
|
||||
assertEquals(listOf("smooth", "neutral", "aggressive"), viewModel.styles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `weight input filters non-digits`() {
|
||||
coEvery { repository.fetchCatalog() } returns Result.success(
|
||||
CatalogResponse(emptyList(), emptyList(), emptyList(), emptyList())
|
||||
)
|
||||
viewModel = CreateBuildViewModel(repository)
|
||||
viewModel.updateRiderWeight("75abc")
|
||||
|
||||
assertEquals("75", viewModel.uiState.riderWeight)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.User
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class HomeViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: HomeViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is loading`() {
|
||||
viewModel = HomeViewModel(repository)
|
||||
|
||||
assertTrue(viewModel.uiState.isLoading)
|
||||
assertNull(viewModel.uiState.user)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadUser sets user on success`() = runTest {
|
||||
val user = User("1", "test@example.com", "Test User")
|
||||
coEvery { repository.getCurrentUser() } returns Result.success(user)
|
||||
|
||||
viewModel = HomeViewModel(repository)
|
||||
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
assertEquals(user, viewModel.uiState.user)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadUser handles null user`() = runTest {
|
||||
coEvery { repository.getCurrentUser() } returns Result.success(null)
|
||||
|
||||
viewModel = HomeViewModel(repository)
|
||||
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
assertNull(viewModel.uiState.user)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadUser handles failure`() = runTest {
|
||||
coEvery { repository.getCurrentUser() } returns Result.failure(RuntimeException("Network error"))
|
||||
|
||||
viewModel = HomeViewModel(repository)
|
||||
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
assertNull(viewModel.uiState.user)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout calls repository and triggers callback`() = runTest {
|
||||
coEvery { repository.logout() } returns Result.success(Unit)
|
||||
coEvery { repository.getCurrentUser() } returns Result.success(null)
|
||||
|
||||
viewModel = HomeViewModel(repository)
|
||||
|
||||
var called = false
|
||||
viewModel.logout { called = true }
|
||||
|
||||
assertTrue(called)
|
||||
coVerify { repository.logout() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.User
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LoginViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: LoginViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is empty`() {
|
||||
viewModel = LoginViewModel(repository)
|
||||
|
||||
assertEquals("", viewModel.uiState.email)
|
||||
assertEquals("", viewModel.uiState.password)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
assertNull(viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateEmail updates state`() {
|
||||
viewModel = LoginViewModel(repository)
|
||||
viewModel.updateEmail("test@example.com")
|
||||
|
||||
assertEquals("test@example.com", viewModel.uiState.email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updatePassword updates state`() {
|
||||
viewModel = LoginViewModel(repository)
|
||||
viewModel.updatePassword("secret")
|
||||
|
||||
assertEquals("secret", viewModel.uiState.password)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login with blank email shows error`() = runTest {
|
||||
viewModel = LoginViewModel(repository)
|
||||
viewModel.updateEmail("")
|
||||
viewModel.updatePassword("password")
|
||||
|
||||
var called = false
|
||||
viewModel.login { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Email and password are required", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login with blank password shows error`() = runTest {
|
||||
viewModel = LoginViewModel(repository)
|
||||
viewModel.updateEmail("test@example.com")
|
||||
viewModel.updatePassword("")
|
||||
|
||||
var called = false
|
||||
viewModel.login { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Email and password are required", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login success navigates home`() = runTest {
|
||||
viewModel = LoginViewModel(repository)
|
||||
viewModel.updateEmail("test@example.com")
|
||||
viewModel.updatePassword("password123")
|
||||
|
||||
coEvery { repository.login("test@example.com", "password123") } returns Result.success(
|
||||
User("1", "test@example.com", "Test")
|
||||
)
|
||||
|
||||
var called = false
|
||||
viewModel.login { called = true }
|
||||
|
||||
assertTrue(called)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login failure shows error`() = runTest {
|
||||
viewModel = LoginViewModel(repository)
|
||||
viewModel.updateEmail("test@example.com")
|
||||
viewModel.updatePassword("wrong")
|
||||
|
||||
coEvery { repository.login(any(), any()) } returns Result.failure(
|
||||
RuntimeException("Invalid credentials")
|
||||
)
|
||||
|
||||
var called = false
|
||||
viewModel.login { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Invalid credentials", viewModel.uiState.error)
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.*
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class RecommendationViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: RecommendationViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is empty`() {
|
||||
viewModel = RecommendationViewModel(repository)
|
||||
|
||||
assertNull(viewModel.uiState.build)
|
||||
assertNull(viewModel.uiState.recommendation)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuildAndRecommend fetches recommendation on success`() = runTest {
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
frameId = "f1", forkId = "fk1", userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
val response = RecommendResponse("llama3.1:8b", "Fork: 85 PSI")
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(listOf(build))
|
||||
coEvery { repository.getRecommendation(any()) } returns Result.success(response)
|
||||
|
||||
viewModel = RecommendationViewModel(repository)
|
||||
viewModel.loadBuildAndRecommend("b1")
|
||||
|
||||
assertEquals("Trail Beast", viewModel.uiState.build?.name)
|
||||
assertEquals("llama3.1:8b", viewModel.uiState.recommendation?.model)
|
||||
assertEquals("Fork: 85 PSI", viewModel.uiState.recommendation?.recommendation)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuildAndRecommend shows error when build not found`() = runTest {
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(emptyList())
|
||||
|
||||
viewModel = RecommendationViewModel(repository)
|
||||
viewModel.loadBuildAndRecommend("missing")
|
||||
|
||||
assertNull(viewModel.uiState.build)
|
||||
assertEquals("Build not found. Could not fetch recommendation.", viewModel.uiState.error)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuildAndRecommend shows error when recommendation fails`() = runTest {
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(listOf(build))
|
||||
coEvery { repository.getRecommendation(any()) } returns Result.failure(
|
||||
RuntimeException("AI service unavailable")
|
||||
)
|
||||
|
||||
viewModel = RecommendationViewModel(repository)
|
||||
viewModel.loadBuildAndRecommend("b1")
|
||||
|
||||
assertEquals("AI service unavailable", viewModel.uiState.error)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.User
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class RegisterViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: RegisterViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is empty`() {
|
||||
viewModel = RegisterViewModel(repository)
|
||||
|
||||
assertEquals("", viewModel.uiState.email)
|
||||
assertEquals("", viewModel.uiState.password)
|
||||
assertEquals("", viewModel.uiState.name)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register with short password shows error`() = runTest {
|
||||
viewModel = RegisterViewModel(repository)
|
||||
viewModel.updateEmail("test@example.com")
|
||||
viewModel.updatePassword("12345")
|
||||
|
||||
var called = false
|
||||
viewModel.register { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Password must be at least 6 characters", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register with blank email shows error`() = runTest {
|
||||
viewModel = RegisterViewModel(repository)
|
||||
viewModel.updateEmail("")
|
||||
viewModel.updatePassword("password123")
|
||||
|
||||
var called = false
|
||||
viewModel.register { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Email and password are required", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register success navigates home`() = runTest {
|
||||
viewModel = RegisterViewModel(repository)
|
||||
viewModel.updateEmail("new@example.com")
|
||||
viewModel.updatePassword("password123")
|
||||
viewModel.updateName("New User")
|
||||
|
||||
coEvery { repository.register("new@example.com", "password123", "New User") } returns Result.success(
|
||||
User("1", "new@example.com", "New User")
|
||||
)
|
||||
|
||||
var called = false
|
||||
viewModel.register { called = true }
|
||||
|
||||
assertTrue(called)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register failure shows error`() = runTest {
|
||||
viewModel = RegisterViewModel(repository)
|
||||
viewModel.updateEmail("dup@example.com")
|
||||
viewModel.updatePassword("password123")
|
||||
|
||||
coEvery { repository.register(any(), any(), any()) } returns Result.failure(
|
||||
RuntimeException("Email already registered")
|
||||
)
|
||||
|
||||
var called = false
|
||||
viewModel.register { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Email already registered", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register with null name passes null to repository`() = runTest {
|
||||
viewModel = RegisterViewModel(repository)
|
||||
viewModel.updateEmail("test@example.com")
|
||||
viewModel.updatePassword("password123")
|
||||
viewModel.updateName("")
|
||||
|
||||
coEvery { repository.register("test@example.com", "password123", null) } returns Result.success(
|
||||
User("1", "test@example.com", null)
|
||||
)
|
||||
|
||||
var called = false
|
||||
viewModel.register { called = true }
|
||||
|
||||
assertTrue(called)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.*
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ReviewViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: ReviewViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state has default ratings`() {
|
||||
viewModel = ReviewViewModel(repository)
|
||||
|
||||
assertEquals(5, viewModel.uiState.suspFeel)
|
||||
assertEquals(5, viewModel.uiState.frontGrip)
|
||||
assertEquals(5, viewModel.uiState.rearGrip)
|
||||
assertEquals(5, viewModel.uiState.rollSpeed)
|
||||
assertEquals(5, viewModel.uiState.confidence)
|
||||
assertFalse(viewModel.uiState.bottomOut)
|
||||
assertFalse(viewModel.uiState.harshHit)
|
||||
assertFalse(viewModel.uiState.wallow)
|
||||
assertFalse(viewModel.uiState.smallBump)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateSuspFeel coerces to valid range`() {
|
||||
viewModel = ReviewViewModel(repository)
|
||||
viewModel.updateSuspFeel(15)
|
||||
assertEquals(10, viewModel.uiState.suspFeel)
|
||||
|
||||
viewModel.updateSuspFeel(0)
|
||||
assertEquals(1, viewModel.uiState.suspFeel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toggleBottomOut flips boolean`() {
|
||||
viewModel = ReviewViewModel(repository)
|
||||
assertFalse(viewModel.uiState.bottomOut)
|
||||
|
||||
viewModel.toggleBottomOut()
|
||||
assertTrue(viewModel.uiState.bottomOut)
|
||||
|
||||
viewModel.toggleBottomOut()
|
||||
assertFalse(viewModel.uiState.bottomOut)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `submitReview success triggers callback`() = runTest {
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
val builds = listOf(build)
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(builds)
|
||||
|
||||
val review = RideReview(
|
||||
id = "r1", buildId = "b1", suspFeel = 7, frontGrip = 8,
|
||||
rearGrip = 6, rollSpeed = 7, confidence = 9
|
||||
)
|
||||
coEvery { repository.createReview(any()) } returns Result.success(review)
|
||||
|
||||
viewModel = ReviewViewModel(repository)
|
||||
viewModel.loadBuild("b1")
|
||||
viewModel.updateSuspFeel(7)
|
||||
viewModel.updateFrontGrip(8)
|
||||
viewModel.updateRearGrip(6)
|
||||
viewModel.updateRollSpeed(7)
|
||||
viewModel.updateConfidence(9)
|
||||
viewModel.toggleBottomOut()
|
||||
|
||||
var called = false
|
||||
viewModel.submitReview("b1") { called = true }
|
||||
|
||||
assertTrue(called)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `submitReview failure shows error`() = runTest {
|
||||
coEvery { repository.createReview(any()) } returns Result.failure(
|
||||
RuntimeException("Server error")
|
||||
)
|
||||
|
||||
viewModel = ReviewViewModel(repository)
|
||||
viewModel.updateSuspFeel(7)
|
||||
|
||||
var called = false
|
||||
viewModel.submitReview("b1") { called = true }
|
||||
|
||||
assertFalse(called)
|
||||
assertEquals("Server error", viewModel.uiState.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadBuild fetches and sets build`() = runTest {
|
||||
val build = BikeBuild(
|
||||
id = "b1", name = "Trail Beast", riderWeightKg = 75,
|
||||
discipline = "trail", riderLevel = "intermediate", rideStyle = "aggressive",
|
||||
userId = "u1", createdAt = "", updatedAt = ""
|
||||
)
|
||||
coEvery { repository.fetchBuilds() } returns Result.success(listOf(build))
|
||||
|
||||
viewModel = ReviewViewModel(repository)
|
||||
viewModel.loadBuild("b1")
|
||||
|
||||
assertEquals("Trail Beast", viewModel.uiState.build?.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateNotes and updateAdvice update state`() {
|
||||
viewModel = ReviewViewModel(repository)
|
||||
viewModel.updateNotes("Great ride")
|
||||
viewModel.updateAdvice("Increase rebound")
|
||||
|
||||
assertEquals("Great ride", viewModel.uiState.notes)
|
||||
assertEquals("Increase rebound", viewModel.uiState.advice)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.bikeloam.app.ui.viewmodel
|
||||
|
||||
import com.bikeloam.app.MainDispatcherRule
|
||||
import com.bikeloam.app.data.model.User
|
||||
import com.bikeloam.app.data.repository.BikeRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SplashViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val repository: BikeRepository = mockk()
|
||||
private lateinit var viewModel: SplashViewModel
|
||||
|
||||
@Test
|
||||
fun `initial state is not ready`() {
|
||||
viewModel = SplashViewModel(repository)
|
||||
|
||||
assertFalse(viewModel.uiState.isReady)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticated user becomes ready with isAuthenticated true`() = runTest {
|
||||
coEvery { repository.getCurrentUser() } returns Result.success(
|
||||
User("1", "test@example.com", "Test")
|
||||
)
|
||||
|
||||
viewModel = SplashViewModel(repository)
|
||||
|
||||
assertTrue(viewModel.uiState.isReady)
|
||||
assertTrue(viewModel.uiState.isAuthenticated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unauthenticated user becomes ready with isAuthenticated false`() = runTest {
|
||||
coEvery { repository.getCurrentUser() } returns Result.success(null)
|
||||
|
||||
viewModel = SplashViewModel(repository)
|
||||
|
||||
assertTrue(viewModel.uiState.isReady)
|
||||
assertFalse(viewModel.uiState.isAuthenticated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth check failure becomes ready with isAuthenticated false`() = runTest {
|
||||
coEvery { repository.getCurrentUser() } returns Result.failure(RuntimeException("Network error"))
|
||||
|
||||
viewModel = SplashViewModel(repository)
|
||||
|
||||
assertTrue(viewModel.uiState.isReady)
|
||||
assertFalse(viewModel.uiState.isAuthenticated)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user