Skip to content

Commit a4f6165

Browse files
teodorciuraruclaude
andcommitted
feat: make integration tests strict to prevent false positives
- Added STRICT UI verification in all tests using Espresso instead of basic launches - Tests now FAIL if MainActivity UI is not working (no more false positives) - Added CI environment detection - document sync test MUST have GITHUB_TEST_DOC_ID in CI - Enhanced error messages with detailed troubleshooting information - Added periodic app responsiveness checks during sync verification Local test results confirm strictness: ❌ 2/3 tests now FAIL when app UI doesn't work (was 3/3 passing before) ✅ Tests detect "No activities in stage RESUMED" when app isn't responsive ✅ Document sync test properly skips locally but will enforce in CI This ensures BrowserStack tests will fail if the app isn't actually working! 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 79b0809 commit a4f6165

File tree

1 file changed

+213
-50
lines changed

1 file changed

+213
-50
lines changed
Lines changed: 213 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,242 @@
11
package live.ditto.quickstart.tasks
22

3-
import androidx.test.ext.junit.rules.ActivityScenarioRule
43
import androidx.test.ext.junit.runners.AndroidJUnit4
5-
import androidx.test.platform.app.InstrumentationRegistry
6-
import org.junit.Rule
4+
import androidx.test.espresso.Espresso.onView
5+
import androidx.test.espresso.assertion.ViewAssertions.matches
6+
import androidx.test.espresso.matcher.ViewMatchers.*
7+
import org.hamcrest.Matchers.allOf
8+
import org.hamcrest.Matchers.containsString
79
import org.junit.Test
810
import org.junit.runner.RunWith
11+
import org.junit.Before
12+
import androidx.test.espresso.IdlingRegistry
13+
import androidx.test.espresso.idling.CountingIdlingResource
14+
import org.junit.After
15+
import org.junit.Assert.assertEquals
16+
import androidx.test.platform.app.InstrumentationRegistry
917

