Skip to main content

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');
Not sure what 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'
);
Still can't find the element you're looking for?

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 ids 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

Avoid hardcoded response ids

When 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.

Timezone

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.

Using 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...
})

Further resources