diff --git a/formula-android-tests/build.gradle.kts b/formula-android-tests/build.gradle.kts
index 5769be9b7..ce0fe99c0 100644
--- a/formula-android-tests/build.gradle.kts
+++ b/formula-android-tests/build.gradle.kts
@@ -1,5 +1,5 @@
plugins {
- id("com.android.application")
+ id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
}
@@ -10,19 +10,6 @@ apply {
android {
namespace = "com.instacart.formula"
- defaultConfig {
- applicationId = "com.instacart.formula"
- versionCode = 1
- versionName = "1.0"
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
- }
- }
testOptions {
unitTests {
@@ -47,4 +34,5 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
+ testImplementation(project(":test-utils:android"))
}
diff --git a/formula-android-tests/proguard-rules.pro b/formula-android-tests/proguard-rules.pro
deleted file mode 100644
index f1b424510..000000000
--- a/formula-android-tests/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
diff --git a/formula-android-tests/src/main/AndroidManifest.xml b/formula-android-tests/src/main/AndroidManifest.xml
index 3ae9bb658..c48ff6712 100644
--- a/formula-android-tests/src/main/AndroidManifest.xml
+++ b/formula-android-tests/src/main/AndroidManifest.xml
@@ -34,19 +34,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/formula-android-tests/src/test/java/com/instacart/formula/NonBoundActivityTest.kt b/formula-android-tests/src/test/java/com/instacart/formula/NonBoundActivityTest.kt
index decae86ff..aa313f125 100644
--- a/formula-android-tests/src/test/java/com/instacart/formula/NonBoundActivityTest.kt
+++ b/formula-android-tests/src/test/java/com/instacart/formula/NonBoundActivityTest.kt
@@ -1,10 +1,10 @@
package com.instacart.formula
import android.app.Activity
-import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instacart.testutils.android.TestActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -16,7 +16,6 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
class NonBoundActivityTest {
- class TestActivity : Activity()
private val formulaRule = TestFormulaRule(
initFormula = { app ->
diff --git a/formula-android-tests/src/test/java/com/instacart/formula/NonBoundFragmentActivityTest.kt b/formula-android-tests/src/test/java/com/instacart/formula/NonBoundFragmentActivityTest.kt
index 56144c143..a48fadca7 100644
--- a/formula-android-tests/src/test/java/com/instacart/formula/NonBoundFragmentActivityTest.kt
+++ b/formula-android-tests/src/test/java/com/instacart/formula/NonBoundFragmentActivityTest.kt
@@ -1,9 +1,9 @@
package com.instacart.formula
-import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.instacart.testutils.android.TestFragmentActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -11,23 +11,21 @@ import org.junit.rules.RuleChain
import org.junit.runner.RunWith
/**
- * Tests that formula-android module handles non-bound activities gracefully.
+ * Tests that formula-android module handles non-bound fragment activities gracefully.
*/
@RunWith(AndroidJUnit4::class)
class NonBoundFragmentActivityTest {
- class TestActivity : FragmentActivity()
-
private val formulaRule = TestFormulaRule(
initFormula = { app ->
FormulaAndroid.init(app) {}
}
)
- private val activityRule = ActivityScenarioRule(TestActivity::class.java)
+ private val activityRule = ActivityScenarioRule(TestFragmentActivity::class.java)
@get:Rule
val rule = RuleChain.outerRule(formulaRule).around(activityRule)
- lateinit var scenario: ActivityScenario
+ lateinit var scenario: ActivityScenario
@Before
fun setup() {
diff --git a/formula-android/build.gradle.kts b/formula-android/build.gradle.kts
index dddf15a75..da2e21046 100644
--- a/formula-android/build.gradle.kts
+++ b/formula-android/build.gradle.kts
@@ -16,6 +16,7 @@ android {
testOptions {
unitTests.isReturnDefaultValues = true
+ unitTests.isIncludeAndroidResources = true
}
publishing {
@@ -40,5 +41,6 @@ dependencies {
testImplementation(libs.mockito.kotlin)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
+ testImplementation(project(":test-utils:android"))
}
diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityStoreContextImpl.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityStoreContextImpl.kt
index daf80e332..4adc99429 100644
--- a/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityStoreContextImpl.kt
+++ b/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityStoreContextImpl.kt
@@ -59,7 +59,7 @@ internal class ActivityStoreContextImpl : ActivityS
select: Activity.() -> Observable
): Observable {
// TODO: should probably use startedActivity
- return activityAttachEvents()
+ return attachEventRelay
.switchMap {
val activity = activity
if (activity == null) {
@@ -105,8 +105,8 @@ internal class ActivityStoreContextImpl : ActivityS
fun detachActivity(activity: Activity) {
if (this.activity == activity) {
this.activity = null
+ attachEventRelay.accept(false)
}
- attachEventRelay.accept(false)
}
fun updateFragmentLifecycleState(id: FragmentId, newState: Lifecycle.State) {
@@ -121,8 +121,6 @@ internal class ActivityStoreContextImpl : ActivityS
fragmentStateUpdated.accept(contract.tag)
}
- private fun activityAttachEvents(): Observable = attachEventRelay
-
private fun fragmentLifecycleState(tag: String): Observable {
return fragmentStateUpdated
.filter { it == tag }
diff --git a/formula-android/src/test/java/com/instacart/formula/android/internal/ActivityStoreContextTest.kt b/formula-android/src/test/java/com/instacart/formula/android/internal/ActivityStoreContextTest.kt
new file mode 100644
index 000000000..3bb96c449
--- /dev/null
+++ b/formula-android/src/test/java/com/instacart/formula/android/internal/ActivityStoreContextTest.kt
@@ -0,0 +1,92 @@
+package com.instacart.formula.android.internal
+
+import android.os.Looper
+import androidx.fragment.app.FragmentActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.instacart.testutils.android.TestFragmentActivity
+import com.instacart.testutils.android.activity
+import com.instacart.testutils.android.executeOnBackgroundThread
+import com.instacart.testutils.android.throwOnTimeout
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+
+@RunWith(AndroidJUnit4::class)
+class ActivityStoreContextTest {
+
+ @Test fun `started activity returns null until onActivityStarted is called`() {
+ val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
+ val storeContext = ActivityStoreContextImpl()
+
+ // Initially null
+ assertThat(storeContext.startedActivity()).isNull()
+
+ // After attach
+ storeContext.attachActivity(scenario.activity())
+ assertThat(storeContext.startedActivity()).isNull()
+
+ // After on started
+ storeContext.onActivityStarted(scenario.activity())
+ assertThat(storeContext.startedActivity()).isEqualTo(scenario.activity())
+ }
+
+ @Test fun `detaches only if the activity matches`() {
+ val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
+ val storeContext = ActivityStoreContextImpl()
+
+ val oldActivity = scenario.activity()
+ val newActivity = scenario.recreate().activity()
+
+ storeContext.attachActivity(newActivity)
+ storeContext.onActivityStarted(newActivity)
+ storeContext.detachActivity(oldActivity)
+
+ assertThat(storeContext.startedActivity()).isEqualTo(newActivity)
+ }
+
+ @Test fun `send posts events on the main thread`() {
+ val scenario = ActivityScenario.launch(TestFragmentActivity::class.java)
+ val storeContext = ActivityStoreContextImpl()
+ storeContext.attachActivity(scenario.activity())
+ storeContext.onActivityStarted(scenario.activity())
+
+ val effectThread = mutableListOf()
+ storeContext.send { effectThread.add(Looper.myLooper()) }
+ storeContext.sendOnBackgroundThread { effectThread.add(Looper.myLooper()) }
+
+ assertThat(effectThread).containsExactly(
+ Looper.getMainLooper(), Looper.getMainLooper()
+ )
+ }
+
+ @Test
+ fun `send drops the action if there is no started activity`() {
+ val storeContext = ActivityStoreContextImpl()
+
+ val effectThread = mutableListOf()
+ storeContext.send { effectThread.add(Looper.myLooper()) }
+
+ val result = runCatching {
+ storeContext.sendOnBackgroundThread { effectThread.add(Looper.myLooper()) }
+ }
+ assertThat(effectThread).isEmpty()
+ assertThat(result.exceptionOrNull()).hasMessageThat().contains(
+ "timeout"
+ )
+ }
+
+ private fun ActivityStoreContextImpl.sendOnBackgroundThread(
+ action: ActivityType.() -> Unit
+ ) {
+ val sendLatch = CountDownLatch(1)
+ executeOnBackgroundThread {
+ send {
+ action()
+ sendLatch.countDown()
+ }
+ }
+ sendLatch.throwOnTimeout()
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ceade7280..d7e1ae7c7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,3 +17,6 @@ include(
":samples:stopwatch-compose",
":samples:todoapp"
)
+include(
+ ":test-utils:android"
+)
diff --git a/test-utils/android/.gitignore b/test-utils/android/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/test-utils/android/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/test-utils/android/build.gradle.kts b/test-utils/android/build.gradle.kts
new file mode 100644
index 000000000..0d59a49ad
--- /dev/null
+++ b/test-utils/android/build.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "com.instacart.testutils.android"
+}
+
+dependencies {
+ implementation(project(":formula-rxjava3"))
+ implementation(project(":formula-android"))
+
+ implementation(libs.kotlin)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.test.core.ktx)
+ implementation(libs.lifecycle.extensions)
+ implementation(libs.robolectric)
+}
diff --git a/test-utils/android/src/main/AndroidManifest.xml b/test-utils/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d9fd94f38
--- /dev/null
+++ b/test-utils/android/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/test-utils/android/src/main/java/com/instacart/testutils/android/TestActivity.kt b/test-utils/android/src/main/java/com/instacart/testutils/android/TestActivity.kt
new file mode 100644
index 000000000..c3f21cc1b
--- /dev/null
+++ b/test-utils/android/src/main/java/com/instacart/testutils/android/TestActivity.kt
@@ -0,0 +1,5 @@
+package com.instacart.testutils.android
+
+import android.app.Activity
+
+class TestActivity : Activity()
\ No newline at end of file
diff --git a/test-utils/android/src/main/java/com/instacart/testutils/android/TestExtensions.kt b/test-utils/android/src/main/java/com/instacart/testutils/android/TestExtensions.kt
new file mode 100644
index 000000000..759a982c2
--- /dev/null
+++ b/test-utils/android/src/main/java/com/instacart/testutils/android/TestExtensions.kt
@@ -0,0 +1,37 @@
+package com.instacart.testutils.android
+
+import android.app.Activity
+import android.os.Looper
+import androidx.test.core.app.ActivityScenario
+import org.robolectric.Shadows
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+fun ActivityScenario.activity(): A {
+ return get { this }
+}
+
+fun ActivityScenario.get(select: A.() -> T): T {
+ val list: MutableList = mutableListOf()
+ onActivity {
+ list.add(it.select())
+ }
+ return list.first()
+}
+
+fun CountDownLatch.throwOnTimeout() {
+ if (!await(100, TimeUnit.MILLISECONDS)) {
+ throw IllegalStateException("timeout")
+ }
+}
+
+fun executeOnBackgroundThread(action: () -> Unit) {
+ val initLatch = CountDownLatch(1)
+ Executors.newSingleThreadExecutor().execute {
+ action()
+ initLatch.countDown()
+ }
+ initLatch.throwOnTimeout()
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+}
diff --git a/test-utils/android/src/main/java/com/instacart/testutils/android/TestFragmentActivity.kt b/test-utils/android/src/main/java/com/instacart/testutils/android/TestFragmentActivity.kt
new file mode 100644
index 000000000..2da176eda
--- /dev/null
+++ b/test-utils/android/src/main/java/com/instacart/testutils/android/TestFragmentActivity.kt
@@ -0,0 +1,5 @@
+package com.instacart.testutils.android
+
+import androidx.fragment.app.FragmentActivity
+
+class TestFragmentActivity : FragmentActivity()
\ No newline at end of file