P4 Stream P: NotificationPreferencesScreen expansion
Per-category toggle + master toggle + system-settings shortcut matching iOS NotificationPreferencesView depth. DataStore-backed prefs, channel importance rewritten to NONE when category disabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
package com.tt.honeyDue.ui.screens
|
||||
|
||||
import com.tt.honeyDue.viewmodel.NotificationCategoriesController
|
||||
import com.tt.honeyDue.viewmodel.NotificationCategoryKeys
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* P4 Stream P — state-logic tests for the per-category notification UI.
|
||||
*
|
||||
* These mirror the iOS per-category toggles in
|
||||
* `iosApp/iosApp/Profile/NotificationPreferencesView.swift` and exercise
|
||||
* [NotificationCategoriesController] — a commonMain façade used by
|
||||
* [NotificationPreferencesScreen] that delegates persistence to the
|
||||
* Android DataStore-backed `NotificationPreferencesStore` in production
|
||||
* and to a fake in these tests.
|
||||
*
|
||||
* We use plain kotlin.test here (no Compose UI testing) for the same
|
||||
* reasons noted in ThemeSelectionScreenTest / FeatureComparisonScreenTest
|
||||
* — the commonTest recomposer+Dispatchers interplay is flaky on
|
||||
* iosSimulator.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationPreferencesScreenTest {
|
||||
|
||||
/** In-memory stand-in for NotificationPreferencesStore. */
|
||||
private class FakeStore(
|
||||
initial: Map<String, Boolean> = NotificationCategoryKeys.ALL.associateWith { true },
|
||||
) {
|
||||
val state = initial.toMutableMap()
|
||||
val setCategoryCalls: MutableList<Pair<String, Boolean>> = mutableListOf()
|
||||
val setAllCalls: MutableList<Boolean> = mutableListOf()
|
||||
|
||||
suspend fun setCategoryEnabled(id: String, enabled: Boolean) {
|
||||
setCategoryCalls += (id to enabled)
|
||||
state[id] = enabled
|
||||
}
|
||||
|
||||
suspend fun setAllEnabled(enabled: Boolean) {
|
||||
setAllCalls += enabled
|
||||
NotificationCategoryKeys.ALL.forEach { state[it] = enabled }
|
||||
}
|
||||
|
||||
suspend fun loadAll(): Map<String, Boolean> =
|
||||
NotificationCategoryKeys.ALL.associateWith { state[it] ?: true }
|
||||
}
|
||||
|
||||
private fun controllerFor(store: FakeStore) = NotificationCategoriesController(
|
||||
loadAll = { store.loadAll() },
|
||||
setCategory = { id, v -> store.setCategoryEnabled(id, v) },
|
||||
setAll = { v -> store.setAllEnabled(v) },
|
||||
)
|
||||
|
||||
@Test
|
||||
fun categoryKeys_matchNotificationChannels() {
|
||||
// Parity guard: if NotificationChannels ever adds/removes a channel,
|
||||
// the category keys used by this screen must update in lockstep.
|
||||
assertEquals(
|
||||
listOf("task_reminder", "task_overdue", "residence_invite", "subscription"),
|
||||
NotificationCategoryKeys.ALL,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialState_loadsAllCategoriesEnabled() = runTest {
|
||||
val store = FakeStore()
|
||||
val controller = controllerFor(store)
|
||||
val snapshot = controller.load()
|
||||
|
||||
assertEquals(4, snapshot.size)
|
||||
snapshot.values.forEach { assertTrue(it, "Every category starts enabled") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toggleCategory_invokesSetCategoryEnabled() = runTest {
|
||||
val store = FakeStore()
|
||||
val controller = controllerFor(store)
|
||||
|
||||
controller.onCategoryToggle("task_reminder", false)
|
||||
|
||||
assertEquals(1, store.setCategoryCalls.size)
|
||||
assertEquals("task_reminder" to false, store.setCategoryCalls[0])
|
||||
assertFalse(store.state["task_reminder"]!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toggleDifferentCategories_isolatesUpdates() = runTest {
|
||||
val store = FakeStore()
|
||||
val controller = controllerFor(store)
|
||||
|
||||
controller.onCategoryToggle("task_overdue", false)
|
||||
controller.onCategoryToggle("subscription", false)
|
||||
|
||||
assertFalse(store.state["task_overdue"]!!)
|
||||
assertFalse(store.state["subscription"]!!)
|
||||
// Untouched categories remain enabled
|
||||
assertTrue(store.state["task_reminder"]!!)
|
||||
assertTrue(store.state["residence_invite"]!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun masterToggle_off_invokesSetAllEnabledFalse() = runTest {
|
||||
val store = FakeStore()
|
||||
val controller = controllerFor(store)
|
||||
|
||||
controller.onMasterToggle(false)
|
||||
|
||||
assertEquals(listOf(false), store.setAllCalls)
|
||||
assertFalse(store.state["task_reminder"]!!)
|
||||
assertFalse(store.state["task_overdue"]!!)
|
||||
assertFalse(store.state["residence_invite"]!!)
|
||||
assertFalse(store.state["subscription"]!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun masterToggle_on_reenablesAllCategories() = runTest {
|
||||
val store = FakeStore(
|
||||
initial = mapOf(
|
||||
"task_reminder" to false,
|
||||
"task_overdue" to false,
|
||||
"residence_invite" to false,
|
||||
"subscription" to false,
|
||||
),
|
||||
)
|
||||
val controller = controllerFor(store)
|
||||
|
||||
controller.onMasterToggle(true)
|
||||
|
||||
assertEquals(listOf(true), store.setAllCalls)
|
||||
NotificationCategoryKeys.ALL.forEach { id ->
|
||||
assertTrue(store.state[id]!!, "Category $id should be re-enabled")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeMasterState_trueWhenAllEnabled_falseOtherwise() {
|
||||
val allOn = mapOf(
|
||||
"task_reminder" to true,
|
||||
"task_overdue" to true,
|
||||
"residence_invite" to true,
|
||||
"subscription" to true,
|
||||
)
|
||||
val oneOff = allOn.toMutableMap().apply { put("task_overdue", false) }
|
||||
|
||||
assertTrue(NotificationCategoriesController.computeMasterState(allOn))
|
||||
assertFalse(NotificationCategoriesController.computeMasterState(oneOff))
|
||||
assertFalse(NotificationCategoriesController.computeMasterState(emptyMap()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user