diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 17bf544b4..2f1951336 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -6,6 +6,8 @@
*/
@file:Suppress("UnstableApiUsage")
+import dev.msfjarvis.claw.gradle.addTestDependencies
+
plugins {
id("dev.msfjarvis.claw.android-application")
id("dev.msfjarvis.claw.rename-artifacts")
@@ -14,6 +16,7 @@ plugins {
id("dev.msfjarvis.claw.sentry")
id("dev.msfjarvis.claw.versioning-plugin")
alias(libs.plugins.aboutlibraries)
+ alias(libs.plugins.android.junit5)
alias(libs.plugins.anvil)
alias(libs.plugins.modulegraphassert)
alias(libs.plugins.whetstone)
@@ -25,6 +28,7 @@ plugins {
android {
namespace = "dev.msfjarvis.claw.android"
defaultConfig.applicationId = "dev.msfjarvis.claw.android"
+ defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildFeatures.compose = true
composeOptions {
useLiveLiterals = false
@@ -113,4 +117,9 @@ dependencies {
implementation(projects.web)
kapt(libs.dagger.compiler)
+
+ addTestDependencies(project)
+ androidTestImplementation(libs.androidx.test.espresso.core)
+ androidTestImplementation(libs.androidx.test.uiautomator)
+ androidTestImplementation(libs.leakcanary.android.test)
}
diff --git a/android/src/androidTest/AndroidManifest.xml b/android/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..0da99d934
--- /dev/null
+++ b/android/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt b/android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt
new file mode 100644
index 000000000..338f72540
--- /dev/null
+++ b/android/src/androidTest/kotlin/dev/msfjarvis/claw/android/HeapGrowthCheck.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2024 Harsh Shandilya.
+ * Use of this source code is governed by an MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+package dev.msfjarvis.claw.android
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.google.common.truth.Truth.assertThat
+import de.mannodermaus.junit5.ActivityScenarioExtension
+import leakcanary.repeatingAndroidInProcessScenario
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import shark.ObjectGrowthDetector
+import shark.forAndroidHeap
+
+class HeapGrowthCheck {
+ @JvmField
+ @RegisterExtension
+ val scenarioExtension = ActivityScenarioExtension.launch()
+ private val detector = ObjectGrowthDetector.forAndroidHeap().repeatingAndroidInProcessScenario()
+ private lateinit var device: UiDevice
+
+ @BeforeEach
+ fun setUp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ }
+
+ @Test
+ fun verify_heap_growth() {
+ val heapGrowth = detector.findRepeatedlyGrowingObjects { device.exploreScreens() }
+ assertThat(heapGrowth.growingObjects).isEmpty()
+ }
+
+ private companion object {
+ fun UiDevice.exploreScreens() {
+ listOf("HOTTEST", "NEWEST", "SAVED").forEach { tag ->
+ waitForObject(By.res(tag)).click()
+ waitForIdle()
+ }
+ }
+
+ private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 10_000L): UiObject2 {
+ if (wait(Until.hasObject(selector), timeout)) {
+ return findObject(selector)
+ }
+
+ error("Object with selector [$selector] not found")
+ }
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9a2a93a73..142d04a8f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,7 @@
[versions]
aboutLibraries = "11.1.4"
agp = "8.5.0-beta01"
+android-junit5 = "1.10.0.0"
benchmark = "1.3.0-alpha04"
coil = "2.6.0"
# @keep used for kotlinCompilerExtensionVersion
@@ -12,6 +13,7 @@ junit = "5.10.2"
konvert = "3.2.0"
kotlin = "1.9.24"
kotlinResult = "2.0.0"
+leakcanary = "3.0-alpha-4"
lifecycle = "2.8.0-rc01"
retrofit = "2.11.0"
richtext = "1.0.0-alpha01"
@@ -89,6 +91,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
+leakcanary-android-test = { module = "com.squareup.leakcanary:leakcanary-android-test", version.ref = "leakcanary" }
material3-pulltorefresh = "eu.bambooapps:compose-material3-pullrefresh:1.1.1"
napier = "io.github.aakira:napier:2.7.1"
okhttp-bom = "com.squareup.okhttp3:okhttp-bom:4.12.0"
@@ -114,6 +117,7 @@ unfurl = "me.saket.unfurl:unfurl:1.7.0"
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }
android-lint = { id = "com.android.lint", version.ref = "agp" }
+android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "android-junit5" }
android-test = { id = "com.android.test", version.ref = "agp" }
anvil = "com.squareup.anvil:2.5.0-beta09"
baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" }