diff --git a/CHANGELOG.md b/CHANGELOG.md index c62a878..5a4fd2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## In Git * handle multiple classes (features) and methods (scenarios) specified in `class` argument to better align with tools which requests specific scenarios to be executed +* support fields injection in steps classes with Hilt ## [7.14.0] - 2023-09-25 diff --git a/README.md b/README.md index e2b16bd..7a7318d 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,13 @@ class KotlinSteps( Such class: - must have `@HiltAndroidTest` annotation to let Hilt generate injecting code -- can have Cucumber managed objects in constructor +- can have Cucumber managed objects like hooks injected in constructor +- can have Cucumber managed objects injected in fields but such objects have to be annotated with `@Singleton` annotation and constructor has to be annotated with `@Inject` annotation - can have Hilt managed objects injected only using field injection - cannot have them injected in constructor +- can have objects injected in base class +Also: +after each scenario Hilt will clear all objects and create new ones (even these marked as @Singleton) (like it does for each test class in Junit) ##### 2. @WithJunitRule diff --git a/build.gradle b/build.gradle index 52b3230..3381ba7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ import java.time.Duration buildscript { - ext.kotlin_version = '1.9.10' - ext.hilt_version = '2.48' + ext.kotlin_version = '1.9.21' + ext.hilt_version = '2.48.1' repositories { google() mavenCentral() @@ -11,7 +11,7 @@ buildscript { maven { url 'https://jitpack.io' } } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' + classpath 'com.android.tools.build:gradle:8.1.4' classpath "io.github.gradle-nexus:publish-plugin:1.3.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" @@ -26,7 +26,7 @@ ext { buildToolsVersion = '34.0.0' minSdkVersion = '14' - robolectricVersion = '4.10.3' + robolectricVersion = '4.11.1' snapshotRepository = "https://oss.sonatype.org/content/repositories/snapshots" releaseRepository = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" diff --git a/cucumber-android-hilt/src/main/java/io/cucumber/android/hilt/HiltObjectFactory.kt b/cucumber-android-hilt/src/main/java/io/cucumber/android/hilt/HiltObjectFactory.kt index 4fd5b7f..c1451f3 100644 --- a/cucumber-android-hilt/src/main/java/io/cucumber/android/hilt/HiltObjectFactory.kt +++ b/cucumber-android-hilt/src/main/java/io/cucumber/android/hilt/HiltObjectFactory.kt @@ -1,24 +1,29 @@ package io.cucumber.android.hilt +import android.app.Application +import androidx.test.core.app.ApplicationProvider import dagger.hilt.android.internal.testing.HiltExposer import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.internal.GeneratedComponentManager import io.cucumber.core.backend.ObjectFactory import io.cucumber.junit.TestRuleAccessor import io.cucumber.junit.TestRulesData import io.cucumber.junit.TestRulesExecutor import org.junit.rules.TestRule import org.junit.runner.Description +import java.util.Locale import java.util.concurrent.Executors +import javax.inject.Provider @HiltAndroidTest -class HiltObjectFactory:ObjectFactory { +class HiltObjectFactory : ObjectFactory { private val executor = Executors.newSingleThreadExecutor() private lateinit var rulesExecutor: TestRulesExecutor private val testDescription = Description.createTestDescription(javaClass, "start") - private val objects = hashMapOf,Any?>() + private val objects = hashMapOf, Any?>() override fun start() { @@ -41,11 +46,11 @@ class HiltObjectFactory:ObjectFactory { objects.clear() } - override fun addClass(glueClass: Class<*>?): Boolean = true + override fun addClass(glueClass: Class<*>?): Boolean = true override fun getInstance(glueClass: Class): T { @Suppress("UNCHECKED_CAST") - return objects.getOrPut(glueClass){ + return objects.getOrPut(glueClass) { val instance = createInstance(glueClass) injectWithHilt(glueClass, instance) instance @@ -56,10 +61,22 @@ class HiltObjectFactory:ObjectFactory { HiltExposer.getTestComponentData(glueClass)?.testInjector()?.injectTest(instance) } - private fun createInstance(glueClass: Class):T { - return glueClass.declaredConstructors.single().let { constructor -> - @Suppress("UNCHECKED_CAST") - constructor.newInstance(*constructor.parameterTypes.map { getInstance(it) }.toTypedArray()) as T - } + private fun createInstance(glueClass: Class): T { + return tryFindProviderInHiltComponent(glueClass) ?: createInstanceUsingConstructor(glueClass) + } + + private fun createInstanceUsingConstructor(glueClass: Class) = glueClass.declaredConstructors.single().let { constructor -> + @Suppress("UNCHECKED_CAST") + constructor.newInstance(*constructor.parameterTypes.map { getInstance(it) }.toTypedArray()) as T + } + + + @Suppress("UNCHECKED_CAST") + private fun tryFindProviderInHiltComponent(glueClass: Class): T? { + val component = (ApplicationProvider.getApplicationContext() as GeneratedComponentManager).generatedComponent() + return component.javaClass.declaredFields.find { it.name == "${glueClass.simpleName.replaceFirstChar { it.lowercase(Locale.ROOT) }}Provider" }?.let { + it.isAccessible = true + it.get(component) as Provider + }?.get() } } \ No newline at end of file diff --git a/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/HiltObjectFactoryTest.kt b/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/HiltObjectFactoryTest.kt index 600c79c..68539f8 100644 --- a/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/HiltObjectFactoryTest.kt +++ b/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/HiltObjectFactoryTest.kt @@ -87,4 +87,18 @@ class HiltObjectFactoryTest { hiltObjectFactory.stop() } + + @Test + fun `inject cucumber and hilt dependencies using field injection into base class`() { + hiltObjectFactory.start() + + val someSteps1 = hiltObjectFactory.getInstance(StepsWithBaseClass::class.java) + val hook = hiltObjectFactory.getInstance(SomeCucumberHook::class.java) + + + assertNotNull(someSteps1.someDependencies) + assertNotNull(someSteps1.someSingletonDependency) + assertSame(someSteps1.someCucumberHook, hook) + } + } \ No newline at end of file diff --git a/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/SomeCucumberHook.kt b/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/SomeCucumberHook.kt new file mode 100644 index 0000000..c54aff6 --- /dev/null +++ b/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/SomeCucumberHook.kt @@ -0,0 +1,9 @@ +package io.cucumber.android.hilt + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SomeCucumberHook @Inject constructor() { + +} \ No newline at end of file diff --git a/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/StepsWithBaseClass.kt b/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/StepsWithBaseClass.kt new file mode 100644 index 0000000..005deae --- /dev/null +++ b/cucumber-android-hilt/src/test/java/io/cucumber/android/hilt/StepsWithBaseClass.kt @@ -0,0 +1,19 @@ +package io.cucumber.android.hilt + +import dagger.hilt.android.testing.HiltAndroidTest +import javax.inject.Inject + +abstract class BaseSteps { + + @Inject + lateinit var someCucumberHook: SomeCucumberHook + + @Inject + lateinit var someDependencies: SomeDependencies +} + +@HiltAndroidTest +class StepsWithBaseClass:BaseSteps() { + @Inject + lateinit var someSingletonDependency: SomeSingletonDependency +} \ No newline at end of file diff --git a/cucumber-android-hilt/src/test/resources/robolectric.properties b/cucumber-android-hilt/src/test/resources/robolectric.properties index 3742098..e69de29 100644 --- a/cucumber-android-hilt/src/test/resources/robolectric.properties +++ b/cucumber-android-hilt/src/test/resources/robolectric.properties @@ -1 +0,0 @@ -sdk=33 \ No newline at end of file diff --git a/cucumber-android/src/test/resources/robolectric.properties b/cucumber-android/src/test/resources/robolectric.properties index db416e0..9f71914 100644 --- a/cucumber-android/src/test/resources/robolectric.properties +++ b/cucumber-android/src/test/resources/robolectric.properties @@ -1,2 +1 @@ -sdk=33 shadows=io.cucumber.android.shadows.ExtendedShadowPackageManager,io.cucumber.android.shadows.ShadowDexFile \ No newline at end of file diff --git a/cucumber-junit-rules-support/src/test/resources/robolectric.properties b/cucumber-junit-rules-support/src/test/resources/robolectric.properties index 3742098..e69de29 100644 --- a/cucumber-junit-rules-support/src/test/resources/robolectric.properties +++ b/cucumber-junit-rules-support/src/test/resources/robolectric.properties @@ -1 +0,0 @@ -sdk=33 \ No newline at end of file diff --git a/cukeulator/build.gradle b/cukeulator/build.gradle index 15f59ff..b4841b9 100644 --- a/cukeulator/build.gradle +++ b/cukeulator/build.gradle @@ -5,8 +5,8 @@ apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' ext { - composeCompilerVersion = '1.5.3' - composeVersion = '1.5.1' + composeCompilerVersion = '1.5.6' + composeVersion = '1.5.4' composeMaterialVersion = composeVersion } @@ -71,8 +71,8 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.activity:activity-compose:1.7.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'androidx.activity:activity-compose:1.8.2' implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.28.0" implementation "androidx.compose.ui:ui-tooling:$composeVersion" implementation "androidx.compose.runtime:runtime:$composeVersion" @@ -93,6 +93,6 @@ dependencies { androidTestImplementation project(":cucumber-android-hilt") //desugaring - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3" + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" } \ No newline at end of file diff --git a/cukeulator/src/androidTest/java/cucumber/cukeulator/test/BaseKotlinSteps.kt b/cukeulator/src/androidTest/java/cucumber/cukeulator/test/BaseKotlinSteps.kt new file mode 100644 index 0000000..3861902 --- /dev/null +++ b/cukeulator/src/androidTest/java/cucumber/cukeulator/test/BaseKotlinSteps.kt @@ -0,0 +1,25 @@ +package cucumber.cukeulator.test + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import cucumber.cukeulator.GreetingService +import javax.inject.Inject + +abstract class BaseKotlinSteps: SemanticsNodeInteractionsProvider { + + @Inject + lateinit var composeRuleHolder: ComposeRuleHolder + + @Inject + lateinit var greetingService: GreetingService + + override fun onAllNodes(matcher: SemanticsMatcher, useUnmergedTree: Boolean): SemanticsNodeInteractionCollection { + return composeRuleHolder.composeRule.onAllNodes(matcher, useUnmergedTree) + } + + override fun onNode(matcher: SemanticsMatcher, useUnmergedTree: Boolean): SemanticsNodeInteraction { + return composeRuleHolder.composeRule.onNode(matcher, useUnmergedTree) + } +} \ No newline at end of file diff --git a/cukeulator/src/androidTest/java/cucumber/cukeulator/test/ComposeRuleHolder.kt b/cukeulator/src/androidTest/java/cucumber/cukeulator/test/ComposeRuleHolder.kt index 821543c..0d0b9c5 100644 --- a/cukeulator/src/androidTest/java/cucumber/cukeulator/test/ComposeRuleHolder.kt +++ b/cukeulator/src/androidTest/java/cucumber/cukeulator/test/ComposeRuleHolder.kt @@ -4,16 +4,20 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createEmptyComposeRule import io.cucumber.junit.WithJunitRule import org.junit.Rule +import javax.inject.Inject +import javax.inject.Singleton @WithJunitRule("not @CustomComposable") -class ComposeRuleHolder { +@Singleton +class ComposeRuleHolder @Inject constructor() { @get:Rule(order = 1) val composeRule = createEmptyComposeRule() } @WithJunitRule("@CustomComposable") -class CustomComposableRuleHolder { +@Singleton +class CustomComposableRuleHolder @Inject constructor(){ @get:Rule(order = 1) val composeRule = createComposeRule() diff --git a/cukeulator/src/androidTest/java/cucumber/cukeulator/test/KotlinSteps.kt b/cukeulator/src/androidTest/java/cucumber/cukeulator/test/KotlinSteps.kt index 0f470de..a7252bc 100644 --- a/cukeulator/src/androidTest/java/cucumber/cukeulator/test/KotlinSteps.kt +++ b/cukeulator/src/androidTest/java/cucumber/cukeulator/test/KotlinSteps.kt @@ -1,6 +1,5 @@ package cucumber.cukeulator.test -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText import androidx.test.core.app.ApplicationProvider @@ -10,22 +9,17 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.platform.app.InstrumentationRegistry import cucumber.cukeulator.ComposeTestActivity -import cucumber.cukeulator.GreetingService import cucumber.cukeulator.R import dagger.hilt.android.testing.HiltAndroidTest import io.cucumber.java.en.Then import io.cucumber.java.en.When import org.junit.Assert -import javax.inject.Inject @HiltAndroidTest class KotlinSteps( - val composeRuleHolder: ComposeRuleHolder, val scenarioHolder: ActivityScenarioHolder -):SemanticsNodeInteractionsProvider by composeRuleHolder.composeRule { +): BaseKotlinSteps() { - @Inject - lateinit var greetingService:GreetingService @Then("I should see {string} on the display") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae50424..95b7f19 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip