Yuito-app-android/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt

217 lines
6.9 KiB
Kotlin

package com.keylesspalace.tusky.components.timeline
import android.os.Looper
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.paging.AsyncPagingDataDiffer
import androidx.paging.ExperimentalPagingApi
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.gson.Gson
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.components.timeline.TimelinePagingAdapter.Companion.TimelineDifferCallback
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import okhttp3.Headers
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import retrofit2.Response
import java.util.concurrent.Executors
@ExperimentalCoroutinesApi
@Config(sdk = [29])
@RunWith(AndroidJUnit4::class)
class TimelineViewModelTest {
@get:Rule
val instantRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val accountManager: AccountManager = mock {
on { activeAccount } doReturn AccountEntity(
id = 1,
domain = "mastodon.example",
accessToken = "token",
isActive = true
)
}
private val eventHub = EventHub()
private lateinit var db: AppDatabase
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
shadowOf(Looper.getMainLooper()).idle()
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.addTypeConverter(Converters(Gson()))
.setTransactionExecutor(Executors.newSingleThreadExecutor())
.allowMainThreadQueries()
.build()
}
@After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
db.close()
}
@Test
@ExperimentalPagingApi
fun shouldLoadNetworkTimeline() = runBlocking {
val api: MastodonApi = mock {
on { publicTimeline(local = true, maxId = null, sinceId = null, limit = 30) } doReturn Single.just(
Response.success(
listOf(
mockStatus("6"),
mockStatus("5"),
mockStatus("4")
),
Headers.headersOf(
"Link", "<https://mastodon.examples/api/v1/favourites?limit=30&max_id=1>; rel=\"next\", <https://mastodon.example/api/v1/favourites?limit=30&min_id=5>; rel=\"prev\""
)
)
)
on { publicTimeline(local = true, maxId = "1", sinceId = null, limit = 30) } doReturn Single.just(
Response.success(emptyList())
)
on { getFilters() } doReturn Single.just(emptyList())
}
val viewModel = NetworkTimelineViewModel(
TimelineCases(api, eventHub),
api,
eventHub,
accountManager,
mock(),
FilterModel()
)
viewModel.init(TimelineViewModel.Kind.PUBLIC_LOCAL, null, emptyList())
val differ = AsyncPagingDataDiffer(
diffCallback = TimelineDifferCallback,
updateCallback = NoopListCallback(),
workerDispatcher = testDispatcher
)
viewModel.statuses.take(2).collectLatest {
testScope.launch {
differ.submitData(it)
}
}
assertEquals(
listOf(
mockStatusViewData("6"),
mockStatusViewData("5"),
mockStatusViewData("4")
),
differ.snapshot().items
)
}
// ToDo: Find out why Room & coroutines are not playing nice here
// @Test
@ExperimentalPagingApi
fun shouldLoadCachedTimeline() = runBlocking {
val api: MastodonApi = mock {
on { homeTimeline(limit = 30) } doReturn Single.just(
Response.success(
listOf(
mockStatus("6"),
mockStatus("5"),
mockStatus("4")
)
)
)
on { homeTimeline(maxId = "1", sinceId = null, limit = 30) } doReturn Single.just(
Response.success(emptyList())
)
on { getFilters() } doReturn Single.just(emptyList())
}
val viewModel = CachedTimelineViewModel(
TimelineCases(api, eventHub),
api,
eventHub,
accountManager,
mock(),
FilterModel(),
db,
Gson()
)
viewModel.init(TimelineViewModel.Kind.HOME, null, emptyList())
val differ = AsyncPagingDataDiffer(
diffCallback = TimelineDifferCallback,
updateCallback = NoopListCallback(),
workerDispatcher = testDispatcher
)
viewModel.statuses.take(1000).collectLatest {
testScope.launch {
differ.submitData(it)
}
}
assertEquals(
listOf(
mockStatusViewData("6"),
mockStatusViewData("5"),
mockStatusViewData("4")
),
differ.snapshot().items
)
}
}
class NoopListCallback : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) {}
override fun onMoved(fromPosition: Int, toPosition: Int) {}
override fun onInserted(position: Int, count: Int) {}
override fun onRemoved(position: Int, count: Int) {}
}