This blog post shows how to control native <select>
HTML elements from Cypress tests. We will also look at how to work with a very popular wrapper library called Select2, that supplants the native <select>
elements with an additional HTML markup.
Note: you can find the source code for this post in the recipe "Select widgets".
Single value select element
Let's take a page where a user selects a single state from a list of choices. Maybe it is their home state, or the state they would like to avoid where their nemesis lives.
<label for="my-state">
Pick one state
</label>
<select id="my-state" name="my-state" style="width: 50%">
<optgroup label="Alaskan/Hawaiian Time Zone">
<option value="AK">Alaska</option>
<option value="HI">Hawaii</option>
</optgroup>
<optgroup label="Pacific Time Zone">
<option value="CA">California</option>
<option value="NV">Nevada</option>
...
</select>
Modern browsers render the native <select>
pretty well. Here is Chrome 80 showing the list of states grouped by timezone.
We can set the selected state from a Cypress test using the .select command.
beforeEach(() => {
cy.visit('index.html')
})
describe('HTML select element', () => {
context('single value', () => {
it('sets MA', () => {
// https://on.cypress.io/select
// set using value
// <option value="MA">Massachusetts</option>
cy.get('#my-state').select('MA')
// confirm the selected value
cy.get('#my-state').should('have.value', 'MA')
})
})
})
Note: to skip repeating the same code the following test snippets will omit beforeEach
and describe
blocks.
Cypress command .select allows one to select an option using its value or its text content. The next test selects the option with text content "Massachusetts".
it('sets Massachusetts', () => {
// https://on.cypress.io/select
// set using text
// <option value="MA">Massachusetts</option>
cy.get('#my-state').select('Massachusetts')
// confirm the selected value
cy.get('#my-state').should('have.value', 'MA')
})
Selecting multiple values
HTML allows a single <select>
element to have multiple options to be picked
<label for="my-states">
Pick several states
</label>
<select id="my-states" name="my-states" multiple style="width: 50%">
...
</select>
The test works the same way - we just need to be careful when checking the selected value - we need to use deep equality to compare the two arrays.
it('adds several states', () => {
// https://on.cypress.io/select
// select multiple values by passing an array
cy.get('#my-states').select(['MA', 'VT', 'CT'])
// confirm the selected value - note that the values are sorted
// and because it is an array we need to use deep equality
// against the yielded list from ".invoke('val')"
cy.get('#my-states').invoke('val').should('deep.equal', ['CT', 'MA', 'VT'])
})
The test works and picks 3 states
Tip: note the blue outline around the <select>
element after the test is finished. The element still has the browser focus - which can surprise you while interacting with the Cypress GUI. For example, when pressing "r" to re-run the tests, you will instead change the state to "Rhode Island"!
Thus, this particular test is nice to end with blurring the <select>
element
it('adds several states', () => {
// https://on.cypress.io/select
// select multiple values by passing an array
cy.get('#my-states').select(['MA', 'VT', 'CT'])
// confirm the selected value - note that the values are sorted
// and because it is an array we need to use deep equality
// against the yielded list from ".invoke('val')"
cy.get('#my-states').invoke('val').should('deep.equal', ['CT', 'MA', 'VT'])
// remove the focus from <select> element
cy.get('#my-states').blur()
})
Single value using Select2
The standard <select>
element is ok, but the wrapper libraries like Select2 make it even better. First, you need to load the jQuery and the Select2 libraries and initialize the widget like this:
<head>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
</head>
<body>
<label for="favorite-state">
Pick your favorite state
</label>
<select class="js-example-basic-single" id="favorite-state" style="width: 50%">
...
</select>
<script src="app.js"></script>
</body>
The "app.js" file creates the HTML widget that shows up in place of the <select>
element.
// app.js
$(document).ready(function () {
$('.js-example-basic-single').select2()
})
The Select2 widget to me looks nicer than the stock <select>
element, plus it has autocomplete input that is helpful.
If we write a Cypress test to set the single value against the <select>
element wrapped using Select2 ... it will fail.
it('selects Massachusetts', () => {
cy.log('--- Force select ---')
// https://on.cypress.io/select
cy.get('#favorite-state').select('MA')
// confirm the value of the selected element
cy.get('#favorite-state').should('have.value', 'MA')
})
When Select2 renders itself, it hides the original <select>
element, leaving just a single pixel-high rectangle. Cypress checks if the element is visible before selecting a value, and rejects such barely visible elements, because the user could not select an element this way.
We can force Cypress to by-pass its built-in .select checks by using force: true
option.
it('selects Massachusetts', () => {
cy.log('--- Force select ---')
// https://on.cypress.io/select
cy.get('#favorite-state').select('MA', { force: true })
// confirm the value of the selected element
cy.get('#favorite-state').should('have.value', 'MA')
})
We have the <select>
element with the right value - but does the Select2 HTML wrapper show the right state's name? To see how the widget renders the selection, inspect its markup using the DevTools Element panel after the test has finished.
You can find the same selector even faster by using the Cypress Selector Playground which tries to pick a good selector for a visible element.
After you click the "Copy" button, you have the entire cy.get('#select2-favorite-state-container')
command in your clipboard - paste it in your test and go!
it('selects Massachusetts', () => {
cy.log('--- Force select ---')
// https://on.cypress.io/select
// avoid error message that select element is covered
// by the rendered Select2 widget by using "force: true" option
cy.get('#favorite-state').select('MA', { force: true })
// confirm the value of the selected element
cy.get('#favorite-state').should('have.value', 'MA')
// confirm Select2 widget renders the state name
cy.get('#select2-favorite-state-container').should('have.text', 'Massachusetts')
})
Selecting single value by typing
Let's select a state by typing part of its name. Here is where knowing the HTML markup the Select2 creates is crucial in order to send the right commands from the test. Luckily, we can always inspect the HTML structure using the DevTools.
First, notice that the user has to click the widget before the the input element appears.
Let's click from our test too, but we can't click on the original <select>
element - instead let's click on the HTML element Select2 adds to the page. We can use the Cypress Selector Playground tool just like before to pick the selector and the command and copy paste them into the test
it('selects Massachusetts by typing', () => {
cy.log('--- Pick state by typing ---')
cy.get('#select2-favorite-state-container').click()
})
The test opens the input widget, and we can check the selector for the input element to type into.
We could copy the selector cy.get('.select2-search')
suggested by the tool BUT this is very fragile. Notice in this case the selector does not have anything connecting it to the original <select class="js-example-basic-single" id="favorite-state">
element. If our page has several Select2 widgets, the cy.get('.select2-search')
command would return all of them breaking the test, because Cypress refuses to type into multiple elements at once.
In situations likes this, you need to look closely at the HTML markup to see if there is anything specific that ties the widget back to its original element.
Notice the Select2 widget has an attribute that has a part of the original <select>
name - the aria-control=select2-favorite-state-results
. We can use this attribute as a selector!
it('selects Massachusetts by typing', () => {
cy.log('--- Pick state by typing ---')
cy.get('#select2-favorite-state-container').click()
cy.get('input[aria-controls="select2-favorite-state-results"]').type('Mass{enter}')
})
We can extend the test to confirm the selected value and the shown text are correct
it('selects Massachusetts by typing', () => {
cy.log('--- Pick state by typing ---')
// first, open the widget
cy.get('#select2-favorite-state-container').click()
// then type into the widget's input element
// use custom selector to handle cases when there are multiple widgets
// on the page
cy.get('input[aria-controls="select2-favorite-state-results"]').type('Mass{enter}')
// confirm the value of the selected element
cy.get('#favorite-state').should('have.value', 'MA')
// confirm Select2 widget renders the state name
cy.get('#select2-favorite-state-container').should('have.text', 'Massachusetts')
})
Just to show the widget in action, you can delay typing each character using .type command's options:
cy.get('input[aria-controls="select2-favorite-state-results"]')
.type('Mass{enter}', {
delay: 500,
})
Multiple values using Select2
Let's move to the next situation: when Select2 widget wraps a <select>
element that allows picking multiple values. The <select>
HTML markup is:
<label for="states">
Pick several states
</label>
<select class="js-example-basic-multiple" id="states"
name="states" multiple="multiple" style="width: 90%">
...
</select>
We can set the values directly on the select#states
element from the test:
it('adds several states', () => {
// https://on.cypress.io/select
// select multiple values by passing an array
// again, have to use "force": "true" because the actual
// select is covered by Select2 widget
cy.get('#states').select(['MA', 'VT', 'CT'], { force: true })
// confirm the selected value - note that the values are sorted
// and because it is an array we need to use deep equality
// against the yielded list from ".invoke('val')"
cy.get('#states').invoke('val').should('deep.equal', ['CT', 'MA', 'VT'])
})
How do we confirm the "pills" displayed by the Select2 widget - the x Connecticut
, x Massachusetts
, x Vermont
we see there? Unfortunately, Cypress Selector Playground is really geared towards selecting individual elements, as the next screenshot shows
Let's open DevTools console and look at the markup using the Elements panel.
Our original element select#states
is immediately followed by .select2
element that contains the list items with every selected state. The combined selector for this would be #states + .select2 .select2-selection__choice
- first it finds an element with id states
, then the next element (CSS +
operator) with class select2
, then inside an element with class
. Let's write our test:select2-selection__choice
it('adds several states', () => {
// https://on.cypress.io/select
// select multiple values by passing an array
// again, have to use "force": "true" because the actual
// select is covered by Select2 widget
cy.get('#states').select(['MA', 'VT', 'CT'], { force: true })
// confirm the selected value - note that the values are sorted
// and because it is an array we need to use deep equality
// against the yielded list from ".invoke('val')"
cy.get('#states').invoke('val').should('deep.equal', ['CT', 'MA', 'VT'])
// confirm Select2 widget renders the 3 state names
cy.get('#states + .select2 .select2-selection__choice')
// use ".should(cb)" to confirm the displayed values
// https://on.cypress.io/should#Function
.should((list) => {
expect(list[0].title).to.equal('Connecticut')
expect(list[1].title).to.equal('Massachusetts')
expect(list[2].title).to.equal('Vermont')
})
})
The test confirms that these New England states are indeed what we wanted to select.
What if we want to find states by typing, not by setting them by value? Let's type using the markup shown by the Elements panel. The test delays each character by 500ms for clarity
it('adds several states by typing', () => {
// Select2 HTML widget is inserted after the corresponding <select> element
// thus we can find it using " + " CSS selector
cy.get('#states + .select2').click()
// after we click on the Select2 widget, the search drop down and input appear
.find('.select2-search')
// type parts of the states' names, just like a real user would
.type('Verm{enter}Mass{enter}Conn{enter}', { delay: 500 })
// confirm the selected value - note that the values are sorted
// and because it is an array we need to use deep equality
// against the yielded list from ".invoke('val')"
cy.get('#states').invoke('val').should('deep.equal', ['CT', 'MA', 'VT'])
})
Removing selected item
What if the user selects a few items, then removes one of them? Can we test that? Again, let's look at the markup after selecting a few items.
Each Select2 item has a span with class select2-selection__choice__remove
. It is a mouthful, but we can use it to remove an item from the test.
it('removes state', () => {
cy.get('#states + .select2').click()
.find('.select2-search')
.type('Verm{enter}Mass{enter}Conn{enter}')
// confirm 3 states are selected
cy.get('#states').invoke('val')
.should('deep.equal', ['CT', 'MA', 'VT'])
// remove Connecticut, they don't have a major league team anyway
cy.get('#states + .select2')
.contains('.select2-selection__choice', 'Connecticut')
.find('.select2-selection__choice__remove').click()
// when removing an item, Select2 automatically expands choices
// we can close them by pressing "Enter"
cy.get('#states + .select2 .select2-search')
.type('{enter}')
// confirm the remaining selections
cy.get('#states').invoke('val').should('deep.equal', ['MA', 'VT'])
})
The test is fast - blink, and you will miss it - but you can still see a state being added and deleted correctly.
Programmatic control
Select2 allow the application code to retrieve selected items programmatically. For example, you can get currently selected items by calling method select2
with argument data
.
$('#mySelect2').select2('data');
Cypress tests can directly invoke this method from the test - because the test runs inside the browser.
it('returns selected items', () => {
cy.get('#states').select(['MA', 'VT', 'CT'], { force: true })
// https://select2.org/programmatic-control/retrieving-selections
cy.get('#states').invoke('select2', 'data')
})
Once the test finishes, click on the INVOKE
command to see the yielded value - it is a list of selected states (with extra properties added by Select2 code)
From each object, let's pick the text
fields and assert the expected states are selected. Cypress bundles Lodash that allows us to map each object to its property elegantly.
it('returns selected items', () => {
cy.get('#states').select(['MA', 'VT', 'CT'], { force: true })
// https://select2.org/programmatic-control/retrieving-selections
cy.get('#states').invoke('select2', 'data')
.should((list) => {
const names = Cypress._.map(list, 'text')
expect(names).to.deep.equal([
'Connecticut',
'Massachusetts',
'Vermont'
])
})
})
Fetched Data
Select2 allows to load choices from a remote server using an Ajax call. Here is the initialization code fetching a list of users:
$('.js-example-remote-data').select2({
placeholder: 'Pick a user',
ajax: {
url: 'https://jsonplaceholder.cypress.io/users',
dataType: 'json',
delay: 250,
processResults (data) {
return {
results: $.map(data, function (item) {
return {
text: item.name,
id: item.id,
}
}),
}
},
},
})
Our test should be careful not limit the element search to the initial empty list of choices. For example, this test WILL NOT work
it('selects a value - WILL NOT WORK, JUST A DEMO', () => {
// clicking on the container starts Ajax call
cy.get('#select2-user-container').click()
// we know the choice elements have this class
cy.get('.select2-results__option')
// so let's try finding the element with the text
// WILL NOT WORK
.contains('Leanne Graham').click()
// confirm the right user is found
cy.get('#user').should('have.value', '1')
cy.get('#select2-user-container').should('have.text', 'Leanne Graham')
})
The above test fails - even though we can see the user with the name we are looking for.
The reason for the failure is described in our Retry-ability Guide. By splitting the command cy.get(...).contains(...)
the test "falls" into a trap. It finds the loading placeholder element, and then only searches that part of the DOM, while the true list is created in a different DOM element. You can see the problem by hovering over the "get .select2-results__option" command.
There are several solutions to this problem, all shown in cypress-example-recipes repository. Perhaps the simplest one is to use a single command to query the element by text.
it('selects a value by retrying', () => {
// clicking on the container starts Ajax call
cy.get('#select2-user-container').click()
// we need to retry getting the option until it is found
// then we can click it. To retry finding the element with text
// we should use https://on.cypress.io/contains
cy.contains('.select2-results__option', 'Leanne Graham').click()
// confirm the right user is found
cy.get('#user').should('have.value', '1')
cy.get('#select2-user-container').should('have.text', 'Leanne Graham')
})
The cy.contains command retries until Select2 fetches and displays the list of options and an option with text "Leanne Graham" is present.
Conclusions
If you are working with a library that wraps a native HTML element with a widget, like Select2 does, it is important to know the additional markup. The test has to interact with the new markup to control the widget and assert it is wired correctly. In this post, we have looked at the native <select>
element and its wrapper Select2 and how Cypress end-to-end tests can work in both situations.
- cy.select command
- "Select widgets" recipe has the source code for this blog post