Max Schmitt

July 1 2019

Testing mobile, tablet and desktop devices with Cypress

Update (December 12th 2019): There is now a --config-file parameter for the Cypress CLI that could offer a simpler solution than using Cypress.run. Check out the Cypress CLI docs on this.


At Wunderflats, we recently expanded our testing setup for our landlord-dashboard to enable us to run our Cypress integration tests not only on desktop, but also on mobile and tablet.

We didn't want to write separate tests for our mobile, tablet and desktop versions of the app. Because they share many components and business logic, we wanted to just adapt our existing desktop-tests so they can run against mobile and tablet devices as well.

So we needed to find a way to run Cypress multiple times – once for each device type. Setting this up didn't take long and to our pleasant surprise most tests also needed no adjustments at all or only very small ones.

In this article I will show you how we got this to work.

The problem: Changing viewport size is not enough

Cypress makes it easy to test responsive apps by allowing you to configure the viewport size with cy.viewport.

But for our use-case this wasn't enough because while our app does have responsive elements that change depending on screen width, it also serves fundamentally different versions of the app layout (depending on if the device is mobile or tablet or desktop) by checking the user agent string on the server.

And while it is possible to configure the user agent in Cypress, it's not possible to do that after Cypress has started. So we need a way to configure Cypress' user agent before we launch Cypress.

Our solution: Launching Cypress via module API

We created a simple CLI tool as a thin wrapper around Cypress using the Cypress module API. The module API lets you launch Cypress programmatically from any node.js script.

We came up with the following command line interface for our little Cypress wrapper:

$ node integration-tests.js
--mobile Run in mobile mode
--tablet Run in tablet mode
--open Open Cypress

When you look at the script under the hood, you can see that it's not doing much more than parsing a few command line arguments. Check it out:

integration-tests.js

// Require Cypress
const cypress = require('cypress')
// Get command line arguments
const argv = process.argv.slice(2)
// Determine the device type
const device = argv.includes('--mobile') ? 'mobile' : argv.includes('--tablet') ? 'tablet' : 'desktop'
const cypressOptions = {
// Expose the device type as Cypress environment variables
env: {
isMobile: device === 'mobile',
isTablet: device === 'tablet',
isDesktop: device === 'desktop',
},
config: {
baseUrl: `http://localhost:8080`,
// Mobile: emulate iPhone 5
...(device === 'mobile' && {
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1',
viewportWidth: 320,
viewportHeight: 568,
}),
// Tablet: emulate iPad in landscape mode
...(device === 'tablet' && {
userAgent:
'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1',
viewportWidth: 1024,
viewportHeight: 768,
}),
// Desktop: use default browser user agent
...(device === 'desktop' && {
viewportWidth: 1440,
viewportHeight: 900,
}),
},
}
function runCypress() {
// Use --open to open the Cypress UI instead of running
// the tests in headless mode from the command line
if (argv.includes('--open')) {
return cypress.open(cypressOptions)
}
return cypress.run(cypressOptions)
}
runCypress()
.then((results) => {
if (results.totalFailed > 0 || results.failures > 0) {
// Make sure to exit with an error code if tests failed
process.exit(1)
}
})
.catch((err) => {
console.error(err.stack || err)
process.exit(1)
})

Now in our testing code we are able to easily check which device is being emulated with Cypress.env:

cypress/integration/homepage.spec.js

describe('Homepage', function () {
it('shows authenticated user', function () {
if (Cypress.env('isMobile')) {
cy.get('.hamburger-menu-button').click()
cy.get('.mobile-menu').should('contain', 'Logged in as Max')
} else {
cy.get('.app-header').should('contain', 'Logged in as Max')
}
})
})

After getting this running, we edited our setup for Bitbucket Pipelines and now we're running our test suite for desktop, mobile and tablet-devices in parallel. All this took about one full day of work (for about 120 tests) and in the process we discovered quite a few bugs that had gone unnoticed previously.

For more information, have a look at the official Cypress example for using the Cypress module API. Also check out the Cypress module API documentation if you want to see what else is possible.

Thanks for reading. I hope this post was helpful to you. Feel free to reach out to me on Twitter if you have any questions!