Skip to content

Commit

Permalink
Increase code coverage (pt4).
Browse files Browse the repository at this point in the history
  • Loading branch information
Laimiux committed Sep 17, 2024
1 parent 819e5de commit 693dacd
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.instacart.formula.android.BackCallback
import com.instacart.formula.android.FormulaFragment
import com.instacart.formula.android.FragmentEnvironment
import com.instacart.formula.android.FragmentStore
import com.instacart.formula.android.events.FragmentLifecycleEvent
import com.instacart.formula.test.TestBackCallbackRenderModel
import com.instacart.formula.test.TestKey
import com.instacart.formula.test.TestKeyWithId
Expand Down Expand Up @@ -43,6 +44,7 @@ class FormulaFragmentTest {
private var onPreCreated: (TestFragmentActivity) -> Unit = {}
private var updateThreads = linkedSetOf<Thread>()
private val errors = mutableListOf<Throwable>()
private val fragmentLifecycleEvents = mutableListOf<FragmentLifecycleEvent>()
private val formulaRule = TestFormulaRule(
initFormula = { app ->
val environment = FragmentEnvironment(
Expand Down Expand Up @@ -74,6 +76,9 @@ class FormulaFragmentTest {
stateChanges(it)
}
))
},
onFragmentLifecycleEvent = {
fragmentLifecycleEvents.add(it)
}
)
}
Expand All @@ -83,6 +88,7 @@ class FormulaFragmentTest {
cleanUp = {
lastState = null
updateThreads = linkedSetOf()
fragmentLifecycleEvents.clear()
}
)

Expand All @@ -104,6 +110,11 @@ class FormulaFragmentTest {
navigateBack()

assertThat(activeContracts()).containsExactly(TestKey()).inOrder()

assertThat(fragmentLifecycleEvents).hasSize(3)
assertThat(fragmentLifecycleEvents[0]).isInstanceOf(FragmentLifecycleEvent.Added::class.java)
assertThat(fragmentLifecycleEvents[1]).isInstanceOf(FragmentLifecycleEvent.Added::class.java)
assertThat(fragmentLifecycleEvents[2]).isInstanceOf(FragmentLifecycleEvent.Removed::class.java)
}

