adding tests around notification rendering

This commit is contained in:
Adam Brown 2022-05-29 16:10:00 +01:00
parent 4861c44e9c
commit e1029dc497
13 changed files with 174 additions and 5 deletions

View File

@ -174,6 +174,7 @@ internal class FeatureModules internal constructor(
context,
workModule.workScheduler(),
intentFactory = coreAndroidModule.intentFactory(),
dispatchers = coroutineDispatchers,
)
}

View File

@ -2,7 +2,11 @@ package app.dapk.st.core
import kotlinx.coroutines.*
data class CoroutineDispatchers(val io: CoroutineDispatcher = Dispatchers.IO, val global: CoroutineScope = GlobalScope)
data class CoroutineDispatchers(
val io: CoroutineDispatcher = Dispatchers.IO,
val main: CoroutineDispatcher = Dispatchers.Main,
val global: CoroutineScope = GlobalScope,
)
suspend fun <T> CoroutineDispatchers.withIoContext(
block: suspend CoroutineScope.() -> T

View File

@ -0,0 +1,14 @@
package fixture
import app.dapk.st.core.CoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
object CoroutineDispatchersFixture {
fun aCoroutineDispatchers() = CoroutineDispatchers(
Dispatchers.Unconfined,
main = Dispatchers.Unconfined,
global = CoroutineScope(Dispatchers.Unconfined)
)
}

View File

@ -12,7 +12,6 @@ fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
runTest { testBody(ExpectTest(coroutineContext)) }
}
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
private val expects = mutableListOf<Pair<Int, suspend MockKVerificationScope.() -> Unit>>()

View File

@ -0,0 +1,12 @@
package fake
import android.app.Notification
import io.mockk.mockk
class FakeNotification {
val instance = mockk<Notification>()
}
fun aFakeNotification() = FakeNotification().instance

View File

@ -0,0 +1,14 @@
package fake
import android.app.NotificationManager
import io.mockk.mockk
import io.mockk.verify
class FakeNotificationManager {
val instance = mockk<NotificationManager>()
fun verifyCancelled(tag: String, id: Int) {
verify { instance.cancel(tag, id) }
}
}

View File

@ -20,4 +20,5 @@ dependencies {
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
}

View File

