cashapp / paparazzi

Render your Android screens without a physical device or emulator
https://cashapp.github.io/paparazzi/
Apache License 2.0
2.22k stars 210 forks source link

Snapshots are not generated for Android Views that are annotated with Dagger's `@AndroidEntryPoint` #1249

Open Tinder-FroWarner opened 5 months ago

Tinder-FroWarner commented 5 months ago

Description

We are working on creating a snapshot test for an Android View with injected dependencies via Dagger/Hilt, but are running into an issue when generating the snapshot if a view is annotated with @AndroidEntryPoint.

Steps to Reproduce

The following gives an example of the error and setup.

Exception

java.lang.IllegalStateException: Could not find an Application in the given context: com.android.layoutlib.bridge.android.BridgeContext@6425422c 
at dagger.hilt.android.internal.Contexts.getApplication(Contexts.java:42)
at dagger.hilt.android.internal.managers.ViewComponentManager.getParentContext(ViewComponentManager.java:146)
at dagger.hilt.android.internal.managers.ViewComponentManager.getParentComponentManager(ViewComponentManager.java:128)
at dagger.hilt.android.internal.managers.ViewComponentManager.createComponent(ViewComponentManager.java:86) 
at dagger.hilt.android.internal.managers.ViewComponentManager.generatedComponent(ViewComponentManager.java:77)
...

Example Code

Example of View:

@AndroidEntryPoint
class CustomView(
    context: Context,
    attrs: AttributeSet? = null,
) : CustomComponentView<CustomModel>(context, attrs) {

    private val binding = CustomViewBinding.inflate(LayoutInflater.from(context), this)

    @Inject
    lateinit var injectedLinkLauncher: InjectedLinkLauncher

    override fun bind(uiModel: CustomModel) {
        // ...
    }
// ... 

Example of Test:

class CustomViewSnapshotTest {
    @get:Rule
    val paparazzi = Paparazzi(
        deviceConfig = PIXEL_5,
        maxPercentDifference = 0.01
    )

    @Test
    fun `CustomView - content`() {
        val view = CustomView(paparazzi.context)
        view.bind(
            uiModel = CustomComponentUiModel.CustomModel(
                text = "Content",
                 // ...
            )
        )
        paparazzi.snapshot(view)
    }
}

Expected behavior

Snapshots generate and tests pass.

Actual Behavior

Snapshots only generate and tests pass when commenting out the injected vals and @AndroidEntryPoint i.e.

// @AndroidEntryPoint
class CustomView(
    context: Context,
    attrs: AttributeSet? = null,
) : CustomComponentView<CustomModel>(context, attrs) {

    private val binding = CustomViewBinding.inflate(LayoutInflater.from(context), this)

   // @Inject
   // lateinit var injectedLinkLauncher: InjectedLinkLauncher

    override fun bind(uiModel: CustomModel) {
        // ...
    }
// commented out injectedLinkLauncher usage as well

Additional information:

jrodbx commented 5 months ago

@Tinder-FroWarner

Is the view located in the main source set or in a test source set? If the latter, you likely need to add a "testKapt" or "kspTest" configuration.

If not, mind providing a sample project? This is very specific to your setup and I'm inclined to consider this a Hilt configuration issue without something to help easily repro.

jrodbx commented 1 month ago

Took another look at this. The issue is that BridgeContext inherits from Context, not ContextWrapper, therefore dagger.hilt.android.internal.Contexts is unable to find the application context here: https://github.com/google/dagger/blob/a8581e0a62b7cb3d1f5a13da26a8f40e22aad3d0/java/dagger/hilt/android/internal/Contexts.java

One option might be to file an issue with the Dagger team to special case layoutlib's BridgeContext, but I could understand that request being turned down. I'll file this as a feature request with the LayoutLib team and see where that goes.

jrodbx commented 1 month ago

Tracked here: https://issuetracker.google.com/issues/342557695

drinkthestars commented 2 weeks ago

Thanks for creating the issue with the LayoutLib team @jrodbx! 🙏🏼

Ran into a similar issue when trying to create a snapshot test for a View that uses Hilt's EntryPointAccessors.fromApplication to inject something from applicationContext. I assume it's for a similar reason?

Any thoughts on a possible workaround for the time being? Is it possible to somehow make snapshot tests to mock/"give" them an "application context" or otherwise mock the injected dependency itself? Thanks!