Testing Cloudscape Design Select Component

Writing an end-to-end Cypress test picking an option from the Cloudscape Design React Select component.

Recently I showed how to interact with the Microsoft FAST design Select component. Someone asked how they could interact with Cloudscape Design components, in particular with its React Select component. In particular, these components include a test wrapper utilities that are supposed to help write stable tests. Well, you will see that those utilities are less than helpful, and you can get a simpler and better test just by using Cypress best practices for selecting elements.

🎁 You can find my source code in the repo bahmutov/cypress-cloudscape-example.

The application page

My application uses the standard demo of the React Select component from the Cloudscape documentation pages. I only added the data-testid attribute.

src/App.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import * as React from 'react'
import Select from '@cloudscape-design/components/select'

export default () => {
const [selectedOption, setSelectedOption] = React.useState({
label: 'Option 1',
value: '1',
})
return (
<Select
selectedOption={selectedOption}
onChange={({ detail }) => setSelectedOption(detail.selectedOption)}
options={[
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
{ label: 'Option 4', value: '4' },
{ label: 'Option 5', value: '5' },
]}
selectedAriaLabel="Selected"
data-testid="my-select"
ariaLabel="Select an option"
/>
)
}

The app runs using Vite. My initial test simply visits the page.

cypress/e2e/select.cy.js
1
2
3
4
5
6
7
8
9
// enables intelligent code completion for Cypress commands
// https://on.cypress.io/intelligent-code-completion
/// <reference types="cypress" />

describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
})
})

The page with the Select component

Let's see the HTML markup the Cloudscape Select component places on the page. It is a lot of custom styling. But we see the data-testid attribute, so we can find the component's top element at least.

The HTML markup

1
2
3
4
5
6
describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
cy.get('[data-testid=my-select]')
})
})

Get the Select element by test id

Let's make the test stronger. We know that the first option should be selected by default. Thus the element should show "Option 1" at the start. Instead of cy.get we can use cy.contains command.

1
2
3
4
5
6
describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
cy.contains('[data-testid=my-select]', 'Option 1')
})
})

Get the Select element by test id and the text inside

Initially the select element is closed. We can confirm it by checking the aria-expanded attribute on the button that opens it.

1
2
3
4
5
6
7
8
describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
cy.contains('[data-testid=my-select]', 'Option 1').find(
'button[aria-expanded=false]',
)
})
})

Confirm the select is closed at the start

Let's open the Select dropdown. We need to click on it. The select closes when we move the mouse away, so here is my trick to "catch" the dropdown HTML markup: add cy.wait and hover over the WAIT command to restore the DOM snapshot.

1
2
3
4
5
6
7
8
9
describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
cy.contains('[data-testid=my-select]', 'Option 1')
.find('button[aria-expanded=false]')
.click()
.wait(500)
})
})

Click on the "WAIT" command and inspect the HTML markup.

Inspect the opened dropdown HTML

Now we can confirm the Select is open and the "Option 1" is selected by default.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
cy.contains('[data-testid=my-select]', 'Option 1')
.find('button[aria-expanded=false]')
.click()
cy.contains('[data-testid=my-select]', 'Option 1')
.find('button[aria-expanded=true]')
.get('[role=option]')
.should('have.length', 5)
.contains('[role=option]', 'Option 1')
.should('have.attr', 'aria-selected', 'true')
})
})

So far, so good.

The option 1 is selected in the dropdown

Now let's select the "Option 3" and confirm the dropdown closes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe('Cloudscape Design System: Select', () => {
it('selects an option', () => {
cy.visit('/')
cy.contains('[data-testid=my-select]', 'Option 1')
.find('button[aria-expanded=false]')
.click()
cy.contains('[data-testid=my-select]', 'Option 1')
.find('button[aria-expanded=true]')
.get('[role=option]')
.should('have.length', 5)
.contains('[role=option]', 'Option 1')
.should('have.attr', 'aria-selected', 'true')

cy.log('**select option 3**')
cy.contains('[data-testid=my-select] [role=option]', 'Option 3').click()
cy.contains('[data-testid=my-select]', 'Option 3')
cy.log('**dropdown is closed**')
cy.get('[data-testid=my-select] [role=listbox]').should('not.be.visible')
})
})

Looking good. We are using data-testid plus aria attributes and roles to find and control the elements.

Cloudscape test wrappers

For something a little less impressive, let's try using Select test wrappers that Cloudscape Design recommends. We can import the testing utility in our Cypress spec. The wrapper gives us ... a selector string. Let's see how we can select the component's element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import createWrapper from '@cloudscape-design/components/test-utils/selectors'

describe('Cloudscape Design System: Select', () => {
it('uses test selector', () => {
const select = createWrapper().findSelect()
const openDropdown = select.findDropdown().findOpenDropdown()
cy.visit('/')
cy.get(select.findDropdown().toSelector()).click()
cy.get(openDropdown.toSelector()).should('be.visible')
cy.get(select.findDropdown().findOption(3).toSelector()).click()
cy.get(select.findDropdown().toSelector()).should(
'include.text',
'Option 3',
)
})
})

Oh my. The helpers like createWrapper().findSelect().findDropdown().toSelector() return the long mangles class name prefixes.

The test picks option 3

🤔 Maybe I am using the test wrappers incorrectly? I followed the examples in the documentation. If you can suggest a better way, I am all ears.

The test passes, but it is really cumbersome to read, and the Command Log is less than helpful. I think these Cloudscape test wrappers might be useful for Jest / Enzyme / js-dom test runners, but they only hurt the developers that use Cypress and can see the actual HTML markup shown by the browser. If I were designing a testing wrapper for these components I would concentrate on semantic HTML attributes, rather than the mangled class names.