Skip to content

Commit

Permalink
Increase formula-android code coverage.
Browse files Browse the repository at this point in the history
  • Loading branch information
Laimiux committed Sep 9, 2024
1 parent 3c093b8 commit b568c21
Show file tree
Hide file tree
Showing 18 changed files with 281 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,51 @@ class TestFragmentLifecycleCallback : FragmentLifecycleCallback {
var hasOnStop = false
var hasOnSaveInstanceState = false
var hasOnDestroyView = false
var hasCalledLowMemory = false

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
hasOnViewCreated = true
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
hasOnActivityCreated = true
}

override fun onStart() {
super.onStart()
hasOnStart = true
}

override fun onResume() {
super.onResume()
hasOnResume = true
}

// teardown
override fun onPause() {
super.onPause()
hasOnPauseEvent = true
}

override fun onStop() {
super.onStop()
hasOnStop = true
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
hasOnSaveInstanceState = true
}

override fun onDestroyView() {
hasOnDestroyView
super.onDestroyView()
hasOnDestroyView = true
}

override fun onLowMemory() {
super.onLowMemory()
hasCalledLowMemory = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import kotlinx.parcelize.Parcelize

@Parcelize
data class TestLifecycleKey(
override val tag: String = "task list",
override val tag: String = "test-lifecycle",
) : FragmentKey
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.instacart.formula.android.ActivityStore
import com.instacart.formula.android.FragmentState
import com.instacart.formula.android.FragmentKey
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.test.TestKey
import com.instacart.formula.test.TestKeyWithId
import com.instacart.formula.test.TestFragmentActivity
import com.instacart.formula.test.TestLifecycleKey
import com.jakewharton.rxrelay3.PublishRelay
import io.reactivex.rxjava3.core.Observable
import org.junit.Before
Expand All @@ -29,17 +33,23 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

@RunWith(AndroidJUnit4::class)
class FragmentFlowRenderViewTest {
class FormulaFragmentTest {

class HeadlessFragment : Fragment()

private var lastState: FragmentState? = null
private val stateChangeRelay = PublishRelay.create<Pair<FragmentKey, Any>>()
private var onPreCreated: (TestFragmentActivity) -> Unit = {}
private var updateThreads = linkedSetOf<Thread>()
private val errors = mutableListOf<Throwable>()
private val formulaRule = TestFormulaRule(
initFormula = { app ->
FormulaAndroid.init(app) {
val environment = FragmentEnvironment(
onScreenError = { _, error ->
errors.add(error)
}
)
FormulaAndroid.init(app, environment) {
activity<TestFragmentActivity> {
ActivityStore(
configureActivity = { activity ->
Expand All @@ -53,7 +63,16 @@ class FragmentFlowRenderViewTest {
},
fragmentStore = FragmentStore.init {
bind(TestFeatureFactory<TestKey> { stateChanges(it) })
bind(TestFeatureFactory<TestKeyWithId> { stateChanges(it) })
bind(TestFeatureFactory<TestKeyWithId>(
applyOutput = { output ->
if (output == "crash") {
throw IllegalStateException("crashing")
}
},
state = {
stateChanges(it)
}
))
}
)
}
Expand All @@ -63,7 +82,8 @@ class FragmentFlowRenderViewTest {
cleanUp = {
lastState = null
updateThreads = linkedSetOf()
})
}
)

private val activityRule = ActivityScenarioRule(TestFragmentActivity::class.java)

Expand Down Expand Up @@ -255,6 +275,26 @@ class FragmentFlowRenderViewTest {
assertThat(updateThreads).containsExactly(Thread.currentThread())
}

@Test fun `notify fragment environment if setOutput throws an error`() {
val key = TestKeyWithId(1)
navigateToTaskDetail(id = key.id)

val activity = activity()
sendStateUpdate(key, "crash")
assertThat(activity.renderCalls).isNotEmpty()

assertThat(errors).hasSize(1)
}

@Test
fun toStringContainsTagAndKey() {
val fragment = FormulaFragment.newInstance(TestLifecycleKey())
val toStringValue = fragment.toString()
assertThat(toStringValue).isEqualTo(
"test-lifecycle -> TestLifecycleKey(tag=test-lifecycle)"
)
}

private fun navigateBack() {
scenario.onActivity { it.onBackPressed() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.google.common.truth.Truth.assertThat
import com.instacart.formula.android.ActivityStore
import com.instacart.formula.android.Feature
import com.instacart.formula.android.FeatureFactory
import com.instacart.formula.android.FormulaFragment
import com.instacart.formula.android.FragmentStore
import com.instacart.formula.android.ViewFactory
import com.instacart.formula.test.TestFragmentActivity
Expand All @@ -28,12 +29,14 @@ class FragmentLifecycleTest {
private lateinit var activityController: ActivityController<TestFragmentActivity>
private lateinit var lifecycleCallback: TestFragmentLifecycleCallback
private lateinit var contract: TestLifecycleKey
private lateinit var activityRef: TestFragmentActivity

@get:Rule val formulaRule = TestFormulaRule(initFormula = { app ->
FormulaAndroid.init(app) {
activity<TestFragmentActivity> {
ActivityStore(
configureActivity = { activity ->
activityRef = activity
lifecycleCallback = TestFragmentLifecycleCallback()
contract = TestLifecycleKey()
activity.initialContract = contract
Expand Down Expand Up @@ -85,6 +88,15 @@ class FragmentLifecycleTest {
assertThat(lifecycleCallback.hasOnSaveInstanceState).isTrue()
}

@Test fun `low memory`() {
val fragment = activityRef.supportFragmentManager.fragments
.filterIsInstance<FormulaFragment>()
.first()

fragment.onLowMemory()
assertThat(lifecycleCallback.hasCalledLowMemory).isTrue()
}

// Unfortunately, we cannot test destroy view with Robolectric
// https://github.com/robolectric/robolectric/issues/1945
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import com.instacart.formula.test.TestFragmentActivity
import io.reactivex.rxjava3.core.Observable

class TestFeatureFactory<Key : FragmentKey>(
private val state: (Key) -> Observable<Any>
private val applyOutput: (Any) -> Unit = {},
private val state: (Key) -> Observable<Any>,
) : FeatureFactory<Unit, Key> {
override fun initialize(dependencies: Unit, key: Key): Feature {
return Feature(
state = state(key),
viewFactory = ViewFactory.fromLayout(R.layout.test_empty_layout) {
val renderView = object : RenderView<Any> {
override val render: Renderer<Any> = Renderer {
(view.context as TestFragmentActivity).renderCalls.add(Pair(key, it))
override val render: Renderer<Any> = Renderer { value ->
(view.context as TestFragmentActivity).renderCalls.add(Pair(key, value))
applyOutput(value)
}
}
featureView(renderView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,10 @@ package com.instacart.formula.android
* Used to indicate that a screen render model
* handles back presses.
*/
interface BackCallback {
fun interface BackCallback {

/**
* Returns true if it handles back press.
*/
fun onBackPressed(): Boolean

companion object {
inline operator fun invoke(crossinline op: () -> Boolean): BackCallback {
return object : BackCallback {
override fun onBackPressed(): Boolean = op()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ import io.reactivex.rxjava3.core.Observable
class FeatureView<RenderModel>(
val view: View,
val setOutput: (RenderModel) -> Unit,
val lifecycleCallbacks: FragmentLifecycleCallback? = null,
val lifecycleCallbacks: FragmentLifecycleCallback?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlin.reflect.KClass
*/
class FeaturesBuilder<Dependencies> {
companion object {
inline fun <Dependencies> build(
fun <Dependencies> build(
init: FeaturesBuilder<Dependencies>.() -> Unit
): Features<Dependencies> {
return FeaturesBuilder<Dependencies>().apply(init).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ class FormulaFragment : Fragment(), BaseFormulaFragment<Any> {

@JvmStatic
fun newInstance(key: FragmentKey): FormulaFragment {
return FormulaFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_CONTRACT, key)
}
val fragment = FormulaFragment()
fragment.arguments = Bundle().apply {
putParcelable(ARG_CONTRACT, key)
}
FormulaAndroid.fragmentEnvironment().fragmentDelegate.onNewInstance(
fragmentId = fragment.formulaFragmentId
)
return fragment
}
}

Expand All @@ -39,26 +42,11 @@ class FormulaFragment : Fragment(), BaseFormulaFragment<Any> {
private val fragmentDelegate: FragmentEnvironment.FragmentDelegate
get() = environment.fragmentDelegate

private var calledNewInstance = false

private var featureView: FeatureView<Any>? = null
private var output: Any? = null

private val lifecycleCallback: FragmentLifecycleCallback?
get() = featureView?.lifecycleCallbacks

override fun setArguments(args: Bundle?) {
super.setArguments(args)

/**
* To ensure that we have both fragment key and formula instance id, we need
* to wait for arguments to be set.
*/
if (!calledNewInstance) {
calledNewInstance = true
fragmentDelegate.onNewInstance(formulaFragmentId)
}
}
private val lifecycleCallback: FragmentLifecycleCallback
get() = featureView?.lifecycleCallbacks ?: FragmentLifecycleCallback.NO_OP

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val viewFactory = FormulaFragmentDelegate.viewFactory(this) ?: run {
Expand All @@ -75,46 +63,46 @@ class FormulaFragment : Fragment(), BaseFormulaFragment<Any> {
super.onViewCreated(view, savedInstanceState)
tryToSetState()

lifecycleCallback?.onViewCreated(view, savedInstanceState)
lifecycleCallback.onViewCreated(view, savedInstanceState)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
lifecycleCallback?.onActivityCreated(savedInstanceState)
lifecycleCallback.onActivityCreated(savedInstanceState)
}

override fun onStart() {
super.onStart()
lifecycleCallback?.onStart()
lifecycleCallback.onStart()
}

override fun onResume() {
super.onResume()
lifecycleCallback?.onResume()
lifecycleCallback.onResume()
}

override fun onPause() {
super.onPause()
lifecycleCallback?.onPause()
lifecycleCallback.onPause()
}

override fun onStop() {
super.onStop()
lifecycleCallback?.onStop()
lifecycleCallback.onStop()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
lifecycleCallback?.onSaveInstanceState(outState)
lifecycleCallback.onSaveInstanceState(outState)
}

override fun onLowMemory() {
super.onLowMemory()
lifecycleCallback?.onLowMemory()
lifecycleCallback.onLowMemory()
}

override fun onDestroyView() {
lifecycleCallback?.onDestroyView()
lifecycleCallback.onDestroyView()
super.onDestroyView()
featureView = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import android.view.View
* [androidx.fragment.app.Fragment.onDestroy] or [androidx.fragment.app.Fragment.onDetach]
*/
interface FragmentLifecycleCallback {
companion object {
internal val NO_OP = object : FragmentLifecycleCallback {}
}

/**
* See [androidx.fragment.app.Fragment.onViewCreated]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ class FragmentStore @PublishedApi internal constructor(
companion object {
val EMPTY = init { }

inline fun init(
crossinline init: FeaturesBuilder<Unit>.() -> Unit
fun init(
init: FeaturesBuilder<Unit>.() -> Unit
): FragmentStore {
return init(Unit, init)
}

inline fun <Component> init(
fun <Component> init(
rootComponent: Component,
crossinline init: FeaturesBuilder<Component>.() -> Unit
init: FeaturesBuilder<Component>.() -> Unit
): FragmentStore {
val features = FeaturesBuilder.build(init)
return init(rootComponent, features)
Expand Down
Loading

0 comments on commit b568c21

Please sign in to comment.