Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/8676 scmainvm tests #9142

Merged
merged 10 commits into from
Feb 7, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

import org.wordpress.android.ui.news.LocalNewsService;
import org.wordpress.android.ui.news.NewsService;
import org.wordpress.android.ui.sitecreation.NewSiteCreationStepsProvider;
import org.wordpress.android.ui.sitecreation.SiteCreationStep;
import org.wordpress.android.ui.stats.refresh.StatsFragment;
import org.wordpress.android.ui.stats.refresh.lists.StatsListFragment;
import org.wordpress.android.util.wizard.WizardManager;
import org.wordpress.android.viewmodel.helpers.ConnectionStatus;
import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData;

Expand All @@ -33,6 +36,11 @@ public static NewsService provideLocalNewsService(Context context) {
@ContributesAndroidInjector
abstract StatsFragment contributeStatsFragment();

@Provides
public static WizardManager<SiteCreationStep> provideWizardManager(NewSiteCreationStepsProvider stepsProvider) {
return new WizardManager<>(stepsProvider.getSteps());
}

@Provides
public static LiveData<ConnectionStatus> provideConnectionStatusLiveData(Context context) {
return new ConnectionStatusLiveData(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ import javax.inject.Inject

private const val KEY_CURRENT_STEP = "key_current_step"
private const val KEY_SITE_CREATION_STATE = "key_site_creation_state"
private val SITE_CREATION_STEPS =
listOf(
SiteCreationStep.fromString("site_creation_segments"),
SiteCreationStep.fromString("site_creation_verticals"),
SiteCreationStep.fromString("site_creation_site_info"),
SiteCreationStep.fromString("site_creation_domains"),
SiteCreationStep.fromString("site_creation_site_preview")
)

@Parcelize
@SuppressLint("ParcelCreator")
Expand All @@ -44,9 +36,12 @@ data class SiteCreationState(

typealias NavigationTarget = WizardNavigationTarget<SiteCreationStep, SiteCreationState>

class NewSiteCreationMainVM @Inject constructor(private val tracker: NewSiteCreationTracker) : ViewModel() {
class NewSiteCreationMainVM @Inject constructor(
private val tracker: NewSiteCreationTracker,
private val wizardManager: WizardManager<SiteCreationStep>
) : ViewModel() {
private var isStarted = false
private lateinit var wizardManager: WizardManager<SiteCreationStep>

private lateinit var siteCreationState: SiteCreationState

val navigationTargetObservable: SingleEventObservable<NavigationTarget> by lazy {
Expand All @@ -65,11 +60,10 @@ class NewSiteCreationMainVM @Inject constructor(private val tracker: NewSiteCrea
if (savedInstanceState == null) {
tracker.trackSiteCreationAccessed()
siteCreationState = SiteCreationState()
wizardManager = WizardManager(SITE_CREATION_STEPS)
} else {
siteCreationState = savedInstanceState.getParcelable(KEY_SITE_CREATION_STATE)
val currentStepIndex = savedInstanceState.getInt(KEY_CURRENT_STEP)
wizardManager = WizardManager(SITE_CREATION_STEPS, currentStepIndex)
wizardManager.setCurrentStepIndex(currentStepIndex)
}
isStarted = true
if (savedInstanceState == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.wordpress.android.ui.sitecreation

import org.wordpress.android.util.wizard.WizardStep
import javax.inject.Inject
import javax.inject.Singleton

enum class SiteCreationStep : WizardStep {
SEGMENTS, VERTICALS, SITE_INFO, DOMAINS, SITE_PREVIEW;
Expand All @@ -18,3 +20,16 @@ enum class SiteCreationStep : WizardStep {
}
}
}

@Singleton
class NewSiteCreationStepsProvider @Inject constructor() {
fun getSteps(): List<SiteCreationStep> {
return listOf(
SiteCreationStep.fromString("site_creation_segments"),
SiteCreationStep.fromString("site_creation_verticals"),
SiteCreationStep.fromString("site_creation_site_info"),
SiteCreationStep.fromString("site_creation_domains"),
SiteCreationStep.fromString("site_creation_site_preview")
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package org.wordpress.android.util.wizard
import android.arch.lifecycle.LiveData
import org.wordpress.android.viewmodel.SingleLiveEvent

private const val DEFAULT_STEP_INDEX = -1

class WizardManager<T : WizardStep>(
private val steps: List<T>,
private var currentStepIndex: Int = -1
private val steps: List<T>
) {
private var currentStepIndex: Int = DEFAULT_STEP_INDEX
val stepsCount = steps.size
val currentStep: Int
get() = currentStepIndex
Expand Down Expand Up @@ -41,6 +43,13 @@ class WizardManager<T : WizardStep>(
throw IllegalStateException("Step $T is not present.")
}
}

fun setCurrentStepIndex(stepIndex: Int) {
if (!isIndexValid(stepIndex)) {
throw IllegalStateException("Invalid index.")
}
currentStepIndex = stepIndex
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import android.arch.lifecycle.Observer
* Note that only one observer can be subscribed.
*/
class SingleEventObservable<T>(private val sourceLiveData: LiveData<T>) {
private var lastEvent: T? = null
var lastEvent: T? = null
private set

fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (sourceLiveData.hasObservers()) {
Expand All @@ -29,4 +30,16 @@ class SingleEventObservable<T>(private val sourceLiveData: LiveData<T>) {
}
})
}

fun observeForever(observer: Observer<T>) {
if (sourceLiveData.hasObservers()) {
throw IllegalStateException("SingleEventObservable can be observed only by a single observer.")
}
sourceLiveData.observeForever {
if (it !== lastEvent) {
lastEvent = it
observer.onChanged(it)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package org.wordpress.android.ui.sitecreation

import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.Observer
import android.os.Bundle
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argThat
import com.nhaarman.mockitokotlin2.clearInvocations
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.wordpress.android.ui.sitecreation.NewSiteCreationMainVM.NewSiteCreationScreenTitle.ScreenTitleEmpty
import org.wordpress.android.ui.sitecreation.NewSiteCreationMainVM.NewSiteCreationScreenTitle.ScreenTitleGeneral
import org.wordpress.android.ui.sitecreation.NewSiteCreationMainVM.NewSiteCreationScreenTitle.ScreenTitleStepCount
import org.wordpress.android.ui.sitecreation.misc.NewSiteCreationTracker
import org.wordpress.android.ui.sitecreation.previews.NewSitePreviewViewModel.CreateSiteState
import org.wordpress.android.ui.sitecreation.previews.NewSitePreviewViewModel.CreateSiteState.SiteCreationCompleted
import org.wordpress.android.util.wizard.WizardManager
import org.wordpress.android.viewmodel.SingleLiveEvent

private const val LOCAL_SITE_ID = 1
private const val SEGMENT_ID = 1L
private const val VERTICAL_ID = "m1v1"
private const val SITE_TITLE = "test title"
private const val SITE_TAG_LINE = "test tagLine"
private const val DOMAIN = "test.domain.com"
private const val STEP_COUNT = 20
private const val FIRST_STEP_INDEX = 1
private const val LAST_STEP_INDEX = STEP_COUNT

@RunWith(MockitoJUnitRunner::class)
class NewSiteCreationMainVMTest {
@Rule
@JvmField val rule = InstantTaskExecutorRule()

@Mock lateinit var tracker: NewSiteCreationTracker
@Mock lateinit var navigationTargetObserver: Observer<NavigationTarget>
@Mock lateinit var wizardFinishedObserver: Observer<CreateSiteState>
@Mock lateinit var savedInstanceState: Bundle
@Mock lateinit var wizardManager: WizardManager<SiteCreationStep>
@Mock lateinit var siteCreationStep: SiteCreationStep
private val wizardManagerNavigatorLiveData = SingleLiveEvent<SiteCreationStep>()

private lateinit var viewModel: NewSiteCreationMainVM

@Before
fun setUp() {
whenever(wizardManager.navigatorLiveData).thenReturn(wizardManagerNavigatorLiveData)
whenever(wizardManager.showNextStep()).then {
wizardManagerNavigatorLiveData.value = siteCreationStep
Unit
}
viewModel = NewSiteCreationMainVM(tracker, wizardManager)
viewModel.start(null)
viewModel.navigationTargetObservable.observeForever(navigationTargetObserver)
viewModel.wizardFinishedObservable.observeForever(wizardFinishedObserver)
whenever(wizardManager.stepsCount).thenReturn(STEP_COUNT)
// clear invocations since viewModel.start() calls wizardManager.showNextStep
clearInvocations(wizardManager)
}

@Test
fun skipClickedResultsInNextStep() {
viewModel.onSkipClicked()
verify(wizardManager).showNextStep()
}

@Test
fun segmentSelectedResultsInNextStep() {
viewModel.onSegmentSelected(SEGMENT_ID)
verify(wizardManager).showNextStep()
}

@Test
fun verticalSelectedResultsInNextStep() {
viewModel.onVerticalsScreenFinished(VERTICAL_ID)
verify(wizardManager).showNextStep()
}

@Test
fun siteInfoFinishedResultsInNextStep() {
viewModel.onInfoScreenFinished(SITE_TITLE, null)
verify(wizardManager).showNextStep()
}

@Test
fun domainSelectedResultsInNextStep() {
viewModel.onDomainsScreenFinished(DOMAIN)
verify(wizardManager).showNextStep()
}

@Test
fun siteCreationStateUpdatedWithSelectedSegment() {
viewModel.onSegmentSelected(SEGMENT_ID)
assertThat(currentWizardState(viewModel).segmentId).isEqualTo(SEGMENT_ID)
}

@Test
fun siteCreationStateUpdatedWithSelectedVertical() {
viewModel.onVerticalsScreenFinished(VERTICAL_ID)
assertThat(currentWizardState(viewModel).verticalId).isEqualTo(VERTICAL_ID)
}

@Test
fun siteCreationStateUpdatedWithSiteInfo() {
viewModel.onInfoScreenFinished(
SITE_TITLE,
SITE_TAG_LINE
)
assertThat(currentWizardState(viewModel).siteTitle).isEqualTo(SITE_TITLE)
assertThat(currentWizardState(viewModel).siteTagLine).isEqualTo(SITE_TAG_LINE)
}

@Test
fun siteCreationStateUpdatedWithSelectedDomain() {
viewModel.onDomainsScreenFinished(DOMAIN)
assertThat(currentWizardState(viewModel).domain).isEqualTo(DOMAIN)
}

@Test
fun wizardFinishedInvokedOnSitePreviewCompleted() {
val state = SiteCreationCompleted(LOCAL_SITE_ID)
viewModel.onSitePreviewScreenFinished(state)

val captor = ArgumentCaptor.forClass(CreateSiteState::class.java)
verify(wizardFinishedObserver).onChanged(captor.capture())

assertThat(captor.value).isEqualTo(state)
}

@Test
fun onBackPressedPropagatedToWizardManager() {
viewModel.onBackPressed()
verify(wizardManager).onBackPressed()
}

@Test
fun backNotSuppressedWhenNotLastStep() {
whenever(wizardManager.isLastStep()).thenReturn(false)
assertThat(viewModel.shouldSuppressBackPress()).isFalse()
}

@Test
fun backSuppressedForLastStep() {
whenever(wizardManager.isLastStep()).thenReturn(true)
assertThat(viewModel.shouldSuppressBackPress()).isTrue()
}

@Test
fun titleForFirstStepIsGeneralSiteCreation() {
whenever(wizardManager.stepPosition(siteCreationStep)).thenReturn(FIRST_STEP_INDEX)
assertThat(viewModel.screenTitleForWizardStep(siteCreationStep))
.isInstanceOf(ScreenTitleGeneral::class.java)
}

@Test
fun titleForLastStepIsEmptyTitle() {
whenever(wizardManager.stepPosition(siteCreationStep)).thenReturn(LAST_STEP_INDEX)
assertThat(viewModel.screenTitleForWizardStep(siteCreationStep))
.isInstanceOf(ScreenTitleEmpty::class.java)
}

@Test
fun titlesForOtherThanFirstAndLastStepIsStepCount() {
(FIRST_STEP_INDEX + 1 until STEP_COUNT).forEach { stepIndex ->
malinajirka marked this conversation as resolved.
Show resolved Hide resolved
whenever(wizardManager.stepPosition(siteCreationStep)).thenReturn(stepIndex)

assertThat(viewModel.screenTitleForWizardStep(siteCreationStep))
.isInstanceOf(ScreenTitleStepCount::class.java)
}
}

@Test
fun siteCreationStateWrittenToBundle() {
viewModel.writeToBundle(savedInstanceState)
verify(savedInstanceState).putParcelable(any(), argThat { this is SiteCreationState })
}

@Test
fun siteCreationStateRestored() {
val expectedState = SiteCreationState()
whenever(savedInstanceState.getParcelable<SiteCreationState>("key_site_creation_state"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be great if we can use the current constants in NewSiteCreationMainVM for these strings to avoid tests going out of sync with the VM. I believe making those public is the lesser of two evils here and there is a big risk with having them public. What do you think?

Note that this is for key_site_creation_state, key_current_step and key_site_creation_state string values. I am keeping all this to a single comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I was thinking about this for some time. I miss "package" scope so much :(. The reason why I felt like it's better to hardcode it in tests is that having them public "pollutes" autocomplete and we can't push anything to the develop without a working unit tests anyway. However, I don't feel strongly about it - updated in 464757c

.thenReturn(expectedState)

// we need to create a new instance of the VM as the `viewModel` has already been started in setUp()
val newViewModel = NewSiteCreationMainVM(tracker, wizardManager)
newViewModel.start(savedInstanceState)

/* we need simulate navigation to the next step as wizardManager.showNextStep() isn't invoked
when the VM is restored from a savedInstanceState. */
wizardManagerNavigatorLiveData.value = siteCreationStep

newViewModel.navigationTargetObservable.observeForever(navigationTargetObserver)
assertThat(currentWizardState(newViewModel)).isSameAs(expectedState)
}

@Test
fun siteCreationStepIndexRestored() {
val index = 17
whenever(savedInstanceState.getInt("key_current_step")).thenReturn(index)

// siteCreationState is not nullable - we need to set it
whenever(savedInstanceState.getParcelable<SiteCreationState>("key_site_creation_state"))
.thenReturn(SiteCreationState())

// we need to create a new instance of the VM as the `viewModel` has already been started in setUp()
val newViewModel = NewSiteCreationMainVM(tracker, wizardManager)
newViewModel.start(savedInstanceState)

verify(wizardManager).setCurrentStepIndex(index)
}

private fun currentWizardState(vm: NewSiteCreationMainVM) =
vm.navigationTargetObservable.lastEvent!!.wizardState
}