P1: Shared FixtureDataManager (empty + populated) for cross-platform snapshots
InMemoryDataManager + Fixtures with deterministic data (fixed clock 2026-04-15,
2 residences, 8 tasks, 3 contractors, 5 documents). FixtureDataManager.empty()
and .populated() factories. Exposed to Swift via SKIE.
Expanded IDataManager surface (5 -> 22 members) so fixtures cover every
StateFlow and lookup helper screens read: myResidences, allTasks,
tasksByResidence, documents, documentsByResidence, contractors, residenceTypes,
taskFrequencies, taskPriorities, taskCategories, contractorSpecialties,
taskTemplates, taskTemplatesGrouped, residenceSummaries, upgradeTriggers,
promotions, plus get{ResidenceType,TaskFrequency,TaskPriority,TaskCategory,
ContractorSpecialty}(id) lookup helpers. DataManager implementation is a pure
override-keyword addition — no behavior change.
Enables P2 (Android gallery) + P3 (iOS gallery) to render real screens against
identical inputs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package com.tt.honeyDue.testing
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Guarantees the fixtures consumed by the parity-gallery render deterministic,
|
||||
* self-consistent data on every run. These tests do not invoke Compose — they
|
||||
* assert structural invariants the snapshot tests rely on (unique ids,
|
||||
* non-empty lookups in empty state, reachable references between tasks and
|
||||
* residences, etc.).
|
||||
*
|
||||
* Failures here surface BEFORE the snapshot suite tries to record goldens
|
||||
* against inconsistent data — keeps the parity gallery honest.
|
||||
*/
|
||||
class FixtureDataManagerTest {
|
||||
|
||||
// ==================== EMPTY STATE ====================
|
||||
|
||||
@Test
|
||||
fun empty_hasNoResidences() {
|
||||
val dm = FixtureDataManager.empty()
|
||||
assertTrue(dm.residences.value.isEmpty(), "empty() must have no residences")
|
||||
assertNull(dm.myResidences.value, "empty() must have null myResidencesResponse")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty_hasNoTasksContractorsDocuments() {
|
||||
val dm = FixtureDataManager.empty()
|
||||
assertNull(dm.allTasks.value, "empty() must have null task kanban")
|
||||
assertTrue(dm.contractors.value.isEmpty(), "empty() must have no contractors")
|
||||
assertTrue(dm.documents.value.isEmpty(), "empty() must have no documents")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty_retainsLookupsForPickers() {
|
||||
// Even in empty state, form pickers need lookup data — otherwise the
|
||||
// "add first task" / "add first contractor" flows can't render dropdowns.
|
||||
val dm = FixtureDataManager.empty()
|
||||
assertTrue(dm.taskCategories.value.isNotEmpty(), "taskCategories must be populated in empty()")
|
||||
assertTrue(dm.taskPriorities.value.isNotEmpty(), "taskPriorities must be populated in empty()")
|
||||
assertTrue(dm.taskFrequencies.value.isNotEmpty(), "taskFrequencies must be populated in empty()")
|
||||
assertTrue(dm.residenceTypes.value.isNotEmpty(), "residenceTypes must be populated in empty()")
|
||||
assertTrue(dm.contractorSpecialties.value.isNotEmpty(), "contractorSpecialties must be populated in empty()")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty_providesFreeTierSubscription() {
|
||||
val dm = FixtureDataManager.empty()
|
||||
val sub = dm.subscription.value
|
||||
assertNotNull(sub, "empty() should still have a free-tier SubscriptionStatus")
|
||||
assertEquals("free", sub.tier)
|
||||
}
|
||||
|
||||
// ==================== POPULATED STATE ====================
|
||||
|
||||
@Test
|
||||
fun populated_hasExpectedResidenceCounts() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
assertEquals(2, dm.residences.value.size, "populated() must have 2 residences")
|
||||
val myRes = dm.myResidences.value
|
||||
assertNotNull(myRes)
|
||||
assertEquals(2, myRes.residences.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populated_hasKanbanBoardAndEightTasks() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val kanban = dm.allTasks.value
|
||||
assertNotNull(kanban, "populated() must have a kanban board")
|
||||
val allTasks = kanban.columns.flatMap { it.tasks }
|
||||
assertEquals(8, allTasks.size, "populated() must surface exactly 8 tasks across columns")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populated_tasksAreDistributedAcrossExpectedColumns() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val kanban = dm.allTasks.value
|
||||
assertNotNull(kanban)
|
||||
val byColumn = kanban.columns.associate { it.name to it.count }
|
||||
// 2 overdue + 3 due-soon + 2 upcoming + 1 completed = 8
|
||||
assertEquals(2, byColumn["overdue_tasks"], "expected 2 overdue tasks")
|
||||
assertEquals(3, byColumn["due_soon_tasks"], "expected 3 due-soon tasks")
|
||||
assertEquals(2, byColumn["upcoming_tasks"], "expected 2 upcoming tasks")
|
||||
assertEquals(1, byColumn["completed_tasks"], "expected 1 completed task")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populated_hasExpectedContractorsAndDocuments() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
assertEquals(3, dm.contractors.value.size, "populated() must have 3 contractors")
|
||||
assertEquals(5, dm.documents.value.size, "populated() must have 5 documents")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populated_providesPremiumSubscriptionAndUser() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val sub = dm.subscription.value
|
||||
assertNotNull(sub)
|
||||
assertEquals("pro", sub.tier, "populated() should render on the premium tier")
|
||||
val user = dm.currentUser.value
|
||||
assertNotNull(user, "populated() must have a current user for profile screens")
|
||||
}
|
||||
|
||||
// ==================== STRUCTURAL INVARIANTS ====================
|
||||
|
||||
@Test
|
||||
fun populatedIdsAreUniqueAcrossCollections() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val residenceIds = dm.residences.value.map { it.id }
|
||||
assertEquals(residenceIds.size, residenceIds.toSet().size, "residence ids must be unique")
|
||||
|
||||
val allTasks = dm.allTasks.value?.columns?.flatMap { it.tasks }.orEmpty()
|
||||
val taskIds = allTasks.map { it.id }
|
||||
assertEquals(taskIds.size, taskIds.toSet().size, "task ids must be unique")
|
||||
|
||||
val contractorIds = dm.contractors.value.map { it.id }
|
||||
assertEquals(contractorIds.size, contractorIds.toSet().size, "contractor ids must be unique")
|
||||
|
||||
val documentIds = dm.documents.value.mapNotNull { it.id }
|
||||
assertEquals(documentIds.size, documentIds.toSet().size, "document ids must be unique")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populatedTasksReferenceExistingResidences() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val residenceIds = dm.residences.value.map { it.id }.toSet()
|
||||
val allTasks = dm.allTasks.value?.columns?.flatMap { it.tasks }.orEmpty()
|
||||
allTasks.forEach { task ->
|
||||
assertTrue(
|
||||
residenceIds.contains(task.residenceId),
|
||||
"task id=${task.id} references residence id=${task.residenceId} which isn't in the fixture residence list",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populatedDocumentsReferenceExistingResidences() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val residenceIds = dm.residences.value.map { it.id }.toSet()
|
||||
dm.documents.value.forEach { doc ->
|
||||
val rid = doc.residenceId ?: doc.residence
|
||||
assertTrue(
|
||||
residenceIds.contains(rid),
|
||||
"document id=${doc.id} references residence id=$rid which isn't in the fixture residence list",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populatedLookupHelpersResolveIds() {
|
||||
// InMemoryDataManager.getTaskPriority/Category/Frequency must resolve
|
||||
// ids that appear on populated() tasks — otherwise task cards render
|
||||
// with null categories/priorities, which breaks the parity gallery.
|
||||
val dm = FixtureDataManager.populated()
|
||||
val allTasks = dm.allTasks.value?.columns?.flatMap { it.tasks }.orEmpty()
|
||||
allTasks.forEach { task ->
|
||||
task.categoryId?.let {
|
||||
assertNotNull(dm.getTaskCategory(it), "category id=$it must resolve on populated()")
|
||||
}
|
||||
task.priorityId?.let {
|
||||
assertNotNull(dm.getTaskPriority(it), "priority id=$it must resolve on populated()")
|
||||
}
|
||||
task.frequencyId?.let {
|
||||
assertNotNull(dm.getTaskFrequency(it), "frequency id=$it must resolve on populated()")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun populatedTotalSummaryMatchesTaskDistribution() {
|
||||
val dm = FixtureDataManager.populated()
|
||||
val summary = dm.totalSummary.value
|
||||
assertNotNull(summary)
|
||||
assertEquals(2, summary.totalResidences)
|
||||
assertEquals(8, summary.totalTasks)
|
||||
// 8 total - 1 completed = 7 pending (overdue + due_soon + upcoming)
|
||||
assertEquals(7, summary.totalPending)
|
||||
assertEquals(2, summary.totalOverdue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fixturesAreDeterministic() {
|
||||
// Two calls to populated() should produce identical data.
|
||||
val a = FixtureDataManager.populated()
|
||||
val b = FixtureDataManager.populated()
|
||||
assertEquals(a.residences.value.map { it.id }, b.residences.value.map { it.id })
|
||||
assertEquals(
|
||||
a.allTasks.value?.columns?.flatMap { it.tasks }?.map { it.id },
|
||||
b.allTasks.value?.columns?.flatMap { it.tasks }?.map { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fixedDateIsStable() {
|
||||
// The parity gallery's determinism depends on a fixed clock. If a
|
||||
// refactor accidentally swaps Fixtures.FIXED_DATE for Clock.System.now(),
|
||||
// snapshot tests will go red every day — catch it here first.
|
||||
assertEquals("2026-04-15", Fixtures.FIXED_DATE.toString())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user