3

I have this class which we use as a base class for our screenshot tests

abstract class ScreenshotTest {

    @get:Rule
    val rule = createComposeRule()

    protected fun snapshotComposable(
        name: String,
        composable: @Composable () -> Unit,
    ) {
        rule.mainClock.autoAdvance = false
        rule.setContent {
            composable()
        }
        rule.waitForIdle()
        rule.onAllNodes(isRoot())
            .onFirst()
            .captureToImage()
            .asAndroidBitmap()
            .writeToTestStorage(name)
    }
}

everything works fine, but if the composable's height is bigger than the device's height, the output bitmap is clipped to the device's resolution. Is there a way to capture the whole composable without this limitation?

Billda
  • 5,707
  • 2
  • 25
  • 44

2 Answers2

0

The point of a screenshot test is to see if everything is ok on the actual screen of the device. Imagine you have a composable gallery that contains 500 images that are placed inside a LazyColumn, or maybe an inner scroll - how would you expect your desired approach to work in these cases? It will be completely impossible not only for these but for a lot of other cases also. And making a selective approach that screenshots only those already rendered widgets fully, disregarding the device screen limitation, would be unwise, at the least.

UI testing requires not only checking the app for flaws in its look(UI) but also in its behavior(UX).

As far as I know, what you want is not quite possible due to a lot of reasons - at least for now and at least if you are taking your image from the screen on the device during testing. We can play trying to render the widget image directly in the GPU buffer - I've done it with regular views, but I assume composable view can also be tricked this way - though it is only applicable for some cases and completely demolishes the purpose of the screenshot testing.

It is not all bad, though - it all is possible in exponential time)) I would recommend you combine screenshot testing with regular instrumental testing(as a lot of people do, actually) and benefit from the synergy.

So firstly, decide the screen sizes you want to compare - tablet, long, foldable etc. Then define how the app should behave and look for each screen. After that, create desired screenshots(or seed your database during the first(or special) run of the test).

Then run the screenshot test of the first portion of the visible screen, and after that - using instrumental testing tools, scroll the screen programmatically to the needed extent and make the second screenshot test(then third, fourth, etc - scroll however you like). The main idea here is the predictable scroll for tests that will require static mock data for both test runs and screenshot making.

For the scrolling and other not screenshot matching, use this, and for the actual scroll use:

composeTestRule
    .onNode(matcher)// this should be a matcher of the whole screen(if the whole screen is scrollable)
    .performGesture { swipeUp() } // Down/Left/Right will work also. swipeWithVelocity may be even more beneficial - but I don't know for sure - never tried it.

Here is a cool cheetsheet of all the stuff you can do relatively easily with this thing.

As you can imagine, the process of establishing the testing might be not that pleasant or fast depending on the screens complexity and amount of screenshots needed. It is standard, though, and any dev will understand it easily, and moreover, it is natural for the Android platform, so there shouldn't be any unexpected obscure problems(there may be but fewer, and some may be already documented)

I know that this post is not the real answer to your issue, but in my opinion, it is the sole correct way to achieve what you currently want. But, maybe I don't know something, and there is some other solution. I would be really glad if it was true, since I'd already done, what you are trying to do, some time ago for a lot of screens and it was not as enjoyable as it sounds)

Either way - hope it helps - at least with the defining path or assigning story points.

Pavlo Ostasha
  • 14,527
  • 11
  • 35
0

For that, you need to adjust the size of the composable being rendered to a size that fits the entire content. You can achieve this by wrapping your composable inside a Box and setting its size to match the size of the content using the Modifier.fillMaxSize() modifier.

protected fun snapshotComposable(
    name: String,
    composable: @Composable () -> Unit,
) {
    rule.mainClock.autoAdvance = false
    val bitmap = createBitmapFromComposable(composable)
    bitmap.writeToTestStorage(name)
}

private fun createBitmapFromComposable(composable: @Composable () -> Unit): Bitmap {
    val bitmapConfig = Bitmap.Config.ARGB_8888
    val displayMetrics = Resources.getSystem().displayMetrics
    val screenWidth = displayMetrics.widthPixels
    val screenHeight = displayMetrics.heightPixels

    val bitmap = Bitmap.createBitmap(screenWidth, screenHeight, bitmapConfig)
    val canvas = Canvas(bitmap)
    val density = displayMetrics.density

    val composition = rememberComposition { CanvasScope(canvas) }
    composition.setContent {
        Box(modifier = Modifier.fillMaxSize()) {
            composable()
        }
    }

    val root = composition.root
    root.measure(
        Constraints(
            minWidth = 0,
            maxWidth = screenWidth.px.roundToInt(),
            minHeight = 0,
            maxHeight = screenHeight.px.roundToInt()
        )
    )
    root.layout(
        0,
        0,
        root.measuredWidth,
        root.measuredHeight
    )
    root.draw(density)

    return bitmap
}
Dinesh
  • 1,410
  • 2
  • 16
  • 29