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
88 changes: 88 additions & 0 deletions Samples/Flix/FlixHilt/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

buildscript {
ext {
versions = [
'compose': [
'compilerExt': '1.5.14',
'ui' : '1.6.7',
]
]
}
}

plugins {
id 'com.android.library'
alias(libs.plugins.google.devtools.ksp)
id 'com.google.dagger.hilt.android'
id 'dev.testify'
alias(libs.plugins.compose.compiler)
}

android {
namespace 'dev.testify.samples.flix.hilt'
compileSdk 36

defaultConfig {
minSdkVersion 26
targetSdkVersion 36

// Hilt tests need HiltTestApplication. This setting is module-wide, which is why the
// example lives here rather than in :FlixSample — see HiltTestRunner.
testInstrumentationRunner "dev.testify.samples.flix.hilt.HiltTestRunner"
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_25
targetCompatibility JavaVersion.VERSION_25
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_25)
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion "${versions.compose.compilerExt}"
}
}

dependencies {
def composeBom = platform('androidx.compose:compose-bom:2025.08.01')
implementation composeBom

implementation "androidx.appcompat:appcompat:1.7.1"
implementation 'androidx.compose.material3:material3:1.3.2'
implementation "androidx.hilt:hilt-navigation-compose:1.2.0"
implementation "com.google.dagger:hilt-android:2.60.1"
ksp "com.google.dagger:hilt-compiler:2.60.1"

// HiltComposableTestActivity lives in the `debug` source set, which cannot see the
// `androidTest` dependencies below. It extends ComposableTestActivity, which implements an
// interface from the core library, so both artifacts are needed to compile it.
//
// `compileOnly` is enough: at run time the Testify classes come from the test APK, as they do
// for ComposableTestActivity itself. `debugImplementation` would instead add Testify's own test
// dependencies to the module and clash with the versions `androidTest` resolves.
debugCompileOnly project(":Library")
debugCompileOnly project(path: ":ComposeExtensions")

androidTestImplementation composeBom
androidTestImplementation project(":Library")
androidTestImplementation project(path: ":ComposeExtensions")
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation "androidx.test:core-ktx:1.4.0"
androidTestImplementation "androidx.test:rules:1.7.0"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:${versions.compose.ui}"

androidTestImplementation "com.google.dagger:hilt-android-testing:2.60.1"
kspAndroidTest "com.google.dagger:hilt-compiler:2.60.1"
}

testify {
applicationPackageId "dev.testify.samples.flix.hilt.test"
testPackageId "dev.testify.samples.flix.hilt.test"
autoImplementLibrary false
}
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
@@ -0,0 +1,87 @@
/*
* The MIT License (MIT)
*
* 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.
*/

package dev.testify.samples.flix.hilt

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.test.core.app.launchActivity
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dev.testify.annotation.ScreenshotInstrumentation
import dev.testify.compose.scenario.ComposableScreenshotScenarioRule
import org.junit.Rule
import org.junit.Test

/**
* Demonstrates screenshot testing a composable that resolves its dependencies through Hilt.
*
* The composable under test calls `hiltViewModel()`, which needs an `@AndroidEntryPoint` host. The
* three pieces that make that work:
*
* 1. [HiltComposableTestActivity] in the `debug` source set, an `@AndroidEntryPoint` subclass of
* `ComposableTestActivity`, declared in `src/debug/AndroidManifest.xml`.
* 2. [HiltTestRunner] as the module's `testInstrumentationRunner`.
* 3. [HiltAndroidRule] ordered before the Testify rule, so the component is ready before the
* activity launches.
*
* `ComposableScreenshotRule` cannot be used here: it hardcodes `ComposableTestActivity` as its
* activity. `ComposableScreenshotScenarioRule` lets the test choose the host.
*
* @see <a href="https://testify.dev/docs/recipes/hilt">Testing composables that use Hilt</a>
*/
@HiltAndroidTest
class HiltComposableScreenshotTest {

@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1)
val rule = ComposableScreenshotScenarioRule()

/**
* Resolves [SampleViewModel] through Hilt and renders a value that comes from it. If the
* injection fails the test throws rather than capturing, so a passing baseline is itself the
* assertion that Hilt resolved through [HiltComposableTestActivity].
*/
@Composable
private fun HiltBackedContent() {
val viewModel = hiltViewModel<SampleViewModel>()
Text(text = viewModel.label)
}

