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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## Unreleased

- https://github.com/ndtp/android-testify/pull/269
- `dev.testify.internal.extensions.cyan` moved to `dev.testify.extensions.cyan`
- Java interop for `findAnnotation` method is now available from `AnnotationExtensionsKtx`
- Java interop for `instrumentationPrintln` method is now available from `InstrumentationRegistryExtensionsKt`
- Java interop for `getModuleName` method is now available from `InstrumentationRegistryExtensionsKt`
- `fun Context.updateLocale(locale: Locale?): Context` is now public
- `fun getMetaDataBundle(context: Context): Bundle?` is now public

## 5.0.1

* Fix Testify plugin crash on Android Gradle Plugin 9+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil
import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid
import dev.testify.ScreenshotLifecycle
import dev.testify.accessibility.exception.AccessibilityErrorsException
import dev.testify.extensions.cyan
import dev.testify.internal.extensions.TestInstrumentationRegistry
import dev.testify.internal.extensions.TestInstrumentationRegistry.instrumentationPrintln
import dev.testify.internal.extensions.TestInstrumentationRegistry.isRecordMode
import dev.testify.internal.extensions.cyan
import dev.testify.testDescription
import java.util.Locale

Expand Down
60 changes: 60 additions & 0 deletions Ext/Ktx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Testify — Android Screenshot Testing — Kotlin Extensions

<a href="https://search.maven.org/artifact/dev.testify/testify-kts"><img alt="Maven Central" src="https://img.shields.io/maven-central/v/dev.testify/testify-ktx?color=%236e40ed&label=dev.testify%3Atestify-ktx"/></a>

**Kotlin extensions for Android Testify, providing more idiomatic and helper APIs to work with screenshot testing in Android.**

The new KTX library packages up a set of foundational utilities that originally lived deep inside Testify’s screenshot testing engine. These components are broadly useful for any instrumentation test suite. By extracting and stabilizing these internals, the library provides a standalone toolkit that improves the reliability, predictability, and ergonomics of your androidTest environment, even if you never call a screenshot API.

Why Use Testify KTX?

- Adds idiomatic Kotlin helpers around core Testify APIs, reducing boilerplate.
- Provides a simplified set of file I/O utilities for files on the emulator SD card, `data/data` directory, or Test Storage.
- Includes utilities for working with annotations, device identification, and test instrumentation.

# Set up testify-ktx

**Root build.gradle**

```groovy
plugins {
id("dev.testify") version "5.0.0" apply false
}
```

**settings.gradle**

Ensure that `mavenCentral()` is available in `dependencyResolutionManagement`.

**Application build.gradle**
```groovy
dependencies {
androidTestImplementation "dev.testify:testify-ktx:3.2.3"
}
```

---

# License

MIT License

Copyright (c) 2026 ndtp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
110 changes: 110 additions & 0 deletions Ext/Ktx/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

plugins {
id 'com.android.library'
id 'kotlin-android'
id 'org.jetbrains.dokka'
id 'maven-publish'
id 'signing'
}

ext {
pom = [
publishedGroupId : 'dev.testify',
artifact : 'testify-ktx',
libraryName : 'testify-ktx',
libraryDescription: 'Kotlin extension methods and helpers for Android Testify',
siteUrl : 'https://github.com/ndtp/android-testify',
gitUrl : 'https://github.com/ndtp/android-testify.git',
licenseName : 'The MIT License',
licenseUrl : 'https://opensource.org/licenses/MIT',
author : 'ndtp'
]
}

version = project.findProperty("testify_version") ?: "0.0.1-SNAPSHOT"
group = pom.publishedGroupId
archivesBaseName = pom.artifact

android {
namespace "dev.testify.ktx"

lintOptions {
abortOnError true
warningsAsErrors true
textOutput 'stdout'
textReport true
xmlReport false
}

defaultConfig {
compileSdkVersion = libs.versions.compileSdk.get().toInteger()
minSdkVersion libs.versions.minSdk.get().toInteger()
targetSdkVersion libs.versions.targetSdk.get().toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

libraryVariants.configureEach { variant ->
variant.outputs.all {
outputFileName = "${archivesBaseName}-${version}.aar"
}
}

testOptions {
unitTests.returnDefaultValues = true
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen { false }
showStandardStreams = true
}
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}

dependencies {
implementation libs.androidx.monitor
implementation libs.androidx.rules
implementation libs.androidx.test.storage
implementation libs.androidx.uiautomator
implementation libs.core.ktx
implementation libs.material

testImplementation libs.mockk

androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.runner
}
packagingOptions {
resources {
excludes += [
'MANIFEST.MF',
'META-INF/LICENSE.md',
'META-INF/LICENSE-notice.md'
]
}
}
publishing {
singleVariant("release") {
// if you don't want sources/javadoc, remove these lines
withSourcesJar()
withJavadocJar()
}
}
}

afterEvaluate {
apply from: "../../publish.build.gradle"
}

