From 03cdc6ce15c3c8e174846d05f1746ada3a91d26c Mon Sep 17 00:00:00 2001 From: Abhay Sood Date: Wed, 14 Aug 2024 14:07:42 +0530 Subject: [PATCH] chore(android): add end to end tests for event collection --- android/gradle/libs.versions.toml | 2 + android/measure/build.gradle.kts | 5 + .../src/androidTest/AndroidManifest.xml | 20 +- .../sh/measure/android/MeasureEventsTest.kt | 679 ++++++++++++++++++ .../android/NoopPeriodicEventExporter.kt | 17 + .../java/sh/measure/android/TestActivity.kt | 109 ++- .../measure/android/TestMeasureInitializer.kt | 332 +++++++++ .../java/sh/measure/android/TestRobot.kt | 117 +++ .../androidTest/res/layout/activity_test.xml | 146 +++- .../main/java/sh/measure/android/Measure.kt | 39 +- .../MeasureOkHttpApplicationInterceptor.kt | 5 + 11 files changed, 1453 insertions(+), 18 deletions(-) create mode 100644 android/measure/src/androidTest/java/sh/measure/android/MeasureEventsTest.kt create mode 100644 android/measure/src/androidTest/java/sh/measure/android/NoopPeriodicEventExporter.kt create mode 100644 android/measure/src/androidTest/java/sh/measure/android/TestMeasureInitializer.kt create mode 100644 android/measure/src/androidTest/java/sh/measure/android/TestRobot.kt diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 9c46a885f..92a050e40 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -36,6 +36,7 @@ kotlinx-serialization-json = "1.6.2" google-material = "1.11.0" mockito-kotlin = "5.1.0" nhaarman-mockito-kotlin = "2.2.0" +orchestrator = "1.5.0" semver = "1.1.2" squareup-curtains = "1.2.4" squareup-okhttp = "4.12.0" @@ -77,6 +78,7 @@ androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", versi androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose-ui" } androidx-compose-runtime-android = { module = "androidx.compose.runtime:runtime-android", version.ref = "androidx-runtime-android" } androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-uiautomator" } +androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" } agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm-util" } asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm-util" } diff --git a/android/measure/build.gradle.kts b/android/measure/build.gradle.kts index 0c8a865ee..095b99d84 100644 --- a/android/measure/build.gradle.kts +++ b/android/measure/build.gradle.kts @@ -62,6 +62,7 @@ android { defaultConfig { minSdk = 21 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["clearPackageData"] = "true" consumerProguardFiles("consumer-rules.pro") } @@ -88,6 +89,7 @@ android { unitTests { isIncludeAndroidResources = true isReturnDefaultValues = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" } } buildFeatures { @@ -183,4 +185,7 @@ dependencies { androidTestImplementation(libs.androidx.activity.compose) androidTestImplementation(libs.androidx.navigation.compose) androidTestImplementation(libs.androidx.rules) + androidTestImplementation(libs.androidx.uiautomator) + androidTestImplementation(libs.squareup.okhttp.mockwebserver) + androidTestUtil(libs.androidx.orchestrator) } diff --git a/android/measure/src/androidTest/AndroidManifest.xml b/android/measure/src/androidTest/AndroidManifest.xml index 67a63a697..99e33cee7 100644 --- a/android/measure/src/androidTest/AndroidManifest.xml +++ b/android/measure/src/androidTest/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - - - - - - + + + + + + + diff --git a/android/measure/src/androidTest/java/sh/measure/android/MeasureEventsTest.kt b/android/measure/src/androidTest/java/sh/measure/android/MeasureEventsTest.kt new file mode 100644 index 000000000..b68ce860e --- /dev/null +++ b/android/measure/src/androidTest/java/sh/measure/android/MeasureEventsTest.kt @@ -0,0 +1,679 @@ +package sh.measure.android + +import android.Manifest +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.IdlingRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import okhttp3.Headers +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import sh.measure.android.config.MeasureConfig +import sh.measure.android.events.EventType +import sh.measure.android.exceptions.ExceptionData +import sh.measure.android.lifecycle.ActivityLifecycleType +import sh.measure.android.lifecycle.AppLifecycleType +import java.util.concurrent.TimeUnit + +/** + * Functional tests for the Measure SDK. + * + * The tests use the Measure SDK as it would be used in a real app. The only difference is that + * periodic export is disabled. The tests help verify that the SDK is able to track events + * and send them to the server. + * + * The tests use a MockWebServer to intercept the requests sent by the SDK and verify that the + * expected events are being sent. + * + * These tests also depend on `ANDROIDX_TEST_ORCHESTRATOR`, which is setup in + * the build.gradle file. Certain tests depend on the application being launched from scratch, + * which is made possible by adding `testInstrumentationRunnerArguments["clearPackageData"] = "true"` + * + * However, the tests do not verify the correctness of the data sent in the events. This can + * be improved in the future by adding a way to parse the multi-part requests made by the SDK. + * + * Also note that certain tests are ignored if the test harness cannot trigger the events + * in a reliable way, these must be verified manually. + * + * See [TestRobot] for helper methods to interact with the test app. + */ +@RunWith(AndroidJUnit4::class) +class MeasureEventsTest { + private lateinit var robot: TestRobot + private val mockWebServer: MockWebServer = MockWebServer() + + @Before + fun setup() { + mockWebServer.start(port = 8080) + // mockWebServer.enqueue(MockResponse().setResponseCode(200)) + robot = TestRobot() + } + + @After + fun teardown() { + mockWebServer.shutdown() + } + + @Test + fun tracksExceptionEvent() { + // Given + robot.disableDefaultExceptionHandler() + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { + // When + robot.crashApp() + } + // Then + assertEventTracked(EventType.EXCEPTION) + } + } + + @Test + fun givenScreenshotOnCrashEnabledThenTracksExceptionEventWithScreenshot() { + // Given + robot.disableDefaultExceptionHandler() + robot.initializeMeasure(MeasureConfig(enableLogging = true, trackScreenshotOnCrash = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { + // When + robot.crashApp() + } + val requestBody = getLastRequestBody() + + // Then + assertEventTracked(requestBody, EventType.EXCEPTION) + assertScreenshot(requestBody, true) + } + } + + @Test + fun givenScreenshotOnCrashDisabledThenTracksExceptionEventWithoutScreenshot() { + // Given + robot.disableDefaultExceptionHandler() + robot.initializeMeasure(MeasureConfig(enableLogging = true, trackScreenshotOnCrash = false)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { + // When + robot.crashApp() + } + val requestBody = getLastRequestBody() + + // Then + assertEventTracked(requestBody, EventType.EXCEPTION) + assertScreenshot(requestBody, false) + } + } + + @Test + fun tracksAnrEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + triggerAnr() + + // Then + assertEventTracked(EventType.ANR) + } + } + + @Test + fun givenScreenshotOnCrashEnabledThenTracksAnrEventWithScreenshot() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true, trackScreenshotOnCrash = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + triggerAnr() + val requestBody = getLastRequestBody() + + // Then + assertEventTracked(requestBody, EventType.ANR) + assertScreenshot(requestBody, true) + } + } + + @Test + fun givenScreenshotOnCrashDisabledThenTracksAnrEventWithoutScreenshot() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true, trackScreenshotOnCrash = false)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + triggerAnr() + val requestBody = getLastRequestBody() + + // Then + assertEventTracked(requestBody, EventType.ANR) + assertScreenshot(requestBody, false) + } + } + + @Test + fun tracksGestureViewClickEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + robot.clickButton() + triggerExport() + + // Then + assertEventTracked(EventType.CLICK) + } + } + + @Test + fun tracksGestureComposeClickEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + robot.clickComposeButton() + triggerExport() + val requestBody = getLastRequestBody() + + // Then + assertEventTracked(requestBody, EventType.CLICK) + // simply checking for click might introduce a false positive + // so we also check the expected body of the event + Assert.assertTrue(requestBody.contains("compose_button")) + Assert.assertTrue(requestBody.contains("androidx.compose.ui.platform.AndroidComposeView")) + } + } + + @Test + fun tracksGestureLongClickEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + robot.longClickButton() + triggerExport() + + // Then + assertEventTracked(EventType.LONG_CLICK) + } + } + + @Test + fun tracksGestureScrollEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + robot.scrollDown() + triggerExport() + + // Then + assertEventTracked(EventType.SCROLL) + } + } + + + @Test + fun tracksGestureComposeScrollEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + robot.composeScrollDown() + triggerExport() + + // Then + assertEventTracked(EventType.SCROLL) + } + } + + @Test + fun tracksLifecycleActivityEvents() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + it.moveToState(Lifecycle.State.DESTROYED) + triggerExport() + val body = getLastRequestBody() + + // Then + Assert.assertTrue(body.containsEvent(EventType.LIFECYCLE_ACTIVITY)) + Assert.assertTrue(body.contains(ActivityLifecycleType.CREATED)) + Assert.assertTrue(body.contains(ActivityLifecycleType.RESUMED)) + Assert.assertTrue(body.contains(ActivityLifecycleType.PAUSED)) + Assert.assertTrue(body.contains(ActivityLifecycleType.DESTROYED)) + } + } + + @Test + fun tracksLifecycleApplicationEvents() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + + // When + robot.pressHomeButton() + triggerExport() + val body = getLastRequestBody() + + // Then + Assert.assertTrue(body.containsEvent(EventType.LIFECYCLE_APP)) + Assert.assertTrue(body.contains(AppLifecycleType.FOREGROUND)) + Assert.assertTrue(body.contains(AppLifecycleType.BACKGROUND)) + } + } + + @Test + fun tracksColdLaunchEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + + // WHen + it.moveToState(Lifecycle.State.RESUMED) + triggerExport() + + // Then + assertEventTracked(EventType.COLD_LAUNCH) + } + } + + @Test + fun tracksWarmLaunchEvent() { + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.recreate() + triggerExport() + assertEventTracked(EventType.WARM_LAUNCH) + } + } + + @Test + @Ignore("Unable to trigger hot launch in tests") + fun tracksHotLaunchEvent() { + // Implementation would go here if we could reliably trigger a hot launch in tests + } + + @Test + @Ignore("Changing network requires a real internet connection to be available") + fun tracksNetworkChangeEvent() { + robot.grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE) + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + val networkEnabled = robot.isInternetAvailable() + robot.enableWiFi(!networkEnabled) + robot.enableMobileData(!networkEnabled) + // Network state change takes some time to reflect, so wait for a bit + // an idle resource would be better. + Thread.sleep(3000) + triggerExport() + + // reset network state + robot.enableWiFi(networkEnabled) + robot.enableMobileData(networkEnabled) + assertEventTracked(EventType.NETWORK_CHANGE) + } + } + + @Test + fun givenDefaultConfigThenTracksHttpEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest(activity, "http://localhost:8080") + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + assertEventTracked(EventType.HTTP) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenUrlAllowlistContainsUrlThenTracksHttpEvent() { + // Given + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + httpUrlAllowlist = listOf("localhost"), + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest(activity, "http://localhost:8080") + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + assertEventTracked(EventType.HTTP) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenUrlAllowlistDoesNotContainUrlThenDoesNotTrackHttpEvent() { + // GIven + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + httpUrlAllowlist = listOf("allowed.com"), + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest(activity, "http://localhost:8080") + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + assertEventNotTracked(EventType.HTTP) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenUrlBlocklistContainsUrlThenDoesNotTrackHttpEvent() { + // Given + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + httpUrlBlocklist = listOf("localhost"), + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest(activity, "http://localhost:8080") + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + assertEventNotTracked(EventType.HTTP) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenTrackBodyAndTrackHeadersAreEnabledThenTracksHeaders() { + // Given + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + trackHttpBody = true, + trackHttpHeaders = true, + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest( + activity, + "http://localhost:8080", + headers = Headers.Builder().add("x-header-key", "x-header-value").build() + ) + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + val body = getLastRequestBody() + assertEventTracked(body, EventType.HTTP) + Assert.assertTrue(body.contains("x-header-key")) + Assert.assertTrue(body.contains("x-header-value")) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenTrackHeadersIsDisabledThenDoesNotTrackHeaders() { + // Given + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + trackHttpBody = true, + trackHttpHeaders = false, + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest( + activity, + "http://localhost:8080", + headers = Headers.Builder().add("x-header-key", "x-header-value").build() + ) + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + val body = getLastRequestBody() + assertEventTracked(body, EventType.HTTP) + Assert.assertFalse(body.contains("x-header-key")) + Assert.assertFalse(body.contains("x-header-value")) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenTrackBodyIsDisabledThenDoesNotTrackBody() { + // Given + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + trackHttpBody = false, + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest( + activity, + "http://localhost:8080", + headers = Headers.Builder().add("x-header-key", "x-header-value").build(), + requestBody = "request_body" + ) + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + val body = getLastRequestBody() + + // Then + assertEventTracked(body, EventType.HTTP) + Assert.assertFalse(body.contains("request_body")) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun givenTrackBodyIsEnabledThenTracksBody() { + // Given + robot.initializeMeasure( + MeasureConfig( + enableLogging = true, + trackHttpBody = true, + ) + ) + ActivityScenario.launch(TestActivity::class.java).use { + it.moveToState(Lifecycle.State.RESUMED) + it.onActivity { activity -> + IdlingRegistry.getInstance().register(activity.httpIdlingResource) + + // When + robot.makeNetworkRequest( + activity, + "http://localhost:8080", + headers = Headers.Builder().add("x-header-key", "x-header-value").build(), + requestBody = "request_body" + ) + activity.httpIdlingResource.registerIdleTransitionCallback { + triggerExport() + + // Then + val body = getLastRequestBody() + assertEventTracked(body, EventType.HTTP) + Assert.assertTrue(body.contains("request_body")) + } + IdlingRegistry.getInstance().unregister(activity.httpIdlingResource) + } + } + } + + @Test + fun tracksMemoryUsageEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + // When + it.moveToState(Lifecycle.State.RESUMED) + triggerExport() + + // Then + assertEventTracked(EventType.MEMORY_USAGE) + } + } + + @Test + fun tracksCpuUsageEvent() { + // Given + robot.initializeMeasure(MeasureConfig(enableLogging = true)) + ActivityScenario.launch(TestActivity::class.java).use { + + // When + it.moveToState(Lifecycle.State.RESUMED) + triggerExport() + + // Then + assertEventTracked(EventType.CPU_USAGE) + } + } + + @Test + @Ignore("Unable to trigger trim memory callbacks in tests") + fun tracksTrimMemoryEvent() { + // Implementation would go here if we could reliably trigger trim memory in tests + } + + private fun String.containsEvent(eventType: String): Boolean { + return contains("\"type\":\"$eventType\"") + } + + private fun triggerExport() { + Measure.simulateAppCrash( + type = EventType.EXCEPTION, + data = ExceptionData( + exceptions = emptyList(), + threads = emptyList(), + handled = false, + foreground = true, + ), + timestamp = 987654321L, + attributes = mutableMapOf(), + attachments = mutableListOf(), + ) + } + + private fun triggerAnr() { + Measure.simulateAnr( + data = ExceptionData( + exceptions = emptyList(), + threads = emptyList(), + handled = false, + foreground = true, + ), + timestamp = 987654321L, + attributes = mutableMapOf(), + attachments = mutableListOf(), + ) + } + + private fun assertEventTracked(body: String, eventType: String) { + Assert.assertTrue(body.containsEvent(eventType)) + } + + private fun assertScreenshot(requestBody: String, expected: Boolean) { + if (expected) { + Assert.assertTrue(requestBody.contains("\"type\":\"screenshot\"")) + } else { + Assert.assertFalse(requestBody.contains("\"type\":\"screenshot\"")) + } + } + + private fun assertEventTracked(eventType: String) { + val body = getLastRequestBody() + Assert.assertTrue(body.containsEvent(eventType)) + } + + private fun assertEventNotTracked(eventType: String) { + val body = getLastRequestBody() + Assert.assertFalse(body.containsEvent(eventType)) + } + + private fun getLastRequestBody(): String { + val request = mockWebServer.takeRequest(timeout = 500, unit = TimeUnit.MILLISECONDS) + Assert.assertNotNull(request) + return request!!.body.readUtf8() + } +} \ No newline at end of file diff --git a/android/measure/src/androidTest/java/sh/measure/android/NoopPeriodicEventExporter.kt b/android/measure/src/androidTest/java/sh/measure/android/NoopPeriodicEventExporter.kt new file mode 100644 index 000000000..745f8b085 --- /dev/null +++ b/android/measure/src/androidTest/java/sh/measure/android/NoopPeriodicEventExporter.kt @@ -0,0 +1,17 @@ +package sh.measure.android + +import sh.measure.android.exporter.PeriodicEventExporter + +internal class NoopPeriodicEventExporter: PeriodicEventExporter { + override fun onAppForeground() { + // No-op + } + + override fun onAppBackground() { + // No-op + } + + override fun onColdLaunch() { + // No-op + } +} \ No newline at end of file diff --git a/android/measure/src/androidTest/java/sh/measure/android/TestActivity.kt b/android/measure/src/androidTest/java/sh/measure/android/TestActivity.kt index 85778a869..9fc084ffa 100644 --- a/android/measure/src/androidTest/java/sh/measure/android/TestActivity.kt +++ b/android/measure/src/androidTest/java/sh/measure/android/TestActivity.kt @@ -1,12 +1,117 @@ package sh.measure.android -import android.app.Activity import android.os.Bundle +import android.widget.Button +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.unit.dp +import androidx.test.espresso.idling.CountingIdlingResource +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import sh.measure.android.okhttp.MeasureEventListenerFactory import sh.measure.android.test.R +import java.io.IOException -class TestActivity : Activity() { +class TestActivity : ComponentActivity() { + companion object { + const val IDLING_RES_HTTP_REQUEST = "http_request" + } + + val httpIdlingResource = CountingIdlingResource(IDLING_RES_HTTP_REQUEST) + + @OptIn(ExperimentalComposeUiApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_test) + findViewById