Kotlin Test doesn't work due to layout being too wide

89 Views Asked by At

Summary:

  • Cannot pass multiple UI unit tests in Jetpack Compose, after change in code, that were working as expected before
  • Assumed that it has something to do with adding width modifiers, as that was the difference between last working commit
    • Rearranging the order seems to work to show the required composables, but then doesn't allow the entire view to be displayed or tested
      • So isolation testing of composable is fine (OK), testing the entire view at once doesn't seem to be possible after this change (NOK)
    • Removing the width modifiers works as well, but then the UI is not aligned as intended (for multiple Rows to same similar width fields rather than be dynamic)
  • Assumed that I should be able to have infinite width to use even if it doesn't get displayed, rather than a minimum that should really already fit for desktop views.
    • If this is correct how would I be able to update the size of the testing content window, from a minimum to an expected window size?

Minimal Reproducible Code:

import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
    val appName = "ComposableTest"

    Window(
        onCloseRequest = ::exitApplication,
        title = appName,
        state = rememberWindowState(width = 1500.dp, height = 750.dp)
    ) {
        val sortRow = SortRow()
        sortRow.createSortRow()
    }
}

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import org.junit.Rule
import org.junit.Test

class SortRowTest {

    // Compose
    @get:Rule
    var compose = createComposeRule()

    @Test
    fun `everything should be visible`() {
        compose.setContent {
            val sortRow = SortRow()
            sortRow.createSortRow()
        }
        val checkbox = "CheckBox"
        val sortName = "SortName"
        val dueDate = "SortDueDate"
        val status = "SortStatus"
        val deleteButton = "IconDeleteSort"
        val extraOptions = "IconDropDown"

        compose.onNodeWithTag(checkbox)
            .assertExists()
            .assertIsDisplayed()

        compose.onNodeWithTag(sortName)
            .assertExists()
            .assertIsDisplayed()

        compose.onNodeWithTag(dueDate)
            .assertExists()
            .assertIsDisplayed()

        compose.onNodeWithTag(status)
            .assertExists()
            .assertIsDisplayed()

        compose.onNodeWithTag(deleteButton)
            .assertExists()
            .assertIsDisplayed()

        compose.onNodeWithTag(extraOptions)
            .assertExists()
            .assertIsDisplayed()
    }
}

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material.Checkbox
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp

class SortRow {

    @Composable
    fun createSortRow() {
        Row {
            val isChecked = remember { mutableStateOf(false) }
            createCheckbox(isChecked)
            createSortName()
            createSortDate()
            createSortStatus()
            createDeleteButton()
            createExtraOptions()
        }
    }

    @Composable
    private fun createExtraOptions() {
        IconButton(
            onClick = {
            },
            modifier = Modifier.testTag("IconDropDown")
        ) {
            Icon(
                Icons.Default.MoreVert,
                contentDescription = "Drop Down Menu Options"
            )
        }
    }

    @Composable
    private fun createDeleteButton() {
        IconButton(
            onClick = {
            },
            modifier = Modifier.testTag("IconDeleteSort")
        ) {
            Icon(
                Icons.Outlined.Delete,
                contentDescription = null,
                tint = Color.Red
            )
        }
    }

    @Composable
    private fun createSortStatus() {
        Box {
            TextField(
                value = "TEST",
                onValueChange = {
                },
                trailingIcon = {
                    Icon(
                        Icons.Filled.ArrowDropDown,
                        "contentDescription",
                        Modifier
                            .testTag("SortStatusIcon")
                    )
                },
                modifier = Modifier
                    .testTag("SortStatus")
                    .width(250.dp)
            )
        }
    }

    @Composable
    private fun createSortDate() {
        Row {
            OutlinedTextField(
                value = "TEST",
                onValueChange = { },
                label = {
                    Text("Due Date")
                },
                trailingIcon = {
                    Icon(
                        Icons.Filled.ArrowDropDown,
                        "contentDescription",
                        Modifier
                            .testTag("DueDateIcon")
                    )
                },
                modifier = Modifier
                    .testTag("SortDueDate")
                    .width(250.dp)
            )
        }
    }

