Cypress.io
When testing is easy, developers build better things faster and with confidence.
@henrictrotzig
@bahmutov
@bahmutov
@henrictrotzig
also see https://on.cypress.io/visual-testing
What's the difference between these images?Β
.general-item-container {
margin-left: 5px;
}
button {
border-radius: var(--button-broder-radius);
}
Run test suite to generate a set of screenshots
Compare those screenshots with a known base
Review diffs manually, approve or reject β
Merge PR/change with the confidence of a Swedish person buying furniture at IKEA πΈπͺ π π
producing consistent images and storing them is difficult π
review workflow is difficult π
describe('apple-music-js app', () => {
// a tour of app's features without any visual tests
it('works', () => {
cy.visit('/');
cy.get('[data-test="welcome-closing"]')
.should('be.visible');
cy.get('[data-test="welcome-closing"]')
.should('not.be.visible');
cy.log('**picking an album**')
cy.contains('Artists').click();
cy.contains('Coldplay').click({ force: true });
cy.contains('A Head Full of Dreams').click();
cy.contains('Hymn for the Weekend').click();
cy.log('**mini player controls**')
cy.get('[data-test="mini-pause"]').click();
cy.get('[data-test="mini-controls"]').click();
cy.log('**large player controls**')
cy.get('[data-test=play]').click()
cy.get('[data-test=pause]').should('be.visible')
cy.get('[data-test=pause]').click()
cy.get('[data-test="close-controls"]').click()
});
});
describe('apple-music-js app', () => {
// a tour of app's features without any visual tests
it('works', () => {
cy.visit('/');
cy.get('[data-test="welcome-closing"]')
.should('be.visible');
cy.get('[data-test="welcome-closing"]')
.should('not.be.visible');
cy.log('**picking an album**')
cy.contains('Artists').click();
cy.contains('Coldplay').click({ force: true });
cy.contains('A Head Full of Dreams').click();
cy.contains('Hymn for the Weekend').click();
cy.log('**mini player controls**')
cy.get('[data-test="mini-pause"]').click();
cy.get('[data-test="mini-controls"]').click();
cy.log('**large player controls**')
cy.get('[data-test=play]').click()
cy.get('[data-test=pause]').should('be.visible')
cy.get('[data-test=pause]').click()
cy.get('[data-test="close-controls"]').click()
});
});
# .circleci/config.yml
version: 2.1
orbs:
cypress: cypress-io/cypress@1
workflows:
build:
jobs:
- cypress/run:
start: npm run start
npm install --save-dev happo.io happo-cypress
// At the top of cypress/support/commands.js
import 'happo-cypress';
// In cypress/plugins/index.js
const happoTask = require('happo-cypress/task');
module.exports = (on, config) => {
on('task', happoTask);
};
// https://docs.happo.io/docs/cypress
const { RemoteBrowserTarget } = require('happo.io')
module.exports = {
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
targets: {
chrome: new RemoteBrowserTarget('chrome', {
viewport: '1024x768',
}),
},
}
firefox, chrome
internet explorer (version 11),
edge, safari, ios-safari (runs on iPhone 7)
module.exports = {
targets: {
// The first part ('chrome-mobile' in this case) is just a name we give
// the specific browser target. You'll see this name in the reports generated
// as part of a happo run.
'chrome-mobile': new RemoteBrowserTarget('chrome', {
viewport: '320x640',
}),
'chrome-medium': new RemoteBrowserTarget('chrome', {
viewport: '800x600',
}),
'firefox-desktop': new RemoteBrowserTarget('firefox', {
viewport: '1024x768',
}),
'internet-explorer': new RemoteBrowserTarget('internet explorer', {
viewport: '800x600',
}),
'ios-safari': new RemoteBrowserTarget('ios-safari', {
viewport: '375x667',
}),
},
};
{
"baseUrl": "http://localhost:3000",
"viewportWidth": 400,
"viewportHeight": 700
}
cypress.json
// https://docs.happo.io/docs/cypress
const { RemoteBrowserTarget } = require('happo.io')
// use the same resolution as cypress.json
const cypressConfig = require('./cypress.json')
const width = cypressConfig.viewportWidth || 1000
const height = cypressConfig.viewportHeight || 660
const viewport = `${width}x${height}`
module.exports = {
apiKey: process.env.HAPPO_API_KEY,
apiSecret: process.env.HAPPO_API_SECRET,
targets: {
chrome: new RemoteBrowserTarget('chrome', {
viewport,
}),
},
}
.happo.js
describe('city page', () => {
it('is functional', () => {
cy.visit('/stockholm');
cy.get('nav').happoScreenshot({ component: 'Navbar' });
cy.get('nav [data-test="dropdown-button"]').click();
cy.get('nav').happoScreenshot({
component: 'Navbar',
variant: 'Dropdown open',
});
cy.get('[data-test="subscribe-form"]').happoScreenshot({
component: 'Subscribe form',
});
});
});
describe('city page', () => {
it('is functional', () => {
cy.visit('/stockholm');
cy.get('nav').happoScreenshot({ component: 'Navbar' });
cy.get('nav [data-test="dropdown-button"]').click(); // action
cy.get('nav').happoScreenshot({
component: 'Navbar',
variant: 'Dropdown open',
});
cy.get('[data-test="subscribe-form"]').happoScreenshot({
component: 'Subscribe form',
});
});
});
Has the app updated in response to click?
describe('city page', () => {
it('is functional', () => {
cy.visit('/stockholm');
cy.get('nav').happoScreenshot({ component: 'Navbar' });
cy.get('nav [data-test="dropdown-button"]').click(); // action
cy.get('nav [data-test="dropdown-open"]').should('be.visible');
cy.get('nav').happoScreenshot({
component: 'Navbar',
variant: 'Dropdown open',
});
cy.get('[data-test="subscribe-form"]').happoScreenshot({
component: 'Subscribe form',
});
});
});
it('renders a chart with deployment marker, timeseries data', () => {
const options = { timeout: CHART_LOADING_TIMEOUT };
cy.getByTestId('latency-chart')
.assertChartHasDeployMarker({
options,
})
.getByTestId('latency-chart')
.happoScreenshot();
cy.getByTestId('error-chart')
.assertChartHasDeployMarker({
options,
})
.getByTestId('error-chart')
.happoScreenshot();
cy.getByTestId('rate-chart')
.assertChartHasDeployMarker({
options,
})
.getByTestId('rate-chart')
.happoScreenshot();
});
describe('apple-music-js app', () => {
it('can be used to play a song', () => {
cy.visit('/');
cy.log('**loading screen**');
cy.get('[data-test="welcome-closing"]').should('be.visible');
cy.get('body').happoScreenshot({ component: 'Loading screen' });
cy.get('[data-test="welcome-closing"]').should('not.be.visible');
cy.get('body').happoScreenshot({ component: 'Main screen' });
cy.log('**picking an album**');
cy.contains('Artists').click();
cy.contains('Coldplay');
cy.get('body').happoScreenshot({ component: 'Artists screen' });
cy.contains('Coldplay').click({ force: true });
cy.contains('A Head Full of Dreams').click();
cy.get('[data-test="album-button"]').happoScreenshot({
component: 'Album button',
});
cy.get('[data-test="mini-controls"]').happoScreenshot({
component: 'Mini controls',
variant: 'no track selected',
});
cy.log('**picking a song**');
cy.contains('Hymn for the Weekend').click();
cy.log('**mini player controls**');
cy.get('[data-test="mini-pause"]').click();
cy.get('[data-test="mini-controls"]').happoScreenshot({
component: 'Mini controls',
variant: 'track selected',
});
cy.get('[data-test="mini-controls"]').click();
cy.log('**large player controls**');
cy.get('[data-test=play]').click();
cy.get('[data-test=pause]').should('be.visible');
cy.get('[data-test=pause]').click();
cy.get('[data-test="controls"]').happoScreenshot({
component: 'Large controls',
});
cy.get('[data-test="close-controls"]').click();
});
});
cy.get('.game__cell').each(
$cell => $cell.css('opacity', '0'))
// take snapshot of the entire board
const str = '713.94528294851637568...914871935246425186379936472185.8..4.7...57..849..4....8..'
const sudoku = getSudoku()
cy.stub(sudoku, 'generate').returns(str)
cy.stub(Math, 'random').returns(0.5)
cy.clock()
mount(<App />)
cy.get('.game__cell--filled')
.should('have.length', 45)
// take snapshot of the entire board
cy.wait(500)
// take snapshot
Time for Q & A
By Cypress.io
Happo.io is a cross-platform, cross-browser screenshot testing tool for modern user interfaces. By integrating Happo screenshots with your Cypress test suite, you can easily ensure both functional and visual quality in your applications.
When testing is easy, developers build better things faster and with confidence.