@ScreenshotInstrumentation
@Test
fun default() {
launchActivity<HiltComposableTestActivity>().use { scenario ->
rule
.withScenario(scenario)
.setCompose {
HiltBackedContent()
}
.assertSame()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* The MIT License (MIT)
*
* 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.
*/

package dev.testify.samples.flix.hilt

import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication

/**
* Substitutes [HiltTestApplication] for the app's own [Application] so Hilt can build its test
* component.
*
* `testInstrumentationRunner` is module-wide, which is why this module exists. Pointing the `Flix`
* sample at this runner would replace its `FlixApplication`, and with it the Coil `ImageLoader` that
* `FlixApplication` supplies through `ImageLoaderFactory` — so its image-backed screenshot tests
* would capture before their images had drawn. Keeping the Hilt example in its own module leaves
* that suite alone.
*/
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
10 changes: 10 additions & 0 deletions Samples/Flix/FlixHilt/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application android:largeHeap="true">
<!--suppress AndroidDomInspection -->
<activity
android:name="dev.testify.samples.flix.hilt.HiltComposableTestActivity"
android:theme="@style/Theme.AppCompat.NoActionBar" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* The MIT License (MIT)
*
* 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.
*/

package dev.testify.samples.flix.hilt

import dagger.hilt.android.AndroidEntryPoint
import dev.testify.ComposableTestActivity

/**
* A Hilt-aware host for composables under test.
*
* Hilt injects only into activities annotated with [AndroidEntryPoint]. Testify's
* [ComposableTestActivity] is not, so a composable that resolves a dependency — through
* `hiltViewModel()`, for example — fails inside it with:
*
* ```
* java.lang.IllegalStateException: Given component holder class dev.testify.ComposableTestActivity
* does not implement interface dagger.hilt.internal.GeneratedComponent or interface
* dagger.hilt.internal.GeneratedComponentManager
* ```
*
* Subclassing it here adds the annotation without changing anything else, so
* `ComposableScreenshotScenarioRule` can host Hilt-backed composables.
*
* This lives in the `debug` source set, with its manifest entry in `src/debug/AndroidManifest.xml`
* and Testify on the debug compile classpath as `debugCompileOnly`. That is the arrangement an
* application module needs — a test-only activity has no business in a shipping build — and it is
* what the [Hilt recipe](https://testify.dev/docs/recipes/hilt) describes, so this sample mirrors
* the recipe step for step.
*
* It is not the only arrangement that works. In a library module the test APK *is* the application,
* so the activity can equally live in `androidTest` with an `androidTest` manifest and no
* `compileOnly` at all. `debug` is used here to match the recipe.
*
* @see HiltComposableScreenshotTest
*/
@AndroidEntryPoint
class HiltComposableTestActivity : ComposableTestActivity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* The MIT License (MIT)
*
* 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.
*/

package dev.testify.samples.flix.hilt

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

/**
* A view model with nothing in it, so the test has something for Hilt to resolve.
*
* The point of [HiltComposableScreenshotTest] is that `hiltViewModel()` resolves at all inside a
* screenshot test. Giving this no dependencies and no mutable state keeps the rendered output
* deterministic, so a failure means the injection broke rather than the data changed.
*/
@HiltViewModel
class SampleViewModel @Inject constructor() : ViewModel() {
val label: String = "injected by Hilt"
}
16 changes: 16 additions & 0 deletions bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,22 @@ workflows:
- test_package: dev.testify.samples.flix.library.test
- test_runner: androidx.test.runner.AndroidJUnitRunner
- verbose: true
- android-build@1:
inputs:
- variant: DebugAndroidTest
- module: FlixHilt
- git::https://github.com/ndtp/bitrise-step-android-testify.git@main:
timeout: 1800
title: Run Screenshot Tests
inputs:
- adb_command: "./gradlew FlixHilt:screenshotTest"
- module: ":FlixHilt"
- app_apk: "./Samples/Flix/FlixHilt/build/outputs/apk/androidTest/debug/FlixHilt-debug-androidTest.apk"
- app_package: dev.testify.samples.flix.hilt.test
- test_apk: "./Samples/Flix/FlixHilt/build/outputs/apk/androidTest/debug/FlixHilt-debug-androidTest.apk"
- test_package: dev.testify.samples.flix.hilt.test
- test_runner: dev.testify.samples.flix.hilt.HiltTestRunner
- verbose: true
- custom-test-results-export@1.0:
is_always_run: true
timeout: 120
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencyResolutionManagement {

include ':Accessibility'
include ':ComposeExtensions'
include ':FlixHilt'
include ':FlixLibrary'
include ':FlixSample'
include ':FullscreenCaptureMethod'
Expand All @@ -32,6 +33,7 @@ includeBuild("./Plugins/Gradle") { name = "Plugin" }

project(':Accessibility').projectDir = new File("./Ext/Accessibility")
project(':ComposeExtensions').projectDir = new File("./Ext/Compose")
project(':FlixHilt').projectDir = new File("./Samples/Flix/FlixHilt")
project(':FlixLibrary').projectDir = new File("./Samples/Flix/FlixLibrary")
project(':FlixSample').projectDir = new File("./Samples/Flix")
project(':FullscreenCaptureMethod').projectDir = new File("./Ext/Fullscreen")
Expand Down
Loading