In the previous post, we created an accessible React accordion component. Let's test it. I don't see much sense of writing unit tests for this kind of components. Snapshot tests don't provide much value either. I believe end-to-end (e2e) tests are the best choice here (but for testing hooks I would prefer unit tests).
Will try to test it with Cypress. Cypress uses headless Chrome, which has a devtools protocol, which supposes to have better integration than previous similar solutions.
Install Cypress
Cypress easy once you understand how to start it. It took me... more than I expected to understand how to get started. They have huge documentation, which is hard to navigate (at least for me).
Sometimes too much documentation is as bad as too little
But I figured it out after some experimentation. Install Cypress
yarn add cypress --dev
Run it the first time
yarn cypress open
It will create a lot of files. Close Cypress window. Delete everything from cypress/integration
.
Add cypress.json
to the root of the project.
{
"baseUrl": "http://localhost:3000/"
}
Now in one terminal, you can start dev server yarn start
and in second one Cypress yarn cypress open
and start to write tests.
Configure Cypress
But how to run tests in CI? For this you need another npm package:
yarn add --dev start-server-and-test
Change package.json
"scripts": {
"test": "yarn test:e2e && yarn test:unit",
"test:unit": "react-scripts test",
"cypress-run": "cypress run",
"test:e2e": "start-server-and-test start http://localhost:3000 cypress-run"
}
Almost there. Add one more package
yarn add cypress-plugin-tab --dev
In cypress/support/index.js
import "./commands";
import "cypress-plugin-tab";
Add to .gitignore
cypress/screenshots
cypress/videos
Now we done.
Planning tests
This part I like.
Let's create test file cypress/integration/Accordion.js
:
describe("Accordion", () => {
before(() => {
cy.visit("/");
});
// your tests here
});
It will open the root page of the server (we will use dev server) before tests.
We saw WAI-ARIA Authoring Practices 1.1. in the previous post:
- Space or Enter
- When focus is on the accordion header of a collapsed section, expands the section.
- Tab
- Moves focus to the next focusable element.
- All focusable elements in the accordion are included in the page Tab sequence.
We simply can copy-paste it "as is" in test file:
describe("Space or Enter", () => {
xit("When focus is on the accordion header of a collapsed section, expands the section", () => {});
});
describe("Tab", () => {
xit("Moves focus to the next focusable element.", () => {});
xit("All focusable elements in the accordion are included in the page Tab sequence.", () => {});
});
-
describe
- adds one more level to the hierarchy (it is optional). -
xit
- a test which will be skipped, as soon as we will implement actual test we will change it toit
-
it
- a test,it("name of the test", <body of the test>)
Isn't it beautiful? We can directly copy-paste test definitions from WAI-ARIA specification.
Writing tests
Let's write actual tests.
First of all, we need to agree on the assumptions about the tested page:
- there is only one accordion component
- there are three section in it: "section 1", "section 2", "section 3"
- section 2 is expanded other sections are collapsed
- there is a link in section 2
- there is a button after the accordion
First test: "Space or Enter, When focus is on the accordion header of a collapsed section, expands the section".
Let's find the first panel in accordion and check that it is collapsed. We know from the specification that the panel should have role=region
param and if it is collapsed it should have hidden
param:
cy.get("body")
.find("[role=region]")
.first()
.should("have.attr", "hidden");
Let's find corresponding header e.g. first. We know from the spec that it should have role=button
param. Let's imitate focus
event because users will use Tab
to reach it.
cy.get("body")
.find("[role=button]")
.first()
.focus();
Now let's type Space in focused element
cy.focused().type(" ");
Let's check that section expanded (opposite of the first action):
cy.get("body")
.find("[role=region]")
.first()
.should("not.have.attr", "hidden");
I guess it is pretty straightforward (if you are familiar with any e2e testing tool, they all have similar APIs).
It was easy to write all tests according to spec plus specs for the mouse.
Flaky tests
The only flaky part is when we use React to switch focus e.g. up arrow, down arrow, end, home. Change of focus, in this case, is not immediate (compared to browsers Tab
). So I was forced to add a small delay to fix the problem:
describe("Home", () => {
it("When focus is on an accordion header, moves focus to the first accordion header.", () => {
cy.contains("section 2").focus();
cy.focused().type("{home}");
cy.wait(100); // we need to wait to make sure React has enough time to switch focus
cy.focused().contains("section 1");
});
});
Conclusion
I like how the specification can be directly translated to e2e tests. This is one of the benefits of writing a11y components - all behavior is described and tests are planned. I want to try to write the next component BDD style (tests first).