Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RoborazziRule needs ViewInteraction as constructor parameter #294

Open
realdadfish opened this issue Apr 10, 2024 · 2 comments
Open

RoborazziRule needs ViewInteraction as constructor parameter #294

realdadfish opened this issue Apr 10, 2024 · 2 comments

Comments

@realdadfish
Copy link

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
Copy link
Owner

@realdadfish
Copy link
Author

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

  • in @Before, setup Activity / Fragment scenario and launch in CREATED state
  • in the test method, stub data (e.g. that are needed for initial loading of the screen)
  • in the test method, move the lifecycle to RESUMED state
  • verify things

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants