P7 Stream W: TaskFormState + validation (iOS parity)

Pure-function field validators matching iOS TaskFormStates.swift error
strings. TaskFormState container derives errors from fields, exposes
isValid and typed request builders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 13:22:53 -05:00
parent edc22c0d2b
commit 3700968d00
4 changed files with 489 additions and 0 deletions
@@ -0,0 +1,184 @@
package com.tt.honeyDue.ui.screens.task
import kotlinx.datetime.LocalDate
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* Behaviour spec for TaskFormState — the container that wires TaskFormFields
* to the pure validators and to typed request builders.
*/
class TaskFormStateTest {
private fun validFields(
residenceId: Int? = 10,
) = TaskFormFields(
title = "Change HVAC filter",
description = "Replace the filter in the hallway unit",
priorityId = 1,
categoryId = 2,
frequencyId = 3,
dueDate = LocalDate(2026, 5, 1),
estimatedCost = "25.50",
residenceId = residenceId,
)
@Test
fun empty_fields_isValid_false_and_toCreateRequest_null() {
val state = TaskFormState()
assertFalse(state.isValid)
assertNull(state.toCreateRequest())
}
@Test
fun empty_fields_errors_populated_for_all_required_fields() {
val state = TaskFormState()
val errors = state.errors
assertEquals("Title is required", errors.title)
assertEquals("Please select a priority", errors.priorityId)
assertEquals("Please select a category", errors.categoryId)
assertEquals("Please select a frequency", errors.frequencyId)
assertEquals("Property is required", errors.residenceId)
assertNull(errors.estimatedCost) // empty cost is valid
}
@Test
fun all_required_fields_set_isValid_true_and_errors_clear() {
val state = TaskFormState(validFields())
assertTrue(state.isValid)
val errors = state.errors
assertNull(errors.title)
assertNull(errors.priorityId)
assertNull(errors.categoryId)
assertNull(errors.frequencyId)
assertNull(errors.residenceId)
assertNull(errors.estimatedCost)
}
@Test
fun toCreateRequest_maps_fields_correctly_when_valid() {
val state = TaskFormState(validFields())
val request = state.toCreateRequest()
assertNotNull(request)
assertEquals(10, request.residenceId)
assertEquals("Change HVAC filter", request.title)
assertEquals("Replace the filter in the hallway unit", request.description)
assertEquals(2, request.categoryId)
assertEquals(1, request.priorityId)
assertEquals(3, request.frequencyId)
assertEquals("2026-05-01", request.dueDate)
assertEquals(25.50, request.estimatedCost)
}
@Test
fun toCreateRequest_blank_description_maps_to_null() {
val state = TaskFormState(validFields().copy(description = " "))
val request = state.toCreateRequest()
assertNotNull(request)
assertNull(request.description)
}
@Test
fun toCreateRequest_empty_estimatedCost_maps_to_null() {
val state = TaskFormState(validFields().copy(estimatedCost = ""))
val request = state.toCreateRequest()
assertNotNull(request)
assertNull(request.estimatedCost)
}
@Test
fun toCreateRequest_null_dueDate_maps_to_null() {
val state = TaskFormState(validFields().copy(dueDate = null))
val request = state.toCreateRequest()
assertNotNull(request)
assertNull(request.dueDate)
}
@Test
fun toCreateRequest_missing_residence_returns_null() {
val state = TaskFormState(validFields(residenceId = null))
assertFalse(state.isValid)
assertNull(state.toCreateRequest())
}
@Test
fun toCreateRequest_invalid_estimatedCost_returns_null() {
val state = TaskFormState(validFields().copy(estimatedCost = "not a number"))
assertFalse(state.isValid)
assertNull(state.toCreateRequest())
assertEquals(
"Estimated cost must be a valid number",
state.errors.estimatedCost,
)
}
@Test
fun update_recomputes_errors_and_isValid() {
val state = TaskFormState()
assertFalse(state.isValid)
assertEquals("Title is required", state.errors.title)
state.update(validFields())
assertTrue(state.isValid)
assertNull(state.errors.title)
state.update(state.fields.copy(title = ""))
assertFalse(state.isValid)
assertEquals("Title is required", state.errors.title)
}
@Test
fun validate_returns_current_errors_snapshot() {
val state = TaskFormState()
val snapshot = state.validate()
assertEquals("Title is required", snapshot.title)
assertEquals("Please select a priority", snapshot.priorityId)
}
@Test
fun toUpdateRequest_maps_fields_correctly() {
val state = TaskFormState(validFields())
val request = state.toUpdateRequest(taskId = 42)
assertNotNull(request)
assertEquals("Change HVAC filter", request.title)
assertEquals("Replace the filter in the hallway unit", request.description)
assertEquals(2, request.categoryId)
assertEquals(1, request.priorityId)
assertEquals(3, request.frequencyId)
assertEquals("2026-05-01", request.dueDate)
assertEquals(25.50, request.estimatedCost)
}
@Test
fun toUpdateRequest_when_invalid_returns_null() {
val state = TaskFormState()
assertNull(state.toUpdateRequest(taskId = 42))
}
@Test
fun toUpdateRequest_ignores_residence_requirement() {
// Edit mode doesn't need residenceId — the task already has one.
val state = TaskFormState(validFields(residenceId = null))
// State as a whole still reports invalid (residenceId missing), but
// toUpdateRequest should still produce a request because updates do
// not carry residenceId. This matches iOS where isEditMode bypasses
// the residence check.
val request = state.toUpdateRequest(taskId = 99)
assertNotNull(request)
assertEquals("Change HVAC filter", request.title)
}
@Test
fun update_does_not_mutate_prior_fields_reference() {
val state = TaskFormState()
val initial = state.fields
state.update(validFields())
// `fields` has moved to the new value; original TaskFormFields is unchanged.
assertEquals("", initial.title)
assertEquals("Change HVAC filter", state.fields.title)
}
}
@@ -0,0 +1,117 @@
package com.tt.honeyDue.ui.screens.task
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
/**
* Pure-function validator tests for TaskFormValidation.
*
* Error strings are copied verbatim from
* `iosApp/iosApp/Task/TaskFormView.swift::validateForm()`
* and `iosApp/iosApp/Core/FormStates/TaskFormStates.swift`.
*/
class TaskFormValidationTest {
// --- validateTitle ------------------------------------------------------
@Test
fun validateTitle_emptyString_returnsTitleRequired() {
assertEquals("Title is required", TaskFormValidation.validateTitle(""))
}
@Test
fun validateTitle_blankString_returnsTitleRequired() {
assertEquals("Title is required", TaskFormValidation.validateTitle(" "))
}
@Test
fun validateTitle_nonEmpty_returnsNull() {
assertNull(TaskFormValidation.validateTitle("Change air filter"))
}
// --- validatePriorityId -------------------------------------------------
@Test
fun validatePriorityId_null_returnsPleaseSelectPriority() {
assertEquals(
"Please select a priority",
TaskFormValidation.validatePriorityId(null)
)
}
@Test
fun validatePriorityId_set_returnsNull() {
assertNull(TaskFormValidation.validatePriorityId(1))
}
// --- validateCategoryId -------------------------------------------------
@Test
fun validateCategoryId_null_returnsPleaseSelectCategory() {
assertEquals(
"Please select a category",
TaskFormValidation.validateCategoryId(null)
)
}
@Test
fun validateCategoryId_set_returnsNull() {
assertNull(TaskFormValidation.validateCategoryId(2))
}
// --- validateFrequencyId ------------------------------------------------
@Test
fun validateFrequencyId_null_returnsPleaseSelectFrequency() {
assertEquals(
"Please select a frequency",
TaskFormValidation.validateFrequencyId(null)
)
}
@Test
fun validateFrequencyId_set_returnsNull() {
assertNull(TaskFormValidation.validateFrequencyId(3))
}
// --- validateEstimatedCost ---------------------------------------------
@Test
fun validateEstimatedCost_empty_returnsNull() {
assertNull(TaskFormValidation.validateEstimatedCost(""))
}
@Test
fun validateEstimatedCost_validInteger_returnsNull() {
assertNull(TaskFormValidation.validateEstimatedCost("100"))
}
@Test
fun validateEstimatedCost_validDecimal_returnsNull() {
assertNull(TaskFormValidation.validateEstimatedCost("49.99"))
}
@Test
fun validateEstimatedCost_nonNumeric_returnsError() {
assertEquals(
"Estimated cost must be a valid number",
TaskFormValidation.validateEstimatedCost("abc")
)
}
// --- validateResidenceId -----------------------------------------------
@Test
fun validateResidenceId_null_returnsPropertyRequired() {
assertEquals(
"Property is required",
TaskFormValidation.validateResidenceId(null)
)
}
@Test
fun validateResidenceId_set_returnsNull() {
assertNull(TaskFormValidation.validateResidenceId(5))
}
}