End-to-end
User flows are covered in end-to-end (E2E) tests in the hub. We utilise Cypress and Testing Library for this. The tests are located in the e2e-tests/cypress/integration
subfolder in the root of the hub repository.
Best practices
Finding/querying elements
To select elements on the page, we follow the guidelines recommended by the Testing Library authors. One of the core principles is that tests should interact with the website in the same way as a person.
That means that elements should be selected by their role and labels rather than internal attributes that only a developer would know about.
❌ ** DON'T DO THIS: **
cy.find('#contact-form-submit-button');
cy.findByTestId('password-field');
✅ ** DO THIS: **
cy.findByRole('dialog', {name: 'Contact us'}).within(() => {
cy.findByRole('button', {name: 'Send message'});
});
cy.findByLabelText('Password');
role
an element has?You can inspect the role and label of any HTML element using the "Accessibility" tab in the side panel of the "Elements" section of Chrome's DevTools.
Element query utils
Take your time to browse through the existing tests to get a feel for how we query elements, and explore the available utility functions that speed up recurring selections, e.g. findModal
, findAlert
, openPlaylist
, openAssetViewer
, …
List elements
To test whether an element exists in a list, it can be tempting to try a query like cy.findByRole('listitem', {name: 'Content of item'}).should('exist')
. Unfortunately, this doesn't work: Unlike buttons or headings, <li>
elements are not "named" by their content.
We also don't want to add aria-
attributes to elements just for the sake of making testing easier, as that can lead to a bad experience for people using screen readers and similar tools.
So, a good way to run these checks instead, is by searching for the parent list element (<ul>
, <ol>
, or <dl>
) and running our checks within.
Most list elements don't have labels, but they can often be found nearby headings. For example, given the following HTML structure, we could test for list items as shown below.
<h2>Shopping list</h2>
<ul>
<li>Milk</li>
<li>Bread</li>
</ul>
cy.findByRole('heading', {
name: 'Shopping list',
level: 2,
})
.next('ul')
.within(() => {
cy.contains('Milk').should('exist');
cy.contains('Bread').should('exist');
cy.contains('Eggs').should('not.exist');
});
Chaining commands & Cypress errors
Cypress can sometimes throw errors when chaining commands on interactive elements. This happens because Cypress loses the reference to the selected element due to the way React updates the DOM (by recreating it on each render rather than doing fine-grained updates).
To avoid these kinds of errors, re-query elements after interacting with them:
❌ ** DON'T DO THIS: **
cy.findByRole('button', {name: 'Play video'})
.click()
.should('have.attr', 'aria-pressed');
✅ ** DO THIS: **
cy.findByRole('button', {name: 'Play video'}).click();
cy.findByRole('button', {name: 'Play video'}).should(
'have.attr',
'aria-pressed'
);
If you're not able to find an element using one of the approaches described above, this could hint at accessibility issues in our HTML. Have a look at the rendered HTML output of our app and make sure that all interactive elements have explicit labels and that the HTML regions of the page are correctly structured and labelled, too.
Using response ids
When needing to utilise response id
s in a test, utilise Cypress' ability to retrieve an id
of a created resource.
To do this, add an alias to the resource and then refer to the alias later on to receive the resource id
.
See also: Cypress as
and aliases
id
sWhen a response id
, e.g. the id of a freshly created asset or playlist, needs to be used in a test, avoid hardcoding the id
in the test. Hardcoding the response id can make the test fragile and prone to break if any test data changes occur.
❌ ** DON'T DO THIS: **
This uses a hardcoded id
it('tests a playlist', () => {
cy.createPlaylist(playlistNameA).audience('all').addContent([asset1]);
...
cy.visit('/playlists/3'); // 3 is the hardcoded id - Avoid this!
})
✅ ** DO THIS: **
it('tests a playlist', () => {
cy.createPlaylist(playlistNameA)
.audience('all')
.addContent([asset1])
.as('playlistA'); // Create an alias for the playlist
...
cy.then(function () {
const {playlistId} = this.playlistA; // Use the alias
cy.visit(`/playlists/${playlistId}`);
});
})
✅ ** OR DO THIS: **
it('tests a playlist', () => {
cy.createPlaylist(playlistNameA)
.audience('all')
.addContent([asset1])
.as('playlistA'); // Create an alias for the playlist
...
cy.get(`@${playlistA}`).then(playlist => { // Use the alias
const {playlistId} = playlist;
cy.visit(`/playlists/${playlistId}`);
});
})
Intercepting responses
When testing a feature that relies on an asynchrounous response from the server, it might be necessary to wait for the response to be received before continuing with the test.
Instead of waiting an arbritrary amount of time, it is better to intercept the request or response and wait for it to resolve. To do this, use the cy.intercept
command to intercept the request.
This can be combined with aliasing so the intercept can be used mutliple times in a test.
❌ ** DON'T DO THIS: **
This waits a set amount of time using cy.wait()
it('creates a draft weblink', () => {
cy.findByRole('textbox', {name: 'URL'})
.should('be.empty')
.type(newWebLink);
cy.findByRole('button', {name: 'Next'}).click();
cy.wait(500);
})
✅ ** DO THIS: **
This creates an alias of cy.intercept()
and waits for the response whenever it is called
it('creates a draft weblink', () => {
cy.intercept('POST', '/api/assets').as('createDraftLink');
...
cy.findByRole('textbox', {name: 'URL'})
.should('be.empty')
.type(newWebLink);
cy.findByRole('button', {name: 'Next'}).click();
cy.wait('@createDraftLink');
})
As well as simply intercepting and waiting for an action, cy.intercept()
can spy on response data as well as stubbing responses.
To read more see: Cypress intercept
Utils
Date management
setUtcDate
util
Use the setUtcDate
util when testing date related features. It enables you to set the "today date" of the test.
The default today date of the setUtcDate
util is the 01/01/2031
.
Be sure to import the utility functions at the beginning of the test, ideally in the beforeEach
clause.
Despite the util setting a UTC time, the timezone of the local test environment will not necessarily be UTC.
To ensure your test is running correctly locally the same as it does via CircleCi, set the timezone of your laptop to UTC and run the test.
Use case example:
import {setUtcDate} from '../helpers/utils';
describe('Update dates', () => {
const dateToday = '01/01/2031';
const dateTomorrow = '02/01/2031';
beforeEach(() => {
setUtcDate();
});
it('shows correct date', () => {
// your test, e.g....
cy.findByRole('textbox', {name: 'Date *'})
.should('have.value', dateToday)
.clear();
cy.findByRole('textbox', {name: 'Date *'}).type(
`${dateTomorrow}{enter}`
);
});
});
Locale management
setLocale
util
The setLocale
util is located under e2e-tests/cypress/integration/helpers/utils.js
.
The function is used to ensure the locale is set to the same intended value when the tests are running locally and remotely.
Be sure to import the utility functions at the beginning of the test.
setLocale
Please note - this is a Chrome specific hack from cypress-io/cypress/issues/7890.
The util will not work when running the tests via Electron.
Use case example:
import {setLocale} from '../helpers/utils';
it('shows correct locale', () => {
setLocale('en-GB');
// your test...
})