    @Composable
    private fun createSortName() {
        Row {
            TextField(
                value = "Test",
                onValueChange = {
                },
                maxLines = 1,
                modifier = Modifier
                    .testTag("SortName")
                    .width(500.dp)
            )
        }
    }

    @Composable
    private fun createCheckbox(isChecked: MutableState<Boolean>) {
        Checkbox(
            checked = isChecked.value,
            onCheckedChange = {
            },
            enabled = true,
            modifier = Modifier
                .testTag("CheckBox")
        )
    }
}

As you might be able to see, the window is fine and can be seen clearly during the run task. Whereas in the test it won't allow the deleteButton to be made visible, and then assumed any further components after that I might add in future.

Things that fix this, but don't then allow me to have the UI that is expected:

  1. Removing the width modifiers - allows the test to pass, UI is a bit funny afterwards
  2. Moving only the parts that I need for isolation testing to the left - this allows the extra functionality to test properly for each different component, but means that I am unable to test that everything works together in the expected order, such as for when I am testing the view as a whole.

Is there a way to allow for the test to work and keep the UI in tact at the same time?

As this is the minimal reproducible code, my actual production code is slightly more complex and could have different problems. I will follow up on that or if it is a totally different problem, then I will create a more specific or possibly new question.

1

There are 1 best solutions below

0
On

Tracing through the code and finding some experimental APIs has allowed a solution to form. While it does have some limitations, it at least allows the content to be accessed and tested in wider formats. It is my assumption that the default dimensions for the content are 1024x760 based on the code parameter defaults for the compose test function. Needing to create a custom dimension is still in the experimental phase, but I would prefer something like

compose.setContent (width, height) { 
    // Add compose content
}

But this is not the case for the moment, as below:

    @OptIn(ExperimentalTestApi::class)
    @Test
    fun `run desktop test`() {
        runDesktopComposeUiTest(1500, 750) {
            setContent {
                Column {
                    val sortRow = SortRow()
                    sortRow.createSortRow()
                }
            }

            val checkbox = "CheckBox"
            val sortName = "SortName"
            val dueDate = "SortDueDate"
            val status = "SortStatus"
            val deleteButton = "IconDeleteSort"
            val extraOptions = "IconDropDown"

            println(onRoot(useUnmergedTree = true).printToString())

            onNodeWithTag(checkbox, useUnmergedTree = true)
                .assertExists()
                .assertIsDisplayed()

            onNodeWithTag(sortName, useUnmergedTree = true)
                .assertExists()
                .assertIsDisplayed()

            onNodeWithTag(dueDate, useUnmergedTree = true)
                .assertExists()
                .assertIsDisplayed()

            onNodeWithTag(status, useUnmergedTree = true)
                .assertExists()
                .assertIsDisplayed()

            onNodeWithTag(deleteButton, useUnmergedTree = true)
                .assertExists()
                .assertIsDisplayed()

            onNodeWithTag(extraOptions, useUnmergedTree = true)
                .assertExists()
                .assertIsDisplayed()
        }
    }

Changing the dimensions to 1000, etc. proves that at least this works as expected. Showing errors when the component is not displayed vs the above example where there is enough room for the component to be displayed.

Current Limitations/Notes, based on question example:

  1. IDEs can't see which exact line is in error and will report the error at the runDesktopComposeUiTest function
    • while stack traces continue to work so you can find the exact line needed
  2. Using this function directly means any 'compose rules' that are stated within the class won't be needed for intended tests, needing slight refactoring for those test cases, as they would be unique and local to that test - 'compose.setContent' as opposed to just 'setContent' - this includes the node functions as well, such as onNodeWithTag and onRoot in the example.