Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ class PublishingConventionPlugin : Plugin<Project> {

private fun Project.configureJacoco() {
configure<JacocoPluginExtension> {
toolVersion = "0.8.11" // Compatible with newer JDKs
toolVersion = "0.8.15" // Compatible with newer JDKs
}

tasks.withType<Test>().configureEach {
configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf("jdk.internal.*")
}
}

// AGP 9.0+ built-in Jacoco support or manual configuration.
Expand Down
8 changes: 8 additions & 0 deletions places-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ android {
buildConfig = true
compose = true
}

testOptions {
unitTests {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}
}

dependencies {
Expand Down Expand Up @@ -83,6 +90,7 @@ dependencies {
testImplementation(libs.google.truth)
testImplementation(kotlin("test"))
testImplementation(libs.robolectric)
testImplementation(libs.mockk)

testImplementation(libs.ui.test.junit4)
testImplementation(libs.ui.test.manifest)
Expand Down
23 changes: 23 additions & 0 deletions places-compose/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="androidx.activity.ComponentActivity"
android:exported="true" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import com.google.gson.annotations.SerializedName

data class ReverseGeocodingResponse(
@SerializedName("status") val status: String,
@SerializedName("error_message") val errorMessage: String? = null,
@SerializedName("address_descriptor") val addressDescriptor: AddressDescriptor?,
@SerializedName("plus_code") val plusCode: PlusCode?,
@SerializedName("results") val addresses: List<AddressDto>
@SerializedName("results") val addresses: List<AddressDto> = emptyList()
)

data class AddressDescriptor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.android.libraries.places.compose.autocomplete.components

import android.text.SpannableString
import androidx.activity.ComponentActivity
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.google.android.libraries.places.compose.autocomplete.data.meters
import com.google.android.libraries.places.compose.autocomplete.models.AutocompletePlace
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

/**
* UI unit tests verifying the [PlacesAutocompleteTextField] and [AutocompletePlaceRow] composables.
*
* This test suite covers:
* 1. Search text field rendering with query text and placeholder.
* 2. Entering text and triggering [onQueryChanged].
* 3. Rendering place predictions with primary and secondary texts.
* 4. Selecting a place prediction item and triggering [onSelected].
* 5. Clearing search text via the clear trailing icon button.
* 6. Clicking back navigation button when [onBackClicked] is provided.
*/
@RunWith(RobolectricTestRunner::class)
class PlacesAutocompleteTextFieldTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

private val samplePlaces = listOf(
AutocompletePlace(
placeId = "place_1",
primaryText = SpannableString("Googleplex"),
secondaryText = SpannableString("1600 Amphitheatre Pkwy, Mountain View, CA"),
distance = 350.meters
),
AutocompletePlace(
placeId = "place_2",
primaryText = SpannableString("Golden Gate Bridge"),
secondaryText = SpannableString("San Francisco, CA"),
distance = 15000.meters
)
)

@Test
fun placesAutocompleteTextField_displaysPlaceholderAndInitialPredictions() {
composeTestRule.setContent {
MaterialTheme {
PlacesAutocompleteTextField(
searchText = "",
predictions = samplePlaces,
onQueryChanged = {},
placeHolderText = "Search Google Maps"
)
}
}

// Verify search field and placeholder
composeTestRule.onNodeWithTag("placesAutocompleteSearchField").assertIsDisplayed()
composeTestRule.onNodeWithText("Search Google Maps").assertIsDisplayed()

// Verify prediction rows
composeTestRule.onNodeWithText("Googleplex").assertIsDisplayed()
composeTestRule.onNodeWithText("1600 Amphitheatre Pkwy, Mountain View, CA").assertIsDisplayed()
composeTestRule.onNodeWithText("Golden Gate Bridge").assertIsDisplayed()
composeTestRule.onNodeWithText("San Francisco, CA").assertIsDisplayed()
}

@Test
fun placesAutocompleteTextField_typingUpdatesQuery() {
var query by mutableStateOf("")

composeTestRule.setContent {
MaterialTheme {
PlacesAutocompleteTextField(
searchText = query,
predictions = emptyList(),
onQueryChanged = { query = it },
placeHolderText = "Search here"
)
}
}

composeTestRule.onNodeWithTag("placesAutocompleteSearchField")
.performTextInput("Mountain View")

assertThat(query).isEqualTo("Mountain View")
}

@Test
fun placesAutocompleteTextField_selectingPlaceInvokesCallback() {
var selectedPlace: AutocompletePlace? = null

composeTestRule.setContent {
MaterialTheme {
PlacesAutocompleteTextField(
searchText = "Google",
predictions = samplePlaces,
onQueryChanged = {},
onSelected = { selectedPlace = it }
)
}
}

composeTestRule.onNodeWithText("Googleplex").performClick()

assertThat(selectedPlace).isNotNull()
assertThat(selectedPlace?.placeId).isEqualTo("place_1")
assertThat(selectedPlace?.primaryText.toString()).isEqualTo("Googleplex")
}

@Test
fun placesAutocompleteTextField_clearButtonClearsQuery() {
var query by mutableStateOf("Initial query")

composeTestRule.setContent {
MaterialTheme {
PlacesAutocompleteTextField(
searchText = query,
predictions = emptyList(),
onQueryChanged = { query = it }
)
}
}

composeTestRule.onNodeWithContentDescription("Clear").performClick()

assertThat(query).isEmpty()
}

@Test
fun placesAutocompleteTextField_backButtonClickedInvokesCallback() {
var backClicked = false

composeTestRule.setContent {
MaterialTheme {
PlacesAutocompleteTextField(
searchText = "",
predictions = emptyList(),
onQueryChanged = {},
onBackClicked = { backClicked = true }
)
}
}

composeTestRule.onNodeWithContentDescription("Back").performClick()

assertThat(backClicked).isTrue()
}

@Test
fun placesAutocompleteTextField_previewsRenderCleanly() {
composeTestRule.setContent {
AutocompleteFieldPreview()
AutocompletePlaceRowPreview()
AutocompletePlaceRowPreviewShortText()
AutocompletePlaceRowPreviewLongRows()
}

composeTestRule.onNodeWithText("463 km").assertIsDisplayed()
composeTestRule.onNodeWithText("this is a primary test").assertIsDisplayed()
composeTestRule.onNodeWithText("REI").assertIsDisplayed()
}

@Test
fun placesAutocompleteTextField_darkThemeAndNotScrollable() {
composeTestRule.setContent {
MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) {
PlacesAutocompleteTextField(
searchText = "Dark test",
predictions = samplePlaces,
onQueryChanged = {},
scrollable = false,
selectedPlace = samplePlaces.first()
)
}
}

composeTestRule.onNodeWithText("Googleplex").assertIsDisplayed()
}

@Test
fun autocompletePlaceRow_expandedStateRendersAndInvokesCallbacks() {
var placeSelected = false
var expandedClicked = false

composeTestRule.setContent {
MaterialTheme {
AutocompletePlaceRow(
autocompletePlace = samplePlaces.first(),
isSelected = true,
onPlaceSelected = { placeSelected = true },
onExpandClick = { expandedClicked = true },
isExpanded = true,
primaryTextMaxLines = 1,
secondaryTextMaxLines = 1
)
}
}

composeTestRule.onNodeWithText("Googleplex").performClick()
assertThat(placeSelected).isTrue()
}
}
Loading