cashapp / paparazzi

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

Composables with NavHost are not rendered correctly #635

Open magdamagda opened 1 year ago

magdamagda commented 1 year ago

Description When calling paparazzi.snapshot(...) for composable that has NavHost inside test fails with error NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner. After mocking ViewModelStoreOwner test runs successfully, snapshot is generated but NavHost is empty.

Steps to Reproduce I try to take screenshot for following composable:

paparazzi.snapshot("test") {
            NavHost(
                    navController = navController,
                    startDestination = "route"
                ) {
                    composable("route") {
                        Text(text = "Test")
                    }
                }
}

As a result I get an error: NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner

When I try to mock missing ViewModelStoreOwner like that:

paparazzi.snapshot("test") {
            mockkStatic(ViewTreeViewModelStoreOwner::class)
            every {ViewTreeViewModelStoreOwner.get(any())} returns ViewModelStoreOwner { ViewModelStore() }
            val navController = rememberNavController()
            NavHost(
                navController = navController,
                startDestination = "route"
            ) {
                composable("route") {
                    Text(text = "Test")
                }
            }
}

Rendered screenshot is empty.

Expected behavior I would expect screenshot to have "Test" label displayed.

Additional information:

alexvanyo commented 1 year ago

In the second example (where a ViewModelStore is available), this likely an issue with the navigation component in the same vein as #513. The initial route is not immediately displayed, so similarly a NavHost won't display the start destination in a @Preview.

A NavHost would likely need to support being used in a @Preview before it would be able to be rendered by Paparazzi, unless there is support added for async effects.

nuhkoca commented 1 year ago

@alexvanyo I use animateContentSize or other animate*AsStates and am facing this issue, too. My tests are empty even if I wrap those into @Preview it still doesn't work. Any idea?

JakeWharton commented 1 year ago

Is your example a real-world one or contrived? I do not think you should place your entire navigational infrastructure into the composable under test. You should be placing individual screens.

Also please, please, please do not use mocking to solve these types of problems. Compose has a hierarchical service loader that you can use to insert services that are required by children. For ViewModelStoreOwner you can use https://developer.android.com/reference/kotlin/androidx/lifecycle/viewmodel/compose/LocalViewModelStoreOwner like this:

CompositionLocal(LocalViewModelStoreOwner provides yourFake) {
  RegularComposeStuff()
}
magdamagda commented 1 year ago

What if screen has top app bar, bottom bar and navigation between them? Then only option to test whole screen is to put composable with navigation under test. I think similar issue is with snackbars scaffoldState.snackbarHostState.showSnackbar() runs asynchronously and snackbars are not rendered.

magdamagda commented 1 year ago

Is there any chance it will be fixed on paparazzi site? Or should we wait for support for async effect in @Preview?

alexvanyo commented 1 year ago

For the NavHost in particular, 2.6.0-alpha06 includes a change that allows @Previews to show the start destination, which means this is likely to work in Paparazzi now as well.