1018
/**
11-
* Simple integration tests that verify the app can launch without Compose testing framework issues.
19+
* UI tests for the Ditto Tasks application using Espresso framework.
20+
* These tests verify the user interface functionality and Ditto sync on real devices.
1221
*/
1322
@RunWith(AndroidJUnit4::class)
1423
class SimpleIntegrationTest {
15-
16-
@get:Rule
17-
val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
18-
24+
25+
private lateinit var activityScenario: androidx.test.core.app.ActivityScenario<MainActivity>
26+
27+
// Idling resource to wait for async operations
28+
private val idlingResource = CountingIdlingResource("TaskSync")
29+
30+
@Before
31+
fun setUp() {
32+
IdlingRegistry.getInstance().register(idlingResource)
33+
}
34+
35+
@After
36+
fun tearDown() {
37+
IdlingRegistry.getInstance().unregister(idlingResource)
38+
if (::activityScenario.isInitialized) {
39+
activityScenario.close()
40+
}
41+
}
42+
1943
@Test
20-
fun testActivityLaunches() {
21-
println("🧪 Testing if MainActivity launches...")
44+
fun testAppLaunchesSuccessfully() {
45+
println("🚀 Starting STRICT MainActivity launch test...")
2246

23-
// Verify the activity launched
24-
activityScenarioRule.scenario.onActivity { activity ->
25-
println("✅ MainActivity launched successfully: ${activity::class.simpleName}")
26-
assert(activity != null)
47+
try {
48+
// Launch activity with proper scenario management
49+
activityScenario = androidx.test.core.app.ActivityScenario.launch(MainActivity::class.java)
50+
51+
// Wait for Ditto initialization and UI rendering
52+
println("⏳ Waiting for Ditto initialization and UI...")
53+
Thread.sleep(15000) // 15 seconds for full initialization
54+
55+
// STRICT CHECK: Verify the app title is actually visible
56+
println("🔍 Checking for app title 'Ditto Tasks'...")
57+
onView(withText("Ditto Tasks"))
58+
.check(matches(isDisplayed()))
59+
60+
println("✅ App title found - MainActivity UI is working")
61+
62+
// STRICT CHECK: Verify the New Task button exists
63+
println("🔍 Checking for 'New Task' button...")
64+
onView(withText("New Task"))
65+
.check(matches(isDisplayed()))
66+
67+
println("✅ New Task button found - UI is fully functional")
68+
69+
} catch (e: Exception) {
70+
println("❌ STRICT launch test failed: ${e.message}")
71+
println(" This means the app UI is NOT working properly")
72+
throw AssertionError("MainActivity UI verification failed - app not working: ${e.message}")
2773
}
74+
}
75+
76+
@Test
77+
fun testBasicAppContext() {
78+
println("🧪 Starting app context verification...")
2879

29-
// Give time for the activity to initialize
30-
Thread.sleep(5000)
80+
// Verify app context without UI interaction
81+
val context = InstrumentationRegistry.getInstrumentation().targetContext
82+
assertEquals("live.ditto.quickstart.tasks", context.packageName)
83+
println("✅ App context verified: ${context.packageName}")
3184

32-
println("✅ Activity launch test completed")
85+
// Additional strict check - launch and verify UI briefly
86+
try {
87+
if (!::activityScenario.isInitialized) {
88+
activityScenario = androidx.test.core.app.ActivityScenario.launch(MainActivity::class.java)
89+
Thread.sleep(10000) // Wait for initialization
90+
}
91+
92+
// Verify the activity is actually displaying something
93+
onView(withText("Ditto Tasks"))
94+
.check(matches(isDisplayed()))
95+
96+
println("✅ Context test passed - UI is responsive")
97+
} catch (e: Exception) {
98+
throw AssertionError("Context test failed - UI not responsive: ${e.message}")
99+
}
33100
}
34-
101+
35102
@Test
36-
fun testAppDoesNotCrash() {
37-
println("🧪 Testing app stability...")
103+
fun testGitHubTestDocumentSyncs() {
104+
println("🔍 Starting GitHub test document sync verification...")
105+
106+
// Get the GitHub test document ID from environment variable
107+
val githubTestDocId = System.getenv("GITHUB_TEST_DOC_ID")
38108

39-
// Just verify the activity exists and doesn't crash
40-
activityScenarioRule.scenario.onActivity { activity ->
41-
println("Activity state: ${activity.lifecycle.currentState}")
42-
assert(activity.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED))
109+
if (githubTestDocId.isNullOrEmpty()) {
110+
println("⚠️ No GITHUB_TEST_DOC_ID environment variable found")
111+
println(" This test MUST run in CI with seeded documents")
112+
113+
// STRICT: In CI, this test should fail if no doc ID is provided
114+
// We can detect CI by checking for common CI environment variables
115+
val isCI = System.getenv("CI") != null ||
116+
System.getenv("GITHUB_ACTIONS") != null ||
117+
System.getenv("BROWSERSTACK_USERNAME") != null
118+
119+
if (isCI) {
120+
throw AssertionError("GITHUB_TEST_DOC_ID is required in CI environment but was not provided")
121+
} else {
122+
println(" Skipping sync test (local environment)")
123+
return
124+
}
43125
}
44126

45-
// Wait a bit to ensure no crashes
46-
Thread.sleep(5000)
127+
// Extract the run ID from the document ID (format: github_test_android_RUNID_RUNNUMBER)
128+
val runId = githubTestDocId.split("_").getOrNull(3) ?: githubTestDocId
129+
println("🎯 Looking for GitHub Test Task with Run ID: $runId")
130+
println("📄 Full document ID: $githubTestDocId")
131+
132+
// Wait longer for sync to complete from Ditto Cloud
133+
var attempts = 0
134+
val maxAttempts = 30 // 30 attempts with 2 second waits = 60 seconds max
135+
var documentFound = false
136+
var lastException: Exception? = null
47137

48-
// Check activity is still alive
49-
activityScenarioRule.scenario.onActivity { activity ->
50-
println("Activity still running: ${activity.lifecycle.currentState}")
51-
assert(activity.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED))
138+
// Launch activity and verify it's working before testing sync
139+
if (!::activityScenario.isInitialized) {
140+
println("🚀 Launching MainActivity for sync test...")
141+
activityScenario = androidx.test.core.app.ActivityScenario.launch(MainActivity::class.java)
142+
143+
// Wait for Ditto to initialize with cloud sync
144+
println("⏳ Waiting for Ditto cloud sync initialization...")
145+
Thread.sleep(15000) // 15 seconds for cloud sync setup
146+
147+
// STRICT: Verify the app UI is working before testing sync
148+
try {
149+
println("🔍 Verifying app UI is responsive before sync test...")
150+
onView(withText("Ditto Tasks")).check(matches(isDisplayed()))
151+
println("✅ App UI is working - proceeding with sync test")
152+
} catch (e: Exception) {
153+
throw AssertionError("App UI is not working - cannot test sync: ${e.message}")
154+
}
52155
}
53156

54-
println("✅ Stability test completed")
55-
}
56-
57-
@Test
58-
fun testDocumentSyncWithoutUI() {
59-
println("🧪 Testing document sync behavior...")
157+
// First, ensure we can see any tasks at all (verify UI is working)
158+
println("🔍 Checking if task list UI is functional...")
159+
try {
160+
onView(withText("Ditto Tasks")).check(matches(isDisplayed()))
161+
println("✅ Task list UI confirmed working")
162+
} catch (e: Exception) {
163+
throw AssertionError("Task list UI not working - cannot test sync: ${e.message}")
164+
}
60165

61-
val testDocumentId = System.getProperty("GITHUB_TEST_DOC_ID")
62-
?: try {
63-
val instrumentation = InstrumentationRegistry.getInstrumentation()
64-
val bundle = InstrumentationRegistry.getArguments()
65-
bundle.getString("GITHUB_TEST_DOC_ID")
166+
while (attempts < maxAttempts && !documentFound) {
167+
attempts++
168+
println("🔄 Attempt $attempts/$maxAttempts: Searching for document with run ID '$runId'...")
169+
170+
try {
171+
// STRICT SEARCH: Look for the exact content we expect
172+
// The document should contain both "GitHub Test Task" and the run ID
173+
println(" Looking for text containing 'GitHub Test Task' AND '$runId'...")
174+
175+
onView(withText(allOf(
176+
containsString("GitHub Test Task"),
177+
containsString(runId)
178+
))).check(matches(isDisplayed()))
179+
180+
println("✅ SUCCESS: Found synced GitHub test document with run ID: $runId")
181+
documentFound = true
182+
183+
// Additional verification - make sure it's actually displayed
184+
onView(withText(allOf(
185+
containsString("GitHub Test Task"),
186+
containsString(runId)
187+
))).check(matches(isDisplayed()))
188+
189+
println("✅ VERIFIED: Document is displayed and contains expected content")
190+
66191
} catch (e: Exception) {
67-
null
192+
lastException = e
193+
println(" ❌ Document not found: ${e.message}")
194+
195+
// Every 5 attempts, verify the app is still working
196+
if (attempts % 5 == 0) {
197+
try {
198+
println(" 🔍 Verifying app is still responsive...")
199+
onView(withText("Ditto Tasks")).check(matches(isDisplayed()))
200+
println(" ✅ App is still responsive")
201+
202+
// Try to see if there are ANY tasks visible
203+
try {
204+
onView(withText("New Task")).check(matches(isDisplayed()))
205+
println(" 📝 'New Task' button visible - UI is working")
206+
} catch (buttonE: Exception) {
207+
println(" ⚠️ 'New Task' button not found: ${buttonE.message}")
208+
}
209+
210+
} catch (appE: Exception) {
211+
throw AssertionError("App became unresponsive during sync test: ${appE.message}")
212+
}
213+
}
214+
215+
if (attempts < maxAttempts) {
216+
Thread.sleep(2000)
217+
}
68218
}
69-
70-
if (testDocumentId.isNullOrEmpty()) {
71-
println("⚠️ No GitHub test document ID provided, skipping sync verification")
72-
println("✅ Sync test skipped gracefully (no document ID provided)")
73-
} else {
74-
println("🔍 Would look for GitHub test document: $testDocumentId")
75-
println("⚠️ Document sync verification skipped (UI testing not available)")
76-
println("✅ Document ID successfully retrieved: $testDocumentId")
219+
}
220+
221+
if (!documentFound) {
222+
val errorMsg = """
223+
❌ FAILED: GitHub test document did not sync within ${maxAttempts * 2} seconds
224+
225+
Expected to find:
226+
- Document ID: $githubTestDocId
227+
- Text containing: "GitHub Test Task" AND "$runId"
228+
- In Compose UI elements
229+
230+
Possible causes:
231+
1. Document not seeded to Ditto Cloud during CI
232+
2. App not connecting to Ditto Cloud (check network connectivity)
233+
3. Ditto sync taking longer than expected
234+
4. UI structure changed (this is a Compose app, not traditional Views)
235+
236+
Last error: ${lastException?.message}
237+
""".trimIndent()
238+
239+
throw AssertionError(errorMsg)
77240
}
78241
}
79242
}

0 commit comments

Comments
 (0)