7

I have a composable that set the background color and I would like to test that.

@Composable
fun MyComposableButton(
    enabledColor: Color,
    disableColor: Color,
    isEnabled: Boolean = true,
) {
    val buttonBackgroundColor = if (enabled) enabledColor else disableColor
    Button(
        ...
        enabled = enabled,
        colors = ButtonDefaults.textButtonColors(
            backgroundColor = buttonBackgroundColor
        )
    ) { ... }
}

I'm expecting to write a tests like: verifyEnabledBackgroundColor and verifyDisabledBakcgroundColor.

I can't find any assertion directly available on the compose testing, and when trying to create my own I find that the SemanticMatcther uses a SemanticNode, but the constructor is internal for the latest so that is no go.

I try to mock the Color but I couldn't and according to this answer high API level would be required, which is a no for my project.

How can I test setting the background color for a composable?

cutiko
  • 9,887
  • 3
  • 45
  • 59

5 Answers5

6

To correct cutiko accepted answer :

Validating color of composable based on colorspace.name does not work since the returned value is just a name of the color space. In other words test will pass regardless of the actual color.

Actual solution:

If the purpose of the test is to distinguish if the actual color of the composable is correct, for instance in a case where the color of a composable changes dynamically we need to use method .readPixels which provide "ARGB values packed into an Int. "

example usage:

val array = IntArray(20)
composeTestRule.onNodeWithTag(TestTags.CONTENT_TEXT_FIELD_TAG).captureToImage()
            .readPixels(array, startY = 500, startX = 200, width = 5, height = 4)
array.forEach { it shouldNotBe Colors().Red.convert(ColorSpaces.Srgb).hashCode() }
array.forEach { it shouldBe Colors().Pink.convert(ColorSpaces.Srgb).hashCode() }
  • It does seem to work, and you are right, my solution is wrong. I'm having problems with `readPixels`, can you please expand you answer to know why the arguments? – cutiko Jun 23 '22 at 16:44
5

[UPDATE] Please read the other answer https://stackoverflow.com/a/72629280/4017501

After a lot of trials and errors, I found a way to do it. There is an extension captureImage that has the color space. With that, we can find the color name and make an equal assertion.

It has some limitations though: it is the surface underlying the node, so multiple nodes or gradients maybe won't work.

fun SemanticsNodeInteraction.assertBackgroundColor(expectedBackground: Color) {
    val capturedName = captureToImage().colorSpace.name
    assertEquals(expectedBackground.colorSpace.name, capturedName)
}

I made an extension for reusability, for example:

composeTestRule.setContent {
    ...
}

composeTestRule.onNodeWithText(someText).assertBackgroundColor(YourColor)

Beware, this might be working because in the testing we are making sure to pass our theme:

composeTestRule.setContent {
    OurSuperCoolTheme { //your compose }
}
cutiko
  • 9,887
  • 3
  • 45
  • 59
  • 1
    Man, it does not do the job :). It returns just the colorspace name which will be technically same for all colors. Test it with colors :). Tested it, not working. – Pietrek Jan 26 '22 at 06:59
  • @Pietrek thanks for letting me know, going to take a look at it – cutiko Jan 26 '22 at 11:52
  • 1
    no problem, if I find the solution I will post the answer here. Fingers crossed :) – Pietrek Jan 27 '22 at 12:36
3

Not perfect, but allows me to know if an icon is tinted with the correct color. Based on previous answers.

fun SemanticsNodeInteraction.assertContainsColor(tint: Color): SemanticsNodeInteraction {
    val imageBitmap = captureToImage()
    val buffer = IntArray(imageBitmap.width * imageBitmap.height)
    imageBitmap.readPixels(buffer, 0, 0, imageBitmap.width - 1, imageBitmap.height - 1)
    val pixelColors = PixelMap(buffer, 0, 0, imageBitmap.width - 1, imageBitmap.height - 1)

    (0 until imageBitmap.width).forEach { x ->
        (0 until imageBitmap.height).forEach { y ->
            if (pixelColors[x, y] == tint) return this
        }
    }
    throw AssertionError("Assert failed: The component does not contain the color")
}
Miguel Sesma
  • 750
  • 5
  • 15
  • Seems a litle wastefull to iterate the entire view. So how is that working if the color of the component is different than the background? – cutiko Aug 23 '22 at 10:39
  • No, this only tries to find a specific color in the composable, at any point. I use it for detecting the tint of icons that I change depending on some conditions. Is not that bad, at least with small composables. – Miguel Sesma Aug 24 '22 at 09:33
  • Ho! I get it know! If any pixel matches, then is ok. Is it worthy for a comment, or was I just clueless? – cutiko Aug 24 '22 at 11:34
  • I'm not very kin on comments. I prefer semantic names like `assertContainsColor` – Miguel Sesma Aug 25 '22 at 13:09
  • Apples and oranges, but have as you please, is your answer – cutiko Aug 25 '22 at 19:39
1

Maybe is an old post, but you could use Semantics matchers.

First, you need to create a semantic property and an extension like this

val ColorRes = SemanticsPropertyKey<Color>("ColorRes")
var SemanticsPropertyReceiver.colorRes by ColorRes

Instead of use "ColorRes" you can use an explicit name to specify which background color is,something like "CardBackgroundColor".

Then create a Matcher

fun hasBackgroundColor(expectedBackgroundColor: Color) =
    SemanticsMatcher.expectValue(ColorRes, expectedBackgroundColor)

Then add a semantic modifier to the composable like this

Row(
    modifier = Modifier
                .testTag("someTestTag")
                .semantics {
                    colorRes = theColorYouWant
                }
    )

and finally create the test

@Test
fun `Assert the background color`() {
//Some code to setContent ...
        composeTestRule
            .onNode(
                hasTestTag("deviceSignalContainer") and hasBackgroundColor(YourColor.Red),
                useUnmergedTree = true
            ).assertIsDisplayed()
    }

And that's all.

Armin
  • 11
  • 3
  • The problem is you have to modify the compose component code. If someone misses the new convention and uses `Modifier.background` as is standard it won't work. And it brings specific problems with other components like: what is going to happen with `Surface` or `Card` those components have to use the background color in the argument function, it can not be provided via `modifier`. It is good to have alternatives, though. – cutiko Dec 08 '22 at 19:08
  • well, will be necessary to use an explicit name, instead of "colorRes" you can use "backgroundColorRes" or "SurfaceBackgroundColorRes" depends of the necesity. – Armin Feb 23 '23 at 13:03
0

Based on : https://stackoverflow.com/a/70682865/19411871 and https://stackoverflow.com/a/72629280/19411871

i did this:

fun SemanticsNodeInteraction.assertBackgroundColor(expectedBackground: Color) {
    val array = IntArray(20)
    captureToImage()
        .readPixels(
            array,
            startY = 100,
            startX = 200,
            width = 5,
            height = 4
        )
    array.forEach {
    Assert.assertEquals(expectedBackground.convert(ColorSpaces.Srgb).hashCode(), it)
}
arist0v
  • 79
  • 8