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:
@@ -0,0 +1,169 @@
|
||||
package com.tt.honeyDue.notifications
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.yield
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/**
|
||||
* P4 Stream P — tests for [NotificationPreferencesStore].
|
||||
*
|
||||
* Robolectric-backed because the store both reads/writes DataStore and
|
||||
* rewrites Android [android.app.NotificationChannel] importance when a
|
||||
* category toggle flips.
|
||||
*
|
||||
* Mirrors the iOS behaviour in
|
||||
* `iosApp/iosApp/Profile/NotificationPreferencesView.swift` where each
|
||||
* category toggle persists independently and a master switch can disable
|
||||
* everything in one tap.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
|
||||
class NotificationPreferencesStoreTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var store: NotificationPreferencesStore
|
||||
private lateinit var manager: NotificationManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
// Clean slate
|
||||
manager.notificationChannels.forEach { manager.deleteNotificationChannel(it.id) }
|
||||
NotificationChannels.ensureChannels(context)
|
||||
store = NotificationPreferencesStore(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() = runTest {
|
||||
store.clearAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaults_allCategoriesEnabled() = runTest {
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.TASK_REMINDER))
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.TASK_OVERDUE))
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.RESIDENCE_INVITE))
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.SUBSCRIPTION))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaults_masterToggleEnabled() = runTest {
|
||||
assertTrue(store.isAllEnabled())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setCategoryEnabled_false_persists() = runTest {
|
||||
store.setCategoryEnabled(NotificationChannels.TASK_REMINDER, false)
|
||||
assertFalse(store.isCategoryEnabled(NotificationChannels.TASK_REMINDER))
|
||||
// Other categories untouched
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.TASK_OVERDUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setCategoryEnabled_roundtrip_trueThenFalseThenTrue() = runTest {
|
||||
val id = NotificationChannels.TASK_OVERDUE
|
||||
store.setCategoryEnabled(id, false)
|
||||
assertFalse(store.isCategoryEnabled(id))
|
||||
store.setCategoryEnabled(id, true)
|
||||
assertTrue(store.isCategoryEnabled(id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAllEnabled_false_disablesEveryCategory() = runTest {
|
||||
store.setAllEnabled(false)
|
||||
assertFalse(store.isAllEnabled())
|
||||
assertFalse(store.isCategoryEnabled(NotificationChannels.TASK_REMINDER))
|
||||
assertFalse(store.isCategoryEnabled(NotificationChannels.TASK_OVERDUE))
|
||||
assertFalse(store.isCategoryEnabled(NotificationChannels.RESIDENCE_INVITE))
|
||||
assertFalse(store.isCategoryEnabled(NotificationChannels.SUBSCRIPTION))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAllEnabled_true_reenablesEveryCategory() = runTest {
|
||||
store.setAllEnabled(false)
|
||||
store.setAllEnabled(true)
|
||||
assertTrue(store.isAllEnabled())
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.TASK_REMINDER))
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.TASK_OVERDUE))
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.RESIDENCE_INVITE))
|
||||
assertTrue(store.isCategoryEnabled(NotificationChannels.SUBSCRIPTION))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun observePreferences_emitsInitialSnapshot() = runTest {
|
||||
val snapshot = store.observePreferences().first()
|
||||
assertEquals(true, snapshot[NotificationChannels.TASK_REMINDER])
|
||||
assertEquals(true, snapshot[NotificationChannels.TASK_OVERDUE])
|
||||
assertEquals(true, snapshot[NotificationChannels.RESIDENCE_INVITE])
|
||||
assertEquals(true, snapshot[NotificationChannels.SUBSCRIPTION])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun observePreferences_emitsUpdatesOnChange() = runTest {
|
||||
// Collect first two distinct emissions: the initial snapshot and the
|
||||
// update produced by flipping TASK_OVERDUE.
|
||||
val collected = mutableListOf<Map<String, Boolean>>()
|
||||
val job = launch {
|
||||
store.observePreferences().take(2).toList(collected)
|
||||
}
|
||||
// Let the first emission land, then flip the flag.
|
||||
yield()
|
||||
store.setCategoryEnabled(NotificationChannels.TASK_OVERDUE, false)
|
||||
job.join()
|
||||
|
||||
assertEquals(2, collected.size)
|
||||
assertEquals(true, collected[0][NotificationChannels.TASK_OVERDUE])
|
||||
assertEquals(false, collected[1][NotificationChannels.TASK_OVERDUE])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setCategoryEnabled_false_rewritesChannelImportanceToNone() = runTest {
|
||||
// Precondition: TASK_REMINDER was created with IMPORTANCE_DEFAULT.
|
||||
val before = manager.getNotificationChannel(NotificationChannels.TASK_REMINDER)
|
||||
assertEquals(NotificationManager.IMPORTANCE_DEFAULT, before.importance)
|
||||
|
||||
store.setCategoryEnabled(NotificationChannels.TASK_REMINDER, false)
|
||||
|
||||
val after = manager.getNotificationChannel(NotificationChannels.TASK_REMINDER)
|
||||
assertEquals(NotificationManager.IMPORTANCE_NONE, after.importance)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAllEnabled_false_silencesAllChannels() = runTest {
|
||||
store.setAllEnabled(false)
|
||||
|
||||
listOf(
|
||||
NotificationChannels.TASK_REMINDER,
|
||||
NotificationChannels.TASK_OVERDUE,
|
||||
NotificationChannels.RESIDENCE_INVITE,
|
||||
NotificationChannels.SUBSCRIPTION,
|
||||
).forEach { id ->
|
||||
val channel = manager.getNotificationChannel(id)
|
||||
assertEquals(
|
||||
"Channel $id should be IMPORTANCE_NONE after master toggle off",
|
||||
NotificationManager.IMPORTANCE_NONE,
|
||||
channel.importance,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user