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
17 changes: 8 additions & 9 deletions .agents/skills/android-maps-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ dependencies {

Use `withTestMap { ... }` to spin up a measured, laid-out, initialized `GoogleMap` in one line and assert state with `com.google.maps.android.robolectric.truth.assertThat`:

Reference: `// [START maps_robolectric_demo_test]` in [`MapDemoTest.kt`](../../../app/src/test/java/com/example/androidmapsrobolectric/MapDemoTest.kt#L36-L83)
Reference: region `maps_robolectric_demo_test` in [`MapDemoTest.kt`](../../../app/src/test/java/com/example/androidmapsrobolectric/MapDemoTest.kt#L36-L83)

```kotlin
// [START maps_robolectric_demo_test]
Expand Down Expand Up @@ -240,7 +240,7 @@ class MapDemoTest {

If you prefer plain boolean expressions with zero Google Truth dependency, import `assertMap`, `assertMarker`, `distanceTo`, and `meters` directly from `com.google.maps.android.robolectric.powerassert`:

Reference: `// [START maps_robolectric_power_assert]` in [`LazyAssertionsComparisonTest.kt`](../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/LazyAssertionsComparisonTest.kt#L151-L187)
Reference: region `maps_robolectric_power_assert` in [`LazyAssertionsComparisonTest.kt`](../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/LazyAssertionsComparisonTest.kt#L151-L187)

```kotlin
// [START maps_robolectric_power_assert]
Expand Down Expand Up @@ -288,7 +288,7 @@ Reference: `// [START maps_robolectric_power_assert]` in [`LazyAssertionsCompari

Pass the test `GoogleMap` from `createTestMap()` directly into your production controller (`StoreLocatorMapManager`), or locate an embedded `MapView` inside an `Activity` or Jetpack Compose screen via `requireMapView()`:

Reference: `// [START maps_robolectric_manager_test]` in [`StoreLocatorMapManagerTest.kt`](../../../app/src/test/java/com/example/androidmapsrobolectric/manager/StoreLocatorMapManagerTest.kt#L37-L129)
Reference: region `maps_robolectric_manager_test` in [`StoreLocatorMapManagerTest.kt`](../../../app/src/test/java/com/example/androidmapsrobolectric/manager/StoreLocatorMapManagerTest.kt#L37-L129)

```kotlin
// [START maps_robolectric_manager_test]
Expand Down Expand Up @@ -386,7 +386,7 @@ class StoreLocatorMapManagerTest {
// [END maps_robolectric_manager_test]
```

Reference: `// [START robolectric_view_hierarchy_lookup]` in [`ShadowMapViewKotlinTest.kt`](../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/ShadowMapViewKotlinTest.kt#L511-L521)
Reference: region `robolectric_view_hierarchy_lookup` in [`ShadowMapViewKotlinTest.kt`](../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/ShadowMapViewKotlinTest.kt#L511-L521)

```kotlin
// [START robolectric_view_hierarchy_lookup]
Expand All @@ -406,7 +406,7 @@ Reference: `// [START robolectric_view_hierarchy_lookup]` in [`ShadowMapViewKotl

### Paradigm 1D: Comprehensive Java Headless JVM Testing (`:robolectric-testing`)

Reference: `// [START maps_robolectric_java_demo_test]` in [`MapViewDemoTest.java`](../../../app/src/test/java/com/example/androidmapsrobolectric/MapViewDemoTest.java#L86-L152)
Reference: region `maps_robolectric_java_demo_test` in [`MapViewDemoTest.java`](../../../app/src/test/java/com/example/androidmapsrobolectric/MapViewDemoTest.java#L86-L152)

```java
// [START maps_robolectric_java_demo_test]
Expand Down Expand Up @@ -483,7 +483,7 @@ Reference: `// [START maps_robolectric_java_demo_test]` in [`MapViewDemoTest.jav

Use `enableGoldenTesting(...)` to replace live Google tiles with deterministic synthetic tiles (`CoordinateGrid`, `SolidColorHash`, `Checkerboard`, or `Custom`) and verify renders with `captureSnapshot()`, `assertMatchesGolden(goldenFile)`, or `GoldenImageDiff.assertMatches(...)`:

Reference: `// [START maps_sample_golden_map_test]` in [`SampleGoldenMapTest.kt`](../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/sample/SampleGoldenMapTest.kt#L46-L128)
Reference: region `maps_sample_golden_map_test` in [`SampleGoldenMapTest.kt`](../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/sample/SampleGoldenMapTest.kt#L46-L128)

```kotlin
// [START maps_sample_golden_map_test]
Expand Down Expand Up @@ -571,7 +571,7 @@ public class SampleGoldenMapTest {
// [END maps_sample_golden_map_test]
```

Reference: `// [START golden_diff_artifacts_and_record]` in [`GoldenImageDiffTest.kt`](../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/GoldenImageDiffTest.kt#L109-L146)
Reference: region `golden_diff_artifacts_and_record` in [`GoldenImageDiffTest.kt`](../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/GoldenImageDiffTest.kt#L109-L146)

```kotlin
// [START golden_diff_artifacts_and_record]
Expand Down Expand Up @@ -620,7 +620,7 @@ Reference: `// [START golden_diff_artifacts_and_record]` in [`GoldenImageDiffTes

Extend `BaseVisualTest` and call `verifyScreenshotWithGoldenFallback(...)`. On clean emulators or CI, `BaseVisualTest` checks bundled test APK assets (`src/androidTest/assets/goldens/<testName>.png`) first for an instant zero-quota fast pass, and only consults Gemini Vision when no baseline exists yet or pixel diff exceeds `tolerancePercent`:

Reference: `// [START maps_visual_test_sample]` in [`VisualMapSampleTest.kt`](../../../app/src/androidTest/java/com/example/androidmapsrobolectric/VisualMapSampleTest.kt#L28-L72)
Reference: region `maps_visual_test_sample` in [`VisualMapSampleTest.kt`](../../../app/src/androidTest/java/com/example/androidmapsrobolectric/VisualMapSampleTest.kt#L28-L71)

```kotlin
// [START maps_visual_test_sample]
Expand Down Expand Up @@ -663,7 +663,6 @@ class VisualMapSampleTest : BaseVisualTest() {
testName = "store_locator_initial_state",
screenshotBitmap = screenshotBitmap,
prompt = prompt,
passCondition = { it.contains("PASSED", ignoreCase = true) },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ class VisualMapTestTemplate : BaseVisualTest() {
testName = "map_visual_integrity_baseline",
screenshotBitmap = screenshot,
prompt = verificationPrompt,
passCondition = { it.contains("PASSED", ignoreCase = true) },
tolerancePercent = 0.5,
updateGoldenOnPass = true,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ Traditional screenshot testing fails on complex map apps for multiple reasons:

---

## 2. Dynamic Gemini Flash Model Discovery
## 2. Gemini Model Selection

Instead of hardcoding a model string that becomes deprecated or obsolete over time, `GeminiVisualTestHelper` dynamically queries `https://generativelanguage.googleapis.com/v1beta/models` on first run to resolve the newest stable `flash` model (e.g. `gemini-2.5-flash`).
`GeminiVisualTestHelper` uses the pinned `DEFAULT_FALLBACK_MODEL` unless a model is configured, so a new model release cannot change test verdicts on its own. Opting into `latest` queries `https://generativelanguage.googleapis.com/v1beta/models` once per session for the newest stable `flash` model and caches it in memory. The API key is sent in the `x-goog-api-key` header, never in the URL.

Once resolved, the model name is cached in-memory across the entire test session. You can also override the model:
Model resolution order:
- Explicit parameter: `GeminiVisualTestHelper(preferredModel = "gemini-2.5-flash")`
- System property: `-Dgemini.model="gemini-2.5-flash"`
- Latest stable Flash model, looked up once per session: `GeminiVisualTestHelper.LATEST_MODEL` or `-Dgemini.model=latest`
- Neither set: the pinned `GeminiVisualTestHelper.DEFAULT_FALLBACK_MODEL`, without a network call

---

Expand Down Expand Up @@ -75,15 +77,15 @@ If no key is configured and no matching golden baseline is found, tests using Ge
```
3. **Fast Zero-Quota CI & Fresh Emulator Runs**: On clean CI emulators, `BaseVisualTest` automatically loads `src/androidTest/assets/goldens/<testName>.png` from the test APK assets. When the screenshot matches within `tolerancePercent`, the test passes in milliseconds with **zero Gemini API calls** (and doesn't even require `GEMINI_API_KEY` to be set).

Reference: `// [START maps_visual_golden_fallback]` in [`BaseVisualTest.kt`](../../../../visual-testing/src/main/java/com/google/maps/android/testing/visual/BaseVisualTest.kt#L147-L218)
Reference: region `maps_visual_golden_fallback` in [`BaseVisualTest.kt`](../../../../visual-testing/src/main/java/com/google/maps/android/testing/visual/BaseVisualTest.kt#L160-L237)

```kotlin
// [START maps_visual_golden_fallback]
suspend fun verifyScreenshotWithGoldenFallback(
testName: String,
screenshotBitmap: Bitmap,
prompt: String,
passCondition: (String) -> Boolean = { it.contains("PASSED", ignoreCase = true) },
passCondition: (String) -> Boolean = { isPassedVerdict(it) },
goldensDirectory: File = File(context.getExternalFilesDir(null) ?: context.filesDir, "goldens"),
tolerancePercent: Double = 0.5,
updateGoldenOnPass: Boolean = true,
Expand Down Expand Up @@ -132,7 +134,13 @@ Reference: `// [START maps_visual_golden_fallback]` in [`BaseVisualTest.kt`](../
passed,
)

if (updateGoldenOnPass) {
if (goldenBitmap != null) {
Log.w(
"BaseVisualTest",
"Gemini approved '$testName', but the screenshot differs from its golden baseline. The golden was " +
"not updated. If the change is intended, replace the golden with the new screenshot.",
)
} else if (updateGoldenOnPass) {
try {
goldensDirectory.mkdirs()
FileOutputStream(goldenFile).use { out ->
Expand All @@ -156,7 +164,7 @@ Reference: `// [START maps_visual_golden_fallback]` in [`BaseVisualTest.kt`](../

## 5. End-to-End Instrumented Test

Reference: `// [START maps_visual_test_sample]` in [`VisualMapSampleTest.kt`](../../../../app/src/androidTest/java/com/example/androidmapsrobolectric/VisualMapSampleTest.kt#L28-L72)
Reference: region `maps_visual_test_sample` in [`VisualMapSampleTest.kt`](../../../../app/src/androidTest/java/com/example/androidmapsrobolectric/VisualMapSampleTest.kt#L28-L71)

```kotlin
// [START maps_visual_test_sample]
Expand Down Expand Up @@ -199,7 +207,6 @@ class VisualMapSampleTest : BaseVisualTest() {
testName = "store_locator_initial_state",
screenshotBitmap = screenshotBitmap,
prompt = prompt,
passCondition = { it.contains("PASSED", ignoreCase = true) },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Screenshot and golden image testing for interactive maps has historically suffer

`GoogleMap.awaitMapLoaded()` and `GoogleMap.captureSnapshot()` automatically idle `ShadowLooper.idleMainLooper()` when running under Robolectric (while suspending for real callbacks on Android devices), and `GoogleMap.assertMatchesGolden(goldenFile)` / `GoogleMap.assertMatchesGoldenBlocking(goldenFile)` capture and verify snapshots in a single line:

Reference: `// [START maps_golden_map_extensions]` in [`GoogleMapExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/GoogleMapExtensions.kt#L29-L162)
Reference: region `maps_golden_map_extensions` in [`GoogleMapExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/GoogleMapExtensions.kt#L29-L162)

```kotlin
// [START maps_golden_map_extensions]
Expand Down Expand Up @@ -187,7 +187,7 @@ private fun idleMainLooperIfRobolectric() {

## 4. Jetpack Compose Integration (`rememberGoldenTileProvider`)

Reference: `// [START maps_golden_compose_extensions]` in [`GoldenComposeExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/GoldenComposeExtensions.kt#L22-L51)
Reference: region `maps_golden_compose_extensions` in [`GoldenComposeExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/GoldenComposeExtensions.kt#L22-L51)

```kotlin
// [START maps_golden_compose_extensions]
Expand Down Expand Up @@ -226,7 +226,7 @@ public fun rememberGoldenTileProvider(

## 5. Recording Baselines (`-Dmaps.golden.record=true`) & Failure Artifacts (`_actual.png` / `_diff.png`)

Reference: `// [START golden_diff_artifacts_and_record]` in [`GoldenImageDiffTest.kt`](../../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/GoldenImageDiffTest.kt#L109-L146)
Reference: region `golden_diff_artifacts_and_record` in [`GoldenImageDiffTest.kt`](../../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/GoldenImageDiffTest.kt#L109-L146)

```kotlin
// [START golden_diff_artifacts_and_record]
Expand Down Expand Up @@ -273,7 +273,7 @@ Reference: `// [START golden_diff_artifacts_and_record]` in [`GoldenImageDiffTes

## 6. End-to-End Sample Test

Reference: `// [START maps_sample_golden_map_test]` in [`SampleGoldenMapTest.kt`](../../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/sample/SampleGoldenMapTest.kt#L46-L128)
Reference: region `maps_sample_golden_map_test` in [`SampleGoldenMapTest.kt`](../../../../golden-testing/src/test/java/com/google/maps/android/testing/golden/sample/SampleGoldenMapTest.kt#L46-L128)

```kotlin
// [START maps_sample_golden_map_test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This guide covers testing Google Maps within **Jetpack Compose** applications an

Both `:robolectric-testing` (`com.google.maps.android.robolectric.ktx`) and `:golden-testing` (`com.google.maps.android.testing.golden`) provide view hierarchy traversal helpers to extract the hosted `MapView` from an `Activity` or Jetpack Compose `AndroidView` container:

Reference: `// [START robolectric_view_hierarchy_lookup]` in [`ShadowMapViewKotlinTest.kt`](../../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/ShadowMapViewKotlinTest.kt#L511-L521)
Reference: region `robolectric_view_hierarchy_lookup` in [`ShadowMapViewKotlinTest.kt`](../../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/ShadowMapViewKotlinTest.kt#L511-L521)

```kotlin
// [START robolectric_view_hierarchy_lookup]
Expand All @@ -24,7 +24,7 @@ Reference: `// [START robolectric_view_hierarchy_lookup]` in [`ShadowMapViewKotl
// [END robolectric_view_hierarchy_lookup]
```

Reference: `// [START maps_compose_test_extensions]` in [`ComposeMapTestExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/ComposeMapTestExtensions.kt#L36-L110)
Reference: region `maps_compose_test_extensions` in [`ComposeMapTestExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/ComposeMapTestExtensions.kt#L36-L110)

```kotlin
// [START maps_compose_test_extensions]
Expand Down Expand Up @@ -110,7 +110,7 @@ public fun GoogleMap.assertCameraTarget(expected: LatLng, tolerance: Distance =

Inject deterministic synthetic tiles directly into Maps Compose with `rememberGoldenTileProvider()`:

Reference: `// [START maps_golden_compose_extensions]` in [`GoldenComposeExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/GoldenComposeExtensions.kt#L22-L51)
Reference: region `maps_golden_compose_extensions` in [`GoldenComposeExtensions.kt`](../../../../golden-testing/src/main/java/com/google/maps/android/testing/golden/GoldenComposeExtensions.kt#L22-L51)

```kotlin
// [START maps_golden_compose_extensions]
Expand Down Expand Up @@ -151,7 +151,7 @@ public fun rememberGoldenTileProvider(

`android-maps-testing` faithfully models `ShadowProjection` and `ShadowGoogleMap` so that distance-based marker clustering algorithms (like `DistanceBasedAlgorithm` and `NonHierarchicalDistanceBasedAlgorithm`) operate identically on the local JVM:

Reference: `// [START maps_robolectric_clustering_test]` in [`MapsUtilsClusteringIntegrationTest.kt`](../../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/MapsUtilsClusteringIntegrationTest.kt#L45-L167)
Reference: region `maps_robolectric_clustering_test` in [`MapsUtilsClusteringIntegrationTest.kt`](../../../../robolectric-testing/src/test/java/com/google/maps/android/robolectric/shadows/MapsUtilsClusteringIntegrationTest.kt#L45-L167)

```kotlin
// [START maps_robolectric_clustering_test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ flowchart TD

## 4. Production Map Controller Implementation

Reference: `// [START maps_robolectric_manager]` in [`StoreLocatorMapManager.kt`](../../../../app/src/main/java/com/example/androidmapsrobolectric/manager/StoreLocatorMapManager.kt#L31-L157)
Reference: region `maps_robolectric_manager` in [`StoreLocatorMapManager.kt`](../../../../app/src/main/java/com/example/androidmapsrobolectric/manager/StoreLocatorMapManager.kt#L31-L157)

```kotlin
// [START maps_robolectric_manager]
Expand Down Expand Up @@ -231,7 +231,7 @@ class StoreLocatorMapManager(

## 5. Testing the Controller on JVM with `createTestMap` & Domain Assertions

Reference: `// [START maps_robolectric_manager_test]` in [`StoreLocatorMapManagerTest.kt`](../../../../app/src/test/java/com/example/androidmapsrobolectric/manager/StoreLocatorMapManagerTest.kt#L37-L129)
Reference: region `maps_robolectric_manager_test` in [`StoreLocatorMapManagerTest.kt`](../../../../app/src/test/java/com/example/androidmapsrobolectric/manager/StoreLocatorMapManagerTest.kt#L37-L129)

```kotlin
// [START maps_robolectric_manager_test]
Expand Down
Loading
Loading