Skip to content

Commit 40ec198

Browse files
nbradburyclaude
andcommitted
Add tests for ReaderSubscriptionSettingsViewModel
Tests cover: - start() initialization with and without existing subscription - onNotifyPostsToggled() success, failure, and no network cases - onEmailPostsToggled() success, failure, and no network cases - onEmailCommentsToggled() success, failure, and no network cases - Snackbar events for errors and no network - Cleanup calling useCase.cleanup() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1923323 commit 40ec198

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package org.wordpress.android.ui.reader.subscription
2+
3+
import kotlinx.coroutines.ExperimentalCoroutinesApi
4+
import kotlinx.coroutines.flow.first
5+
import org.assertj.core.api.Assertions.assertThat
6+
import org.junit.Before
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.mockito.Mock
10+
import org.mockito.junit.MockitoJUnitRunner
11+
import org.mockito.kotlin.verify
12+
import org.mockito.kotlin.whenever
13+
import org.wordpress.android.BaseUnitTest
14+
import org.wordpress.android.R
15+
import org.wordpress.android.fluxc.model.SubscriptionModel
16+
import org.wordpress.android.ui.pages.SnackbarMessageHolder
17+
import org.wordpress.android.ui.reader.subscription.ReaderBlogSubscriptionUseCase.UpdateResult
18+
import org.wordpress.android.ui.utils.UiString.UiStringRes
19+
import org.wordpress.android.viewmodel.Event
20+
21+
private const val BLOG_ID = 123L
22+
private const val BLOG_NAME = "Test Blog"
23+
private const val BLOG_URL = "https://test.wordpress.com"
24+
25+
@ExperimentalCoroutinesApi
26+
@RunWith(MockitoJUnitRunner::class)
27+
class ReaderSubscriptionSettingsViewModelTest : BaseUnitTest() {
28+
29+
@Mock
30+
lateinit var subscriptionUseCase: ReaderBlogSubscriptionUseCase
31+
32+
private lateinit var viewModel: ReaderSubscriptionSettingsViewModel
33+
34+
private var snackbarEvents: MutableList<Event<SnackbarMessageHolder>> = mutableListOf()
35+
36+
@Before
37+
fun setup() {
38+
viewModel = ReaderSubscriptionSettingsViewModel(
39+
subscriptionUseCase,
40+
testDispatcher()
41+
)
42+
43+
viewModel.snackbarEvents.observeForever { snackbarEvents.add(it) }
44+
}
45+
46+
// region start
47+
@Test
48+
fun `start initializes ui state with blog info`() = test {
49+
whenever(subscriptionUseCase.getSubscriptionForBlog(BLOG_ID)).thenReturn(null)
50+
51+
viewModel.start(BLOG_ID, BLOG_NAME, BLOG_URL)
52+
53+
val state = viewModel.uiState.first { it != null }
54+
assertThat(state?.blogId).isEqualTo(BLOG_ID)
55+
assertThat(state?.blogName).isEqualTo(BLOG_NAME)
56+
assertThat(state?.blogUrl).isEqualTo(BLOG_URL)
57+
}
58+
59+
@Test
60+
fun `start initializes ui state with subscription settings when subscription exists`() = test {
61+
val subscription = SubscriptionModel().apply {
62+
blogId = BLOG_ID.toString()
63+
shouldNotifyPosts = true
64+
shouldEmailPosts = true
65+
shouldEmailComments = false
66+
}
67+
whenever(subscriptionUseCase.getSubscriptionForBlog(BLOG_ID)).thenReturn(subscription)
68+
69+
viewModel.start(BLOG_ID, BLOG_NAME, BLOG_URL)
70+
71+
val state = viewModel.uiState.first { it != null }
72+
assertThat(state?.notifyPostsEnabled).isTrue()
73+
assertThat(state?.emailPostsEnabled).isTrue()
74+
assertThat(state?.emailCommentsEnabled).isFalse()
75+
}
76+
77+
@Test
78+
fun `start initializes ui state with default values when subscription not found`() = test {
79+
whenever(subscriptionUseCase.getSubscriptionForBlog(BLOG_ID)).thenReturn(null)
80+
81+
viewModel.start(BLOG_ID, BLOG_NAME, BLOG_URL)
82+
83+
val state = viewModel.uiState.first { it != null }
84+
assertThat(state?.notifyPostsEnabled).isFalse()
85+
assertThat(state?.emailPostsEnabled).isFalse()
86+
assertThat(state?.emailCommentsEnabled).isFalse()
87+
}
88+
// endregion
89+
90+
// region onNotifyPostsToggled
91+
@Test
92+
fun `onNotifyPostsToggled sets loading state`() = test {
93+
initializeViewModel()
94+
whenever(subscriptionUseCase.updateNotifyPosts(BLOG_ID, true)).thenReturn(UpdateResult.Success)
95+
96+
viewModel.onNotifyPostsToggled(true)
97+
98+
// After completion, loading should be false
99+
val state = viewModel.uiState.value
100+
assertThat(state?.isLoading).isFalse()
101+
}
102+
103+
@Test
104+
fun `onNotifyPostsToggled updates state on success`() = test {
105+
initializeViewModel()
106+
whenever(subscriptionUseCase.updateNotifyPosts(BLOG_ID, true)).thenReturn(UpdateResult.Success)
107+
108+
viewModel.onNotifyPostsToggled(true)
109+
110+
val state = viewModel.uiState.value
111+
assertThat(state?.notifyPostsEnabled).isTrue()
112+
assertThat(state?.isLoading).isFalse()
113+
}
114+
115+
@Test
116+
fun `onNotifyPostsToggled reverts state on failure`() = test {
117+
initializeViewModel(notifyPostsEnabled = false)
118+
whenever(subscriptionUseCase.updateNotifyPosts(BLOG_ID, true))
119+
.thenReturn(UpdateResult.Failure("Error"))
120+
121+
viewModel.onNotifyPostsToggled(true)
122+
123+
val state = viewModel.uiState.value
124+
assertThat(state?.notifyPostsEnabled).isFalse()
125+
}
126+
127+
@Test
128+
fun `onNotifyPostsToggled shows error snackbar on failure`() = test {
129+
initializeViewModel()
130+
whenever(subscriptionUseCase.updateNotifyPosts(BLOG_ID, true))
131+
.thenReturn(UpdateResult.Failure("Error"))
132+
133+
viewModel.onNotifyPostsToggled(true)
134+
135+
assertThat(snackbarEvents).hasSize(1)
136+
val message = snackbarEvents.first().peekContent().message
137+
assertThat(message).isEqualTo(UiStringRes(R.string.reader_subscription_settings_update_error))
138+
}
139+
140+
@Test
141+
fun `onNotifyPostsToggled reverts state on no network`() = test {
142+
initializeViewModel(notifyPostsEnabled = false)
143+
whenever(subscriptionUseCase.updateNotifyPosts(BLOG_ID, true)).thenReturn(UpdateResult.NoNetwork)
144+
145+
viewModel.onNotifyPostsToggled(true)
146+
147+
val state = viewModel.uiState.value
148+
assertThat(state?.notifyPostsEnabled).isFalse()
149+
}
150+
151+
@Test
152+
fun `onNotifyPostsToggled shows no network snackbar on no network`() = test {
153+
initializeViewModel()
154+
whenever(subscriptionUseCase.updateNotifyPosts(BLOG_ID, true)).thenReturn(UpdateResult.NoNetwork)
155+
156+
viewModel.onNotifyPostsToggled(true)
157+
158+
assertThat(snackbarEvents).hasSize(1)
159+
val message = snackbarEvents.first().peekContent().message
160+
assertThat(message).isEqualTo(UiStringRes(R.string.no_network_message))
161+
}
162+
// endregion
163+
164+
// region onEmailPostsToggled
165+
@Test
166+
fun `onEmailPostsToggled updates state on success`() = test {
167+
initializeViewModel()
168+
whenever(subscriptionUseCase.updateEmailPosts(BLOG_ID, true)).thenReturn(UpdateResult.Success)
169+
170+
viewModel.onEmailPostsToggled(true)
171+
172+
val state = viewModel.uiState.value
173+
assertThat(state?.emailPostsEnabled).isTrue()
174+
assertThat(state?.isLoading).isFalse()
175+
}
176+
177+
@Test
178+
fun `onEmailPostsToggled reverts state on failure`() = test {
179+
initializeViewModel(emailPostsEnabled = false)
180+
whenever(subscriptionUseCase.updateEmailPosts(BLOG_ID, true))
181+
.thenReturn(UpdateResult.Failure("Error"))
182+
183+
viewModel.onEmailPostsToggled(true)
184+
185+
val state = viewModel.uiState.value
186+
assertThat(state?.emailPostsEnabled).isFalse()
187+
}
188+
189+
@Test
190+
fun `onEmailPostsToggled shows error snackbar on failure`() = test {
191+
initializeViewModel()
192+
whenever(subscriptionUseCase.updateEmailPosts(BLOG_ID, true))
193+
.thenReturn(UpdateResult.Failure("Error"))
194+
195+
viewModel.onEmailPostsToggled(true)
196+
197+
assertThat(snackbarEvents).hasSize(1)
198+
val message = snackbarEvents.first().peekContent().message
199+
assertThat(message).isEqualTo(UiStringRes(R.string.reader_subscription_settings_update_error))
200+
}
201+
202+
@Test
203+
fun `onEmailPostsToggled reverts state on no network`() = test {
204+
initializeViewModel(emailPostsEnabled = false)
205+
whenever(subscriptionUseCase.updateEmailPosts(BLOG_ID, true)).thenReturn(UpdateResult.NoNetwork)
206+
207+
viewModel.onEmailPostsToggled(true)
208+
209+
val state = viewModel.uiState.value
210+
assertThat(state?.emailPostsEnabled).isFalse()
211+
}
212+
213+
@Test
214+
fun `onEmailPostsToggled shows no network snackbar on no network`() = test {
215+
initializeViewModel()
216+
whenever(subscriptionUseCase.updateEmailPosts(BLOG_ID, true)).thenReturn(UpdateResult.NoNetwork)
217+
218+
viewModel.onEmailPostsToggled(true)
219+
220+
assertThat(snackbarEvents).hasSize(1)
221+
val message = snackbarEvents.first().peekContent().message
222+
assertThat(message).isEqualTo(UiStringRes(R.string.no_network_message))
223+
}
224+
// endregion
225+
226+
// region onEmailCommentsToggled
227+
@Test
228+
fun `onEmailCommentsToggled updates state on success`() = test {
229+
initializeViewModel()
230+
whenever(subscriptionUseCase.updateEmailComments(BLOG_ID, true)).thenReturn(UpdateResult.Success)
231+
232+
viewModel.onEmailCommentsToggled(true)
233+
234+
val state = viewModel.uiState.value
235+
assertThat(state?.emailCommentsEnabled).isTrue()
236+
assertThat(state?.isLoading).isFalse()
237+
}
238+
239+
@Test
240+
fun `onEmailCommentsToggled reverts state on failure`() = test {
241+
initializeViewModel(emailCommentsEnabled = false)
242+
whenever(subscriptionUseCase.updateEmailComments(BLOG_ID, true))
243+
.thenReturn(UpdateResult.Failure("Error"))
244+
245+
viewModel.onEmailCommentsToggled(true)
246+
247+
val state = viewModel.uiState.value
248+
assertThat(state?.emailCommentsEnabled).isFalse()
249+
}
250+
251+
@Test
252+
fun `onEmailCommentsToggled shows error snackbar on failure`() = test {
253+
initializeViewModel()
254+
whenever(subscriptionUseCase.updateEmailComments(BLOG_ID, true))
255+
.thenReturn(UpdateResult.Failure("Error"))
256+
257+
viewModel.onEmailCommentsToggled(true)
258+
259+
assertThat(snackbarEvents).hasSize(1)
260+
val message = snackbarEvents.first().peekContent().message
261+
assertThat(message).isEqualTo(UiStringRes(R.string.reader_subscription_settings_update_error))
262+
}
263+
264+
@Test
265+
fun `onEmailCommentsToggled reverts state on no network`() = test {
266+
initializeViewModel(emailCommentsEnabled = false)
267+
whenever(subscriptionUseCase.updateEmailComments(BLOG_ID, true)).thenReturn(UpdateResult.NoNetwork)
268+
269+
viewModel.onEmailCommentsToggled(true)
270+
271+
val state = viewModel.uiState.value
272+
assertThat(state?.emailCommentsEnabled).isFalse()
273+
}
274+
275+
@Test
276+
fun `onEmailCommentsToggled shows no network snackbar on no network`() = test {
277+
initializeViewModel()
278+
whenever(subscriptionUseCase.updateEmailComments(BLOG_ID, true)).thenReturn(UpdateResult.NoNetwork)
279+
280+
viewModel.onEmailCommentsToggled(true)
281+
282+
assertThat(snackbarEvents).hasSize(1)
283+
val message = snackbarEvents.first().peekContent().message
284+
assertThat(message).isEqualTo(UiStringRes(R.string.no_network_message))
285+
}
286+
// endregion
287+
288+
// region cleanup
289+
@Test
290+
fun `onCleared calls useCase cleanup`() = test {
291+
// Access the onCleared method via reflection since it's protected
292+
val method = viewModel.javaClass.getDeclaredMethod("onCleared")
293+
method.isAccessible = true
294+
method.invoke(viewModel)
295+
296+
verify(subscriptionUseCase).cleanup()
297+
}
298+
// endregion
299+
300+
private suspend fun initializeViewModel(
301+
notifyPostsEnabled: Boolean = false,
302+
emailPostsEnabled: Boolean = false,
303+
emailCommentsEnabled: Boolean = false
304+
) {
305+
val subscription = SubscriptionModel().apply {
306+
blogId = BLOG_ID.toString()
307+
shouldNotifyPosts = notifyPostsEnabled
308+
shouldEmailPosts = emailPostsEnabled
309+
shouldEmailComments = emailCommentsEnabled
310+
}
311+
whenever(subscriptionUseCase.getSubscriptionForBlog(BLOG_ID)).thenReturn(subscription)
312+
viewModel.start(BLOG_ID, BLOG_NAME, BLOG_URL)
313+
// Wait for start to complete
314+
viewModel.uiState.first { it != null }
315+
}
316+
}

0 commit comments

Comments
 (0)