Friday, 21 August 2020

Espresso tests: Match child view by position within RecyclerView

Let's say you want to write a user interface test that matches the child view at a particular position within a RecyclerView and you want to assert some properties on that child view. When you search for this online, you'll come across a whole host of solutions that do work but, sadly, are not very fluent or Espresso-esque. You'll see solutions that will lead you to write assertions like the following:

onView(withRecyclerView(R.id.recyclerView).atPositionOnView(0))
    .check(matches(withText("Some Text")))

onView(withId(R.id.recyclerView))
    .check(matches(atPosition(0, withText("Some Text"))))

Like I said: sure, this works, but I feel we can do better. Most notably, what I would change about these is to match on the child view of interest instead and assert properties on that rather than matching on the entire RecyclerView. So what we're aiming for are assertions like the following:

onView(withPositionInRecyclerView(R.id.recyclerView, 0))
    .check(matches(withText("Some Text")))

It's a subtle change and maybe it's just me but I feel this is a little easier on the eye and makes the reading experience a little more pleasurable. If you've made it this far and you're bought in, here's the Matcher class you need to define to make this possible:

/**
 * A matcher that matches the child [View] at the given position
 * within the [RecyclerView] which has the given resource id.
 *
 * Note that it's necessary to scroll the [RecyclerView] to the desired position
 * before attempting to match the child [View] at that position.
 */
class WithPositionInRecyclerViewMatcher(private val recyclerViewId: Int,
                                        private val position: Int) : TypeSafeMatcher<View>() {

    override fun describeTo(description: Description) {
        description.appendText("with position $position in RecyclerView which has id $recyclerViewId")
    }

    override fun matchesSafely(item: View): Boolean {
        val parent = item.parent as? RecyclerView
            ?: return false

        if (parent.id != recyclerViewId)
            return false

        val viewHolder: RecyclerView.ViewHolder = parent.findViewHolderForAdapterPosition(position)
            ?: return false // has no item on such position

        return item == viewHolder.itemView
    }
}

And, for best practice, define a function that wraps this class as follows:

/**
 * @return an instance of [WithPositionInRecyclerViewMatcher] created with the given parameters.
 */
fun withPositionInRecyclerView(recyclerViewId: Int, position: Int): Matcher<View> {
    return WithPositionInRecyclerViewMatcher(recyclerViewId, position)
}

That's it. We're done. Happy testing.

Monday, 17 August 2020

Espresso tests: Wait until view is visible

Whilst searching online for some suggestions on how to wait for a particular View to become visible within an Espresso test, I noticed that all of the suggestions that defined a new ViewAction class, defined one that operated on the root view. I can see why this is necessary in situations where the View in question is not present in the view hierarchy and will enter the view hierarcy at a later point. However, if you're waiting on a View that's present in the view hierarchy to change from one state to another, it's much more elegant to match and operate on that one View specifically rather than maching and operating on the root view.

So, assuming you're waiting on a View that's present in the view hierarcy to change from INVISIBLE or GONE visibility to VISIBLE, you can define a ViewAction class as follows:

/**
 * A [ViewAction] that waits up to [timeout] milliseconds for a [View]'s visibility value to change to [View.VISIBLE].
 */
class WaitUntilVisibleAction(private val timeout: Long) : ViewAction {

    override fun getConstraints(): Matcher<View> {
        return any(View::class.java)
    }

    override fun getDescription(): String {
        return "wait up to $timeout milliseconds for the view to become visible"
    }

    override fun perform(uiController: UiController, view: View) {

        val endTime = System.currentTimeMillis() + timeout

        do {
            if (view.visibility == View.VISIBLE) return
            uiController.loopMainThreadForAtLeast(50)
        } while (System.currentTimeMillis() < endTime)

        throw PerformException.Builder()
            .withActionDescription(description)
            .withCause(TimeoutException("Waited $timeout milliseconds"))
            .withViewDescription(HumanReadables.describe(view))
            .build()
    }
}

And define a function that creates an instance of this ViewAction when called, as follows:

/**
 * @return a [WaitUntilVisibleAction] instance created with the given [timeout] parameter.
 */
fun waitUntilVisible(timeout: Long): ViewAction {
    return WaitUntilVisibleAction(timeout)
}

Then, you can then call on this ViewAction in your test methods as follows:

