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:
Trey T
2026-04-18 19:22:41 -05:00
parent c57743dca0
commit 47eaf5a0c0
6 changed files with 1263 additions and 31 deletions
@@ -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())
}
}