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
2 changes: 2 additions & 0 deletions ApiDemos/project/kotlin-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ dependencies {
androidTestImplementation(libs.junit)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":visual-testing"))
androidTestImplementation(libs.uiautomator)

implementation(project(":ApiDemos:common-ui"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2026 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.example.kotlindemos.visual

import android.app.Activity
import android.app.Instrumentation
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import com.example.kotlindemos.BuildConfig
import com.google.maps.android.visualtesting.GeminiVisualTestHelper
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import java.io.File

/**
* Base class for on-device visual verification tests using Gemini Multimodal AI.
*/
abstract class BaseVisualVerificationTest {

protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
protected val context: Context = instrumentation.targetContext
protected val helper = GeminiVisualTestHelper()

@After
fun tearDown() {
helper.close()
}

protected val geminiApiKey: String? by lazy {
try {
val geminiKeyField = try {
BuildConfig::class.java.getField("GEMINI_API_KEY")
} catch (e: NoSuchFieldException) {
null
}
val key = geminiKeyField?.get(null) as? String
if (!key.isNullOrBlank() && key != "DEFAULT_API_KEY") {
key
} else {
null
}
} catch (e: Exception) {
null
}
}

/**
* Captures a screenshot from the connected device via UiDevice.
*/
protected fun captureScreenshot(filename: String = "visual_test_${System.currentTimeMillis()}.png"): Bitmap {
val storageDir = context.getExternalFilesDir(null) ?: context.filesDir
val screenshotFile = File(storageDir, filename)

val taken = uiDevice.takeScreenshot(screenshotFile)
assertTrue("UiDevice failed to capture screenshot: $filename", taken)

val bitmap = BitmapFactory.decodeFile(screenshotFile.absolutePath)
assertTrue("Failed to decode screenshot bitmap: $filename", bitmap != null)

Log.i(TAG, "Screenshot captured: ${screenshotFile.absolutePath} (${bitmap.width}x${bitmap.height})")
return bitmap
}

/**
* Verifies the visual contents of a screenshot using Gemini Multimodal AI.
*/
protected suspend fun verifyScreenshotWithGemini(bitmap: Bitmap, prompt: String) {
val key = geminiApiKey
assumeTrue("GEMINI_API_KEY is not configured; skipping AI visual evaluation", !key.isNullOrBlank())

val response = helper.analyzeImage(bitmap, prompt, key!!)
Log.i(TAG, "Gemini Visual Evaluation Response:\n$response")
assertTrue(
"Gemini visual verification failed. Response: $response",
response?.contains("PASSED", ignoreCase = true) == true
)
}

/**
* Waits for the map container or tiles to settle.
*/
protected fun waitForMap(timeoutMs: Long = 5000) {
uiDevice.waitForIdle(timeoutMs)
Thread.sleep(1500)
}

companion object {
private const val TAG = "BaseVisualVerification"
}
}
Loading
Loading