onView(withId(R.id.myView)).perform(waitUntilVisible(3000L))

You can run with this concept and similarly define view actions that wait on other properties of the view to change state. For example, for the text of a TextView to change to some expected text.

Espresso tests: Match child view by position within parent

I noticed whilst browsing online that all of the answers to the question "can I match the child at a particular index within a particular parent view" required the definition of a new Matcher class. Whilst it's not a huge deal to define a new Matcher class as and when needed, it's not necessary in this case. You can instead get a handle on a particular child of a particular view by joining up the view matchers offered by Espresso into a method as follows:
/**
 * @param parentViewId the resource id of the parent [View].
 * @param position the child index of the [View] to match.
 * @return a [Matcher] that matches the child [View] which has the given [position] within the specified parent.
 */
fun withPositionInParent(parentViewId: Int, position: Int): Matcher<View> {
    return allOf(withParent(withId(parentViewId)), withParentIndex(position))
}

You can use this method as follows:

onView(
    withPositionInParent(R.id.parent, 0)
).check(
    matches(withId(R.id.child))
)

Thursday, 30 July 2020

Xcode UI tests: saveScreenshot(...) extension function

Taking a screenshot as part of an Xcode UI test and adding the screenshot to the test's output is a five-step shuffle as follows:

let screenshot = XCUIApplication().screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
attachment.name = "SomeName"
add(attachment)

Wouldn't it be great if this could be reduced to a single line call as follows:

XCUIApplication().saveScreenshot(to: self, named: "SomeName")

To make this possible, all you need to do is add the following extension function to your UI testing bundle:

extension XCUIScreenshotProviding {
    
    func saveScreenshot(to activity: XCTActivity, named name: String) {
        let attachment = XCTAttachment(screenshot: screenshot())
        attachment.lifetime = .keepAlways
        attachment.name = name
        activity.add(attachment)
    }
}

Saturday, 25 July 2020

Xcode UI tests: waitForNonExistence(...) extension function

The XCUIElement.waitForExistence(timeout:) function is great for waiting on an element to appear whilst an animation or asynchronous operation completes. However, there's equally a need at times to wait for an element to disappear and, sadly, XCUIElement does not offer such a function. Good news though! It turns out that it's not difficult at all to compose an extension function on XCUIElement which does exactly this, as follows:

extension XCUIElement {

    /**
     * Waits the specified amount of time for the element’s `exists` property to become `false`.
     *
     * - Parameter timeout: The amount of time to wait.
     * - Returns: `false` if the timeout expires without the element coming out of existence.
     */
    func waitForNonExistence(timeout: TimeInterval) -> Bool {

        let timeStart = Date().timeIntervalSince1970

        while (Date().timeIntervalSince1970 <= (timeStart + timeout)) {
            if !exists { return true }
        }
 
        return false
    }

Tuesday, 24 December 2019

ASCII, ISO and UTF

I struggle to remember what the ASCII, ISO and UTF acronyms stand for so I'm typing them up here in the hope that they stick!
  • ASCIIAmerican Standard Code for Information Interchange: this is the 7-bit character encoding system (7 bits so a total of 127 characters) and forms the basis of the other two systems.
  • ISOInternational Organisation for Standardisation: this is the 8-bit character encoding system which has a number of different versions each supporting a different set of languages (e.g. ISO-8859-1, ISO-8859-2 etc).
  • UTFUnicode Transformation Format: this is the system which uses a variable number of bytes (usually 1-4) to encode a character and hence supports a significantly larger number of characters than the other two systems.

Friday, 11 October 2019

iOS: Natural text alignment doesn't mean what you think it means

So it turns out that natural text alignment in iOS doesn't mean "left-align left-to-right text and right-align right-to-left text" as it does in Android. For example, if I put English text in a UILabel I'd expect it to be left-aligned and if I put Arabic text in a UILabel I'd expect it to be right-aligned. But no. This is not how it works in iOS. Instead natural text alignment in iOS means "left-align text if the device's language is set to a left-to-right language and right-align text if the device's language is set to a right-to-left language". So, for example, if I set the device's language to English then the text in the UILabel will be left-aligned regardless of its content and if I set the device's language to Arabic then the text in the UILabel will be right-aligned regardless of its content.

For a super simple iOS application which demonstrates this see here.