How I TDD an accessible react accordion

Date: Sun Jun 16 2019 Tags: React, TDD, Accessibility, Opinion

Intro

I have been doing a lot of TDD at work recently, and I got thinking about the best ways to bring TDD into my react workflow.

This isn't going to be a tutorial on the ins and outs, but more about the ways of thinking when working with user interactions.

The component I'm building takes in components and headers and displays them inside an accordion. If you're interested in the final code, or any of the libraries I used, you will find them all at the end :)

How I start

When I'm working with any complex interactions the first place I look is the wai-aria spec. They have tons of examples on common UX patterns, including an example of an accessible accordion.

This spec is the starting point of this entire component TDD cycle. It clearly outlines the way a user should be able to interreact with an accordion, as well as providing a working example.

I like to start with a long list of todo's. All of these todo's might not end up being the final tests, but it helps me think through the problem.

Its also useful for grouping different pieces of work together into clear logically steps. For example, in the todo's below I need to only show 1 component at a time before I can reliably show content for selected header when clicked

describe("Accordion Component", () => {
  it.todo("should render Accordion with test data")
  it.todo("should show the headings passed")
  it.todo("should only show 1 component at a time")
  it.todo("should show content for selected header when header is clicked")
  it.todo("should contain the 3 different components from the test data")
  it.todo("should focus next focusable element with tab")
  it.todo("should focus previous focusable element with tab")
  describe("when header is focused", () => {
    it.todo("should expand header with space")
    it.todo("should expand header with enter")
    it.todo("should focus next header with down arrow")
    it.todo("should focus previous header with up arrow")
    it.todo("should focus first header with down arrow when on last")
    it.todo("should focus last header with up arrow when on first")
    it.todo("should focus last header with up arrow when on first")
    it.todo("should focus first header when home is pressed")
    it.todo("should focus last header when end is pressed")
  })
})

With the tests defined, I would love to just start passing them, but I find it important to lay out the HTML in the same sort of planning way. I won't go through this here, but in my code I just followed the aira spec. Broke it all up into react components that made sense, and updated the correct HTML attributes based on the props passed.

It might be valuable for me in the future to write tests around the HTML, I didn't in this exploration. I'm relying on the interactions to fail if the HTML becomes inaccessibility. However, in hindsight, the screen reader potions of the HTML aren't fully protected.

Writing the tests

Why I write the tests first

While its tempting to dive straight into react, its cleaner and can be more time efficient to just write the tests first. I want to describe what I want to create, so I can easily and simply confirm its been created.

I also want to make sure my test fails before I do anything. Anytime I have been in a situation where updating my tests makes them pass, it forces me to break my code in order to trust it. Which just wastes time.

Why I only think about the current test

I find it very tempting to get caught up in the wider solution. However focusing on the final solution will result in a lot of upfront complexity to manage. This is why I try to think about the smallest amount of code to pass the current test. That way the final solution grows with my understanding of the problem.

In this example, I suspected that I would need useReducer to deal with the state. This sent me down a rabbit hole where I ended up wasting a bunch of time just to show 1 internal component.

In the end I took a step back and just created a const array of Booleans. In doing so I reduced the upfront complexity of the problem and broke it down slowly as I kept passing tests. I did end up using useReducer after all, but my implantation was more robust as it grew with my understanding of the problem.

Things I try not to worry about

I try not to worry about testing the same logic over and over again. A test is always useful as long as it provides some new context. There is no need for DRY (Don't Repeat Yourself) in a test.

I also know I wont catch every edge case in my first pass, if a bug happens in the future, just write a new test so it doesn't happen again. You don't have to get everything right in the first pass. Its just a component :)

What I have in the end

So I have done all of these tests, used testing libraries that emulate how a real user would use it. And created some really robust code. I have all my tests passing, and even added some new tests not in my first todo. And this is what I end up with!

Picture of passing tests

Picture of rendered component

A lovely set of passing tests, and a component that has a long way to go before a user can use it. Its still great, Its really only missing CSS, and CSS shouldn't be tested in most cases. CSS is more of an art and harder to define then JS. But with a nice foundation for the interactions it gives more freedom to just add the design onto an accessible feature.

It also helps protect the accessibility of the component and clearly defined the constraints for the design. It won't catch every accessibility error, but at least it will ensure it functions as the aria spec requires.

Gotchas

Anything new is HARD, learning Jest, React-testing-library & jest-dom is a lot to learn upfront. It also forces you to understand the DOM API, so if that's something you aren't 100% up on some things will be confusing.

I had a lot of fun with it though, and I even added typescript into the mix. But its going to make everything take longer, if you're learning them all for the first time. Especially if you're learning it alone. And that's okay!

Also, managing focus in react is a thing to be aware of, its way outside of the scope of what I'm trying to say here. But think about how you're going to manage focus when the component rerenders. Hint, you will need to learn about ref's

Check out these resources!