Skip to content

Commit d70a724

Browse files
committed
Refactor NotificationPrompt
1 parent b58531a commit d70a724

15 files changed

+209
-123
lines changed

app/build.gradle.kts

+3-1
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ dependencies {
219219
testImplementation(libs.junit.jupiter.params)
220220
testImplementation(libs.junit.junit4)
221221
testRuntimeOnly(libs.junit.jupiter.vintageEngine)
222+
androidTestImplementation(libs.androidJunit5.compose)
222223
androidTestImplementation(libs.junit.jupiter.api)
223-
androidTestImplementation(libs.androidJunit5.core)
224224
androidTestRuntimeOnly(libs.androidJunit5.runner)
225225

226226
testImplementation(libs.robolectric)
@@ -244,6 +244,8 @@ dependencies {
244244
testImplementation(libs.mockk)
245245
testImplementation(libs.mockk.agentJvm)
246246
androidTestImplementation(libs.mockk.android)
247+
248+
testImplementation(libs.turbine)
247249
}
248250

249251
androidComponents {

app/src/androidTest/kotlin/dev/aungkyawpaing/ccdroidx/feature/notification/prompt/NotificationPromptTest.kt

+92-63
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package dev.aungkyawpaing.ccdroidx.feature.notification.prompt
33
import android.content.Context
44
import android.content.Intent
55
import android.provider.Settings
6+
import androidx.compose.ui.test.ExperimentalTestApi
67
import androidx.compose.ui.test.assertIsDisplayed
7-
import androidx.compose.ui.test.junit4.createComposeRule
88
import androidx.compose.ui.test.onNodeWithContentDescription
99
import androidx.compose.ui.test.onNodeWithText
1010
import androidx.compose.ui.test.performClick
@@ -13,93 +13,122 @@ import androidx.test.espresso.intent.Intents
1313
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
1414
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
1515
import androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag
16+
import de.mannodermaus.junit5.compose.createComposeExtension
1617
import dev.aungkyawpaing.ccdroidx.R
18+
import io.mockk.every
1719
import io.mockk.mockk
1820
import io.mockk.verify
21+
import kotlinx.coroutines.flow.flowOf
22+
import kotlinx.coroutines.flow.stateIn
23+
import kotlinx.coroutines.test.runTest
1924
import org.hamcrest.CoreMatchers.allOf
20-
import org.junit.Rule
21-
import org.junit.Test
25+
import org.junit.jupiter.api.BeforeEach
2226
import org.junit.jupiter.api.DisplayName
27+
import org.junit.jupiter.api.Nested
28+
import org.junit.jupiter.api.Test
29+
import org.junit.jupiter.api.extension.RegisterExtension
2330

31+
@ExperimentalTestApi
2432
class NotificationPromptTest {
2533

26-
@get:Rule
27-
val composeTestRule = createComposeRule()
34+
@JvmField
35+
@RegisterExtension
36+
val extension = createComposeExtension()
37+
38+
val notificationPromptViewModel: NotificationPromptViewModel = mockk(relaxed = true)
2839

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

3243
@Test
3344
@DisplayName("does not render Notification Prompt Card when prompt should not be visible")
34-
fun doesNotRenderNotificationPromptCardWhenPromptIsNotVisible() {
35-
composeTestRule.setContent {
36-
NotificationPrompt(
37-
false,
38-
{}
39-
)
45+
fun doesNotRenderNotificationPromptCardWhenPromptIsNotVisible() = runTest {
46+
every {
47+
notificationPromptViewModel.promptIsVisible
48+
} returns flowOf(false).stateIn(this)
49+
50+
extension.use {
51+
setContent {
52+
NotificationPrompt(
53+
notificationPromptViewModel
54+
)
55+
}
56+
57+
onNodeWithText(notificationPromptText).assertDoesNotExist()
4058
}
41-
42-
composeTestRule.onNodeWithText(notificationPromptText).assertDoesNotExist()
4359
}
4460

45-
@Test
46-
@DisplayName("render Notification Prompt Card when prompt should be visible")
47-
fun renderNotificationCardWhenPromptIsVisible() {
48-
composeTestRule.setContent {
49-
NotificationPrompt(
50-
true,
51-
{}
52-
)
53-
}
54-
55-
composeTestRule.onNodeWithText(notificationPromptText).assertIsDisplayed()
56-
composeTestRule.onNodeWithText("ENABLE NOTIFICATION").assertIsDisplayed()
57-
}
61+
@Nested
62+
@DisplayName("When prompt should be visible")
63+
internal inner class WhenPromptIsVisible {
5864

59-
@Test
60-
@DisplayName("invoke onDismissClick on clicking dismiss")
61-
fun invokeOnDismissClickOnClickingDismiss() {
62-
val onDismissPrompt = mockk<() -> Unit>(relaxed = true)
63-
composeTestRule.setContent {
64-
NotificationPrompt(
65-
true,
66-
onDismissPrompt
67-
)
65+
@BeforeEach
66+
fun setUp() = runTest {
67+
every {
68+
notificationPromptViewModel.promptIsVisible
69+
} returns flowOf(true).stateIn(this)
6870
}
6971

70-
val contentDescription = ApplicationProvider.getApplicationContext<Context>()
71-
.getString(R.string.notification_prompt_close_content_description)
72-
composeTestRule.onNodeWithContentDescription(contentDescription).assertIsDisplayed()
73-
composeTestRule.onNodeWithContentDescription(contentDescription).performClick()
74-
75-
verify(exactly = 1) {
76-
onDismissPrompt()
72+
@Test
73+
@DisplayName("render Notification Prompt Card ")
74+
fun renderNotificationCardWhenPromptIsVisible() {
75+
extension.use {
76+
setContent {
77+
NotificationPrompt(
78+
notificationPromptViewModel
79+
)
80+
}
81+
82+
onNodeWithText(notificationPromptText).assertIsDisplayed()
83+
}
7784
}
78-
}
7985

80-
@Test
81-
@DisplayName("invoke onEnableNotification on clicking enable notification")
82-
fun invokeOnEnableNotificationOnClickingEnableNotification() {
83-
val context = ApplicationProvider.getApplicationContext<Context>()
84-
Intents.init()
85-
86-
composeTestRule.setContent {
87-
NotificationPrompt(
88-
true,
89-
{}
90-
)
86+
@Test
87+
@DisplayName("invoke onDismissClick on clicking dismiss")
88+
fun invokeOnDismissClickOnClickingDismiss() {
89+
extension.use {
90+
setContent {
91+
NotificationPrompt(
92+
notificationPromptViewModel
93+
)
94+
}
95+
96+
val contentDescription = ApplicationProvider.getApplicationContext<Context>()
97+
.getString(R.string.notification_prompt_close_content_description)
98+
onNodeWithContentDescription(contentDescription).assertIsDisplayed()
99+
onNodeWithContentDescription(contentDescription).performClick()
100+
}
101+
102+
verify(exactly = 1) {
103+
notificationPromptViewModel.onDismissClick()
104+
}
91105
}
92106

93-
composeTestRule.onNodeWithText("ENABLE NOTIFICATION").performClick()
94-
95-
Intents.intended(
96-
allOf(
97-
hasAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS),
98-
hasExtra(Settings.EXTRA_APP_PACKAGE, context.packageName),
99-
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
107+
@Test
108+
@DisplayName("open notification settings on clicking enable notification")
109+
fun invokeOnEnableNotificationOnClickingEnableNotification() {
110+
val context = ApplicationProvider.getApplicationContext<Context>()
111+
Intents.init()
112+
113+
extension.use {
114+
setContent {
115+
NotificationPrompt(
116+
notificationPromptViewModel
117+
)
118+
}
119+
120+
onNodeWithText("ENABLE NOTIFICATION").performClick()
121+
}
122+
123+
Intents.intended(
124+
allOf(
125+
hasAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS),
126+
hasExtra(Settings.EXTRA_APP_PACKAGE, context.packageName),
127+
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
128+
)
100129
)
101-
)
102130

103-
Intents.release()
131+
Intents.release()
132+
}
104133
}
105134
}

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/notification/prompt/NotificationPrompt.kt

+22-14
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import androidx.compose.material3.MaterialTheme
2222
import androidx.compose.material3.Text
2323
import androidx.compose.material3.TextButton
2424
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.collectAsState
2526
import androidx.compose.ui.Modifier
2627
import androidx.compose.ui.platform.LocalContext
2728
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.tooling.preview.Devices
2830
import androidx.compose.ui.tooling.preview.Preview
2931
import androidx.compose.ui.unit.dp
3032
import androidx.constraintlayout.compose.ConstraintLayout
@@ -33,7 +35,7 @@ import com.google.accompanist.themeadapter.material3.Mdc3Theme
3335
import dev.aungkyawpaing.ccdroidx.R
3436

3537
@Composable
36-
private fun NotificationPromptCard(
38+
fun NotificationPromptContent(
3739
onDismissPrompt: () -> Unit,
3840
onEnableNotification: () -> Unit
3941
) {
@@ -100,15 +102,14 @@ private fun NotificationPromptCard(
100102

101103
@Composable
102104
fun NotificationPrompt(
103-
isNotificationPromptVisible: Boolean,
104-
onDismissNotificationPrompt: () -> Unit,
105+
notificationPromptViewModel: NotificationPromptViewModel,
105106
modifier: Modifier = Modifier,
106107
) {
107108
val context = LocalContext.current
108109

109110
Box(modifier = modifier) {
110111
AnimatedVisibility(
111-
visible = isNotificationPromptVisible,
112+
visible = notificationPromptViewModel.promptIsVisible.collectAsState(false).value,
112113
enter = fadeIn() + slideInVertically(
113114
initialOffsetY = {
114115
it / 2
@@ -120,22 +121,29 @@ fun NotificationPrompt(
120121
},
121122
)
122123
) {
123-
NotificationPromptCard(onDismissPrompt = onDismissNotificationPrompt, onEnableNotification = {
124-
kotlin.runCatching {
125-
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
126-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
127-
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
128-
context.startActivity(settingsIntent)
129-
}
130-
})
124+
NotificationPromptContent(
125+
onDismissPrompt = notificationPromptViewModel::onDismissClick,
126+
onEnableNotification = {
127+
kotlin.runCatching {
128+
val settingsIntent: Intent =
129+
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
130+
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
131+
context.startActivity(settingsIntent)
132+
}
133+
})
131134
}
132135
}
133136
}
134137

135-
@Preview
138+
@Preview(
139+
name = "Phone", device = Devices.PHONE
140+
)
141+
@Preview(
142+
name = "Tablet", device = Devices.TABLET
143+
)
136144
@Composable
137145
fun NotificationPromptPreview() {
138146
Mdc3Theme {
139-
NotificationPrompt(isNotificationPromptVisible = true, {})
147+
NotificationPromptContent({}, {})
140148
}
141149
}

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/notification/prompt/NotificationPromptViewModel.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package dev.aungkyawpaing.ccdroidx.feature.notification.prompt
22

3-
import androidx.lifecycle.LiveData
43
import androidx.lifecycle.ViewModel
5-
import androidx.lifecycle.asLiveData
64
import androidx.lifecycle.viewModelScope
75
import dagger.hilt.android.lifecycle.HiltViewModel
86
import dev.aungkyawpaing.ccdroidx.data.ProjectRepo
97
import dev.aungkyawpaing.ccdroidx.feature.notification.prompt.permssionflow.NotificationPermissionFlow
8+
import kotlinx.coroutines.flow.SharingStarted
9+
import kotlinx.coroutines.flow.StateFlow
1010
import kotlinx.coroutines.flow.combine
1111
import kotlinx.coroutines.flow.map
12+
import kotlinx.coroutines.flow.stateIn
1213
import kotlinx.coroutines.launch
1314
import java.time.Clock
1415
import java.time.LocalDateTime
@@ -22,7 +23,7 @@ class NotificationPromptViewModel @Inject constructor(
2223
private val clock: Clock
2324
) : ViewModel() {
2425

25-
val promptIsVisibleLiveData: LiveData<Boolean> = combine(
26+
val promptIsVisible: StateFlow<Boolean> = combine(
2627
projectRepo.getAll(),
2728
notificationDismissStore.getDismissTimeStamp(),
2829
notificationsPermissionFlow.getFlow(),
@@ -35,7 +36,7 @@ class NotificationPromptViewModel @Inject constructor(
3536
.isAfter(dismissTimeStamp)
3637

3738
return@map thereIsAtLeastOneProject && lastDismissTimeNotWithin14Days && !isPermissionGranted
38-
}.asLiveData()
39+
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
3940

4041
fun onDismissClick() {
4142
viewModelScope.launch {

app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/projectlist/ProjectListPage.kt

+3-8
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@ fun ProjectListPageContent(
168168
onPressSync: () -> Unit,
169169
clearOnProgressSyncedEvent: () -> Unit,
170170
onDeleteProject: (project: Project) -> Unit,
171-
isNotificationPromptVisible: Boolean,
172-
onDismissNotificationPrompt: () -> Unit,
171+
notificationPromptViewModel: NotificationPromptViewModel,
173172
navigator: DestinationsNavigator,
174173
clock: Clock = Clock.systemDefaultZone()
175174
) {
@@ -225,8 +224,7 @@ fun ProjectListPageContent(
225224
)
226225

227226
NotificationPrompt(
228-
isNotificationPromptVisible = isNotificationPromptVisible,
229-
onDismissNotificationPrompt = onDismissNotificationPrompt,
227+
notificationPromptViewModel = notificationPromptViewModel,
230228
modifier = Modifier.constrainAs(notificationPrompt) {
231229
end.linkTo(parent.end)
232230
start.linkTo(parent.start)
@@ -278,10 +276,7 @@ fun ProjectListPage(
278276
clearOnProgressSyncedEvent = projectListViewModel::clearOnProgressSyncedEvent,
279277
onPressSync = projectListViewModel::onPressSync,
280278
onDeleteProject = projectListViewModel::onDeleteProject,
281-
isNotificationPromptVisible = notificationPromptViewModel.promptIsVisibleLiveData.observeAsState(
282-
initial = false
283-
).value,
284-
onDismissNotificationPrompt = notificationPromptViewModel::onDismissClick,
279+
notificationPromptViewModel = notificationPromptViewModel,
285280
navigator = navigator
286281
)
287282
}

app/src/test/java/dev/aungkyawpaing/ccdroidx/feature/add/AddProjectViewModelTest.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import io.mockk.mockk
1414
import kotlinx.coroutines.ExperimentalCoroutinesApi
1515
import kotlinx.coroutines.test.runCurrent
1616
import kotlinx.coroutines.test.runTest
17-
import org.junit.jupiter.api.*
17+
import org.junit.jupiter.api.Assertions
18+
import org.junit.jupiter.api.BeforeEach
19+
import org.junit.jupiter.api.DisplayName
20+
import org.junit.jupiter.api.Nested
21+
import org.junit.jupiter.api.Test
1822
import org.junit.jupiter.api.extension.ExtendWith
1923
import org.junit.jupiter.params.ParameterizedTest
2024
import org.junit.jupiter.params.provider.EnumSource
@@ -23,7 +27,7 @@ import org.junit.jupiter.params.provider.EnumSource
2327
@ExtendWith(InstantTaskExecutorExtension::class)
2428
class AddProjectViewModelTest : CoroutineTest() {
2529

26-
private val projectRepo = mockk<ProjectRepo>()
30+
private val projectRepo = mockk<ProjectRepo>(relaxed = true)
2731
private val mockValidator = mockk<AddProjectInputValidator>()
2832

2933
private val viewModel =

0 commit comments

Comments
 (0)