Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,25 @@ dependencies {
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.test.manifest)
}

tasks.register<Exec>("pullEventTimelines") {
description = "Pull event timeline JSON files from the device after instrumentation tests"
group = "verification"
val outputDir = layout.buildDirectory.dir("outputs/event-timelines").get().asFile
doFirst {
outputDir.mkdirs()
}
commandLine(
"adb", "pull",
"/sdcard/Download/superwall-event-timelines/.",
outputDir.absolutePath,
)
isIgnoreExitValue = true
}

tasks.register<Exec>("clearEventTimelines") {
description = "Clear event timeline files from the device"
group = "verification"
commandLine("adb", "shell", "rm", "-rf", "/sdcard/Download/superwall-event-timelines/")
isIgnoreExitValue = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.example.superapp.utils

import android.os.Environment
import android.util.Log
import com.superwall.superapp.test.UITestInfo
import org.json.JSONArray
import org.json.JSONObject
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import java.io.File

private const val TIMELINE_DIR = "superwall-event-timelines"
private const val TAG = "EventTimeline"

/**
* Writes the event timeline for a [UITestInfo] to a JSON file on device storage.
*
* Output directory: /sdcard/Download/superwall-event-timelines/
*
* Pull results with:
* adb pull /sdcard/Download/superwall-event-timelines/ app/build/outputs/event-timelines/
*/
fun writeTimelineToFile(
testInfo: UITestInfo,
testClassName: String,
testMethodName: String,
) {
val timeline = testInfo.timeline
if (timeline.allEvents().isEmpty()) return

val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
TIMELINE_DIR,
)
dir.mkdirs()

val fileName = "${testClassName}_${testMethodName}.json"
val file = File(dir, fileName)

val json = JSONObject().apply {
put("testClass", testClassName)
put("testMethod", testMethodName)
put("testNumber", testInfo.number)
put("testDescription", testInfo.description)
put("totalDurationMs", timeline.totalDuration().inWholeMilliseconds)
put("eventCount", timeline.allEvents().size)
put("events", JSONArray().apply {
timeline.toSerializableList().forEach { map ->
put(JSONObject().apply {
map.forEach { (k, v) ->
when (v) {
is Map<*, *> -> put(k, JSONObject(v as Map<*, *>))
else -> put(k, v)
}
}
})
}
})
}

file.writeText(json.toString(2))
Log.i(TAG, "Wrote timeline to ${file.absolutePath} (${timeline.allEvents().size} events)")
}

/**
* Overload that derives the filename from [UITestInfo] directly.
* Used by [screenshotFlow] where no JUnit [Description] is available.
*/
fun writeTimelineToFile(testInfo: UITestInfo) {
writeTimelineToFile(
testInfo,
testClassName = "Test${testInfo.number}",
testMethodName = testInfo.testCaseType.titleText(testInfo.number).replace(" ", "_"),
)
}

