@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