Skip to content

Commit 8eadf43

Browse files
Add unit tests for DashboardTopCategoriesViewModel
1 parent 46c3c95 commit 8eadf43

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package com.woocommerce.android.ui.dashboard.topcategories
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import com.woocommerce.android.AppPrefsWrapper
5+
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper
6+
import com.woocommerce.android.tools.NetworkStatus
7+
import com.woocommerce.android.tools.SelectedSite
8+
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType
9+
import com.woocommerce.android.ui.dashboard.DashboardStatsUsageTracksEventEmitter
10+
import com.woocommerce.android.ui.dashboard.DashboardViewModel
11+
import com.woocommerce.android.ui.dashboard.data.TopCategoriesCustomDateRangeDataStore
12+
import com.woocommerce.android.ui.dashboard.domain.DashboardDateRangeFormatter
13+
import com.woocommerce.android.ui.dashboard.domain.GetTopPerformerCategories
14+
import com.woocommerce.android.ui.dashboard.domain.GetTopPerformerCategories.TopPerformerCategory
15+
import com.woocommerce.android.ui.dashboard.domain.GetTopPerformerCategories.TopPerformerCategoryResult
16+
import com.woocommerce.android.ui.dashboard.domain.ObserveLastUpdate
17+
import com.woocommerce.android.util.CurrencyFormatter
18+
import com.woocommerce.android.util.DateUtils
19+
import com.woocommerce.android.util.ResultWithOutdatedFlag
20+
import com.woocommerce.android.util.captureValues
21+
import com.woocommerce.android.util.getOrAwaitValue
22+
import com.woocommerce.android.util.runAndCaptureValues
23+
import com.woocommerce.android.viewmodel.BaseUnitTest
24+
import com.woocommerce.android.viewmodel.ResourceProvider
25+
import com.woocommerce.commons.stats.StatsTimeRange
26+
import kotlinx.coroutines.ExperimentalCoroutinesApi
27+
import kotlinx.coroutines.flow.MutableSharedFlow
28+
import kotlinx.coroutines.flow.MutableStateFlow
29+
import kotlinx.coroutines.flow.flowOf
30+
import kotlinx.coroutines.flow.map
31+
import org.assertj.core.api.Assertions.assertThat
32+
import org.junit.Test
33+
import org.mockito.kotlin.any
34+
import org.mockito.kotlin.anyVararg
35+
import org.mockito.kotlin.doAnswer
36+
import org.mockito.kotlin.doReturn
37+
import org.mockito.kotlin.mock
38+
import org.mockito.kotlin.whenever
39+
import org.wordpress.android.fluxc.model.SiteModel
40+
import org.wordpress.android.fluxc.store.WooCommerceStore
41+
42+
@OptIn(ExperimentalCoroutinesApi::class)
43+
class DashboardTopCategoriesViewModelTest : BaseUnitTest() {
44+
45+
private val sampleCategories = listOf(
46+
TopPerformerCategory(
47+
categoryId = 1L,
48+
name = "Clothing",
49+
quantity = 10,
50+
currency = "USD",
51+
total = 100.0
52+
),
53+
TopPerformerCategory(
54+
categoryId = 2L,
55+
name = "Electronics",
56+
quantity = 5,
57+
currency = "USD",
58+
total = 250.0
59+
)
60+
)
61+
62+
private val parentViewModel: DashboardViewModel = mock {
63+
on { refreshTrigger } doReturn MutableSharedFlow()
64+
}
65+
private val networkStatus: NetworkStatus = mock {
66+
on { isConnected() } doReturn true
67+
}
68+
private val observeLastUpdate: ObserveLastUpdate = mock {
69+
on {
70+
invoke(any(), any<com.woocommerce.android.ui.analytics.hub.sync.AnalyticsUpdateDataStore.AnalyticData>())
71+
} doReturn flowOf(null)
72+
}
73+
private val resourceProvider: ResourceProvider = mock()
74+
private val getTopPerformerCategories: GetTopPerformerCategories = mock()
75+
private val currencyFormatter: CurrencyFormatter = mock()
76+
private val usageTracksEventEmitter: DashboardStatsUsageTracksEventEmitter = mock()
77+
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper = mock()
78+
private val wooCommerceStore: WooCommerceStore = mock()
79+
private val dateUtils: DateUtils = mock()
80+
private val selectedSite: SelectedSite = mock {
81+
on { get() } doReturn SiteModel()
82+
}
83+
private val appPrefFlow = MutableStateFlow(SelectionType.TODAY.name)
84+
private val appPrefsWrapper: AppPrefsWrapper = mock {
85+
on { getActiveTopCategoriesTab() } doAnswer { appPrefFlow.value }
86+
on { observePrefs() } doAnswer { appPrefFlow.map {} }
87+
}
88+
private val customRangeFlow = MutableStateFlow<StatsTimeRange?>(null)
89+
private val customDateRangeDataStore: TopCategoriesCustomDateRangeDataStore = mock {
90+
on { dateRange } doReturn customRangeFlow
91+
}
92+
private val dateFormatter: DashboardDateRangeFormatter = mock {
93+
on { formatRangeDate(any()) } doReturn "Today"
94+
}
95+
96+
private lateinit var viewModel: DashboardTopCategoriesViewModel
97+
98+
private suspend fun setup(prepareMocks: suspend () -> Unit = {}) {
99+
whenever(resourceProvider.getString(any(), anyVararg())).thenReturn("")
100+
whenever(currencyFormatter.formatCurrency(any<java.math.BigDecimal>(), any(), any())).thenReturn("$100.00")
101+
prepareMocks()
102+
val getSelectedDateRange = GetSelectedRangeForTopCategories(
103+
appPrefs = appPrefsWrapper,
104+
customDateRangeDataStore = customDateRangeDataStore,
105+
dateUtils = dateUtils
106+
)
107+
108+
viewModel = DashboardTopCategoriesViewModel(
109+
parentViewModel = parentViewModel,
110+
selectedSite = selectedSite,
111+
networkStatus = networkStatus,
112+
observeLastUpdate = observeLastUpdate,
113+
resourceProvider = resourceProvider,
114+
getTopPerformerCategories = getTopPerformerCategories,
115+
currencyFormatter = currencyFormatter,
116+
usageTracksEventEmitter = usageTracksEventEmitter,
117+
analyticsTrackerWrapper = analyticsTrackerWrapper,
118+
wooCommerceStore = wooCommerceStore,
119+
dateUtils = dateUtils,
120+
appPrefsWrapper = appPrefsWrapper,
121+
customDateRangeDataStore = customDateRangeDataStore,
122+
dateFormatter = dateFormatter,
123+
getSelectedDateRange = getSelectedDateRange,
124+
savedState = SavedStateHandle()
125+
)
126+
}
127+
128+
@Test
129+
fun `when view model is created, then loading state is emitted`() = testBlocking {
130+
setup {
131+
whenever(getTopPerformerCategories.invoke(any(), any())).thenReturn(
132+
flowOf(TopPerformerCategoryResult.Loading)
133+
)
134+
}
135+
136+
// WHEN
137+
val state = viewModel.topCategoriesState.captureValues().last()
138+
139+
// THEN
140+
assertThat(state.isLoading).isTrue()
141+
}
142+
143+
@Test
144+
fun `given network disconnected, when loading, then generic error is shown`() = testBlocking {
145+
setup {
146+
whenever(networkStatus.isConnected()).thenReturn(false)
147+
}
148+
149+
// WHEN
150+
val state = viewModel.topCategoriesState.getOrAwaitValue()
151+
152+
// THEN
153+
assertThat(state.error).isEqualTo(DashboardTopCategoriesViewModel.ErrorType.Generic)
154+
}
155+
156+
@Test
157+
fun `given successful data fetch, when loading completes, then categories are displayed`() = testBlocking {
158+
setup {
159+
whenever(getTopPerformerCategories.invoke(any(), any())).thenReturn(
160+
flowOf(
161+
TopPerformerCategoryResult.Success(
162+
ResultWithOutdatedFlag(sampleCategories, false)
163+
)
164+
)
165+
)
166+
}
167+
168+
// WHEN
169+
val state = viewModel.topCategoriesState.captureValues().last()
170+
171+
// THEN
172+
assertThat(state.isLoading).isFalse()
173+
assertThat(state.error).isNull()
174+
assertThat(state.topCategories).hasSize(2)
175+
assertThat(state.topCategories[0].categoryId).isEqualTo(1L)
176+
assertThat(state.topCategories[1].categoryId).isEqualTo(2L)
177+
}
178+
179+
@Test
180+
fun `given empty data, when loading completes, then empty list is shown`() = testBlocking {
181+
setup {
182+
whenever(getTopPerformerCategories.invoke(any(), any())).thenReturn(
183+
flowOf(
184+
TopPerformerCategoryResult.Success(
185+
ResultWithOutdatedFlag(emptyList(), false)
186+
)
187+
)
188+
)
189+
}
190+
191+
// WHEN
192+
val state = viewModel.topCategoriesState.captureValues().last()
193+
194+
// THEN
195+
assertThat(state.isLoading).isFalse()
196+
assertThat(state.error).isNull()
197+
assertThat(state.topCategories).isEmpty()
198+
}
199+
200+
@Test
201+
fun `when category tapped, then OpenCategoryProducts event is triggered`() = testBlocking {
202+
setup {
203+
whenever(getTopPerformerCategories.invoke(any(), any())).thenReturn(
204+
flowOf(
205+
TopPerformerCategoryResult.Success(
206+
ResultWithOutdatedFlag(sampleCategories, false)
207+
)
208+
)
209+
)
210+
}
211+
212+
// GIVEN
213+
viewModel.selectedDateRange.getOrAwaitValue()
214+
val state = viewModel.topCategoriesState.getOrAwaitValue()
215+
assertThat(state.topCategories).isNotEmpty()
216+
217+
// WHEN
218+
val event = viewModel.event.runAndCaptureValues {
219+
state.topCategories.first().onClick(state.topCategories.first().categoryId)
220+
}.last()
221+
222+
// THEN
223+
assertThat(event).isInstanceOf(DashboardTopCategoriesViewModel.OpenCategoryProducts::class.java)
224+
val openEvent = event as DashboardTopCategoriesViewModel.OpenCategoryProducts
225+
assertThat(openEvent.categoryId).isEqualTo(1L)
226+
assertThat(openEvent.categoryName).isEqualTo("Clothing")
227+
}
228+
229+
@Test
230+
fun `when retry tapped, then refresh is triggered`() = testBlocking {
231+
setup {
232+
whenever(getTopPerformerCategories.invoke(any(), any())).thenReturn(
233+
flowOf(
234+
TopPerformerCategoryResult.Success(
235+
ResultWithOutdatedFlag(sampleCategories, false)
236+
)
237+
)
238+
)
239+
}
240+
241+
// WHEN
242+
viewModel.topCategoriesState.runAndCaptureValues {
243+
viewModel.onRefresh()
244+
}
245+
246+
// THEN
247+
// The refresh call should trigger the getTopPerformerCategories to be called again
248+
// via the refreshTrigger flow. We verify that the state is still valid after refresh.
249+
val state = viewModel.topCategoriesState.getOrAwaitValue()
250+
assertThat(state).isNotNull()
251+
}
252+
253+
@Test
254+
fun `when view all analytics tapped, then OpenAnalytics event is triggered`() = testBlocking {
255+
setup {
256+
whenever(getTopPerformerCategories.invoke(any(), any())).thenReturn(
257+
flowOf(
258+
TopPerformerCategoryResult.Success(
259+
ResultWithOutdatedFlag(sampleCategories, false)
260+
)
261+
)
262+
)
263+
}
264+
265+
// GIVEN
266+
viewModel.selectedDateRange.getOrAwaitValue()
267+
val state = viewModel.topCategoriesState.getOrAwaitValue()
268+
269+
// WHEN
270+
val event = viewModel.event.runAndCaptureValues {
271+
state.onOpenAnalyticsTapped.action()
272+
}.last()
273+
274+
// THEN
275+
assertThat(event).isInstanceOf(DashboardTopCategoriesViewModel.OpenAnalytics::class.java)
276+
}
277+
}

0 commit comments

Comments
 (0)