/**
* JUnit TestWatcher that automatically writes the [com.superwall.superapp.test.EventTimeline]
* from a [UITestInfo] to a JSON file on the device after each test finishes (pass or fail).
* Uses JUnit's [Description] for reliable test class/method naming.
*
* Add to any test class:
* ```
* @get:Rule
* val timelineRule = EventTimelineRule { currentTestInfo }
* ```
*/
class EventTimelineRule(
private val testInfoProvider: () -> UITestInfo?,
) : TestWatcher() {
override fun finished(description: Description) {
val testInfo = testInfoProvider() ?: return
writeTimelineToFile(
testInfo,
description.testClass.simpleName,
description.methodName,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.config.models.ConfigurationStatus
import com.superwall.sdk.paywall.view.ShimmerView
import com.superwall.superapp.MainActivity
import com.superwall.superapp.test.EventTimeline
import com.superwall.superapp.test.TimelineStore
import com.superwall.superapp.test.UITestInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -94,6 +96,7 @@ fun Dropshots.screenshotFlow(
val flow = ScreenshotTestFlow(testInfo).apply(flow)
val scenario = ActivityScenario.launch(MainActivity::class.java)
val testCase = testInfo

println("-----------")
println("Executing test case: ${testCase.number}")
println("Description:\n ${testCase.description}")
Expand Down Expand Up @@ -129,6 +132,8 @@ fun Dropshots.screenshotFlow(
throw e
} finally {
scope.cancel()
writeTimelineToFile(testCase)
TimelineStore.remove("Test #${testCase.number}")
}
}
closeActivity()
Expand Down Expand Up @@ -284,3 +289,34 @@ suspend fun CoroutineScope.delayFor(duration: Duration) =
async(Dispatchers.IO) {
delay(duration)
}.await()

// --- EventTimeline extensions ---

/** Access the timeline for this test info. */
val UITestInfo.eventTimeline: EventTimeline get() = timeline

/** Assert that the duration from timeline start to event T is under the given limit. */
inline fun <reified T : SuperwallEvent> EventTimeline.assertDurationToUnder(limit: Duration) {
val actual = durationTo<T>()
?: error("Event ${T::class.simpleName} was never recorded")
assert(actual <= limit) {
"Expected ${T::class.simpleName} within $limit but took $actual"
}
}

/** Assert that the duration between events A and B is under the given limit. */
inline fun <reified A : SuperwallEvent, reified B : SuperwallEvent> EventTimeline.assertDurationBetweenUnder(limit: Duration) {
val actual = durationBetween<A, B>()
?: error("One or both events (${A::class.simpleName}, ${B::class.simpleName}) were never recorded")
assert(actual <= limit) {
"Expected ${A::class.simpleName} -> ${B::class.simpleName} within $limit but took $actual"
}
}

/** Print a human-readable summary of all captured events to logcat. */
fun EventTimeline.printSummary(tag: String = "EventTimeline") {
allEvents().forEachIndexed { i, event ->
Log.i(tag, "#$i [${event.elapsed}] ${event.eventName} (${event.eventType}) params=${event.params}")
}
Log.i(tag, "Total duration: ${totalDuration()}")
}
11 changes: 9 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
Expand All @@ -29,7 +29,8 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:sharedUserId="com.superwall.superapp.uid tools:targetApi="
android:sharedUserId="com.superwall.superapp.uid"
tools:targetApi="28"
android:sharedUserMaxSdkVersion="32"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication">
Expand All @@ -39,6 +40,12 @@
android:label="@string/title_activity_uitest"
android:theme="@style/Theme.MyApplication" />

<activity
android:name=".test.TimelineViewerActivity"
android:exported="false"
android:label="Event Timeline Viewer"
android:theme="@style/Theme.MyApplication" />

<activity
android:name=".test.WebTestActivity"
android:exported="false"
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/superwall/superapp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package com.superwall.superapp

import android.app.ComponentCaller
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.superwall.sdk.Superwall
Expand All @@ -16,11 +19,32 @@ class MainActivity : AppCompatActivity() {
(applicationContext as MainApplication).events
}

private var gestureDetector: GestureDetector? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
(application as MainApplication).activity = WeakReference(this)

// Double-tap anywhere to open timeline viewer (debug only)
val isDebug = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
if (isDebug) {
gestureDetector = GestureDetector(
this,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
startActivity(
Intent(
this@MainActivity,
com.superwall.superapp.test.TimelineViewerActivity::class.java,
),
)
return true
}
},
)
}

// Setup deep linking handling
respondToDeepLinks()

Expand Down Expand Up @@ -134,6 +158,11 @@ class MainActivity : AppCompatActivity() {
}
*/

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
gestureDetector?.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}

//region Deep Links

override fun onNewIntent(
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/superwall/superapp/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import com.superwall.sdk.paywall.presentation.register
import com.superwall.superapp.purchase.RevenueCatPurchaseController
import com.superwall.superapp.test.EventTimeline
import com.superwall.superapp.test.TimelineStore
import kotlinx.coroutines.flow.MutableSharedFlow
import java.lang.ref.WeakReference

Expand Down Expand Up @@ -70,6 +72,7 @@ class MainApplication :
}

val events = MutableSharedFlow<SuperwallEventInfo>()
val liveTimeline = EventTimeline().also { TimelineStore.register("Live", it) }

fun configureWithAutomaticInitialization() {
Superwall.configure(
Expand All @@ -80,7 +83,7 @@ class MainApplication :
logging.level = LogLevel.debug
paywalls =
PaywallOptions().apply {
shouldPreload = false
shouldPreload = true
}
},
)
Expand Down Expand Up @@ -158,6 +161,7 @@ class MainApplication :
"\tEvent name:" + eventInfo.event.rawName + "" +
",\n\tParams:" + eventInfo.params + "\n",
)
liveTimeline.record(eventInfo)
}

override fun subscriptionStatusDidChange(
Expand Down
Loading
Loading