From c666ab510c3643fc6c4c97fc46ed523555a337ab Mon Sep 17 00:00:00 2001 From: NickHuk Date: Fri, 11 Jun 2021 15:38:28 +0300 Subject: [PATCH 01/13] test anime initial loading (first page) --- .../anilist/AnilistPresenterTest.kt | 81 ++++++++++--------- .../java/com/huchihaitachi/domain/Anime.kt | 4 +- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index b46cee4..9502f23 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -11,70 +11,75 @@ import com.huchihaitachi.domain.Page import com.huchihaitachi.usecase.GetStringResourceUseCase import com.huchihaitachi.usecase.LoadAnimeUseCase import com.huchihaitachi.usecase.LoadPageUseCase +import com.huchihaitachi.usecase.RefreshPageUseCase import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.justRun -import io.mockk.verify -import io.mockk.verifyAll -import io.mockk.verifySequence +import io.mockk.just +import io.mockk.runs +import io.mockk.verifyOrder +import io.reactivex.Scheduler import io.reactivex.Single import io.reactivex.schedulers.Schedulers -import io.reactivex.schedulers.TestScheduler import io.reactivex.subjects.PublishSubject import org.junit.Before import org.junit.Test -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeUnit.MILLISECONDS class AnilistPresenterTest { - @MockK private lateinit var loadPageUseCase: LoadPageUseCase - @MockK private lateinit var loadAnimeUseCase: LoadAnimeUseCase - @MockK private lateinit var getStringResourceUseCase: GetStringResourceUseCase + @MockK private lateinit var loadPage: LoadPageUseCase + @MockK private lateinit var refreshPage: RefreshPageUseCase + @MockK private lateinit var loadAnime: LoadAnimeUseCase + @MockK private lateinit var getStringResource: GetStringResourceUseCase @MockK private lateinit var anilistView: AnilistView - @MockK private lateinit var rxSchedulers: RxSchedulers + @MockK private lateinit var rxSchedulers: RxSchedulers + private lateinit var anime: List + private lateinit var testScheduler: Scheduler @Before fun setup() { MockKAnnotations.init(this) - mockIntents() + setupSchedulers() + setupView() + anime = listOf(Anime(1), Anime(2), Anime(3), Anime(4), Anime(5), Anime(6)) } @Test fun `load first page test`() { - val initialState = AnilistViewState() - val anilistPresenter = AnilistPresenter( - loadPageUseCase, - loadAnimeUseCase, - getStringResourceUseCase, - AnilistViewState(), - rxSchedulers + val initialState = AnilistViewState(pageState = PageState(null, 0, true)) + val presenter = AnilistPresenter( + loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers ) - val anime = listOf( - Anime(1), Anime(2), Anime(3), Anime(4), Anime(1), Anime(2), Anime(3), Anime(4) - ) - val testScheduler = TestScheduler() - every { rxSchedulers.io } returns testScheduler - every { rxSchedulers.ui } returns testScheduler - anilistPresenter.bind(anilistView) val loadAnimePageMock: PublishSubject = PublishSubject.create() every { anilistView.loadAnimePage } returns loadAnimePageMock - every { loadPageUseCase(any(), any()) } returns Single.just(Page(8, 1, true, anime)) - every { anilistView.render(any()) } returns testScheduler.advanceTimeBy(100L, MILLISECONDS) - val expectedResult = AnilistViewState(PAGE, null, PageState(anime, 1, true), null) - anilistPresenter.bindIntents() - testScheduler.advanceTimeBy(10000L, TimeUnit.MILLISECONDS) + every { anilistView.reload } returns PublishSubject.create() + every { anilistView.showDetails } returns PublishSubject.create() + every { anilistView.hideDetails } returns PublishSubject.create() + every { loadPage(any(), any()) } returns Single.just( + Page(1, 1, true, anime, 0, 0) + ) + presenter.bind(anilistView) + presenter.bindIntents() + val expectedInitial = AnilistViewState(pageState = PageState(null, 0, true)) + val expectedLoading = AnilistViewState( + loading = PAGE, + pageState = PageState(null, 0, true), + ) + val expectedResult = AnilistViewState(pageState = PageState(anime, 1, true)) loadAnimePageMock.onNext(Unit) - verifySequence { - anilistView.render(initialState) + verifyOrder { + anilistView.render(expectedInitial) + anilistView.render(expectedLoading) anilistView.render(expectedResult) } } - private fun mockIntents() { - every { anilistView.loadAnimePage } returns PublishSubject.create() - every { anilistView.reload } returns PublishSubject.create() - every { anilistView.showDetails } returns PublishSubject.create() - every { anilistView.hideDetails } returns PublishSubject.create() + private fun setupSchedulers() { + testScheduler = Schedulers.trampoline() + every { rxSchedulers.io } answers { testScheduler } + every { rxSchedulers.ui } answers { testScheduler } + } + + private fun setupView() { + every { anilistView.render(any()) } just runs } } \ No newline at end of file diff --git a/domain/src/main/java/com/huchihaitachi/domain/Anime.kt b/domain/src/main/java/com/huchihaitachi/domain/Anime.kt index 5e9444a..ffb34b1 100644 --- a/domain/src/main/java/com/huchihaitachi/domain/Anime.kt +++ b/domain/src/main/java/com/huchihaitachi/domain/Anime.kt @@ -11,6 +11,6 @@ open class Anime( val duration: Int? = null, //minutes in general val coverImage: String? = null, val bannerImage: String? = null, - override val timeOfBirth: Long, - override val timeToStale: Long + override val timeOfBirth: Long = 0, + override val timeToStale: Long = 0 ) : Dirtyable \ No newline at end of file From 45ea4272c7bb11b395813905516cde76a4598fc9 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Fri, 11 Jun 2021 17:20:04 +0300 Subject: [PATCH 02/13] test reloading --- .../anilist/presentation/AnilistController.kt | 6 +- .../anilist/presentation/AnilistPresenter.kt | 10 +-- .../anilist/presentation/AnilistView.kt | 6 +- .../anilist/AnilistPresenterTest.kt | 66 +++++++++++++++---- 4 files changed, 62 insertions(+), 26 deletions(-) diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt index 1559b7a..2e10106 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt @@ -2,9 +2,7 @@ package com.huchihaitachi.anilist.presentation import android.os.Bundle import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View -import android.view.View.OnTouchListener import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -36,10 +34,10 @@ class AnilistController : Controller(), AnilistView { private val binding: ControllerAnilistBinding get() = _binding!! private val _loadAnimePage: PublishSubject = PublishSubject.create() - override val loadAnimePage: Observable + override val loadPage: Observable get() = _loadAnimePage private val _reload: PublishSubject = PublishSubject.create() - override val reload: Observable + override val refresh: Observable get() = _reload private val _showDetails: PublishSubject = PublishSubject.create() override val showDetails: Observable diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt index ca65f03..16905e9 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt @@ -26,14 +26,14 @@ class AnilistPresenter @Inject constructor( private val refreshPageUseCase: RefreshPageUseCase, private val loadAnimeUseCase: LoadAnimeUseCase, private val getStringResourceUseCase: GetStringResourceUseCase, - anilistViewState: AnilistViewState, - rxSchedulers: RxSchedulers, -) : BasePresenter(anilistViewState, rxSchedulers) { + initialViewState: AnilistViewState, + rxSchedulers: RxSchedulers +) : BasePresenter(initialViewState, rxSchedulers) { override fun bindIntents() { view?.let { view -> // load page - val loadPageIntent = view.loadAnimePage + val loadPageIntent = view.loadPage .observeOn(rxSchedulers.io) .filter { _ -> state.pageState?.hasNextPage == true @@ -43,7 +43,7 @@ class AnilistPresenter @Inject constructor( } .flatMap { _ -> loadPage(state.pageState?.currentPage!! + 1) } //reload - val reloadIntent = view.reload + val reloadIntent = view.refresh .observeOn(rxSchedulers.io) .filter { _ -> state.loading != RELOAD } .flatMap { unit -> refreshPage() } diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistView.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistView.kt index 91fb2c8..e8dc18d 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistView.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistView.kt @@ -1,13 +1,11 @@ package com.huchihaitachi.anilist.presentation import com.huchihaitachi.base.BaseView -import com.huchihaitachi.domain.Anime import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject interface AnilistView: BaseView { - val loadAnimePage: Observable - val reload: Observable + val loadPage: Observable + val refresh: Observable val showDetails: Observable val hideDetails: Observable } \ No newline at end of file diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index 9502f23..7148414 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -4,6 +4,7 @@ import com.huchihaitachi.anilist.presentation.AnilistPresenter import com.huchihaitachi.anilist.presentation.AnilistView import com.huchihaitachi.anilist.presentation.AnilistViewState import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.RELOAD import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState import com.huchihaitachi.base.RxSchedulers import com.huchihaitachi.domain.Anime @@ -30,7 +31,7 @@ class AnilistPresenterTest { @MockK private lateinit var refreshPage: RefreshPageUseCase @MockK private lateinit var loadAnime: LoadAnimeUseCase @MockK private lateinit var getStringResource: GetStringResourceUseCase - @MockK private lateinit var anilistView: AnilistView + @MockK private lateinit var view: AnilistView @MockK private lateinit var rxSchedulers: RxSchedulers private lateinit var anime: List private lateinit var testScheduler: Scheduler @@ -49,15 +50,12 @@ class AnilistPresenterTest { val presenter = AnilistPresenter( loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers ) - val loadAnimePageMock: PublishSubject = PublishSubject.create() - every { anilistView.loadAnimePage } returns loadAnimePageMock - every { anilistView.reload } returns PublishSubject.create() - every { anilistView.showDetails } returns PublishSubject.create() - every { anilistView.hideDetails } returns PublishSubject.create() + val loadPageIntentMock: PublishSubject = PublishSubject.create() + mockIntents(loadPageIntent = loadPageIntentMock) every { loadPage(any(), any()) } returns Single.just( - Page(1, 1, true, anime, 0, 0) + Page(1, anime.size, true, anime, 0, 0) ) - presenter.bind(anilistView) + presenter.bind(view) presenter.bindIntents() val expectedInitial = AnilistViewState(pageState = PageState(null, 0, true)) val expectedLoading = AnilistViewState( @@ -65,14 +63,56 @@ class AnilistPresenterTest { pageState = PageState(null, 0, true), ) val expectedResult = AnilistViewState(pageState = PageState(anime, 1, true)) - loadAnimePageMock.onNext(Unit) + loadPageIntentMock.onNext(Unit) verifyOrder { - anilistView.render(expectedInitial) - anilistView.render(expectedLoading) - anilistView.render(expectedResult) + view.render(expectedInitial) + view.render(expectedLoading) + view.render(expectedResult) } } + @Test + fun `reload test`() { + val initialState = AnilistViewState(pageState = PageState(anime, 1, true)) + val presenter = AnilistPresenter( + loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers + ) + val refreshIntentMock: PublishSubject = PublishSubject.create() + mockIntents(refreshIntent = refreshIntentMock) + val freshAnime = listOf( + Anime(11), Anime(21), Anime(31), Anime(41), Anime(51), Anime(61) + ) + every { refreshPage(any()) } returns Single.just( + Page(1, freshAnime.size, true, freshAnime, 0, 0) + ) + presenter.bind(view) + presenter.bindIntents() + val expectedInitial = AnilistViewState(pageState = PageState(anime, 1, true)) + val expectedLoading = AnilistViewState( + loading = RELOAD, + pageState = PageState(anime, 1, true), + ) + val expectedResult = AnilistViewState(pageState = PageState(freshAnime, 1, true)) + refreshIntentMock.onNext(Unit) + verifyOrder { + view.render(expectedInitial) + view.render(expectedLoading) + view.render(expectedResult) + } + } + + private fun mockIntents( + loadPageIntent: PublishSubject = PublishSubject.create(), + refreshIntent: PublishSubject = PublishSubject.create(), + showDetailsIntent: PublishSubject = PublishSubject.create(), + hideDetailsIntent: PublishSubject = PublishSubject.create() + ) { + every { view.loadPage } returns loadPageIntent + every { view.refresh } returns refreshIntent + every { view.showDetails } returns showDetailsIntent + every { view.hideDetails } returns hideDetailsIntent + } + private fun setupSchedulers() { testScheduler = Schedulers.trampoline() every { rxSchedulers.io } answers { testScheduler } @@ -80,6 +120,6 @@ class AnilistPresenterTest { } private fun setupView() { - every { anilistView.render(any()) } just runs + every { view.render(any()) } just runs } } \ No newline at end of file From 70969dbe3f6bc271cbee88c9312a310939a661d4 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Fri, 11 Jun 2021 18:32:21 +0300 Subject: [PATCH 03/13] implement equals/hashCode, method for instance copying --- .../java/com/huchihaitachi/domain/Anime.kt | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/domain/src/main/java/com/huchihaitachi/domain/Anime.kt b/domain/src/main/java/com/huchihaitachi/domain/Anime.kt index ffb34b1..c558b44 100644 --- a/domain/src/main/java/com/huchihaitachi/domain/Anime.kt +++ b/domain/src/main/java/com/huchihaitachi/domain/Anime.kt @@ -13,4 +13,63 @@ open class Anime( val bannerImage: String? = null, override val timeOfBirth: Long = 0, override val timeToStale: Long = 0 -) : Dirtyable \ No newline at end of file +) : Dirtyable { + + override fun equals(other: Any?) = + this === other || (other is Anime) + && id == other.id + && title == other.title + && type == other.type + && description == other.description + && season == other.season + && seasonYear == other.seasonYear + && episodes == other.episodes + && duration == other.duration + && coverImage == other.coverImage + && bannerImage == other.bannerImage + && timeOfBirth == other.timeOfBirth + && timeToStale == other.timeToStale + + override fun hashCode(): Int { + return id * 31 + + (title?.hashCode() ?: 0) * 31 + + (type?.hashCode() ?: 0) * 31 + + (description?.hashCode() ?: 0) * 31 + + (season?.hashCode() ?: 0) * 31 + + (seasonYear ?: 0) * 31 + + (episodes ?: 0) * 31 + + (duration ?: 0) * 31 + + (coverImage?.hashCode() ?: 0) * 31 + + (bannerImage?.hashCode() ?: 0) * 31 + + timeOfBirth.hashCode() * 31 + + timeToStale.hashCode() * 31 + } + + fun copy( + id: Int = this.id, + title: String? = this.title, + type: Type? = this.type, + description: String? = this.description, + season: Season? = this.season, + seasonYear: Int? = this.seasonYear, + episodes: Int? = this.episodes, + duration: Int? = this.duration, //minutes in general + coverImage: String? = this.coverImage, + bannerImage: String? = this.bannerImage, + timeOfBirth: Long = this.timeOfBirth, + timeToStale: Long = this.timeToStale + ) = Anime( + id, + title, + type, + description, + season, + seasonYear, + episodes, + duration, + coverImage, + bannerImage, + timeOfBirth, + timeToStale + ) +} \ No newline at end of file From 0bd1d1800ebbcaeeb81cf6a568b36cf53bfe070e Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 14 Jun 2021 11:21:05 +0300 Subject: [PATCH 04/13] process a refreshing correctly while open details during a loading --- .../anilist/presentation/AnilistController.kt | 10 +++---- .../anilist/presentation/AnilistPresenter.kt | 30 ++++++++++++------- .../anilist/presentation/AnilistViewState.kt | 2 +- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt index 2e10106..f127bf7 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistController.kt @@ -15,7 +15,7 @@ import com.huchihaitachi.anilist.R import com.huchihaitachi.anilist.databinding.ControllerAnilistBinding import com.huchihaitachi.anilist.di.AnilistSubcomponentProvider import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.RELOAD +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.REFRESH import com.huchihaitachi.anilist.presentation.animeList.AnimeEpoxyController import com.huchihaitachi.base.domain.localized import com.huchihaitachi.base.domain.stringRes @@ -36,9 +36,9 @@ class AnilistController : Controller(), AnilistView { private val _loadAnimePage: PublishSubject = PublishSubject.create() override val loadPage: Observable get() = _loadAnimePage - private val _reload: PublishSubject = PublishSubject.create() + private val _refresh: PublishSubject = PublishSubject.create() override val refresh: Observable - get() = _reload + get() = _refresh private val _showDetails: PublishSubject = PublishSubject.create() override val showDetails: Observable get() = _showDetails @@ -53,7 +53,7 @@ class AnilistController : Controller(), AnilistView { binding.animeListL.pageLoadingPb.visible = state.loading == PAGE binding.animeListL.totalFooterTv.visible = state.loading != PAGE binding.animeListL.totalFooterTv.text = resources?.getString(R.string.total, state.pageState?.anime?.size) - binding.animeListL.animeSrl.isRefreshing = state.loading == RELOAD + binding.animeListL.animeSrl.isRefreshing = state.loading == REFRESH binding.animeListL.errorFooterTv.apply { visible = state.error != null state.error?.let { text = it } @@ -100,7 +100,7 @@ class AnilistController : Controller(), AnilistView { private fun setupSwipeToRefresh() { binding.animeListL.animeSrl.setOnRefreshListener { - _reload.onNext(Unit) + _refresh.onNext(Unit) } } diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt index 16905e9..4a0ff9f 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt @@ -5,7 +5,7 @@ import com.huchihaitachi.anilist.R import com.huchihaitachi.anilist.di.scope.AnilistScope import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.RELOAD +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.REFRESH import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState import com.huchihaitachi.base.BasePresenter import com.huchihaitachi.base.RxSchedulers @@ -37,15 +37,15 @@ class AnilistPresenter @Inject constructor( .observeOn(rxSchedulers.io) .filter { _ -> state.pageState?.hasNextPage == true - && state.loading != RELOAD + && state.loading != REFRESH && state.loading != PAGE && state.loadingEnabled } .flatMap { _ -> loadPage(state.pageState?.currentPage!! + 1) } - //reload - val reloadIntent = view.refresh + //refresh + val refreshIntent = view.refresh .observeOn(rxSchedulers.io) - .filter { _ -> state.loading != RELOAD } + .filter { _ -> state.loading != REFRESH } .flatMap { unit -> refreshPage() } //details val detailsIntent = view.showDetails @@ -55,7 +55,12 @@ class AnilistPresenter @Inject constructor( .toObservable() .map { details -> AnilistPartialState( - details = details + state.loading, + details, + state.pageState?.copy(null), + state.error, + state.loadingEnabled, + state.backoff ) } .onErrorReturn { throwable -> @@ -67,12 +72,13 @@ class AnilistPresenter @Inject constructor( ) } } + //hide details val hideDetailsIntent = view.hideDetails .filter { state.loading == NOT_LOADING } .map { AnilistPartialState(error = state.error) } - val intents = Observable.merge(loadPageIntent, reloadIntent, detailsIntent, hideDetailsIntent) + val intents = Observable.merge(loadPageIntent, refreshIntent, detailsIntent, hideDetailsIntent) intents.scan(state, ::animeStateReducer) .subscribe { s -> state = s @@ -122,7 +128,7 @@ class AnilistPresenter @Inject constructor( pageState = PageState(page.anime, page.currentPage, page.hasNextPage) ) } - .startWith(AnilistPartialState(loading = RELOAD)) + .startWith(AnilistPartialState(loading = REFRESH)) .onErrorReturn { throwable -> AnilistPartialState( error = when(throwable) { @@ -148,11 +154,15 @@ class AnilistPresenter @Inject constructor( changes.loadingEnabled, changes.backoff ) - RELOAD -> + REFRESH -> previousState.copy( changes.loading, changes.details, - changes.pageState?.copy(), + if(changes.details == null) { + changes.pageState?.copy() + } else { + previousState.pageState?.copy() + }, changes.error, changes.loadingEnabled, changes.backoff diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt index a22c541..771299a 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt @@ -21,7 +21,7 @@ data class AnilistViewState( enum class LoadingType { PAGE, - RELOAD, + REFRESH, NOT_LOADING } } \ No newline at end of file From e83a58996899c3c2d937421e8e39624ad98e3151 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 14 Jun 2021 11:21:59 +0300 Subject: [PATCH 05/13] test an opening details while refreshing --- .../anilist/AnilistPresenterTest.kt | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index 7148414..8742474 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -4,11 +4,13 @@ import com.huchihaitachi.anilist.presentation.AnilistPresenter import com.huchihaitachi.anilist.presentation.AnilistView import com.huchihaitachi.anilist.presentation.AnilistViewState import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.RELOAD +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.REFRESH import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState import com.huchihaitachi.base.RxSchedulers import com.huchihaitachi.domain.Anime import com.huchihaitachi.domain.Page +import com.huchihaitachi.domain.Season.FALL +import com.huchihaitachi.domain.Type import com.huchihaitachi.usecase.GetStringResourceUseCase import com.huchihaitachi.usecase.LoadAnimeUseCase import com.huchihaitachi.usecase.LoadPageUseCase @@ -72,7 +74,7 @@ class AnilistPresenterTest { } @Test - fun `reload test`() { + fun `refresh test`() { val initialState = AnilistViewState(pageState = PageState(anime, 1, true)) val presenter = AnilistPresenter( loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers @@ -89,7 +91,7 @@ class AnilistPresenterTest { presenter.bindIntents() val expectedInitial = AnilistViewState(pageState = PageState(anime, 1, true)) val expectedLoading = AnilistViewState( - loading = RELOAD, + loading = REFRESH, pageState = PageState(anime, 1, true), ) val expectedResult = AnilistViewState(pageState = PageState(freshAnime, 1, true)) @@ -101,6 +103,53 @@ class AnilistPresenterTest { } } + @Test + fun `show details while refreshing`() { + val initialState = AnilistViewState( + loading = REFRESH, + pageState = PageState(anime, 1, true) + ) + val presenter = AnilistPresenter( + loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers + ) + val showDetailsIntentMock: PublishSubject = PublishSubject.create() + mockIntents(showDetailsIntent = showDetailsIntentMock) + val animeDetails = Anime( + 1, + "title", + Type.ANIME, + "test description", + FALL, + 1998, + 22, + 17, + "image/test/url", + "banner/test/url", + 0, + 0 + ) + val expectedDetails = animeDetails.copy() + every { loadAnime(any()) } returns Single.just(animeDetails) + presenter.bind(view) + presenter.bindIntents() + showDetailsIntentMock.onNext(1) + val expectedInitial = AnilistViewState( + loading = REFRESH, + pageState = PageState(anime, 1, true) + ) + val expectedResult = AnilistViewState( + REFRESH, + expectedDetails, + PageState(anime, 1, true) + ) + verifyOrder { + view.render(expectedInitial) + view.render(expectedResult) + } + } + + //TODO: test backoff + private fun mockIntents( loadPageIntent: PublishSubject = PublishSubject.create(), refreshIntent: PublishSubject = PublishSubject.create(), From aea74c0536471ab812a8151819f910347e8094c2 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 14 Jun 2021 17:34:33 +0300 Subject: [PATCH 06/13] fix duplicating initial state emission by turning scan() output to hot observable --- anilist/build.gradle | 1 - .../huchihaitachi/anilist/presentation/AnilistPresenter.kt | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/anilist/build.gradle b/anilist/build.gradle index ede4488..360854a 100644 --- a/anilist/build.gradle +++ b/anilist/build.gradle @@ -42,7 +42,6 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.5.0' implementation 'androidx.appcompat:appcompat:1.3.0' diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt index 4a0ff9f..9569be3 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt @@ -16,6 +16,7 @@ import com.huchihaitachi.usecase.LoadPageUseCase import com.huchihaitachi.usecase.RefreshPageUseCase import io.reactivex.Observable import io.reactivex.ObservableSource +import io.reactivex.subjects.PublishSubject import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.pow @@ -78,11 +79,11 @@ class AnilistPresenter @Inject constructor( .map { AnilistPartialState(error = state.error) } + val hotState: PublishSubject = PublishSubject.create() val intents = Observable.merge(loadPageIntent, refreshIntent, detailsIntent, hideDetailsIntent) intents.scan(state, ::animeStateReducer) - .subscribe { s -> - state = s - } + .subscribe(hotState) + hotState.subscribe { s -> state = s } .let(disposables::add) } } From c33ec14346d4ec319b4a32a576973c23888c71d5 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 14 Jun 2021 19:20:46 +0300 Subject: [PATCH 07/13] emit initial state --- base/src/main/java/com/huchihaitachi/base/BasePresenter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/com/huchihaitachi/base/BasePresenter.kt b/base/src/main/java/com/huchihaitachi/base/BasePresenter.kt index 1be2f27..cefbf65 100644 --- a/base/src/main/java/com/huchihaitachi/base/BasePresenter.kt +++ b/base/src/main/java/com/huchihaitachi/base/BasePresenter.kt @@ -7,14 +7,14 @@ abstract class BasePresenter, S : BaseViewState>( initialState: S, protected val rxSchedulers: RxSchedulers ) { - protected var state: S = initialState + private val observableState: BehaviorSubject = BehaviorSubject.create() + protected var state: S = initialState.also(observableState::onNext) set(value) { field = value observableState.onNext(value) } protected val disposables: CompositeDisposable = CompositeDisposable() protected var view: V? = null - private val observableState: BehaviorSubject = BehaviorSubject.create() fun bind(view: V) { this.view = view From 8eda3c0c3e1695e11258527b95324e44e0853a89 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 14 Jun 2021 20:24:28 +0300 Subject: [PATCH 08/13] get rid of reducer function --- .../anilist/presentation/AnilistPresenter.kt | 120 ++++++------------ 1 file changed, 39 insertions(+), 81 deletions(-) diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt index 9569be3..2ee619c 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt @@ -54,18 +54,9 @@ class AnilistPresenter @Inject constructor( .switchMap { id -> loadAnimeUseCase(id) .toObservable() - .map { details -> - AnilistPartialState( - state.loading, - details, - state.pageState?.copy(null), - state.error, - state.loadingEnabled, - state.backoff - ) - } + .map { details -> state.copy(details = details) } .onErrorReturn { throwable -> - AnilistPartialState( + state.copy( error = when (throwable) { is ApolloNetworkException -> getStringResourceUseCase(R.string.no_connection) else -> throwable.message @@ -76,30 +67,35 @@ class AnilistPresenter @Inject constructor( //hide details val hideDetailsIntent = view.hideDetails .filter { state.loading == NOT_LOADING } - .map { - AnilistPartialState(error = state.error) - } - val hotState: PublishSubject = PublishSubject.create() - val intents = Observable.merge(loadPageIntent, refreshIntent, detailsIntent, hideDetailsIntent) - intents.scan(state, ::animeStateReducer) - .subscribe(hotState) - hotState.subscribe { s -> state = s } + .map { state.copy(details = null) } + Observable.merge(loadPageIntent, refreshIntent, detailsIntent, hideDetailsIntent) + .subscribe { s -> state = s } .let(disposables::add) } } - private fun loadPage(pageNum: Int): Observable = + private fun loadPage(pageNum: Int): Observable = loadPageUseCase(pageNum, PER_PAGE) .toObservable() .map { page -> - AnilistPartialState( - pageState = PageState(page.anime, page.currentPage, page.hasNextPage) + state.copy( + loading = NOT_LOADING, + pageState = PageState( + mutableListOf().apply { + state.pageState?.anime?.let(::addAll) + page.anime?.let(::addAll) + }, + page.currentPage, + page.hasNextPage + ), + loadingEnabled = page.hasNextPage ?: true, + backoff = 0 ) } - .startWith(AnilistPartialState(loading = PAGE)) + .startWith(state.copy(loading = PAGE)) .onErrorResumeNext(::loadPageErrorHandler) - private fun loadPageErrorHandler(throwable: Throwable): ObservableSource = + private fun loadPageErrorHandler(throwable: Throwable): ObservableSource = when (throwable) { is ApolloNetworkException -> Observable.timer( @@ -110,28 +106,39 @@ class AnilistPresenter @Inject constructor( }, TimeUnit.MILLISECONDS ) - .map { AnilistPartialState() } + .map { state.copy( + error = null, + loadingEnabled = true + ) } .startWith( - AnilistPartialState( + state.copy( + loading = NOT_LOADING, error = getStringResourceUseCase(R.string.no_connection), loadingEnabled = false, backoff = state.backoff + 1 ) ) - else -> Observable.just(AnilistPartialState(error = throwable.message)) + else -> Observable.just(state.copy( + loading = NOT_LOADING, + error = throwable.message) + ) } - private fun refreshPage(): Observable = + private fun refreshPage(): Observable = refreshPageUseCase(PER_PAGE) .toObservable() .map { page -> - AnilistPartialState( - pageState = PageState(page.anime, page.currentPage, page.hasNextPage) + state.copy( + loading = NOT_LOADING, + pageState = PageState(page.anime, page.currentPage, page.hasNextPage), + loadingEnabled = page.hasNextPage ?: true, + backoff = 0 ) } - .startWith(AnilistPartialState(loading = REFRESH)) + .startWith(state.copy(loading = REFRESH)) .onErrorReturn { throwable -> - AnilistPartialState( + state.copy( + loading = NOT_LOADING, error = when(throwable) { is ApolloNetworkException -> getStringResourceUseCase(R.string.no_connection) else -> throwable.message @@ -139,55 +146,6 @@ class AnilistPresenter @Inject constructor( ) } - private fun animeStateReducer(previousState: AnilistViewState, changes: AnilistPartialState) = - if (changes.error == null) { - when (previousState.loading) { - PAGE -> previousState.copy( - changes.loading, - changes.details, - changes.pageState?.copy( - mutableListOf().apply { - previousState.pageState?.anime?.let(::addAll) - changes.pageState.anime?.let(::addAll) - } - ), - changes.error, - changes.loadingEnabled, - changes.backoff - ) - REFRESH -> - previousState.copy( - changes.loading, - changes.details, - if(changes.details == null) { - changes.pageState?.copy() - } else { - previousState.pageState?.copy() - }, - changes.error, - changes.loadingEnabled, - changes.backoff - ) - NOT_LOADING -> previousState.copy( - changes.loading, - changes.details, - previousState.pageState?.copy(), - changes.error, - changes.loadingEnabled, - previousState.backoff - ) - } - } else { - previousState.copy( - changes.loading, - changes.details, - previousState.pageState?.copy(), - changes.error, - changes.loadingEnabled, - changes.backoff - ) - } - companion object { const val PER_PAGE = 8 const val MAX_BACKOFF = 16000L From 76db96d57ec2632df28fc668e2e330154d0ead5b Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 14 Jun 2021 20:25:36 +0300 Subject: [PATCH 09/13] test refreshing after error and show/hide details --- .../anilist/AnilistPresenterTest.kt | 112 ++++++++++++++++-- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index 8742474..2d5e747 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -1,8 +1,10 @@ package com.huchihaitachi.anilist +import com.apollographql.apollo.exception.ApolloNetworkException import com.huchihaitachi.anilist.presentation.AnilistPresenter import com.huchihaitachi.anilist.presentation.AnilistView import com.huchihaitachi.anilist.presentation.AnilistViewState +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.REFRESH import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState @@ -20,13 +22,16 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.runs +import io.mockk.verify import io.mockk.verifyOrder import io.reactivex.Scheduler import io.reactivex.Single import io.reactivex.schedulers.Schedulers +import io.reactivex.schedulers.TestScheduler import io.reactivex.subjects.PublishSubject import org.junit.Before import org.junit.Test +import java.util.concurrent.TimeUnit.MILLISECONDS class AnilistPresenterTest { @MockK private lateinit var loadPage: LoadPageUseCase @@ -36,18 +41,20 @@ class AnilistPresenterTest { @MockK private lateinit var view: AnilistView @MockK private lateinit var rxSchedulers: RxSchedulers private lateinit var anime: List - private lateinit var testScheduler: Scheduler + private lateinit var trampoline: Scheduler + private lateinit var testSchedulerIo: TestScheduler + private lateinit var testSchedulerUi: TestScheduler @Before fun setup() { MockKAnnotations.init(this) - setupSchedulers() setupView() anime = listOf(Anime(1), Anime(2), Anime(3), Anime(4), Anime(5), Anime(6)) } @Test fun `load first page test`() { + setupTrampoline() val initialState = AnilistViewState(pageState = PageState(null, 0, true)) val presenter = AnilistPresenter( loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers @@ -75,6 +82,7 @@ class AnilistPresenterTest { @Test fun `refresh test`() { + setupTrampoline() val initialState = AnilistViewState(pageState = PageState(anime, 1, true)) val presenter = AnilistPresenter( loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers @@ -104,7 +112,8 @@ class AnilistPresenterTest { } @Test - fun `show details while refreshing`() { + fun `show details while refreshing test`() { + setupTrampoline() val initialState = AnilistViewState( loading = REFRESH, pageState = PageState(anime, 1, true) @@ -148,7 +157,88 @@ class AnilistPresenterTest { } } - //TODO: test backoff + @Test + fun `show details while loading page after error test`() { + setupTrampoline() + val initialState = AnilistViewState( + loading = NOT_LOADING, + pageState = PageState(anime, 1, true)) + every { getStringResource(R.string.no_connection) } returns "No connection" + val presenter = AnilistPresenter( + loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers + ) + val loadPageIntentMock: PublishSubject = PublishSubject.create() + val showDetailsIntentMock: PublishSubject = PublishSubject.create() + val hideDetailsIntent: PublishSubject = PublishSubject.create() + mockIntents( + loadPageIntent = loadPageIntentMock, + showDetailsIntent = showDetailsIntentMock, + hideDetailsIntent = hideDetailsIntent + ) + val details = Anime( + 1, + "title", + Type.ANIME, + "test description", + FALL, + 1998, + 22, + 17, + "image/test/url", + "banner/test/url", + 0, + 0 + ) + val expectedDetails = details.copy() + every { loadPage(any(), any()) } returns Single.error(ApolloNetworkException("no connection")) + every { loadAnime(any()) } returns Single.just(details) + presenter.bind(view) + presenter.bindIntents() + loadPageIntentMock.onNext(Unit) + showDetailsIntentMock.onNext(1) + hideDetailsIntent.onNext(Unit) + loadPageIntentMock.onNext(Unit) + + val expectedInitial = AnilistViewState( + loading = NOT_LOADING, + pageState = PageState(anime, 1, true) + ) + //loading + val expectedSecondState = AnilistViewState( + loading = PAGE, + pageState = PageState(anime, 1, true) + ) + //error + val expectedThirdState = AnilistViewState( + pageState = PageState(anime, 1, true), + error = "No connection", + loadingEnabled = false, + backoff = 1 + ) + //view details while error + val expectedFourthState = AnilistViewState( + pageState = PageState(anime, 1, true), + details = expectedDetails, + error = "No connection", + loadingEnabled = false, + backoff = 1 + ) + //hide details while error + val expectedFifthState = AnilistViewState( + pageState = PageState(anime, 1, true), + error = "No connection", + loadingEnabled = false, + backoff = 1 + ) + verifyOrder { + view.render(expectedInitial) + view.render(expectedSecondState) + view.render(expectedThirdState) + view.render(expectedFourthState) + view.render(expectedFifthState) + } + verify(exactly = 5) { view.render(any()) } + } private fun mockIntents( loadPageIntent: PublishSubject = PublishSubject.create(), @@ -162,10 +252,16 @@ class AnilistPresenterTest { every { view.hideDetails } returns hideDetailsIntent } - private fun setupSchedulers() { - testScheduler = Schedulers.trampoline() - every { rxSchedulers.io } answers { testScheduler } - every { rxSchedulers.ui } answers { testScheduler } + private fun setupTrampoline() { + trampoline = Schedulers.trampoline() + every { rxSchedulers.io } answers { trampoline } + every { rxSchedulers.ui } answers { trampoline } + } + + private fun setupTestScheduler() { + testSchedulerIo = TestScheduler() + every { rxSchedulers.io } answers { testSchedulerIo } + every { rxSchedulers.ui } answers { testSchedulerUi } } private fun setupView() { From a43371452b5f1ae6549e1a1f50a2e1b0338e8f52 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Tue, 15 Jun 2021 19:10:07 +0300 Subject: [PATCH 10/13] fun define scheduler for the rx timer() explicitly --- .../com/huchihaitachi/anilist/presentation/AnilistPresenter.kt | 3 ++- base/src/main/java/com/huchihaitachi/base/RxSchedulers.kt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt index 2ee619c..1291ac7 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt @@ -104,7 +104,8 @@ class AnilistPresenter @Inject constructor( } else { MAX_BACKOFF }, - TimeUnit.MILLISECONDS + TimeUnit.MILLISECONDS, + rxSchedulers.computation ) .map { state.copy( error = null, diff --git a/base/src/main/java/com/huchihaitachi/base/RxSchedulers.kt b/base/src/main/java/com/huchihaitachi/base/RxSchedulers.kt index 2ac4797..8b4b1aa 100644 --- a/base/src/main/java/com/huchihaitachi/base/RxSchedulers.kt +++ b/base/src/main/java/com/huchihaitachi/base/RxSchedulers.kt @@ -12,6 +12,9 @@ class RxSchedulers @Inject constructor() { val io: Scheduler get() = Schedulers.io() + val computation: Scheduler + get() = Schedulers.computation() + val ui: Scheduler get() = AndroidSchedulers.mainThread() } \ No newline at end of file From 9990abf7c0e3d6ee36c35535d0b63d258ac87873 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Tue, 15 Jun 2021 19:14:55 +0300 Subject: [PATCH 11/13] test loading page after backoff --- .../anilist/AnilistPresenterTest.kt | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index 2d5e747..aba2bea 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -31,7 +31,11 @@ import io.reactivex.schedulers.TestScheduler import io.reactivex.subjects.PublishSubject import org.junit.Before import org.junit.Test +import java.util.Collections.addAll +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.SECONDS class AnilistPresenterTest { @MockK private lateinit var loadPage: LoadPageUseCase @@ -42,8 +46,7 @@ class AnilistPresenterTest { @MockK private lateinit var rxSchedulers: RxSchedulers private lateinit var anime: List private lateinit var trampoline: Scheduler - private lateinit var testSchedulerIo: TestScheduler - private lateinit var testSchedulerUi: TestScheduler + private lateinit var testScheduler: TestScheduler @Before fun setup() { @@ -162,7 +165,8 @@ class AnilistPresenterTest { setupTrampoline() val initialState = AnilistViewState( loading = NOT_LOADING, - pageState = PageState(anime, 1, true)) + pageState = PageState(anime, 1, true) + ) every { getStringResource(R.string.no_connection) } returns "No connection" val presenter = AnilistPresenter( loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers @@ -240,6 +244,73 @@ class AnilistPresenterTest { verify(exactly = 5) { view.render(any()) } } + @Test + fun `load after backoff`() { + setupTestScheduler() + val initialState = AnilistViewState( + loading = NOT_LOADING, + pageState = PageState(anime, 1, true) + ) + every { getStringResource(R.string.no_connection) } returns "No connection" + val presenter = AnilistPresenter( + loadPage, refreshPage, loadAnime, getStringResource, initialState, rxSchedulers + ) + val loadPageIntentMock: PublishSubject = PublishSubject.create() + mockIntents(loadPageIntent = loadPageIntentMock) + every { loadPage(any(), any()) } returns Single.error(ApolloNetworkException("no connection")) + presenter.bind(view) + presenter.bindIntents() + loadPageIntentMock.onNext(Unit) + testScheduler.advanceTimeBy(3000L, MILLISECONDS) + val moreAnime = listOf( + Anime(11), Anime(21), Anime(31), Anime(41), Anime(51), Anime(61) + ) + every { loadPage(any(), any()) } returns Single.just( + Page(2, moreAnime.size, true, moreAnime, 0,0) + ) + loadPageIntentMock.onNext(Unit) + testScheduler.triggerActions() + val expectedFourthState = AnilistViewState( + pageState = PageState(anime, 1, true), + backoff = 1 + ) + val expectedFifthState = AnilistViewState( + PAGE, + pageState = PageState(anime, 1, true), + backoff = 1 + ) + val expectedAnime = listOf( + Anime(1), Anime(2), Anime(3), Anime(4), Anime(5), Anime(6), Anime(11), + Anime(21), Anime(31), Anime(41), Anime(51), Anime(61) + ) + val expectedSixthState = AnilistViewState( + pageState = PageState(expectedAnime, 2, true), + ) + val expectedInitial = AnilistViewState( + loading = NOT_LOADING, + pageState = PageState(anime, 1, true) + ) + val expectedSecondState = AnilistViewState( + loading = PAGE, + pageState = PageState(anime, 1, true) + ) + val expectedThirdState = AnilistViewState( + pageState = PageState(anime, 1, true), + error = "No connection", + loadingEnabled = false, + backoff = 1 + ) + verifyOrder { + view.render(expectedInitial) + view.render(expectedSecondState) + view.render(expectedThirdState) + view.render(expectedFourthState) + view.render(expectedFifthState) + view.render(expectedSixthState) + } + + } + private fun mockIntents( loadPageIntent: PublishSubject = PublishSubject.create(), refreshIntent: PublishSubject = PublishSubject.create(), @@ -259,9 +330,10 @@ class AnilistPresenterTest { } private fun setupTestScheduler() { - testSchedulerIo = TestScheduler() - every { rxSchedulers.io } answers { testSchedulerIo } - every { rxSchedulers.ui } answers { testSchedulerUi } + testScheduler = TestScheduler() + every { rxSchedulers.io } answers { testScheduler } + every { rxSchedulers.computation } answers { testScheduler } + every { rxSchedulers.ui } answers { testScheduler } } private fun setupView() { From eeede0bca23d49c986cd073051a20e2661d711e0 Mon Sep 17 00:00:00 2001 From: NickHuk Date: Mon, 21 Jun 2021 10:54:24 +0300 Subject: [PATCH 12/13] rename enum entry --- .../anilist/di/moule/AnilistModule.kt | 4 ++-- .../anilist/presentation/AnilistPartialState.kt | 4 ++-- .../anilist/presentation/AnilistPresenter.kt | 15 +++++++-------- .../anilist/presentation/AnilistViewState.kt | 6 +++--- .../huchihaitachi/anilist/AnilistPresenterTest.kt | 14 +++++--------- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/di/moule/AnilistModule.kt b/anilist/src/main/java/com/huchihaitachi/anilist/di/moule/AnilistModule.kt index 517b887..8b2abf0 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/di/moule/AnilistModule.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/di/moule/AnilistModule.kt @@ -2,7 +2,7 @@ package com.huchihaitachi.anilist.di.moule import com.huchihaitachi.anilist.di.scope.AnilistScope import com.huchihaitachi.anilist.presentation.AnilistViewState -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.VIEW_CONTENT import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState import dagger.Module import dagger.Provides @@ -14,7 +14,7 @@ interface AnilistModule { @AnilistScope @Provides fun provideAnilistState(): AnilistViewState = AnilistViewState( - NOT_LOADING, + VIEW_CONTENT, null, PageState(currentPage = 0, hasNextPage = true), null diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPartialState.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPartialState.kt index f465184..c138668 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPartialState.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPartialState.kt @@ -1,12 +1,12 @@ package com.huchihaitachi.anilist.presentation import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.VIEW_CONTENT import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState import com.huchihaitachi.domain.Anime data class AnilistPartialState ( - val loading: LoadingType = NOT_LOADING, + val loading: LoadingType = VIEW_CONTENT, val details: Anime? = null, val pageState: PageState? = null, val error: String? = null, diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt index 1291ac7..364b42e 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistPresenter.kt @@ -3,7 +3,7 @@ package com.huchihaitachi.anilist.presentation import com.apollographql.apollo.exception.ApolloNetworkException import com.huchihaitachi.anilist.R import com.huchihaitachi.anilist.di.scope.AnilistScope -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.VIEW_CONTENT import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.REFRESH import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState @@ -16,7 +16,6 @@ import com.huchihaitachi.usecase.LoadPageUseCase import com.huchihaitachi.usecase.RefreshPageUseCase import io.reactivex.Observable import io.reactivex.ObservableSource -import io.reactivex.subjects.PublishSubject import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.pow @@ -66,7 +65,7 @@ class AnilistPresenter @Inject constructor( } //hide details val hideDetailsIntent = view.hideDetails - .filter { state.loading == NOT_LOADING } + .filter { state.loading == VIEW_CONTENT } .map { state.copy(details = null) } Observable.merge(loadPageIntent, refreshIntent, detailsIntent, hideDetailsIntent) .subscribe { s -> state = s } @@ -79,7 +78,7 @@ class AnilistPresenter @Inject constructor( .toObservable() .map { page -> state.copy( - loading = NOT_LOADING, + loading = VIEW_CONTENT, pageState = PageState( mutableListOf().apply { state.pageState?.anime?.let(::addAll) @@ -113,14 +112,14 @@ class AnilistPresenter @Inject constructor( ) } .startWith( state.copy( - loading = NOT_LOADING, + loading = VIEW_CONTENT, error = getStringResourceUseCase(R.string.no_connection), loadingEnabled = false, backoff = state.backoff + 1 ) ) else -> Observable.just(state.copy( - loading = NOT_LOADING, + loading = VIEW_CONTENT, error = throwable.message) ) } @@ -130,7 +129,7 @@ class AnilistPresenter @Inject constructor( .toObservable() .map { page -> state.copy( - loading = NOT_LOADING, + loading = VIEW_CONTENT, pageState = PageState(page.anime, page.currentPage, page.hasNextPage), loadingEnabled = page.hasNextPage ?: true, backoff = 0 @@ -139,7 +138,7 @@ class AnilistPresenter @Inject constructor( .startWith(state.copy(loading = REFRESH)) .onErrorReturn { throwable -> state.copy( - loading = NOT_LOADING, + loading = VIEW_CONTENT, error = when(throwable) { is ApolloNetworkException -> getStringResourceUseCase(R.string.no_connection) else -> throwable.message diff --git a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt index 771299a..29fd56c 100644 --- a/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt +++ b/anilist/src/main/java/com/huchihaitachi/anilist/presentation/AnilistViewState.kt @@ -1,11 +1,11 @@ package com.huchihaitachi.anilist.presentation -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.VIEW_CONTENT import com.huchihaitachi.base.BaseViewState import com.huchihaitachi.domain.Anime data class AnilistViewState( - val loading: LoadingType = NOT_LOADING, + val loading: LoadingType = VIEW_CONTENT, val details: Anime? = null, val pageState: PageState? = null, val error: String? = null, @@ -22,6 +22,6 @@ data class AnilistViewState( enum class LoadingType { PAGE, REFRESH, - NOT_LOADING + VIEW_CONTENT } } \ No newline at end of file diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index aba2bea..be37d0b 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -4,7 +4,7 @@ import com.apollographql.apollo.exception.ApolloNetworkException import com.huchihaitachi.anilist.presentation.AnilistPresenter import com.huchihaitachi.anilist.presentation.AnilistView import com.huchihaitachi.anilist.presentation.AnilistViewState -import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.NOT_LOADING +import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.VIEW_CONTENT import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.PAGE import com.huchihaitachi.anilist.presentation.AnilistViewState.LoadingType.REFRESH import com.huchihaitachi.anilist.presentation.AnilistViewState.PageState @@ -31,11 +31,7 @@ import io.reactivex.schedulers.TestScheduler import io.reactivex.subjects.PublishSubject import org.junit.Before import org.junit.Test -import java.util.Collections.addAll -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.MILLISECONDS -import java.util.concurrent.TimeUnit.SECONDS class AnilistPresenterTest { @MockK private lateinit var loadPage: LoadPageUseCase @@ -164,7 +160,7 @@ class AnilistPresenterTest { fun `show details while loading page after error test`() { setupTrampoline() val initialState = AnilistViewState( - loading = NOT_LOADING, + loading = VIEW_CONTENT, pageState = PageState(anime, 1, true) ) every { getStringResource(R.string.no_connection) } returns "No connection" @@ -204,7 +200,7 @@ class AnilistPresenterTest { loadPageIntentMock.onNext(Unit) val expectedInitial = AnilistViewState( - loading = NOT_LOADING, + loading = VIEW_CONTENT, pageState = PageState(anime, 1, true) ) //loading @@ -248,7 +244,7 @@ class AnilistPresenterTest { fun `load after backoff`() { setupTestScheduler() val initialState = AnilistViewState( - loading = NOT_LOADING, + loading = VIEW_CONTENT, pageState = PageState(anime, 1, true) ) every { getStringResource(R.string.no_connection) } returns "No connection" @@ -287,7 +283,7 @@ class AnilistPresenterTest { pageState = PageState(expectedAnime, 2, true), ) val expectedInitial = AnilistViewState( - loading = NOT_LOADING, + loading = VIEW_CONTENT, pageState = PageState(anime, 1, true) ) val expectedSecondState = AnilistViewState( From fe6ef1f8252d9b37be5cea269bc47f6d054732ce Mon Sep 17 00:00:00 2001 From: NickHuk Date: Wed, 11 Aug 2021 18:49:42 +0300 Subject: [PATCH 13/13] fix PR issues --- .../anilist/AnilistPresenterTest.kt | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt index be37d0b..60edec4 100644 --- a/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt +++ b/anilist/src/test/java/com/huchihaitachi/anilist/AnilistPresenterTest.kt @@ -66,11 +66,14 @@ class AnilistPresenterTest { presenter.bind(view) presenter.bindIntents() val expectedInitial = AnilistViewState(pageState = PageState(null, 0, true)) - val expectedLoading = AnilistViewState( + val expectedLoading = expectedInitial.copy( loading = PAGE, pageState = PageState(null, 0, true), ) - val expectedResult = AnilistViewState(pageState = PageState(anime, 1, true)) + val expectedResult = expectedLoading.copy( + loading = VIEW_CONTENT, + pageState = PageState(anime, 1, true) + ) loadPageIntentMock.onNext(Unit) verifyOrder { view.render(expectedInitial) @@ -204,32 +207,18 @@ class AnilistPresenterTest { pageState = PageState(anime, 1, true) ) //loading - val expectedSecondState = AnilistViewState( - loading = PAGE, - pageState = PageState(anime, 1, true) - ) + val expectedSecondState = expectedInitial.copy(loading = PAGE) //error - val expectedThirdState = AnilistViewState( - pageState = PageState(anime, 1, true), + val expectedThirdState = expectedSecondState.copy( + loading = VIEW_CONTENT, error = "No connection", loadingEnabled = false, backoff = 1 ) //view details while error - val expectedFourthState = AnilistViewState( - pageState = PageState(anime, 1, true), - details = expectedDetails, - error = "No connection", - loadingEnabled = false, - backoff = 1 - ) + val expectedFourthState = expectedThirdState.copy(details = expectedDetails) //hide details while error - val expectedFifthState = AnilistViewState( - pageState = PageState(anime, 1, true), - error = "No connection", - loadingEnabled = false, - backoff = 1 - ) + val expectedFifthState = expectedFourthState.copy(details = null) verifyOrder { view.render(expectedInitial) view.render(expectedSecondState) @@ -322,6 +311,7 @@ class AnilistPresenterTest { private fun setupTrampoline() { trampoline = Schedulers.trampoline() every { rxSchedulers.io } answers { trampoline } + every { rxSchedulers.computation } answers { TestScheduler() } every { rxSchedulers.ui } answers { trampoline } }