P7 Stream X: ResidenceFormState + validation (iOS parity)
Pure-function field validators matching iOS ResidenceFormView rules. Mirrors Stream W's TaskFormState pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
package com.tt.honeyDue.ui.screens.residence
|
||||
|
||||
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 [ResidenceFormState] — container that wires
|
||||
* [ResidenceFormFields] to the pure validators and to typed request
|
||||
* builders. Mirrors Stream W's `TaskFormStateTest` structure.
|
||||
*/
|
||||
class ResidenceFormStateTest {
|
||||
|
||||
private fun validFields() = ResidenceFormFields(
|
||||
name = "Main House",
|
||||
propertyTypeId = 2,
|
||||
streetAddress = "123 Main St",
|
||||
apartmentUnit = "Apt 4",
|
||||
city = "Springfield",
|
||||
stateProvince = "IL",
|
||||
postalCode = "62701",
|
||||
country = "USA",
|
||||
bedrooms = "3",
|
||||
bathrooms = "2.5",
|
||||
squareFootage = "1800",
|
||||
lotSize = "0.25",
|
||||
yearBuilt = "1998",
|
||||
description = "Three-bedroom single-family home",
|
||||
isPrimary = true,
|
||||
)
|
||||
|
||||
// --- defaults / empty --------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun default_fields_have_usa_country_and_blank_name() {
|
||||
val fields = ResidenceFormFields()
|
||||
assertEquals("USA", fields.country)
|
||||
assertEquals("", fields.name)
|
||||
assertFalse(fields.isPrimary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty_state_isValid_false_and_name_error_present() {
|
||||
val state = ResidenceFormState()
|
||||
assertFalse(state.isValid)
|
||||
assertEquals("Name is required", state.errors.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty_state_toCreateRequest_null() {
|
||||
assertNull(ResidenceFormState().toCreateRequest())
|
||||
}
|
||||
|
||||
// --- valid path --------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun valid_fields_isValid_true() {
|
||||
val state = ResidenceFormState(validFields())
|
||||
assertTrue(state.isValid)
|
||||
val e = state.errors
|
||||
assertNull(e.name)
|
||||
assertNull(e.bedrooms)
|
||||
assertNull(e.bathrooms)
|
||||
assertNull(e.squareFootage)
|
||||
assertNull(e.lotSize)
|
||||
assertNull(e.yearBuilt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun only_name_required_for_validity() {
|
||||
// iOS canSave: !name.isEmpty — no other required fields.
|
||||
val state = ResidenceFormState(ResidenceFormFields(name = "Cabin"))
|
||||
assertTrue(state.isValid)
|
||||
}
|
||||
|
||||
// --- toCreateRequest ---------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun toCreateRequest_maps_fields_correctly_when_valid() {
|
||||
val state = ResidenceFormState(validFields())
|
||||
val req = state.toCreateRequest()
|
||||
assertNotNull(req)
|
||||
assertEquals("Main House", req.name)
|
||||
assertEquals(2, req.propertyTypeId)
|
||||
assertEquals("123 Main St", req.streetAddress)
|
||||
assertEquals("Apt 4", req.apartmentUnit)
|
||||
assertEquals("Springfield", req.city)
|
||||
assertEquals("IL", req.stateProvince)
|
||||
assertEquals("62701", req.postalCode)
|
||||
assertEquals("USA", req.country)
|
||||
assertEquals(3, req.bedrooms)
|
||||
assertEquals(2.5, req.bathrooms)
|
||||
assertEquals(1800, req.squareFootage)
|
||||
assertEquals(0.25, req.lotSize)
|
||||
assertEquals(1998, req.yearBuilt)
|
||||
assertEquals("Three-bedroom single-family home", req.description)
|
||||
assertEquals(true, req.isPrimary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toCreateRequest_blank_optional_strings_map_to_null() {
|
||||
val state = ResidenceFormState(
|
||||
validFields().copy(
|
||||
streetAddress = "",
|
||||
apartmentUnit = " ",
|
||||
city = "",
|
||||
stateProvince = "",
|
||||
postalCode = "",
|
||||
country = "",
|
||||
description = "",
|
||||
)
|
||||
)
|
||||
val req = state.toCreateRequest()
|
||||
assertNotNull(req)
|
||||
assertNull(req.streetAddress)
|
||||
assertNull(req.apartmentUnit)
|
||||
assertNull(req.city)
|
||||
assertNull(req.stateProvince)
|
||||
assertNull(req.postalCode)
|
||||
assertNull(req.country)
|
||||
assertNull(req.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toCreateRequest_empty_numeric_fields_map_to_null() {
|
||||
val state = ResidenceFormState(
|
||||
validFields().copy(
|
||||
bedrooms = "",
|
||||
bathrooms = "",
|
||||
squareFootage = "",
|
||||
lotSize = "",
|
||||
yearBuilt = "",
|
||||
)
|
||||
)
|
||||
val req = state.toCreateRequest()
|
||||
assertNotNull(req)
|
||||
assertNull(req.bedrooms)
|
||||
assertNull(req.bathrooms)
|
||||
assertNull(req.squareFootage)
|
||||
assertNull(req.lotSize)
|
||||
assertNull(req.yearBuilt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toCreateRequest_invalid_numeric_returns_null() {
|
||||
val state = ResidenceFormState(validFields().copy(bedrooms = "-1"))
|
||||
assertFalse(state.isValid)
|
||||
assertNull(state.toCreateRequest())
|
||||
assertNotNull(state.errors.bedrooms)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toCreateRequest_invalid_yearBuilt_returns_null() {
|
||||
val state = ResidenceFormState(validFields().copy(yearBuilt = "42"))
|
||||
assertFalse(state.isValid)
|
||||
assertNull(state.toCreateRequest())
|
||||
}
|
||||
|
||||
// --- toUpdateRequest ---------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun toUpdateRequest_maps_fields_correctly_when_valid() {
|
||||
val state = ResidenceFormState(validFields())
|
||||
val req = state.toUpdateRequest(residenceId = 42)
|
||||
assertNotNull(req)
|
||||
assertEquals("Main House", req.name)
|
||||
assertEquals(2, req.propertyTypeId)
|
||||
assertEquals(3, req.bedrooms)
|
||||
assertEquals(2.5, req.bathrooms)
|
||||
assertEquals(1800, req.squareFootage)
|
||||
assertEquals(1998, req.yearBuilt)
|
||||
assertEquals(true, req.isPrimary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toUpdateRequest_returns_null_when_invalid() {
|
||||
assertNull(ResidenceFormState().toUpdateRequest(residenceId = 1))
|
||||
}
|
||||
|
||||
// --- update / validate -------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun update_recomputes_errors_and_isValid() {
|
||||
val state = ResidenceFormState()
|
||||
assertFalse(state.isValid)
|
||||
assertEquals("Name is required", state.errors.name)
|
||||
|
||||
state.update(validFields())
|
||||
assertTrue(state.isValid)
|
||||
assertNull(state.errors.name)
|
||||
|
||||
state.update(state.fields.copy(name = ""))
|
||||
assertFalse(state.isValid)
|
||||
assertEquals("Name is required", state.errors.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validate_returns_current_errors_snapshot() {
|
||||
val state = ResidenceFormState()
|
||||
val snapshot = state.validate()
|
||||
assertEquals("Name is required", snapshot.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toUpdateRequest_long_overload_narrows_to_int() {
|
||||
val state = ResidenceFormState(validFields())
|
||||
val req = state.toUpdateRequest(residenceId = 99L)
|
||||
assertNotNull(req)
|
||||
assertEquals("Main House", req.name)
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
package com.tt.honeyDue.ui.screens.residence
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* Pure-function validator tests for [ResidenceFormValidation].
|
||||
*
|
||||
* Rules are derived from iOS:
|
||||
* - `iosApp/iosApp/ResidenceFormView.swift::validateForm()` — only `name`
|
||||
* is required; `canSave: !name.isEmpty`.
|
||||
* - `iosApp/iosApp/Core/FormStates/ResidenceFormState.swift` — numeric
|
||||
* fields (bedrooms, bathrooms, squareFootage, lotSize, yearBuilt) use
|
||||
* `asOptionalInt`/`asOptionalDouble` — empty is valid, non-numeric
|
||||
* silently returns nil. We surface that as a user-facing error here so
|
||||
* the KMM client can tell the user their input was dropped rather than
|
||||
* failing silently.
|
||||
*
|
||||
* Error copy uses the same phrasing style as Stream W's
|
||||
* `TaskFormValidation` ("... is required", "... must be a valid number").
|
||||
*/
|
||||
class ResidenceFormValidationTest {
|
||||
|
||||
// --- validateName ------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun validateName_emptyString_returnsNameRequired() {
|
||||
assertEquals("Name is required", ResidenceFormValidation.validateName(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateName_blankString_returnsNameRequired() {
|
||||
assertEquals("Name is required", ResidenceFormValidation.validateName(" "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateName_nonEmpty_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateName("Home"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateName_atMaxLength_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateName("a".repeat(100)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateName_overMaxLength_returnsError() {
|
||||
assertEquals(
|
||||
"Name must be 100 characters or fewer",
|
||||
ResidenceFormValidation.validateName("a".repeat(101)),
|
||||
)
|
||||
}
|
||||
|
||||
// --- validateBedrooms --------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun validateBedrooms_empty_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateBedrooms(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBedrooms_validInteger_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateBedrooms("3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBedrooms_zero_returnsNull() {
|
||||
// iOS `asOptionalInt` accepts 0. Match that behaviour.
|
||||
assertNull(ResidenceFormValidation.validateBedrooms("0"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBedrooms_negative_returnsError() {
|
||||
assertEquals(
|
||||
"Bedrooms must be a non-negative whole number",
|
||||
ResidenceFormValidation.validateBedrooms("-1"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBedrooms_decimal_returnsError() {
|
||||
assertEquals(
|
||||
"Bedrooms must be a non-negative whole number",
|
||||
ResidenceFormValidation.validateBedrooms("2.5"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBedrooms_nonNumeric_returnsError() {
|
||||
assertEquals(
|
||||
"Bedrooms must be a non-negative whole number",
|
||||
ResidenceFormValidation.validateBedrooms("abc"),
|
||||
)
|
||||
}
|
||||
|
||||
// --- validateBathrooms -------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun validateBathrooms_empty_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateBathrooms(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBathrooms_validDecimal_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateBathrooms("2.5"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBathrooms_validInteger_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateBathrooms("3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBathrooms_negative_returnsError() {
|
||||
assertEquals(
|
||||
"Bathrooms must be a non-negative number",
|
||||
ResidenceFormValidation.validateBathrooms("-0.5"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateBathrooms_nonNumeric_returnsError() {
|
||||
assertEquals(
|
||||
"Bathrooms must be a non-negative number",
|
||||
ResidenceFormValidation.validateBathrooms("abc"),
|
||||
)
|
||||
}
|
||||
|
||||
// --- validateSquareFootage --------------------------------------------
|
||||
|
||||
@Test
|
||||
fun validateSquareFootage_empty_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateSquareFootage(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateSquareFootage_validInteger_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateSquareFootage("1500"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateSquareFootage_zero_returnsError() {
|
||||
assertEquals(
|
||||
"Square footage must be a positive whole number",
|
||||
ResidenceFormValidation.validateSquareFootage("0"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateSquareFootage_negative_returnsError() {
|
||||
assertEquals(
|
||||
"Square footage must be a positive whole number",
|
||||
ResidenceFormValidation.validateSquareFootage("-100"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateSquareFootage_nonNumeric_returnsError() {
|
||||
assertEquals(
|
||||
"Square footage must be a positive whole number",
|
||||
ResidenceFormValidation.validateSquareFootage("big"),
|
||||
)
|
||||
}
|
||||
|
||||
// --- validateLotSize ---------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun validateLotSize_empty_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateLotSize(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateLotSize_validDecimal_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateLotSize("0.25"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateLotSize_negative_returnsError() {
|
||||
assertEquals(
|
||||
"Lot size must be a positive number",
|
||||
ResidenceFormValidation.validateLotSize("-1"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateLotSize_zero_returnsError() {
|
||||
assertEquals(
|
||||
"Lot size must be a positive number",
|
||||
ResidenceFormValidation.validateLotSize("0"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateLotSize_nonNumeric_returnsError() {
|
||||
assertEquals(
|
||||
"Lot size must be a positive number",
|
||||
ResidenceFormValidation.validateLotSize("huge"),
|
||||
)
|
||||
}
|
||||
|
||||
// --- validateYearBuilt -------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun validateYearBuilt_empty_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateYearBuilt(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateYearBuilt_validYear_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateYearBuilt("1998"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateYearBuilt_earliestValid_returnsNull() {
|
||||
assertNull(ResidenceFormValidation.validateYearBuilt("1800"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateYearBuilt_beforeMinimum_returnsError() {
|
||||
assertEquals(
|
||||
"Year built must be between 1800 and the current year",
|
||||
ResidenceFormValidation.validateYearBuilt("1799"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateYearBuilt_wrongDigitCount_returnsError() {
|
||||
assertEquals(
|
||||
"Year built must be a 4-digit year",
|
||||
ResidenceFormValidation.validateYearBuilt("98"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateYearBuilt_nonNumeric_returnsError() {
|
||||
assertEquals(
|
||||
"Year built must be a 4-digit year",
|
||||
ResidenceFormValidation.validateYearBuilt("abcd"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user