@@ -3,16 +3,16 @@ package com.capyreader.app.ui.articles
33import android.app.Application
44import app.cash.turbine.test
55import com.capyreader.app.notifications.NotificationHelper
6- import com.capyreader.app.preferences.AfterReadAllBehavior
76import com.capyreader.app.preferences.AppPreferences
87import com.capyreader.app.preferences.ArticleListVerticalSwipe
98import com.capyreader.app.refresher.RefreshInterval
109import com.capyreader.app.ui.articles.feeds.AngleRefreshState
1110import com.jocmp.capy.Account
1211import com.jocmp.capy.ArticleFilter
12+ import com.jocmp.capy.ArticleStatus
13+ import com.jocmp.capy.Feed
14+ import com.jocmp.capy.Folder
1315import com.jocmp.capy.accounts.Source
14- import com.jocmp.capy.articles.SortOrder
15- import com.jocmp.capy.preferences.Preference
1616import io.mockk.coEvery
1717import io.mockk.every
1818import io.mockk.mockk
@@ -54,20 +54,20 @@ class ArticleScreenViewModelTest {
5454 every { feeds } returns flowOf(emptyList())
5555 every { taggedFeeds } returns flowOf(emptyList())
5656 every { savedSearches } returns flowOf(emptyList())
57- every { canSaveArticleExternally } returns mockPreference(false )
57+ every { canSaveArticleExternally } returns mockk(relaxed = true ) {
58+ every { get() } returns false
59+ every { stateIn(any()) } returns MutableStateFlow (false )
60+ every { changes() } returns flowOf(false )
61+ }
62+ every { countAll(any()) } returns flowOf(emptyMap())
63+ every { countAllBySavedSearch(any()) } returns flowOf(emptyMap())
5864 every { source } returns Source .LOCAL
5965 coEvery { refresh(any()) } returns Result .success(Unit )
6066 }
6167
62- appPreferences = mockk(relaxed = true ) {
63- every { filter } returns mockPreference(ArticleFilter .default())
64- every { refreshInterval } returns mockPreference(RefreshInterval .EVERY_TWO_HOURS )
65- every { enableStickyFullContent } returns mockPreference(false )
66- every { articleListOptions } returns mockk(relaxed = true ) {
67- every { swipeBottom } returns mockPreference(ArticleListVerticalSwipe .DISABLED )
68- every { sortOrder } returns mockPreference(SortOrder .default)
69- every { afterReadAllBehavior } returns mockPreference(AfterReadAllBehavior .default)
70- }
68+ appPreferences = AppPreferences (RuntimeEnvironment .getApplication()).also {
69+ it.clearAll()
70+ it.refreshInterval.set(RefreshInterval .EVERY_TWO_HOURS )
7171 }
7272
7373 notificationHelper = mockk(relaxed = true )
@@ -92,7 +92,7 @@ class ArticleScreenViewModelTest {
9292
9393 @Test
9494 fun `skips initial refresh when refresh interval is manual only` () = runTest {
95- every { appPreferences.refreshInterval } returns mockPreference (RefreshInterval .MANUALLY_ONLY )
95+ appPreferences.refreshInterval.set (RefreshInterval .MANUALLY_ONLY )
9696
9797 val viewModel = buildViewModel()
9898
@@ -102,7 +102,7 @@ class ArticleScreenViewModelTest {
102102
103103 @Test
104104 fun `refreshAll transitions from stopped, running, to settling` () = runTest {
105- every { appPreferences.refreshInterval } returns mockPreference (RefreshInterval .MANUALLY_ONLY )
105+ appPreferences.refreshInterval.set (RefreshInterval .MANUALLY_ONLY )
106106
107107 val viewModel = buildViewModel()
108108
@@ -120,7 +120,7 @@ class ArticleScreenViewModelTest {
120120
121121 @Test
122122 fun `refreshAll guards against double calls while running` () = runTest {
123- every { appPreferences.refreshInterval } returns mockPreference (RefreshInterval .MANUALLY_ONLY )
123+ appPreferences.refreshInterval.set (RefreshInterval .MANUALLY_ONLY )
124124
125125 val viewModel = buildViewModel()
126126
@@ -138,6 +138,82 @@ class ArticleScreenViewModelTest {
138138 }
139139 }
140140
141+ @Test
142+ fun `requestNextFeed opens next feed after current feed's unread count drops to zero` () = runTest {
143+ val initialFilter = ArticleFilter .Feeds (
144+ feedID = " 1" ,
145+ folderTitle = null ,
146+ feedStatus = ArticleStatus .UNREAD
147+ )
148+ appPreferences.filter.set(initialFilter)
149+ appPreferences.articleListOptions.swipeBottom.set(ArticleListVerticalSwipe .NEXT_FEED )
150+
151+ val feedA = Feed (id = " 1" , subscriptionID = " 1" , title = " A" , feedURL = " a" , count = 3 )
152+ val feedB = Feed (id = " 2" , subscriptionID = " 2" , title = " B" , feedURL = " b" , count = 3 )
153+
154+ val feeds = MutableStateFlow (listOf (feedA, feedB))
155+ val counts = MutableStateFlow <Map <String , Long >>(mapOf (" 1" to 3 , " 2" to 3 ))
156+
157+ every { account.feeds } returns feeds
158+ every { account.countAll(any()) } returns counts
159+
160+ val viewModel = buildViewModel()
161+ advanceUntilIdle()
162+
163+ counts.value = mapOf (" 1" to 0 , " 2" to 3 )
164+ advanceUntilIdle()
165+
166+ viewModel.requestNextFeed()
167+ advanceUntilIdle()
168+
169+ val expectedNext = ArticleFilter .Feeds (
170+ feedID = " 2" ,
171+ folderTitle = null ,
172+ feedStatus = ArticleStatus .UNREAD
173+ )
174+ assertEquals(expectedNext, appPreferences.filter.get())
175+ }
176+
177+ @Test
178+ fun `requestNextFeed skips empty children when current folder is marked read` () = runTest {
179+ val initialFilter = ArticleFilter .Folders (
180+ folderTitle = " X" ,
181+ folderStatus = ArticleStatus .UNREAD ,
182+ )
183+ appPreferences.filter.set(initialFilter)
184+ appPreferences.articleListOptions.swipeBottom.set(ArticleListVerticalSwipe .NEXT_FEED )
185+
186+ val x1 = Feed (id = " x1" , subscriptionID = " x1" , title = " X1" , feedURL = " x1" , folderName = " X" )
187+ val x2 = Feed (id = " x2" , subscriptionID = " x2" , title = " X2" , feedURL = " x2" , folderName = " X" )
188+ val y1 = Feed (id = " y1" , subscriptionID = " y1" , title = " Y1" , feedURL = " y1" , folderName = " Y" )
189+
190+ val folderX = Folder (title = " X" , feeds = listOf (x1, x2), expanded = true )
191+ val folderY = Folder (title = " Y" , feeds = listOf (y1), expanded = true )
192+
193+ val folders = MutableStateFlow (listOf (folderX, folderY))
194+ val counts = MutableStateFlow <Map <String , Long >>(
195+ mapOf (" x1" to 2L , " x2" to 2L , " y1" to 3L )
196+ )
197+
198+ every { account.folders } returns folders
199+ every { account.feeds } returns flowOf(emptyList())
200+ every { account.countAll(any()) } returns counts
201+
202+ val viewModel = buildViewModel()
203+ advanceUntilIdle()
204+
205+ counts.value = mapOf (" x1" to 0L , " x2" to 0L , " y1" to 3L )
206+ advanceUntilIdle()
207+
208+ viewModel.requestNextFeed()
209+ advanceUntilIdle()
210+
211+ assertEquals(
212+ ArticleFilter .Folders (folderTitle = " Y" , folderStatus = ArticleStatus .UNREAD ),
213+ appPreferences.filter.get()
214+ )
215+ }
216+
141217 private fun buildViewModel (): ArticleScreenViewModel {
142218 val application = RuntimeEnvironment .getApplication() as Application
143219
@@ -149,12 +225,4 @@ class ArticleScreenViewModelTest {
149225 ioDispatcher = testDispatcher,
150226 )
151227 }
152-
153- private inline fun <reified T > mockPreference (value : T ): Preference <T > {
154- return mockk {
155- every { get() } returns value
156- every { stateIn(any()) } returns MutableStateFlow (value)
157- every { changes() } returns flowOf(value)
158- }
159- }
160228}
0 commit comments