takahirom / roborazzi

Make JVM Android integration test visible 🤖📸
https://takahirom.github.io/roborazzi/
Apache License 2.0
654 stars 25 forks source link

RoborazziRule needs ViewInteraction as constructor parameter #294

Open realdadfish opened 3 months ago

realdadfish commented 3 months ago

I've tried to use the RoborazziRule that comes with this project, but failed, because naturally JUnit rules must be instantiated before a test runs and if the test sets up an Activity at a later stage (e.g. because of test stubbing) or if the Activity is no longer there because the respective ActivityScenario is already closed, screenshot recording fails as Espresso cannot (yet/no longer) work on the ViewInteraction. Maybe this could be changed by providing the CaptureRoot as Lambda instead of the "actual thing" as constructor parameter, like so:

@get:Rule var screenshotRule = RoborazziRule { onView(isRoot()) }

Anyways, this is what I came up with (prettry much restrained to Espresso, no Compose):

class EspressoScreenshotRule : TestWatcher() {
    private lateinit var description: Description
    private val callbacks = mutableListOf<() -> Unit>()
    private val viewInteractions = mutableListOf<ViewInteraction>()

    /**
     * Capture a specific view's state as of now (e.g. `onView(withId(...))`) or
     * the complete screen (e.g. `onView(isRoot())`)
     */
    fun capture(viewInteraction: ViewInteraction, tag: String) {
        viewInteraction.captureRoboImage(
            filePath = description.createFileName(tag),
            roborazziOptions =
                RoborazziOptions(
                    taskType = RoborazziTaskType.Record,
                    contextData =
                        mapOf(
                            "Classes" to description.className.shortenClasspath(),
                            "Tests" to "${description.methodName} (${description.className.shortenClasspath()})"
                        )
                )
        )
    }

    /**
     * Capture the state of a specific view at the point when a test fails
     */
    fun captureOnFailure(viewInteraction: ViewInteraction) {
        viewInteractions.add(viewInteraction)
    }

    /**
     * Run code after all screenshots have been taken, at the end of a test, useful for cleaning up resources
     */
    fun runAfterTest(callback: () -> Unit) {
        callbacks.add(callback)
    }

    override fun starting(description: Description) {
        this.description = description
    }

    override fun failed(e: Throwable, description: Description) {
        if (!isAnyActivityResumed() && viewInteractions.isNotEmpty()) {
            println(
                "WARNING: Can't screenshot ViewInteraction(s) because no Activity is resumed; " +
                    "consider tearing down the activity via `runAfterTest {}`"
            )
            return
        }
        viewInteractions.forEachIndexed { index, interaction ->
            capture(interaction, "failure$index")
        }
    }

    private fun isAnyActivityResumed(): Boolean =
        try {
            viewInteractions.firstOrNull()?.check { _, _ -> }
            true
        } catch (e: NoActivityResumedException) {
            false
        }

    override fun finished(description: Description) {
        callbacks.forEach { it() }
    }
}

private fun Description.createFileName(tag: String): String =
    "build/reports/roborazzi/screenshots/${classMethod()}.$tag.png"

private fun Description.classMethod(): String =
    "${this.className.shortenClasspath()}.${this.methodName.replace(" ", "_")}"

private fun String.shortenClasspath(): String = replace(Regex("\\B\\w+(\\.[a-z])"), "$1")

And the usage is the following:

@Before
fun setup() {
    scenario =
        launchFragmentInContainer(initialState = Lifecycle.State.CREATED) {
            MyFragment()
        }
    screenshotRule.captureOnFailure(onView(isRoot()))
    screenshotRule.runAfterTest {
        scenario.close()
    }
}
takahirom commented 3 months ago

Thanks. May I ask if you've tried using RuleChain? https://github.com/DroidKaigi/conference-app-2023/blob/main/core/testing/src/main/java/io/github/droidkaigi/confsched2023/testing/RobotTestRule.kt#L60

realdadfish commented 3 months ago

Yes, but the thing is, again for stubbing, I need to have the Activity / Fragment state under control. The usual workflow for me is:

So, unless the Activity / Fragment is in RESUMED state, I can nowhere use a ViewInteraction, because Espresso will throw the NoActivityResumedException.