diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 177512f8f..7562f17aa 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -292,6 +292,7 @@ android { testImplementation(libs.robolectric) testImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-test-manifest") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation(libs.androidx.runner) androidTestImplementation(libs.androidx.rules) androidTestImplementation(libs.androidx.core.ktx) diff --git a/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/NavigationSmokeTest.kt b/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/NavigationSmokeTest.kt index 7c098fb1f..23df9c93b 100644 --- a/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/NavigationSmokeTest.kt +++ b/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/NavigationSmokeTest.kt @@ -3,6 +3,10 @@ package com.simplecityapps.shuttle.smoke import android.Manifest import android.content.SharedPreferences import android.os.Build +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario import androidx.test.core.app.launchActivity @@ -34,10 +38,13 @@ import org.junit.Test class NavigationSmokeTest { @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) + val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) - var permissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val composeRule = createEmptyComposeRule() + + @get:Rule(order = 2) + val permissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { GrantPermissionRule.grant(Manifest.permission.READ_MEDIA_AUDIO) } else { GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) @@ -93,7 +100,9 @@ class NavigationSmokeTest { onView(withId(R.id.libraryFragment)).perform(click()) onView(withText("Songs")).perform(click()) - waitForView(allOf(withId(R.id.recyclerView), hasDescendant(withText("Highway to Hell")))) + composeRule.waitUntil(5_000) { + composeRule.onAllNodesWithText("Highway to Hell").fetchSemanticsNodes().isNotEmpty() + } onView(withText("Albums")).perform(click()) waitForView(allOf(withId(R.id.recyclerView), hasDescendant(isDisplayed()))) @@ -102,8 +111,10 @@ class NavigationSmokeTest { waitForView(allOf(withId(R.id.recyclerView), hasDescendant(isDisplayed()))) onView(withText("Genres")).perform(click()) - // Genres uses Compose — just verify the tab didn't crash - waitForView(allOf(withId(R.id.tabLayout), isDescendantOfA(withId(R.id.constraintLayout)))) + // Genres uses Compose with a FastScroller delay that keeps the Compose + // idling resource busy. Advance the clock past it before Espresso tries + // to click the Playlists tab. + composeRule.mainClock.advanceTimeBy(2000) onView(withText("Playlists")).perform(click()) waitForView(allOf(withId(R.id.recyclerView), isDisplayed())) @@ -141,12 +152,13 @@ class NavigationSmokeTest { fun playbackScreens() { scenario = launchActivity() - // Play a song + // Play a song (Songs tab is Compose) onView(withId(R.id.libraryFragment)).perform(click()) onView(withText("Songs")).perform(click()) - waitForView(allOf(withId(R.id.recyclerView), hasDescendant(isDisplayed()))) - onView(allOf(withId(R.id.recyclerView), isDisplayed())) - .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())) + composeRule.waitUntil(5_000) { + composeRule.onAllNodesWithText("Highway to Hell").fetchSemanticsNodes().isNotEmpty() + } + composeRule.onNodeWithText("Highway to Hell").performClick() // Mini player waitForView(allOf(withId(R.id.titleTextView), isDescendantOfA(withId(R.id.sheet1PeekView)))) diff --git a/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/SmokeTestSuite.kt b/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/SmokeTestSuite.kt index 44651ff68..7a75347e4 100644 --- a/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/SmokeTestSuite.kt +++ b/android/app/src/androidTest/java/com/simplecityapps/shuttle/smoke/SmokeTestSuite.kt @@ -3,6 +3,10 @@ package com.simplecityapps.shuttle.smoke import android.Manifest import android.content.SharedPreferences import android.os.Build +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario import androidx.test.core.app.launchActivity @@ -37,10 +41,13 @@ import org.junit.Test class SmokeTestSuite { @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) + val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) - var permissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val composeRule = createEmptyComposeRule() + + @get:Rule(order = 2) + val permissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { GrantPermissionRule.grant(Manifest.permission.READ_MEDIA_AUDIO) } else { GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) @@ -97,8 +104,10 @@ class SmokeTestSuite { onView(withText("Songs")) .perform(click()) - // Wait for Flow to emit data, then verify a song from our test data is visible - waitForView(allOf(withId(R.id.recyclerView), hasDescendant(withText("Highway to Hell")))) + // Songs tab is Compose — use compose rule to find text + composeRule.waitUntil(5_000) { + composeRule.onAllNodesWithText("Highway to Hell").fetchSemanticsNodes().isNotEmpty() + } } @Test @@ -111,10 +120,11 @@ class SmokeTestSuite { onView(withText("Songs")) .perform(click()) - // Wait for Flow to emit data, then tap the first song in the list - waitForView(allOf(withId(R.id.recyclerView), hasDescendant(isDisplayed()))) - onView(allOf(withId(R.id.recyclerView), isDisplayed())) - .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())) + // Songs tab is Compose — wait then tap + composeRule.waitUntil(5_000) { + composeRule.onAllNodesWithText("Highway to Hell").fetchSemanticsNodes().isNotEmpty() + } + composeRule.onNodeWithText("Highway to Hell").performClick() // Mini player should show a song title waitForView(allOf(withId(R.id.titleTextView), isDescendantOfA(withId(R.id.sheet1PeekView)))) @@ -162,10 +172,11 @@ class SmokeTestSuite { onView(withText("Songs")) .perform(click()) - // Wait for Flow to emit data, then tap the first song - waitForView(allOf(withId(R.id.recyclerView), hasDescendant(isDisplayed()))) - onView(allOf(withId(R.id.recyclerView), isDisplayed())) - .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())) + // Songs tab is Compose — wait then tap + composeRule.waitUntil(5_000) { + composeRule.onAllNodesWithText("Highway to Hell").fetchSemanticsNodes().isNotEmpty() + } + composeRule.onNodeWithText("Highway to Hell").performClick() // Wait for mini player, then expand to full playback waitForView(allOf(withId(R.id.sheet1PeekView), isDisplayed())) @@ -190,10 +201,11 @@ class SmokeTestSuite { onView(withText("Songs")) .perform(click()) - // Wait for Flow to emit data, then tap the first song - waitForView(allOf(withId(R.id.recyclerView), hasDescendant(isDisplayed()))) - onView(allOf(withId(R.id.recyclerView), isDisplayed())) - .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())) + // Songs tab is Compose — wait then tap + composeRule.waitUntil(5_000) { + composeRule.onAllNodesWithText("Highway to Hell").fetchSemanticsNodes().isNotEmpty() + } + composeRule.onNodeWithText("Highway to Hell").performClick() // Wait for mini player to appear before expanding to queue waitForView(allOf(withId(R.id.titleTextView), isDescendantOfA(withId(R.id.sheet1PeekView))))