Skip to content

Commit

Permalink
Refactor NotificationPrompt
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-paing committed Oct 10, 2023
1 parent b58531a commit d70a724
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 123 deletions.
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ dependencies {
testImplementation(libs.junit.jupiter.params)
testImplementation(libs.junit.junit4)
testRuntimeOnly(libs.junit.jupiter.vintageEngine)
androidTestImplementation(libs.androidJunit5.compose)
androidTestImplementation(libs.junit.jupiter.api)
androidTestImplementation(libs.androidJunit5.core)
androidTestRuntimeOnly(libs.androidJunit5.runner)

testImplementation(libs.robolectric)
Expand All @@ -244,6 +244,8 @@ dependencies {
testImplementation(libs.mockk)
testImplementation(libs.mockk.agentJvm)
androidTestImplementation(libs.mockk.android)

testImplementation(libs.turbine)
}

androidComponents {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package dev.aungkyawpaing.ccdroidx.feature.notification.prompt
import android.content.Context
import android.content.Intent
import android.provider.Settings
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
Expand All @@ -13,93 +13,122 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag
import de.mannodermaus.junit5.compose.createComposeExtension
import dev.aungkyawpaing.ccdroidx.R
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.test.runTest
import org.hamcrest.CoreMatchers.allOf
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

@ExperimentalTestApi
class NotificationPromptTest {

@get:Rule
val composeTestRule = createComposeRule()
@JvmField
@RegisterExtension
val extension = createComposeExtension()

val notificationPromptViewModel: NotificationPromptViewModel = mockk(relaxed = true)

private val notificationPromptText = ApplicationProvider.getApplicationContext<Context>()
.getString(R.string.notification_prompt_body)

@Test
@DisplayName("does not render Notification Prompt Card when prompt should not be visible")
fun doesNotRenderNotificationPromptCardWhenPromptIsNotVisible() {
composeTestRule.setContent {
NotificationPrompt(
false,
{}
)
fun doesNotRenderNotificationPromptCardWhenPromptIsNotVisible() = runTest {
every {
notificationPromptViewModel.promptIsVisible
} returns flowOf(false).stateIn(this)

extension.use {
setContent {
NotificationPrompt(
notificationPromptViewModel
)
}

onNodeWithText(notificationPromptText).assertDoesNotExist()
}

composeTestRule.onNodeWithText(notificationPromptText).assertDoesNotExist()
}

@Test
@DisplayName("render Notification Prompt Card when prompt should be visible")
fun renderNotificationCardWhenPromptIsVisible() {
composeTestRule.setContent {
NotificationPrompt(
true,
{}
)
}

composeTestRule.onNodeWithText(notificationPromptText).assertIsDisplayed()
composeTestRule.onNodeWithText("ENABLE NOTIFICATION").assertIsDisplayed()
}
@Nested
@DisplayName("When prompt should be visible")
internal inner class WhenPromptIsVisible {

@Test
@DisplayName("invoke onDismissClick on clicking dismiss")
fun invokeOnDismissClickOnClickingDismiss() {
val onDismissPrompt = mockk<() -> Unit>(relaxed = true)
composeTestRule.setContent {
NotificationPrompt(
true,
onDismissPrompt
)
@BeforeEach
fun setUp() = runTest {
every {
notificationPromptViewModel.promptIsVisible
} returns flowOf(true).stateIn(this)
}

val contentDescription = ApplicationProvider.getApplicationContext<Context>()
.getString(R.string.notification_prompt_close_content_description)
composeTestRule.onNodeWithContentDescription(contentDescription).assertIsDisplayed()
composeTestRule.onNodeWithContentDescription(contentDescription).performClick()

verify(exactly = 1) {
onDismissPrompt()
@Test
@DisplayName("render Notification Prompt Card ")
fun renderNotificationCardWhenPromptIsVisible() {
extension.use {
setContent {
NotificationPrompt(
notificationPromptViewModel
)
}

onNodeWithText(notificationPromptText).assertIsDisplayed()
}
}
}

@Test
@DisplayName("invoke onEnableNotification on clicking enable notification")
fun invokeOnEnableNotificationOnClickingEnableNotification() {
val context = ApplicationProvider.getApplicationContext<Context>()
Intents.init()

composeTestRule.setContent {
NotificationPrompt(
true,
{}
)
@Test
@DisplayName("invoke onDismissClick on clicking dismiss")
fun invokeOnDismissClickOnClickingDismiss() {
extension.use {
setContent {
NotificationPrompt(
notificationPromptViewModel
)
}

val contentDescription = ApplicationProvider.getApplicationContext<Context>()
.getString(R.string.notification_prompt_close_content_description)
onNodeWithContentDescription(contentDescription).assertIsDisplayed()
onNodeWithContentDescription(contentDescription).performClick()
}

verify(exactly = 1) {
notificationPromptViewModel.onDismissClick()
}
}

composeTestRule.onNodeWithText("ENABLE NOTIFICATION").performClick()

Intents.intended(
allOf(
hasAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS),
hasExtra(Settings.EXTRA_APP_PACKAGE, context.packageName),
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
@Test
@DisplayName("open notification settings on clicking enable notification")
fun invokeOnEnableNotificationOnClickingEnableNotification() {
val context = ApplicationProvider.getApplicationContext<Context>()
Intents.init()

extension.use {
setContent {
NotificationPrompt(
notificationPromptViewModel
)
}

onNodeWithText("ENABLE NOTIFICATION").performClick()
}

Intents.intended(
allOf(
hasAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS),
hasExtra(Settings.EXTRA_APP_PACKAGE, context.packageName),
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
)
)
)

Intents.release()
Intents.release()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
Expand All @@ -33,7 +35,7 @@ import com.google.accompanist.themeadapter.material3.Mdc3Theme
import dev.aungkyawpaing.ccdroidx.R

@Composable
private fun NotificationPromptCard(
fun NotificationPromptContent(
onDismissPrompt: () -> Unit,
onEnableNotification: () -> Unit
) {
Expand Down Expand Up @@ -100,15 +102,14 @@ private fun NotificationPromptCard(

@Composable
fun NotificationPrompt(
isNotificationPromptVisible: Boolean,
onDismissNotificationPrompt: () -> Unit,
notificationPromptViewModel: NotificationPromptViewModel,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current

Box(modifier = modifier) {
AnimatedVisibility(
visible = isNotificationPromptVisible,
visible = notificationPromptViewModel.promptIsVisible.collectAsState(false).value,
enter = fadeIn() + slideInVertically(
initialOffsetY = {
it / 2
Expand All @@ -120,22 +121,29 @@ fun NotificationPrompt(
},
)
) {
NotificationPromptCard(onDismissPrompt = onDismissNotificationPrompt, onEnableNotification = {
kotlin.runCatching {
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
context.startActivity(settingsIntent)
}
})
NotificationPromptContent(
onDismissPrompt = notificationPromptViewModel::onDismissClick,
onEnableNotification = {
kotlin.runCatching {
val settingsIntent: Intent =
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
context.startActivity(settingsIntent)
}
})
}
}
}

@Preview
@Preview(
name = "Phone", device = Devices.PHONE
)
@Preview(
name = "Tablet", device = Devices.TABLET
)
@Composable
fun NotificationPromptPreview() {
Mdc3Theme {
NotificationPrompt(isNotificationPromptVisible = true, {})
NotificationPromptContent({}, {})
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package dev.aungkyawpaing.ccdroidx.feature.notification.prompt

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.aungkyawpaing.ccdroidx.data.ProjectRepo
import dev.aungkyawpaing.ccdroidx.feature.notification.prompt.permssionflow.NotificationPermissionFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.time.Clock
import java.time.LocalDateTime
Expand All @@ -22,7 +23,7 @@ class NotificationPromptViewModel @Inject constructor(
private val clock: Clock
) : ViewModel() {

val promptIsVisibleLiveData: LiveData<Boolean> = combine(
val promptIsVisible: StateFlow<Boolean> = combine(
projectRepo.getAll(),
notificationDismissStore.getDismissTimeStamp(),
notificationsPermissionFlow.getFlow(),
Expand All @@ -35,7 +36,7 @@ class NotificationPromptViewModel @Inject constructor(
.isAfter(dismissTimeStamp)

return@map thereIsAtLeastOneProject && lastDismissTimeNotWithin14Days && !isPermissionGranted
}.asLiveData()
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)

fun onDismissClick() {
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ fun ProjectListPageContent(
onPressSync: () -> Unit,
clearOnProgressSyncedEvent: () -> Unit,
onDeleteProject: (project: Project) -> Unit,
isNotificationPromptVisible: Boolean,
onDismissNotificationPrompt: () -> Unit,
notificationPromptViewModel: NotificationPromptViewModel,
navigator: DestinationsNavigator,
clock: Clock = Clock.systemDefaultZone()
) {
Expand Down Expand Up @@ -225,8 +224,7 @@ fun ProjectListPageContent(
)

NotificationPrompt(
isNotificationPromptVisible = isNotificationPromptVisible,
onDismissNotificationPrompt = onDismissNotificationPrompt,
notificationPromptViewModel = notificationPromptViewModel,
modifier = Modifier.constrainAs(notificationPrompt) {
end.linkTo(parent.end)
start.linkTo(parent.start)
Expand Down Expand Up @@ -278,10 +276,7 @@ fun ProjectListPage(
clearOnProgressSyncedEvent = projectListViewModel::clearOnProgressSyncedEvent,
onPressSync = projectListViewModel::onPressSync,
onDeleteProject = projectListViewModel::onDeleteProject,
isNotificationPromptVisible = notificationPromptViewModel.promptIsVisibleLiveData.observeAsState(
initial = false
).value,
onDismissNotificationPrompt = notificationPromptViewModel::onDismissClick,
notificationPromptViewModel = notificationPromptViewModel,
navigator = navigator
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
Expand All @@ -23,7 +27,7 @@ import org.junit.jupiter.params.provider.EnumSource
@ExtendWith(InstantTaskExecutorExtension::class)
class AddProjectViewModelTest : CoroutineTest() {

private val projectRepo = mockk<ProjectRepo>()
private val projectRepo = mockk<ProjectRepo>(relaxed = true)
private val mockValidator = mockk<AddProjectInputValidator>()

private val viewModel =
Expand Down
Loading

0 comments on commit d70a724

Please sign in to comment.