In case you don't want to/cannot use the rule, you can use the plain FragmentScenarioConfigurator. This would be its equivalent:
@TestfunsnapFragment() {val screenshotRule = FragmentScenarioConfigurator .setInitialOrientation(Orientation.LANDSCAPE) .setUiMode(UiMode.DAY) .setLocale("de") .setFontSize(FontSize.SMALL) .setDisplaySize(DisplaySize.LARGE) .setTheme(R.style.Custom_Theme) .launchInContainer<MyFragment>( fragmentArgs =bundleOf("arg_key" to "arg_value"), )compareScreenshot( fragment = screeenshotRule.waitForFragment(), name ="your_unique_screenshot_name", ) screenshotRule.close()}
Android View
The simplest way is to use the ActivityScenarioForViewRule, to avoid the need for closing the ActivityScenario.
@get:Ruleval rule =ActivityScenarioForViewRule( config =ViewConfigItem( fontSize = FontSize.NORMAL, locale ="en", orientation = Orientation.PORTRAIT, uiMode = UiMode.DAY, theme = R.style.Custom_Theme, displaySize = DisplaySize.SMALL, ), backgroundColor = TRANSPARENT, )@TestfunsnapViewHolderTest() {// IMPORTANT: The rule inflates a layout inside the activity with its context to inherit the configuration val layout = rule.inflateAndWaitForIdle(R.layout.your_view_holder_layout)// wait asynchronously for layout inflation val viewHolder =waitForMeasuredViewHolder {YourViewHolder(layout).apply {// bind data to ViewHolder here... } }compareScreenshot( holder = viewHolder, heightInPx = viewHolder.itemView.height, name ="your_unique_screenshot_name", )}
In case you don't want to/cannot use the rule, you can use ActivityScenarioConfigurator.ForView(). This would be its equivalent:
// example for ViewHolder@TestfunsnapViewHolderTest() {val activityScenario = ActivityScenarioConfigurator.ForView() .setFontSize(FontSize.NORMAL) .setLocale("en") .setInitialOrientation(Orientation.PORTRAIT) .setUiMode(UiMode.DAY) .setTheme(R.style.Custom_Theme) .setDisplaySize(DisplaySize.SMALL) .launchConfiguredActivity(TRANSPARENT)val activity = activityScenario.waitForActivity()// IMPORTANT: To inherit the configuration, inflate layout inside the activity with its context val layout = activity.inflateAndWaitForIdle(R.layout.your_view_holder_layout)// wait asynchronously for layout inflation val viewHolder =waitForMeasuredViewHolder {YourViewHolder(layout).apply {// bind data to ViewHolder here... } }compareScreenshot( holder = viewHolder, heightInPx = viewHolder.itemView.height, name ="your_unique_screenshot_name", ) activityScenario.close()}
Jetpack Compose
The simplest way is to use the ActivityScenarioForComposableRule, to avoid the need for:
In case you don't want to/cannot use the rule, you can use ActivityScenarioConfigurator.ForComposable() together with createEmptyComposeRule(). This would be its equivalent:
// needs an EmptyComposeRule to be compatible with ActivityScenario@get:Ruleval composeTestRule =createEmptyComposeRule()@TestfunsnapComposableTest() {val activityScenario = ActivityScenarioConfigurator.ForComposable() .setFontSize(FontSize.SMALL) .setLocale("de") .setInitialOrientation(Orientation.PORTRAIT) .setUiMode(UiMode.DAY) .setDisplaySize(DisplaySize.LARGE) .launchConfiguredActivity(TRANSPARENT) .onActivity { it.setContent {AppTheme { // this theme must use isSystemInDarkTheme() internallyyourComposable() } } } activityScenario.waitForActivity()compareScreenshot(rule = composeTestRule, name ="your_unique_screenshot_name") activityScenario.close()}
With Dropshots
If you are using a screenshot library that cannot take a ComposeTestRule as argument (e.g. Dropshots), you can still screenshot the Composable as follows:
// with ActivityScenarioForComposableRuledropshots.assertSnapshot( view = activityScenarioForComposableRule.activity.waitForComposeView(), name ="your_unique_screenshot_name",)
or
// with ActivityScenarioConfigurator.ForComposable()val activityScenario = ActivityScenarioConfigurator.ForComposable()... .launchConfiguredActivity() .onActivity {... }dropshots.assertSnapshot( view = activityScenario.waitForActivity().waitForComposeView(), name ="your_unique_screenshot_name",)
Rendering elevation
Most screenshot testing libraries use Canvas under the hood with Bitmap.Config.ARGB_8888 as default for generating bitmaps (i.e. the screenshots) from the Activities/Fragments/ViewHolders/Views/Dialogs/Composables...
That's because Canvas is supported in all Android versions.
Nevertheless, such bitmaps generated using Canvas have some limitations, e.g. UI elements are rendered without considering elevation (e.g. without shadows). So, how to render screenshots with elevation?
Elevation can be manifested in many ways: e.g. a UI layer on top of another or a shadow in a CardView.
Bitmap
Fortunately, most libraries let you pass a bitmap of the UI as an argument in their record/verify methods. In doing so, we can draw the views with elevation to a bitmap by usingPixelCopy.
Robolectric 4.10+ cannot render shadows or elevation with RNG even with PixelCopy, as stated in this issue
AndroidUiTestingUtils provides methods to easily generate bitmaps from the Activities/Fragments/ViewHolders/Views/Dialogs/Composables:
drawToBitmap(config = Bitmap.Config.ARGB_8888) -> uses Canvas under the hood
drawToBitmapWithElevation(config = Bitmap.Config.ARGB_8888) -> uses PixelCopy under the hood
and one extra to fully screenshot a scrollable Android View:
drawFullScrollableToBitmap(config = Bitmap.Config.ARGB_8888) -> uses Canvas under the hood
Differences between Bitmaps generated via Canvas and Pixel Copy might be specially noticeable on API 31:
If using PixelCopy with ViewHolders/Views/Dialogs/Composables, consider launching the container Activity with transparent background for a more realistic screenshot of the UI component.
ActivityScenarioConfigurator.ForView() // or .ForComposable()... .launchConfiguredActivity(backgroundColor = Color.TRANSPARENT)
or
ActivityScenarioForViewRule( // or ActivityScenarioForComposableRule() viewConfig =..., backgroundColor = Color.TRANSPARENT,)
Otherwise it uses the default Dark/Light Theme background colors (e.g. white and dark grey).
Using PixelCopy instead of Canvas comes with its own drawbacks though. In general, don't use PixelCopy to draw views that don't fit on the screen.
Canvas
PixelCopy
✅ Can render elements beyond the screen,
e.g. long ScrollViews
❌ Cannot render elements beyond the screen, resizing them to fit in the window
if that's the case
❌ Ignores elevation of UI elements
✅ Renders elevation of UI elements
And using PixelCopy in your screenshot tests is as simple as this:
// for UI Components like Activities/Fragments/ViewHolders/Views/DialogscompareScreenshot( bitmap = uiComponent.drawToBitmapWithElevation(), name ="your_unique_screenshot_name",)
// for ComposablescompareScreenshot( bitmap = activity.waitForComposableView().drawToBitmapWithElevation(), name ="your_unique_screenshot_name",)