@Test fun `navigating forward should have both keys in backstack`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class FragmentLifecycleTest {
}
}
bind(featureFactory)
}
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.instacart.formula.android.FragmentId
* Models when a fragment key is attached and detached. Provides a way to indicate
* when to initialize state stream and when to destroy it.
*/
sealed class FragmentLifecycleEvent() {
sealed class FragmentLifecycleEvent {

abstract val fragmentId: FragmentId

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.instacart.formula.android.internal

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import com.instacart.formula.android.FeatureView
import com.instacart.formula.android.ViewFactory
import com.instacart.formula.android.FeatureEvent
Expand All @@ -18,9 +19,22 @@ internal class FormulaFragmentViewFactory(
private var factory: ViewFactory<Any>? = null

override fun create(inflater: LayoutInflater, container: ViewGroup?): FeatureView<Any> {
val viewFactory = viewFactory()
val delegate = environment.fragmentDelegate
return delegate.createView(fragmentId, viewFactory, inflater, container)
}

@VisibleForTesting
internal fun viewFactory(): ViewFactory<Any> {
return factory ?: findViewFactory().apply {
factory = this
}
}

private fun findViewFactory(): ViewFactory<Any> {
val key = fragmentId.key
val featureEvent = featureProvider.getFeature(fragmentId) ?: throw IllegalStateException("Could not find feature for $key.")
val viewFactory = factory ?: when (featureEvent) {
return when (featureEvent) {
is FeatureEvent.MissingBinding -> {
throw IllegalStateException("Missing feature factory or integration for $key. Please check your FragmentStore configuration.")
}
Expand All @@ -31,8 +45,5 @@ internal class FormulaFragmentViewFactory(
featureEvent.feature.viewFactory
}
}
this.factory = viewFactory
val delegate = environment.fragmentDelegate
return delegate.createView(fragmentId, viewFactory, inflater, container)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ internal class FragmentFlowRenderView(
) {

private var fragmentState: FragmentState? = null
private var features: Map<FragmentId, FeatureEvent> = emptyMap()
private val visibleFragments: LinkedList<Fragment> = LinkedList()

private val featureProvider = object : FeatureProvider {
override fun getFeature(id: FragmentId): FeatureEvent? {
return fragmentState?.features?.get(id)
return features[id]
}
}

Expand All @@ -53,10 +54,7 @@ internal class FragmentFlowRenderView(
super.onFragmentViewCreated(fm, f, v, savedInstanceState)

visibleFragments.add(f)

fragmentState?.let {
updateVisibleFragments(it)
}
updateVisibleFragments()

onFragmentViewStateChanged(f.getFormulaFragmentId(), true)
notifyLifecycleStateChanged(f, Lifecycle.State.CREATED)
Expand Down Expand Up @@ -93,7 +91,10 @@ internal class FragmentFlowRenderView(
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
if (FragmentLifecycle.shouldTrack(f)) {
onLifecycleEvent(FragmentLifecycle.createAddedEvent(f))
val event = FragmentLifecycleEvent.Added(
fragmentId = f.getFormulaFragmentId(),
)
onLifecycleEvent(event)
} else {
fragmentEnvironment.logger("Ignoring attach event for fragment: $f")
}
Expand All @@ -103,8 +104,12 @@ internal class FragmentFlowRenderView(
super.onFragmentDetached(fm, f)

// Only trigger detach, when fragment is actually being removed from the backstack
if (FragmentLifecycle.shouldTrack(f) && !FragmentLifecycle.isKept(fm, f)) {
val event = FragmentLifecycle.createRemovedEvent(f)
if (FragmentLifecycle.shouldTrack(f) && f.isRemoving) {
val formulaFragment = f as? BaseFormulaFragment<*>
val event = FragmentLifecycleEvent.Removed(
fragmentId = f.getFormulaFragmentId(),
lastState = formulaFragment?.currentState(),
)
onLifecycleEvent(event)
}
}
Expand All @@ -118,7 +123,8 @@ internal class FragmentFlowRenderView(
Utils.assertMainThread()

fragmentState = state
updateVisibleFragments(state)
features = state.features
updateVisibleFragments()
}

fun onBackPressed(): Boolean {
Expand Down Expand Up @@ -149,7 +155,8 @@ internal class FragmentFlowRenderView(
onLifecycleState.invoke(fragment.getFormulaFragmentId(), newState)
}

private fun updateVisibleFragments(state: FragmentState) {
private fun updateVisibleFragments() {
val state = fragmentState ?: return
visibleFragments.forEachIndices { fragment ->
if (fragment is BaseFormulaFragment<*>) {
state.outputs[fragment.getFormulaFragmentId()]?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package com.instacart.formula.android.internal
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentInspector
import androidx.fragment.app.FragmentManager
import com.instacart.formula.android.FragmentId
import com.instacart.formula.android.FragmentKey
import com.instacart.formula.android.BaseFormulaFragment
import com.instacart.formula.android.FormulaFragment
import com.instacart.formula.android.events.FragmentLifecycleEvent
import java.util.UUID

/**
Expand All @@ -19,19 +17,6 @@ internal object FragmentLifecycle {
internal fun shouldTrack(fragment: Fragment): Boolean {
return !fragment.retainInstance && !FragmentInspector.isHeadless(fragment)
}

internal fun isKept(fragmentManager: FragmentManager, fragment: Fragment): Boolean {
return !fragment.isRemoving
}

internal fun createAddedEvent(f: Fragment): FragmentLifecycleEvent.Added {
return FragmentLifecycleEvent.Added(f.getFormulaFragmentId())
}

internal fun createRemovedEvent(f: Fragment): FragmentLifecycleEvent.Removed {
val fragment = f as? BaseFormulaFragment<*>
return FragmentLifecycleEvent.Removed(f.getFormulaFragmentId(), fragment?.currentState())
}
}

private fun Fragment.getFragmentKey(): FragmentKey {
Expand All @@ -47,12 +32,10 @@ private fun Fragment.getFragmentInstanceId(): String {
return if (this is BaseFormulaFragment<*>) {
val arguments = getOrSetArguments()
val id = arguments.getString(FormulaFragment.ARG_FORMULA_ID, "")
if (id.isNullOrBlank()) {
id.ifBlank {
val initializedId = UUID.randomUUID().toString()
arguments.putString(FormulaFragment.ARG_FORMULA_ID, initializedId)
initializedId
} else {
id
}
} else {
""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.instacart.formula.android.internal

import com.google.common.truth.Truth
import com.instacart.formula.android.Feature
import com.instacart.formula.android.FeatureEvent
import com.instacart.formula.android.FragmentEnvironment
import com.instacart.formula.android.FragmentId
import com.instacart.formula.android.ViewFactory
import io.reactivex.rxjava3.core.Observable
import org.junit.Test
import java.lang.RuntimeException

class FormulaFragmentViewFactoryTest {

@Test fun `throws an exception if feature provider returns null`() {
val viewFactory = viewFactory { null }
val result = runCatching { viewFactory.viewFactory() }
Truth.assertThat(result.exceptionOrNull()).hasMessageThat().contains(
"Could not find feature for "
)
}

@Test fun `throws an exception if feature is not registered`() {
val viewFactory = viewFactory {
FeatureEvent.MissingBinding(it)
}
val result = runCatching { viewFactory.viewFactory() }
Truth.assertThat(result.exceptionOrNull()).hasMessageThat().contains(
"Missing feature factory or integration for"
)
}

@Test fun `throws an exception if feature failed to initialize`() {
val viewFactory = viewFactory {
FeatureEvent.Failure(it, RuntimeException("Something went wrong"))
}
val result = runCatching { viewFactory.viewFactory() }
Truth.assertThat(result.exceptionOrNull()).hasMessageThat().contains(
"Feature failed to initialize:"
)
}

@Test fun `only initializes the view factory once`() {
var timesCalled = 0
val viewFactory = viewFactory {
timesCalled += 1
val feature = Feature(
state = Observable.empty(),
viewFactory = ViewFactory { _, _ ->
error("should not be called")
}
)
FeatureEvent.Init(it, feature)
}

viewFactory.viewFactory()
viewFactory.viewFactory()
viewFactory.viewFactory()
viewFactory.viewFactory()
Truth.assertThat(timesCalled).isEqualTo(1)
}

private fun viewFactory(
delegateGetFeature: (FragmentId) -> FeatureEvent?,
): FormulaFragmentViewFactory {
return FormulaFragmentViewFactory(
environment = FragmentEnvironment(),
fragmentId = FragmentId(
instanceId = "instanceId",
key = EmptyFragmentKey(tag = "tag")
),
featureProvider = object : FeatureProvider {
override fun getFeature(id: FragmentId): FeatureEvent? {
return delegateGetFeature(id)
}
}
)
}
}

0 comments on commit 693dacd

Please sign in to comment.