@ -3,6 +3,7 @@ package app.dapk.st.notifications
import android.app.Notification
import android.app.NotificationManager
import app.dapk.st.core.AppLogTag
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.extensions.ifNull
import app.dapk.st.core.log
import app.dapk.st.matrix.common.RoomId
@ -17,13 +18,14 @@ private const val MESSAGE_NOTIFICATION_ID = 100
class NotificationRenderer(
private val notificationManager: NotificationManager,
private val notificationFactory: NotificationFactory,
private val dispatchers: CoroutineDispatchers,
) {
suspend fun render(allUnread: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>, roomsWithNewEvents: Set<RoomId>, newRooms: Set<RoomId>) {
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
val notifications = notificationFactory.createNotifications(allUnread, roomsWithNewEvents, newRooms)
withContext(Dispatchers.Main) {
withContext(dispatchers.main) {
notifications.summaryNotification.ifNull {
log(AppLogTag.NOTIFICATION, "cancelling summary")
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)

View File

@ -2,6 +2,7 @@ package app.dapk.st.notifications
import android.app.NotificationManager
import android.content.Context
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.ProvidableModule
import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.common.CredentialsStore
@ -22,6 +23,7 @@ class NotificationsModule(
private val context: Context,
private val workScheduler: WorkScheduler,
private val intentFactory: IntentFactory,
private val dispatchers: CoroutineDispatchers,
) : ProvidableModule {
fun pushUseCase() = pushService
@ -30,7 +32,7 @@ class NotificationsModule(
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
fun roomStore() = roomStore
fun notificationsUseCase() = NotificationsUseCase(
NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context, intentFactory)),
NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context, intentFactory), dispatchers),
ObserveUnreadNotificationsUseCaseImpl(roomStore),
NotificationChannels(notificationManager()),
)

View File

@ -0,0 +1,85 @@
package app.dapk.st.notifications
import app.dapk.st.notifications.NotificationFixtures.aNotifications
import fake.FakeNotificationFactory
import fake.FakeNotificationManager
import fake.aFakeNotification
import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers
import fixture.aRoomId
import org.junit.Test
import test.expect
import test.runExpectTest
private const val SUMMARY_ID = 101
private const val ROOM_MESSAGE_ID = 100
private val A_SUMMARY_NOTIFICATION = aFakeNotification()
class NotificationRendererTest {
private val fakeNotificationManager = FakeNotificationManager()
private val fakeNotificationFactory = FakeNotificationFactory()
private val notificationRenderer = NotificationRenderer(
fakeNotificationManager.instance,
fakeNotificationFactory.instance,
aCoroutineDispatchers()
)
@Test
fun `given removed rooms when rendering then cancels notifications with cancelled room ids`() = runExpectTest {
val removedRooms = setOf(aRoomId("id-1"), aRoomId("id-2"))
fakeNotificationFactory.instance.expect { it.createNotifications(emptyMap(), emptySet(), emptySet()) }
fakeNotificationManager.instance.expectUnit {
removedRooms.forEach { removedRoom -> it.cancel(removedRoom.value, ROOM_MESSAGE_ID) }
}
notificationRenderer.render(emptyMap(), removedRooms, emptySet(), emptySet())
verifyExpects()
}
@Test
fun `given summary notification is not created, when rendering, then cancels summary notification`() = runExpectTest {
fakeNotificationFactory.givenNotifications(emptyMap(), emptySet(), emptySet()).returns(aNotifications(summaryNotification = null))
fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) }
notificationRenderer.render(emptyMap(), emptySet(), emptySet(), emptySet())
verifyExpects()
}
@Test
fun `given update is only removals, when rendering, then only renders room dismiss`() = runExpectTest {
fakeNotificationFactory.givenNotifications(emptyMap(), emptySet(), emptySet()).returns(aNotifications(summaryNotification = null))
fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) }
notificationRenderer.render(emptyMap(), emptySet(), emptySet(), emptySet())
verifyExpects()
}
@Test
fun `given rooms with events, when rendering, then notifies summary and new rooms`() = runExpectTest {
val roomNotification = aRoomNotification()
val roomsWithNewEvents = setOf(roomNotification.roomId)
fakeNotificationFactory.givenNotifications(emptyMap(), roomsWithNewEvents, emptySet()).returns(
aNotifications(summaryNotification = A_SUMMARY_NOTIFICATION, delegates = listOf(roomNotification))
)
fakeNotificationManager.instance.expectUnit { it.notify(SUMMARY_ID, A_SUMMARY_NOTIFICATION) }
fakeNotificationManager.instance.expectUnit { it.notify(roomNotification.roomId.value, ROOM_MESSAGE_ID, roomNotification.notification) }
notificationRenderer.render(emptyMap(), emptySet(), roomsWithNewEvents, emptySet())
verifyExpects()
}
private fun aRoomNotification() = NotificationDelegate.Room(
aFakeNotification(),
aRoomId(),
"a summary line",
messageCount = 1,
isAlerting = false
)
}

View File

@ -0,0 +1,19 @@
package fake
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.notifications.NotificationFactory
import io.mockk.coEvery
import io.mockk.mockk
import test.delegateReturn
class FakeNotificationFactory {
val instance = mockk<NotificationFactory>()
fun givenNotifications(allUnread: Map<RoomOverview, List<RoomEvent>>, roomsWithNewEvents: Set<RoomId>, newRooms: Set<RoomId>) = coEvery {
instance.createNotifications(allUnread, roomsWithNewEvents, newRooms)
}.delegateReturn()
}

View File

@ -0,0 +1,12 @@
package app.dapk.st.notifications
import android.app.Notification
object NotificationFixtures {
fun aNotifications(
summaryNotification: Notification? = null,
delegates: List<NotificationDelegate> = emptyList(),
) = Notifications(summaryNotification, delegates)
}

View File

@ -58,7 +58,11 @@ class TestMatrix(
private val preferences = InMemoryPreferences()
private val database = InMemoryDatabase.realInstance(user.roomMember.id.value)
private val coroutineDispatchers = CoroutineDispatchers(Dispatchers.Unconfined, CoroutineScope(Dispatchers.Unconfined))
private val coroutineDispatchers = CoroutineDispatchers(
Dispatchers.Unconfined,
main = Dispatchers.Unconfined,
global = CoroutineScope(Dispatchers.Unconfined)
)
private val storeModule = StoreModule(
database = database,