Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @Preview param Wallpapers #486

Open
sergio-sastre opened this issue Sep 18, 2024 · 7 comments
Open

Support @Preview param Wallpapers #486

sergio-sastre opened this issue Sep 18, 2024 · 7 comments

Comments

@sergio-sastre
Copy link
Contributor

Since this is the Preview param that I consider the hardest to support, because it is something Robolectric doesn't support, I decided to give it a try, and I succeded :)

What I'm not sure is how we could provide support out of the box, because this solution requires the use of
CompositionLocalProvider. That means, the user should configure his code in such a way that it allows to switch Themes dynamically, for instance, in tests.
Assuming the user has defined his AppTheme like this as stated in the Android docu:

@SuppressWarnings
@Composable
private fun AppTheme(
    content: @Composable () -> Unit
) {
    val inDarkMode: Boolean = isSystemInDarkTheme()

    val colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        val context = LocalContext.current
        if (inDarkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
    } else {
        if (inDarkMode) darkThemeColors else lightThemeColors
    }

    MaterialTheme(
        colorScheme = colors,
        content = content
    )
}

He'll have to make the following changes

// This is necessary to switch themes
val LocalAppTheme = staticCompositionLocalOf<(@Composable (content: @Composable () -> Unit) -> Unit)> {
    { content -> DefaultAppTheme(content) } // Default to your real `AppTheme`
}

@SuppressWarnings
@Composable
private fun DefaultAppTheme(
    content: @Composable () -> Unit
) {
    val inDarkMode: Boolean = isSystemInDarkTheme()

    val colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        val context = LocalContext.current
        if (inDarkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
    } else {
        if (inDarkMode) darkThemeColors else lightThemeColors
    }

    MaterialTheme(
        colorScheme = colors,
        content = content
    )
}

@SuppressWarnings
@Composable
fun AppTheme(
    content: @Composable () -> Unit
) {
    val appTheme = LocalAppTheme.current
    // Apply the theme (either the default `AppTheme` or an overridden one in tests)
    appTheme(content)
}

Then one can use MaterialKolor in Roborazzi tests if Wallpapers apply:

@Composable
fun RenderPreview(
    previewComposable: ComposablePreview<AndroidPreviewInfo>
) {
    val seedColor =
        when (previewComposable.previewInfo.wallpaper) {
            Wallpapers.RED_DOMINATED_EXAMPLE -> Color.Red
            Wallpapers.GREEN_DOMINATED_EXAMPLE -> Color.Green
            Wallpapers.YELLOW_DOMINATED_EXAMPLE -> Color.Yellow
            Wallpapers.BLUE_DOMINATED_EXAMPLE -> Color.Blue
            Wallpapers.NONE -> null
            else -> null
        }

    when (seedColor == null) {
        true -> previewComposable()
        false ->
            CompositionLocalProvider(
                LocalAppTheme provides { content -> DynamicMaterialTheme(seedColor) { content() } }
            ) {
                previewComposable()
            }
    }
}

and use it in the generated Roborazzi Test

@Test
    fun snapshot() {
        RobolectricPreviewInfosApplier.applyFor(preview)

        captureRoboImage(
            filePath = filePath(AndroidPreviewScreenshotIdBuilder(preview).build()),
            roborazziOptions = RoborazziOptionsMapper.createFor(preview)
        ) {
            RenderPreview(preview)
        }
    }

I have a PR in "Android Screenshot Testing Playground" where this works :)
sergio-sastre/Android-screenshot-testing-playground#69

As I mentioned, I am a bit uncertain on how we could add support for this since the user needs to adjust his production code to enable this.
The best I come up with is:

  1. Inform the user about what he has to modify in his AppTheme
  2. pass the full Path of that variable in the gradle Plugin
roborazzi {
  generateComposePreviewRobolectricTests {
    enable = true
    // example
    themeProviderPath ="com.example.road.to.effective.snapshot.testing.lazycolumnscreen.LocalAppTheme"
  }
}

then you just need to add that import in the auto-generated test

Do you have any better idea maybe?

@yschimke
Copy link
Contributor

Changing to app code seems only neccesary if the theme is embedded in the components you want to test. In practice my own code generally assumes the Theme is applied higher up, say in the Activity. This is generally safe for libraries if you assume someone else wants to configure say MaterialTheme.

But it does seem to mean that the Previews in AndroidStudio won't match the theme, if this is only applied in RenderPreview.

Is there a WallpaperThemeApplier that could be used in the Preview and therefore automatically in the roborazzi preview test, without needing to refer to previewInfo?

@sergio-sastre
Copy link
Contributor Author

The point is, AppTheme would work out of the box with Compose-Preview Screenshot Test tool and the theming applies properly in the Previews.
So the Previews and the screenshots would match the theme with the dynamic colors.

However, that is not the case in Robolectric. To provide dynamic colors, Android uses a dynamic color engine called Monet, which is not available in Robolectric. Therefore it is not applied when running Robolectric.

The trick I used is to intercept the theme and apply MaterialKolor’s DynamicColorTheme in tests, which emulates Monet.
In doing so, the Previews do match what Roborazzi renders.

I doubt it is possible to apply Wallpapers with some kind of WallpaperThemeApplier

@sergio-sastre
Copy link
Contributor Author

sergio-sastre commented Sep 19, 2024

After researching a bit, I think it could be possible by overriding the existing LocalColorScheme with the one that MaterialKolor provides and production code would not need any change to enable it.

I’ll give it a try once I am back from holidays

@sergio-sastre
Copy link
Contributor Author

sergio-sastre commented Oct 4, 2024

It seems that LocalColorScheme is something fancy that the AI tool I was using made up and it does not exist 😅

Therefore, I believe the best is to create an issue in Robolectric to support the dynamic colors engine Monet and wait for them to have it implemented. I’ll create the issue.

That should make it work out of the box without needing any change in Roborazzi’s code.

@yschimke
Copy link
Contributor

yschimke commented Oct 4, 2024

Would implementing a shadow that called MaterialKolor’s DynamicColorTheme be a usable solution?

@sergio-sastre
Copy link
Contributor Author

sergio-sastre commented Oct 4, 2024

Would implementing a shadow that called MaterialKolor’s DynamicColorTheme be a usable solution?

That’s indeed a pretty good idea!
After a fast check, it seems that the class to Shadow is WallpaperManager. It provides the getWallpaperColor() from api 27 on, which the default Robolectric’s ShadowWallpaperManager does not implement.

Not sure whether that suffices, but I’ll give it a try.
Thanks for the suggestion 🙏

@sergio-sastre
Copy link
Contributor Author

And a more detailed blog about the classes involved here:
https://siddroid.com/post/android/chasing-monet-inside-the-android-framework/#system-ui--monet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants