Extract ComposeBasedTestCase class for setting up Compose test

This commit is contained in:
Nebojsa Vuksic 2025-07-31 15:25:39 +02:00
parent 45ef9a28da
commit 25d2d25132
2 changed files with 138 additions and 103 deletions

View File

@ -0,0 +1,46 @@
package org.jetbrains.plugins.template
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import kotlinx.coroutines.test.runTest
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
import org.junit.Rule
/**
* An abstract base class for Compose-based test cases.
*
* This class provides a framework for running Compose UI tests. It includes a
* test rule for composing UI content and abstracts the content under test.
*/
internal abstract class ComposeBasedTestCase {
@get:Rule
val composableRule = createComposeRule()
/**
* Provides the Composable Content under test.
*/
abstract val contentUnderTest: @Composable () -> Unit
/**
* Runs the given Compose test block in the context of a Compose content test rule.
*/
fun runComposeTest(block: suspend ComposeTestRule.() -> Unit) = runTest {
composableRule.setContentWrappedInTheme {
contentUnderTest()
}
composableRule.block()
}
private fun ComposeContentTestRule.setContentWrappedInTheme(content: @Composable () -> Unit) {
setContent {
IntUiTheme {
content()
}
}
}
}

View File

@ -5,30 +5,33 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.test.runTest
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
import org.jetbrains.plugins.template.weatherApp.model.Location
import org.jetbrains.plugins.template.weatherApp.model.SelectableLocation
import org.jetbrains.plugins.template.weatherApp.services.MyLocationsViewModelApi
import org.jetbrains.plugins.template.weatherApp.ui.MyLocationsListWithEmptyListPlaceholder
import org.junit.Rule
import org.junit.Test
internal class MyLocationListTest {
@get:Rule
val composeRule = createComposeRule()
internal class MyLocationListTest : ComposeBasedTestCase() {
private val noLocations = emptyList<Location>()
private val myLocationsViewModelApi = FakeMyLocationsViewModel(locations = noLocations)
override val contentUnderTest: @Composable () -> Unit = {
MyLocationsListWithEmptyListPlaceholder(
modifier = Modifier.fillMaxWidth(),
myLocationsViewModelApi = myLocationsViewModelApi
)
}
@Test
fun `verify placeholder is shown when no locations is added`() = composeRule.runComposeTest {
fun `verify placeholder is shown when no locations is added`() = runComposeTest {
val myLocationsRobot = MyLocationListRobot(this)
myLocationsRobot
@ -36,23 +39,23 @@ internal class MyLocationListTest {
}
@Test
fun `verify location is selected when user adds location`() = composeRule.runComposeTest { locationsViewModelApi ->
fun `verify location is selected when user adds location`() = runComposeTest {
val myLocationsRobot = MyLocationListRobot(this)
locationsViewModelApi.onAddLocation(Location("Munich", "Germany"))
myLocationsViewModelApi.onAddLocation(Location("Munich", "Germany"))
myLocationsRobot
.verifyListItemWithTextIsSelected("Munich, Germany")
}
@Test
fun `verify item selection when multiple items are present`() = composeRule.runComposeTest { locationsViewModelApi ->
fun `verify item selection when multiple items are present`() = runComposeTest {
val myLocationsRobot = MyLocationListRobot(this)
// Add multiple locations
locationsViewModelApi.onAddLocation(Location("Munich", "Germany"))
locationsViewModelApi.onAddLocation(Location("Berlin", "Germany"))
locationsViewModelApi.onAddLocation(Location("Paris", "France"))
myLocationsViewModelApi.onAddLocation(Location("Munich", "Germany"))
myLocationsViewModelApi.onAddLocation(Location("Berlin", "Germany"))
myLocationsViewModelApi.onAddLocation(Location("Paris", "France"))
// Initially, the last added location (Paris) should be selected
myLocationsRobot.verifyListItemWithTextIsSelected("Paris, France")
@ -65,7 +68,7 @@ internal class MyLocationListTest {
}
@Test
fun `verify item deletion when multiple items are present`() = composeRule.runComposeTest { locationsViewModelApi ->
fun `verify item deletion when multiple items are present`() = runComposeTest {
val myLocationsRobot = MyLocationListRobot(this)
// Add multiple locations
@ -73,15 +76,15 @@ internal class MyLocationListTest {
val berlin = Location("Berlin", "Germany")
val paris = Location("Paris", "France")
locationsViewModelApi.onAddLocation(munich)
locationsViewModelApi.onAddLocation(berlin)
locationsViewModelApi.onAddLocation(paris)
myLocationsViewModelApi.onAddLocation(munich)
myLocationsViewModelApi.onAddLocation(berlin)
myLocationsViewModelApi.onAddLocation(paris)
// Initially, the last added location (Paris) should be selected
myLocationsRobot.verifyListItemWithTextIsSelected("Paris, France")
// Delete the selected location (Paris)
locationsViewModelApi.onDeleteLocation(paris)
myLocationsViewModelApi.onDeleteLocation(paris)
// Verify Paris is no longer in the list and Berlin is now selected
// (as it's the last item in the list after deletion)
@ -90,7 +93,7 @@ internal class MyLocationListTest {
}
@Test
fun `verify middle item deletion when three items are present`() = composeRule.runComposeTest { locationsViewModelApi ->
fun `verify middle item deletion when three items are present`() = runComposeTest {
val myLocationsRobot = MyLocationListRobot(this)
// Add three locations
@ -98,15 +101,15 @@ internal class MyLocationListTest {
val berlin = Location("Berlin", "Germany")
val paris = Location("Paris", "France")
locationsViewModelApi.onAddLocation(munich)
locationsViewModelApi.onAddLocation(berlin)
locationsViewModelApi.onAddLocation(paris)
myLocationsViewModelApi.onAddLocation(munich)
myLocationsViewModelApi.onAddLocation(berlin)
myLocationsViewModelApi.onAddLocation(paris)
// Initially, the last added location (Paris) should be selected
myLocationsRobot.verifyListItemWithTextIsSelected("Paris, France")
// Delete the middle location (Berlin)
locationsViewModelApi.onDeleteLocation(berlin)
myLocationsViewModelApi.onDeleteLocation(berlin)
// Verify Berlin is no longer in the list
myLocationsRobot.verifyItemDoesNotExist("Berlin, Germany")
@ -120,18 +123,18 @@ internal class MyLocationListTest {
}
@Test
fun `verify deletion of the only item in list`() = composeRule.runComposeTest { locationsViewModelApi ->
fun `verify deletion of the only item in list`() = runComposeTest {
val myLocationsRobot = MyLocationListRobot(this)
// Add one location
val munich = Location("Munich", "Germany")
locationsViewModelApi.onAddLocation(munich)
myLocationsViewModelApi.onAddLocation(munich)
// Verify the location is selected
myLocationsRobot.verifyListItemWithTextIsSelected("Munich, Germany")
// Delete the only location
locationsViewModelApi.onDeleteLocation(munich)
myLocationsViewModelApi.onDeleteLocation(munich)
// Verify the location is no longer in the list
myLocationsRobot.verifyItemDoesNotExist("Munich, Germany")
@ -140,20 +143,6 @@ internal class MyLocationListTest {
myLocationsRobot.verifyNoLocationsPlaceHolderVisible()
}
private fun ComposeContentTestRule.runComposeTest(
myLocationsViewModelApi: MyLocationsViewModelApi = this@MyLocationListTest.myLocationsViewModelApi,
block: suspend ComposeContentTestRule.(MyLocationsViewModelApi) -> Unit
) = runTest {
this@runComposeTest.setContentWrappedInTheme {
MyLocationsListWithEmptyListPlaceholder(
modifier = Modifier.fillMaxWidth(),
myLocationsViewModelApi = myLocationsViewModelApi
)
}
this@runComposeTest.block(myLocationsViewModelApi)
}
private class FakeMyLocationsViewModel(
locations: List<Location> = emptyList()
) : MyLocationsViewModelApi {
@ -161,13 +150,13 @@ internal class MyLocationListTest {
private val locationsFlow = MutableStateFlow(locations.toMutableList())
private val selectedItemIndex = MutableStateFlow(if (locations.isNotEmpty()) 0 else -1)
private val _myLocations = locationsFlow
.combine(selectedItemIndex) { locations, selectedIndex ->
locations.mapIndexed { index, location ->
SelectableLocation(location, index == selectedIndex)
}
}
override val myLocationsFlow: Flow<List<SelectableLocation>> = _myLocations
override fun onAddLocation(locationToAdd: Location) {
@ -198,9 +187,8 @@ internal class MyLocationListTest {
}
}
}
}
private class MyLocationListRobot(private val composableRule: ComposeContentTestRule) {
private class MyLocationListRobot(private val composableRule: ComposeTestRule) {
fun clickOnItemWithText(text: String) {
composableRule
@ -236,4 +224,5 @@ private class MyLocationListRobot(private val composableRule: ComposeContentTest
.onNodeWithText(text)
.assertExists()
}
}
}