From e1029dc497682099908e1ea1b017544eb71647e1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 29 May 2022 16:10:00 +0100 Subject: [PATCH] adding tests around notification rendering --- .../kotlin/app/dapk/st/graph/AppModule.kt | 1 + .../app/dapk/st/core/CoroutineDispatchers.kt | 6 +- .../fixture/CoroutineDispatchersFixture.kt | 14 +++ .../kotlin/test/ExpectTestScope.kt | 1 - .../kotlin/fake/FakeNotification.kt | 12 +++ .../kotlin/fake/FakeNotificationManager.kt | 14 +++ features/notifications/build.gradle | 1 + .../st/notifications/NotificationRenderer.kt | 4 +- .../st/notifications/NotificationsModule.kt | 4 +- .../notifications/NotificationRendererTest.kt | 85 +++++++++++++++++++ .../kotlin/fake/FakeNotificationFactory.kt | 19 +++++ .../kotlin/fixture/NotificationFixtures.kt | 12 +++ .../src/test/kotlin/test/TestMatrix.kt | 6 +- 13 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt create mode 100644 domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt create mode 100644 domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt create mode 100644 features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt create mode 100644 features/notifications/src/test/kotlin/fake/FakeNotificationFactory.kt create mode 100644 features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 2edcad5..164d4a9 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -174,6 +174,7 @@ internal class FeatureModules internal constructor( context, workModule.workScheduler(), intentFactory = coreAndroidModule.intentFactory(), + dispatchers = coroutineDispatchers, ) } diff --git a/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt b/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt index 14e8ca2..5281166 100644 --- a/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt +++ b/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt @@ -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 CoroutineDispatchers.withIoContext( block: suspend CoroutineScope.() -> T diff --git a/core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt b/core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt new file mode 100644 index 0000000..f7347c2 --- /dev/null +++ b/core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt @@ -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) + ) +} \ No newline at end of file diff --git a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt index 8da3051..98600e8 100644 --- a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt +++ b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt @@ -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 Unit>>() diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt new file mode 100644 index 0000000..a07820a --- /dev/null +++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt @@ -0,0 +1,12 @@ +package fake + +import android.app.Notification +import io.mockk.mockk + +class FakeNotification { + + val instance = mockk() + +} + +fun aFakeNotification() = FakeNotification().instance \ No newline at end of file diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt new file mode 100644 index 0000000..b8573e3 --- /dev/null +++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt @@ -0,0 +1,14 @@ +package fake + +import android.app.NotificationManager +import io.mockk.mockk +import io.mockk.verify + +class FakeNotificationManager { + + val instance = mockk() + + fun verifyCancelled(tag: String, id: Int) { + verify { instance.cancel(tag, id) } + } +} \ No newline at end of file diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index 087828e..13a9594 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -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")) } \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt index c21e004..68a498a 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt @@ -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>, removedRooms: Set, roomsWithNewEvents: Set, newRooms: Set) { 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) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt index c49181e..c861e60 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt @@ -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()), ) diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt new file mode 100644 index 0000000..8889a10 --- /dev/null +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt @@ -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 + ) +} + + diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationFactory.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationFactory.kt new file mode 100644 index 0000000..7afae99 --- /dev/null +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationFactory.kt @@ -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() + + fun givenNotifications(allUnread: Map>, roomsWithNewEvents: Set, newRooms: Set) = coEvery { + instance.createNotifications(allUnread, roomsWithNewEvents, newRooms) + }.delegateReturn() + +} \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt b/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt new file mode 100644 index 0000000..a34d0a1 --- /dev/null +++ b/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt @@ -0,0 +1,12 @@ +package app.dapk.st.notifications + +import android.app.Notification + +object NotificationFixtures { + + fun aNotifications( + summaryNotification: Notification? = null, + delegates: List = emptyList(), + ) = Notifications(summaryNotification, delegates) + +} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index 10c966e..9f775b3 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -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,