tasks.withType(KotlinJvmCompile).configureEach {
compilerOptions {
allWarningsAsErrors.set(true)
jvmTarget.set(JvmTarget.JVM_21)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Modified work copyright (c) 2022 ndtp
* Modified work copyright (c) 2022-2026 ndtp
* Original work copyright (c) 2019 Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -26,11 +26,13 @@

package dev.testify

import android.app.Activity
import android.graphics.Bitmap
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import dev.testify.core.processor.capture.createBitmapFromDrawingCache
import dev.testify.ktx.TestActivity
import dev.testify.output.getDestination
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
Expand Down Expand Up @@ -61,14 +63,17 @@ class ScreenshotUtilityTest {
@Test
fun createBitmapFromActivity() {
val activity = testActivityRule.activity
val rootView = activity.findViewById<View>(R.id.test_root_view)
val rootView = activity.findViewById<View>(android.R.id.content)
val destination = getDestination(context = activity, fileName = "testing")
val bitmapFile = destination.file

val capturedBitmap = createBitmapFromActivity(
activity = activity,
fileName = "testing",
captureMethod = ::createBitmapFromDrawingCache,
captureMethod = { activity: Activity, view: View? ->
val view: View = view ?: activity.window.decorView
Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
},
screenshotView = rootView
)
assertNotNull(capturedBitmap)
Expand Down
14 changes: 14 additions & 0 deletions Ext/Ktx/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-sdk tools:overrideLibrary="io.mockk, io.mockk.proxy.android" />

<application android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar">
<activity android:name="dev.testify.ktx.TestActivity" />
</application>

</manifest>
14 changes: 14 additions & 0 deletions Ext/Ktx/src/debug/java/dev/testify/ktx/TestActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.testify.ktx

import android.app.Activity
import android.os.Bundle

/**
* This is a test Activity that is used to test the Testify library.
*/
class TestActivity : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2022 ndtp
* Copyright (c) 2022-2026 ndtp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Modified work copyright (c) 2022 ndtp
* Modified work copyright (c) 2022-2026 ndtp
* Original work copyright (c) 2019 Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down Expand Up @@ -60,7 +60,7 @@ val preferredBitmapOptions: BitmapFactory.Options
*
* @param context The [Context] to use when writing the bitmap to disk.
* @param bitmap The [Bitmap] to write to disk. If null, this function will return false.
* @param destination The [Destination] to write the bitmap to.
* @param destination The [dev.testify.output.Destination] to write the bitmap to.
*
* @throws Exception if the destination cannot be found.
*
Expand Down Expand Up @@ -130,7 +130,7 @@ fun loadBaselineBitmapForComparison(
*
* @param activity The [Activity] instance to capture.
* @param fileName The name to use when writing the captured image to disk.
* @param captureMethod a [CaptureMethod] that will return a [Bitmap] from the provided [Activity] and [View]
* @param captureMethod a [dev.testify.CaptureMethod] that will return a [Bitmap] from the provided [Activity] and [View]
* @param screenshotView A [View] found in the [activity]'s view hierarchy.
* If screenshotView is null, defaults to activity.window.decorView.
*
Expand Down Expand Up @@ -182,7 +182,7 @@ fun loadBitmapFromFile(outputPath: String, preferredBitmapOptions: BitmapFactory
/**
* Delete the Bitmap [File] specified by [destination].
*
* @param destination The [Destination] to delete.
* @param destination The [dev.testify.output.Destination] to delete.
*
* @return true if the file was successfully deleted, false otherwise.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2022-2026 ndtp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
@file:JvmName("AnnotationExtensionsKtx")

package dev.testify.annotation

/**
* Find the first [Annotation] in the given [Collection] which is of type [T]
*
* @return Annotation of type T
*/
inline fun <reified T : Annotation> Collection<Annotation>.findAnnotation(): T? =
this.find { it is T } as? T

/**
* Find the first [Annotation] in the given [Collection] which is of type [T]
*
* @return Annotation of type T
*/
fun <T : Annotation> Collection<Annotation>.findAnnotation(clazz: Class<T>): T? =
this.find { clazz.isInstance(it) }?.let { clazz.cast(it) }

/**
* Find the first [Annotation] in the given [Collection] which has the given [name]
*
* @param name - The qualified class name of the requested annotation
*
* @return Annotation of type T
*/
inline fun <reified T : Annotation> Collection<Annotation>.findAnnotation(name: String): T? =
this.find { it.annotationClass.qualifiedName == name } as? T

/**
* Find the first [Annotation] in the given [Collection] which has the given [name]
*
* @param name - The qualified class name of the requested annotation
*
* @return Annotation of type T
*/
fun <T : Annotation> Collection<Annotation>.findAnnotation(name: String, clazz: Class<T>): T? =
this.find { it.annotationClass.qualifiedName == name && clazz.isInstance(it) }?.let { clazz.cast(it) }
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* The MIT License (MIT)
*
* Modified work copyright (c) 2022 ndtp
* Modified work copyright (c) 2022-2026 ndtp
* Original work copyright (c) 2019 Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down Expand Up @@ -31,6 +31,7 @@ import android.view.WindowManager
import dev.testify.internal.extensions.languageTag
import dev.testify.internal.helpers.buildVersionSdkInt
import java.util.Locale
import kotlin.text.iterator

/**
* A typealias for the test class and test name.
Expand Down
Loading