diff --git a/build.gradle b/build.gradle
index 0ee3a51e2..c9420dfd9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,8 @@ plugins {
id 'com.android.library' version libs.versions.agp apply false
id 'org.jetbrains.kotlin.multiplatform' version libs.versions.kotlin apply false
id 'org.jetbrains.compose' version libs.versions.composeMultiplatform apply false
+ id 'de.mannodermaus.android-junit5' version libs.versions.junit5Android apply false
+ id 'tech.apter.junit5.jupiter.robolectric-extension-gradle-plugin' version libs.versions.junit5Robolectric apply false
id 'org.jetbrains.kotlin.android' version libs.versions.kotlin apply false
id "com.vanniktech.maven.publish" version libs.versions.mavenPublish apply false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4a96b7423..cbf874d4c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,6 +5,8 @@ kotlin = "1.9.21"
mavenPublish = "0.25.3"
composeCompiler = "1.5.6"
composeMultiplatform = "1.6.2"
+junit5Android = "1.10.0.0"
+junit5Robolectric = "0.5.2"
robolectric = "4.12.1"
robolectric-android-all = "Q-robolectric-5415296"
@@ -29,6 +31,7 @@ androidx-lifecycle = "2.6.1"
androidx-navigation = "2.7.7"
androidx-test-espresso-core = "3.5.1"
androidx-test-ext-junit = "1.1.5"
+androidx-test-runner = "1.5.2"
kim = "0.17.7"
dropbox-differ = "0.0.2"
@@ -71,6 +74,7 @@ androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx",
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" }
androidx-test-ext-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-ext-junit" }
+androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
kim = { module = "com.ashampoo:kim", version.ref = "kim" }
android-tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
diff --git a/sample-android-junit5/.gitignore b/sample-android-junit5/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/sample-android-junit5/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/sample-android-junit5/build.gradle.kts b/sample-android-junit5/build.gradle.kts
new file mode 100644
index 000000000..e3d674e35
--- /dev/null
+++ b/sample-android-junit5/build.gradle.kts
@@ -0,0 +1,69 @@
+plugins {
+ id("com.android.library")
+ kotlin("android")
+ id("io.github.takahirom.roborazzi")
+ id("de.mannodermaus.android-junit5")
+ id("tech.apter.junit5.jupiter.robolectric-extension-gradle-plugin")
+}
+
+val jvmVersion = JavaVersion.VERSION_17
+
+android {
+ namespace = "com.github.takahirom.roborazzi.sample"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility = jvmVersion
+ targetCompatibility = jvmVersion
+ }
+
+ kotlinOptions {
+ jvmTarget = jvmVersion.toString()
+ }
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(jvmVersion.toString()))
+ }
+}
+
+junitPlatform {
+ configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true")
+}
+
+dependencies {
+ testImplementation(project(":roborazzi"))
+ testImplementation(project(":roborazzi-junit5"))
+
+ implementation(kotlin("stdlib"))
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.google.android.material)
+
+ testImplementation(libs.robolectric)
+ testImplementation(libs.junit.jupiter.api)
+ testImplementation(libs.junit.jupiter.params)
+ testImplementation(libs.androidx.test.espresso.core)
+ testRuntimeOnly(libs.junit.jupiter.engine)
+
+ androidTestImplementation(libs.junit.jupiter.api)
+ androidTestImplementation(libs.androidx.test.runner)
+}
diff --git a/sample-android-junit5/proguard-rules.pro b/sample-android-junit5/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/sample-android-junit5/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt b/sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt
new file mode 100644
index 000000000..3b4e67640
--- /dev/null
+++ b/sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt
@@ -0,0 +1,14 @@
+package com.github.takahirom.roborazzi.sample
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class JUnit5InstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // The basic example instrumentation test, but with JUnit 5
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.github.takahirom.roborazzi.sample.test", appContext.packageName)
+ }
+}
diff --git a/sample-android-junit5/src/main/AndroidManifest.xml b/sample-android-junit5/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..38fc16e91
--- /dev/null
+++ b/sample-android-junit5/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt b/sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt
new file mode 100644
index 000000000..cad333093
--- /dev/null
+++ b/sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt
@@ -0,0 +1,22 @@
+package com.github.takahirom.roborazzi.sample
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.github.takahirom.roborazzi.sample.databinding.ActivityMainBinding
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ binding.updateButton.setOnClickListener {
+ val input = binding.inputEditText.text ?: ""
+
+ binding.descriptionText.text = getString(R.string.text_description_2, input)
+ }
+ }
+}
diff --git a/sample-android-junit5/src/main/res/layout/activity_main.xml b/sample-android-junit5/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..55ea50643
--- /dev/null
+++ b/sample-android-junit5/src/main/res/layout/activity_main.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample-android-junit5/src/main/res/values/attrs.xml b/sample-android-junit5/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..0897eb6e4
--- /dev/null
+++ b/sample-android-junit5/src/main/res/values/attrs.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/sample-android-junit5/src/main/res/values/strings.xml b/sample-android-junit5/src/main/res/values/strings.xml
new file mode 100644
index 000000000..c1f9df1e9
--- /dev/null
+++ b/sample-android-junit5/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+ Caption
+ Original description
+ Updated description: %s
+ Update
+ Enter something
+
diff --git a/sample-android-junit5/src/main/res/values/styles.xml b/sample-android-junit5/src/main/res/values/styles.xml
new file mode 100644
index 000000000..070659133
--- /dev/null
+++ b/sample-android-junit5/src/main/res/values/styles.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/sample-android-junit5/src/test/kotlin/com/github/takahirom/roborazzi/sample/JUnit5ManualTest.kt b/sample-android-junit5/src/test/kotlin/com/github/takahirom/roborazzi/sample/JUnit5ManualTest.kt
new file mode 100644
index 000000000..4800f4fb0
--- /dev/null
+++ b/sample-android-junit5/src/test/kotlin/com/github/takahirom/roborazzi/sample/JUnit5ManualTest.kt
@@ -0,0 +1,76 @@
+package com.github.takahirom.roborazzi.sample
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
+import com.github.takahirom.roborazzi.captureRoboImage
+import org.junit.jupiter.api.DynamicTest.dynamicTest
+import org.junit.jupiter.api.RepeatedTest
+import org.junit.jupiter.api.RepetitionInfo
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestFactory
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.GraphicsMode
+import tech.apter.junit.jupiter.robolectric.RobolectricExtension
+
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+@Config(
+ sdk = [30],
+ qualifiers = RobolectricDeviceQualifiers.NexusOne,
+)
+@ExtendWith(RobolectricExtension::class)
+class JUnit5ManualTest {
+ @Test
+ @Config(qualifiers = "+land")
+ fun captureRoboImageSample() {
+ ActivityScenario.launch(MainActivity::class.java)
+
+ onView(isRoot()).captureRoboImage()
+ onView(withId(R.id.inputEditText)).perform(typeText("hello"))
+ onView(withId(R.id.updateButton)).perform(click())
+ onView(isRoot()).captureRoboImage()
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun parameterizedTest(value: Boolean) {
+ ActivityScenario.launch(MainActivity::class.java)
+
+ onView(withId(R.id.inputEditText)).perform(typeText("parameter=$value"))
+ onView(withId(R.id.updateButton)).perform(click())
+ onView(isRoot()).captureRoboImage()
+ }
+
+ @RepeatedTest(2)
+ fun repeatedTest(repetitionInfo: RepetitionInfo) {
+ ActivityScenario.launch(MainActivity::class.java)
+
+ onView(withId(R.id.inputEditText)).perform(typeText("repeated=${repetitionInfo.currentRepetition}"))
+ onView(withId(R.id.updateButton)).perform(click())
+ onView(isRoot()).captureRoboImage()
+ }
+
+ @TestFactory
+ fun testFactory() = listOf(
+ dynamicTest("First") {
+ ActivityScenario.launch(MainActivity::class.java)
+
+ onView(withId(R.id.inputEditText)).perform(typeText("typed but not pressed"))
+ onView(isRoot()).captureRoboImage()
+ },
+ dynamicTest("Second") {
+ ActivityScenario.launch(MainActivity::class.java)
+
+ onView(withId(R.id.inputEditText)).perform(typeText("typed and pressed"))
+ onView(withId(R.id.updateButton)).perform(click())
+ onView(isRoot()).captureRoboImage()
+ },
+ )
+}
diff --git a/settings.gradle b/settings.gradle
index a58dd6852..1603462eb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -22,6 +22,7 @@ include ':roborazzi-compose'
include ':roborazzi-painter'
include ':sample-android'
+include ':sample-android-junit5'
include ':sample-android-without-compose'
include ':sample-compose-desktop-multiplatform'
include ':sample-compose-desktop-jvm'