diff --git a/.github/workflows/instrumentation-test.yml b/.github/workflows/instrumentation-test.yml new file mode 100644 index 00000000..29435512 --- /dev/null +++ b/.github/workflows/instrumentation-test.yml @@ -0,0 +1,69 @@ +# Copyright 2020 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. + +# A workflow that runs tests on every new pull request +name: Run instrumentation tests + +on: + repository_dispatch: + types: [test] + push: + branches-ignore: ['gh-pages'] + pull_request: + branches-ignore: ['gh-pages'] + workflow_dispatch: + +jobs: + run-instrumentation-test: + runs-on: macOS-latest # enables hardware acceleration in the virtual machine + timeout-minutes: 30 + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Gradle Wrapper Validation + uses: gradle/wrapper-validation-action@v1.0.4 + + - name: Set up JDK 11 + uses: actions/setup-java@v2.3.1 + with: + java-version: '11' + distribution: 'adopt' + + - name: Inject Maps API Key + run: | + # Injecting the key directly into the manifest as secrets-gradle-plugin + # isn't picking up the key when creating a local.properties file here + sed -i -e "s,\${MAPS_API_KEY},$MAPS_API_KEY,g" ./app/src/main/AndroidManifest.xml + env: + MAPS_API_KEY: ${{ secrets.GMP_API_KEY }} + + - name: Build debug + run: ./gradlew assembleDebug + + - name: Run instrumentation tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + target: google_apis + arch: x86 + disable-animations: true + script: ./gradlew :app:connectedCheck --stacktrace + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-reports + path: ./app/build/reports diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74abaa9e..50623fe1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,9 +19,9 @@ on: repository_dispatch: types: [test] push: - branches-ignore: ['gh-pages'] + branches: ['*'] pull_request: - branches-ignore: ['gh-pages'] + branches: ['*'] workflow_dispatch: jobs: diff --git a/app/build.gradle b/app/build.gradle index 64d7d8c9..d087007c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,7 +27,7 @@ android { } buildFeatures { - buildConfig false + buildConfig true compose true } @@ -47,6 +47,15 @@ dependencies { implementation 'com.google.maps.android:maps-ktx:3.3.0' implementation "androidx.core:core-ktx:1.7.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + androidTestImplementation "androidx.test:core:$androidx_test_version" + androidTestImplementation "androidx.test:rules:$androidx_test_version" + androidTestImplementation "androidx.test:runner:$androidx_test_version" + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" } secrets { diff --git a/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt b/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt new file mode 100644 index 00000000..c78b5c34 --- /dev/null +++ b/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt @@ -0,0 +1,167 @@ +package com.google.maps.android.compose + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class GoogleMapViewTests { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + private val startingZoom = 10f + private val startingPosition = LatLng(1.23, 4.56) + private val assertRoundingError = 0.01 + + private lateinit var cameraPositionState: CameraPositionState + + @Before + fun setUp() { + cameraPositionState = CameraPositionState( + position = CameraPosition.fromLatLngZoom( + startingPosition, + startingZoom + ) + ) + + val countDownLatch = CountDownLatch(1) + composeTestRule.setContent { + GoogleMapView( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + onMapLoaded = { + countDownLatch.countDown() + } + ) + } + val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS) + assertTrue("Map loaded", mapLoaded) + } + + @Test + fun testStartingCameraPosition() { + startingPosition.assertEquals(cameraPositionState.position.target) + } + + @Test + fun testCameraReportsMoving() { + zoom(shouldAnimate = true, zoomIn = true) { + composeTestRule.waitUntil(1000) { + cameraPositionState.isMoving + } + assertTrue(cameraPositionState.isMoving) + } + } + + @Test + fun testCameraReportsNotMoving() { + zoom(shouldAnimate = true, zoomIn = true) { + composeTestRule.waitUntil(1000) { + cameraPositionState.isMoving + } + composeTestRule.waitUntil(1000) { + !cameraPositionState.isMoving + } + assertFalse(cameraPositionState.isMoving) + } + } + + @Test + fun testCameraZoomInAnimation() { + zoom(shouldAnimate = true, zoomIn = true) { + composeTestRule.waitUntil(1000) { + cameraPositionState.isMoving + } + composeTestRule.waitUntil(1000) { + !cameraPositionState.isMoving + } + assertEquals( + startingZoom + 1f, + cameraPositionState.position.zoom, + assertRoundingError.toFloat() + ) + } + } + + @Test + fun testCameraZoomIn() { + zoom(shouldAnimate = false, zoomIn = true) { + composeTestRule.waitUntil(1000) { + cameraPositionState.isMoving + } + composeTestRule.waitUntil(1000) { + !cameraPositionState.isMoving + } + assertEquals( + startingZoom + 1f, + cameraPositionState.position.zoom, + assertRoundingError.toFloat() + ) + } + } + + @Test + fun testCameraZoomOut() { + zoom(shouldAnimate = false, zoomIn = false) { + composeTestRule.waitUntil(1000) { + cameraPositionState.isMoving + } + composeTestRule.waitUntil(1000) { + !cameraPositionState.isMoving + } + assertEquals( + startingZoom - 1f, + cameraPositionState.position.zoom, + assertRoundingError.toFloat() + ) + } + } + + @Test + fun testCameraZoomOutAnimation() { + zoom(shouldAnimate = true, zoomIn = false) { + composeTestRule.waitUntil(1000) { + cameraPositionState.isMoving + } + composeTestRule.waitUntil(1000) { + !cameraPositionState.isMoving + } + assertEquals( + startingZoom - 1f, + cameraPositionState.position.zoom, + assertRoundingError.toFloat() + ) + } + } + + private fun zoom(shouldAnimate: Boolean, zoomIn: Boolean, assertionBlock: () -> Unit) { + if (!shouldAnimate) { + composeTestRule.onNodeWithTag("cameraAnimations") + .assertIsDisplayed() + .performClick() + } + composeTestRule.onNodeWithText(if (zoomIn) "+" else "-") + .assertIsDisplayed() + .performClick() + + assertionBlock() + } + + private fun LatLng.assertEquals(other: LatLng) { + assertEquals(latitude, other.latitude, assertRoundingError) + assertEquals(longitude, other.longitude, assertRoundingError) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt index afde4774..a380e974 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMapOptions @@ -57,19 +58,27 @@ import kotlinx.coroutines.launch private const val TAG = "MapSampleActivity" +val singapore = LatLng(1.35, 103.87) +val singapore2 = LatLng(1.40, 103.77) + class MapSampleActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { var isMapLoaded by remember { mutableStateOf(false) } + // Observing and controlling the camera's state can be done with a CameraPositionState + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom(singapore, 11f) + } Box(Modifier.fillMaxSize()) { GoogleMapView( modifier = Modifier.matchParentSize(), + cameraPositionState = cameraPositionState, onMapLoaded = { isMapLoaded = true - } + }, ) if (!isMapLoaded) { AnimatedVisibility( @@ -92,25 +101,17 @@ class MapSampleActivity : ComponentActivity() { } @Composable -private fun GoogleMapView(modifier: Modifier, onMapLoaded: () -> Unit) { - val singapore = LatLng(1.35, 103.87) - val singapore2 = LatLng(1.40, 103.77) - - // Observing and controlling the camera's state can be done with a CameraPositionState - val cameraPositionState = rememberCameraPositionState { - position = CameraPosition.fromLatLngZoom(singapore, 11f) - } - +fun GoogleMapView( + modifier: Modifier, + cameraPositionState: CameraPositionState, + onMapLoaded: () -> Unit, +) { + var uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) } + var shouldAnimateZoom by remember { mutableStateOf(true) } + var ticker by remember { mutableStateOf(0) } var mapProperties by remember { mutableStateOf(MapProperties(mapType = MapType.NORMAL)) } - var uiSettings by remember { - mutableStateOf( - MapUiSettings(compassEnabled = false) - ) - } - var shouldAnimateZoom by remember { mutableStateOf(true) } - var ticker by remember { mutableStateOf(0) } GoogleMap( modifier = modifier, @@ -240,20 +241,17 @@ private fun ZoomControls( MapButton("-", onClick = { onZoomOut() }) MapButton("+", onClick = { onZoomIn() }) Column(verticalArrangement = Arrangement.Center) { - Row(horizontalArrangement = Arrangement.Center) { - Text(text = "Camera Animations On?") - Switch( - isCameraAnimationChecked, - onCheckedChange = onCameraAnimationCheckedChange - ) - } - Row(horizontalArrangement = Arrangement.Center) { - Text(text = "Zoom Controls On?") - Switch( - isZoomControlsEnabledChecked, - onCheckedChange = onZoomControlsCheckedChange - ) - } + Text(text = "Camera Animations On?") + Switch( + isCameraAnimationChecked, + onCheckedChange = onCameraAnimationCheckedChange, + modifier = Modifier.testTag("cameraAnimations"), + ) + Text(text = "Zoom Controls On?") + Switch( + isZoomControlsEnabledChecked, + onCheckedChange = onZoomControlsCheckedChange + ) } } } diff --git a/build.gradle b/build.gradle index b0c1d081..1f03224e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.6.10' - ext.compose_version = '1.2.0-alpha02' + ext.compose_version = '1.2.0-alpha03' + ext.androidx_test_version = '1.4.0' repositories { google() mavenCentral()