mirror of
https://github.com/JetBrains/intellij-platform-plugin-template.git
synced 2025-12-05 06:11:52 +00:00
Add UI test to verify adding and removing locations in WeatherApp
This commit is contained in:
parent
8709e7cf8a
commit
d6bec6e21c
@ -0,0 +1,144 @@
|
||||
package org.jetbrains.plugins.template.weatherApp.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.jetbrains.plugins.template.ComposeBasedTestCase
|
||||
import org.jetbrains.plugins.template.weatherApp.model.Location
|
||||
import org.jetbrains.plugins.template.weatherApp.services.SearchAutoCompletionItemProvider
|
||||
import org.junit.Test
|
||||
|
||||
internal class WeatherAppSampleUiTest : ComposeBasedTestCase() {
|
||||
|
||||
private val fakeSearchProvider = FakeSearchProvider()
|
||||
private val fakeMyLocationsViewModel = FakeMyLocationsViewModel()
|
||||
private val fakeWeatherViewModel = FakeWeatherViewModel()
|
||||
|
||||
override val contentUnderTest: @Composable () -> Unit = {
|
||||
WeatherAppSample(
|
||||
myLocationViewModel = fakeMyLocationsViewModel,
|
||||
weatherViewModelApi = fakeWeatherViewModel,
|
||||
searchAutoCompletionItemProvider = fakeSearchProvider
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add location via search UI then remove it`() = runComposeTest {
|
||||
val robot = WeatherSampleRobot(this)
|
||||
|
||||
// Add a location via UI: type, select autocomplete, click Add
|
||||
robot.focusAndTypeInSearchField("Mun")
|
||||
robot.waitForAutocomplete("Munich, Germany")
|
||||
robot.clickOnAutocompleteItem("Munich, Germany")
|
||||
robot.clickAddButton()
|
||||
|
||||
// Verify the item appears selected in My Locations
|
||||
robot.verifyListItemWithTextIsSelected("Munich, Germany")
|
||||
|
||||
// Remove the item via UI: open context menu with primary click (test mode) and click Delete
|
||||
robot.rightClickOnListItem("Munich, Germany")
|
||||
robot.clickDeleteInContextMenu()
|
||||
|
||||
// Verify empty placeholder is shown again
|
||||
robot.verifyNoLocationsPlaceHolderVisible()
|
||||
}
|
||||
|
||||
private class WeatherSampleRobot(private val rule: ComposeTestRule) {
|
||||
fun idle() = rule.waitForIdle()
|
||||
|
||||
fun focusAndTypeInSearchField(text: String) {
|
||||
val field = rule.onNode(hasSetTextAction())
|
||||
field.performClick()
|
||||
field.performTextInput(text)
|
||||
}
|
||||
|
||||
fun waitForAutocomplete(itemLabel: String) {
|
||||
rule.waitUntil(timeoutMillis = 100) {
|
||||
rule.onAllNodesWithText(itemLabel).fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun clickOnAutocompleteItem(itemLabel: String) {
|
||||
rule.onNodeWithText(itemLabel).performClick()
|
||||
}
|
||||
|
||||
fun clickAddButton() {
|
||||
rule.onNodeWithText("Add").performClick()
|
||||
}
|
||||
|
||||
fun rightClickOnListItem(text: String) {
|
||||
rule.onNodeWithText(text)
|
||||
.assertExists("No node found with text: $text")
|
||||
.performMouseInput { rightClick() }
|
||||
}
|
||||
|
||||
fun clickDeleteInContextMenu() {
|
||||
rule.onNodeWithText("Delete").performClick()
|
||||
}
|
||||
|
||||
fun verifyListItemWithTextIsSelected(text: String) {
|
||||
rule.onNodeWithText(text).assertIsSelected()
|
||||
}
|
||||
|
||||
fun verifyNoLocationsPlaceHolderVisible() {
|
||||
rule.onNodeWithText("No locations added yet. Go and add the first location.").assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeSearchProvider : SearchAutoCompletionItemProvider<Location> {
|
||||
override fun provideSearchableItems(searchTerm: String): List<Location> {
|
||||
if (searchTerm.isBlank()) return emptyList()
|
||||
// Provide a small fixed set that includes Munich and others regardless of query for simplicity
|
||||
return listOf(
|
||||
Location("Munich", "Germany"),
|
||||
Location("Berlin", "Germany"),
|
||||
Location("Paris", "France"),
|
||||
).filter { it.label.contains(searchTerm, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeMyLocationsViewModel : MyLocationsViewModelApi {
|
||||
private val _state = MutableStateFlow(LocationsUIState.empty())
|
||||
|
||||
override val myLocationsUIStateFlow: Flow<LocationsUIState>
|
||||
get() = _state.asStateFlow()
|
||||
|
||||
override fun onAddLocation(locationToAdd: Location) {
|
||||
_state.value = _state.value.withLocationAdded(locationToAdd)
|
||||
}
|
||||
|
||||
override fun onDeleteLocation(locationToDelete: Location) {
|
||||
_state.value = _state.value.withLocationDeleted(locationToDelete)
|
||||
}
|
||||
|
||||
override fun onLocationSelected(selectedLocationIndex: Int) {
|
||||
_state.value = _state.value.withItemAtIndexSelected(selectedLocationIndex)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// no-op for tests
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeWeatherViewModel : WeatherViewModelApi {
|
||||
private val _weatherState = MutableStateFlow<WeatherForecastUIState>(WeatherForecastUIState.Empty)
|
||||
|
||||
override val weatherForecastUIState: Flow<WeatherForecastUIState>
|
||||
get() = _weatherState.asStateFlow()
|
||||
|
||||
override fun onLoadWeatherForecast(location: Location) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun onReloadWeatherForecast() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user