diff --git a/app/src/main/java/com/example/theloop/OnboardingViewModel.kt b/app/src/main/java/com/example/theloop/OnboardingViewModel.kt index 165bae3..77cc461 100644 --- a/app/src/main/java/com/example/theloop/OnboardingViewModel.kt +++ b/app/src/main/java/com/example/theloop/OnboardingViewModel.kt @@ -18,16 +18,23 @@ class OnboardingViewModel @Inject constructor( private val _name = MutableStateFlow("") val name: StateFlow = _name.asStateFlow() + private val _nameError = MutableStateFlow(null) + val nameError: StateFlow = _nameError.asStateFlow() + fun onNameChange(newName: String) { _name.value = newName + _nameError.value = null } fun saveName(): Boolean { val isNameValid = name.value.isNotBlank() if (isNameValid) { + _nameError.value = null viewModelScope.launch { userPreferencesRepository.saveUserName(name.value) } + } else { + _nameError.value = R.string.error_name_blank } return isNameValid } diff --git a/app/src/main/java/com/example/theloop/ui/screens/OnboardingScreen.kt b/app/src/main/java/com/example/theloop/ui/screens/OnboardingScreen.kt index 37457cb..eadf255 100644 --- a/app/src/main/java/com/example/theloop/ui/screens/OnboardingScreen.kt +++ b/app/src/main/java/com/example/theloop/ui/screens/OnboardingScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.health.connect.client.HealthConnectClient @@ -28,6 +29,7 @@ fun OnboardingScreen( val context = LocalContext.current var currentStep by remember { mutableIntStateOf(0) } val name by viewModel.name.collectAsState() + val nameError by viewModel.nameError.collectAsState() val totalSteps = 5 // Permissions state @@ -99,7 +101,6 @@ fun OnboardingScreen( if (currentStep == 0) { // Save Name if (!viewModel.saveName()) { - Toast.makeText(context, "Name cannot be blank", Toast.LENGTH_SHORT).show() return@Button } } @@ -127,7 +128,7 @@ fun OnboardingScreen( verticalArrangement = Arrangement.Center ) { when (currentStep) { - 0 -> WelcomeStep(name, viewModel::onNameChange) + 0 -> WelcomeStep(name, nameError, viewModel::onNameChange) 1 -> PermissionStep( title = "Enable Location", description = "We need your location to show local weather.", @@ -167,7 +168,7 @@ fun OnboardingScreen( } @Composable -fun WelcomeStep(name: String, onNameChange: (String) -> Unit) { +fun WelcomeStep(name: String, nameError: Int?, onNameChange: (String) -> Unit) { Text("Welcome to The Loop", style = MaterialTheme.typography.displaySmall) Spacer(Modifier.height(16.dp)) Text("Your daily dashboard for life.", style = MaterialTheme.typography.bodyLarge) @@ -177,7 +178,9 @@ fun WelcomeStep(name: String, onNameChange: (String) -> Unit) { onValueChange = onNameChange, label = { Text("What's your name?") }, singleLine = true, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + isError = nameError != null, + supportingText = nameError?.let { { Text(stringResource(it)) } } ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 729b406..21c58b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ The Loop + Name cannot be blank Select Temperature Unit Cancel Unknown Location diff --git a/app/src/test/java/com/example/theloop/OnboardingViewModelTest.kt b/app/src/test/java/com/example/theloop/OnboardingViewModelTest.kt new file mode 100644 index 0000000..ca53f0d --- /dev/null +++ b/app/src/test/java/com/example/theloop/OnboardingViewModelTest.kt @@ -0,0 +1,58 @@ +package com.example.theloop + +import androidx.test.core.app.ApplicationProvider +import com.example.theloop.data.repository.UserPreferencesRepository +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class OnboardingViewModelTest { + + private lateinit var viewModel: OnboardingViewModel + private lateinit var userPreferencesRepository: UserPreferencesRepository + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + userPreferencesRepository = UserPreferencesRepository(context) + viewModel = OnboardingViewModel(userPreferencesRepository) + } + + @Test + fun saveName_withBlankName_setsErrorAndReturnsFalse() { + viewModel.onNameChange(" ") + val result = viewModel.saveName() + + assertFalse(result) + assertEquals(R.string.error_name_blank, viewModel.nameError.value) + } + + @Test + fun saveName_withValidName_clearsErrorAndReturnsTrue() { + viewModel.onNameChange("Valid Name") + val result = viewModel.saveName() + + assertTrue(result) + assertNull(viewModel.nameError.value) + } + + @Test + fun onNameChange_clearsError() { + // Set error first + viewModel.onNameChange("") + viewModel.saveName() + assertEquals(R.string.error_name_blank, viewModel.nameError.value) + + // Change name + viewModel.onNameChange("New Name") + assertNull(viewModel.nameError.value) + } +}