test(e2e): add end to end tests via cypress.io

This commit is contained in:
Benedikt Rötsch
2017-10-30 15:54:38 +01:00
committed by Benedikt Rötsch
parent 85ea880352
commit bd9e29b6eb
6 changed files with 309 additions and 1 deletions

3
.gitignore vendored
View File

@@ -59,3 +59,6 @@ typings/
# lock files # lock files
package-lock.json package-lock.json
# cypress test result files
cypress

6
cypress.json Normal file
View File

@@ -0,0 +1,6 @@
{
"baseUrl": "http://localhost:3007",
"fixturesFolder": false,
"integrationFolder": "./test/e2e/specs",
"supportFile": false
}

View File

@@ -9,7 +9,9 @@
"start:production": "NODE_ENV=production node ./bin/www", "start:production": "NODE_ENV=production node ./bin/www",
"lint": "eslint ./app.js routes", "lint": "eslint ./app.js routes",
"format": "eslint --fix . bin --ignore public node_modules", "format": "eslint --fix . bin --ignore public node_modules",
"test": "echo 'test'", "pretest": "npm run lint",
"test": "npm run test:e2e",
"test:e2e": "node test/e2e/run-e2e-test.js",
"test:integration": "jest test/integration", "test:integration": "jest test/integration",
"test:integration:watch": "jest test/integration --watch", "test:integration:watch": "jest test/integration --watch",
"test:unit": "jest test/unit", "test:unit": "jest test/unit",
@@ -32,6 +34,7 @@
"devDependencies": { "devDependencies": {
"cheerio": "^1.0.0-rc.2", "cheerio": "^1.0.0-rc.2",
"cookie": "^0.3.1", "cookie": "^0.3.1",
"cypress": "^1.0.3",
"eslint": "^3.16.0", "eslint": "^3.16.0",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^6.2.1",
"eslint-plugin-promise": "^3.4.2", "eslint-plugin-promise": "^3.4.2",

33
test/e2e/run-e2e-test.js Normal file
View File

@@ -0,0 +1,33 @@
const http = require('http')
const { resolve } = require('path')
require('dotenv').config({ path: 'variables.env' })
const cypress = require('cypress')
const app = require('../../app')
const TEST_PORT = 3007
app.set('port', TEST_PORT)
const server = http.createServer(app)
const { CF_SPACE, CF_ACCESS_TOKEN, CF_PREVIEW_ACCESS_TOKEN } = process.env
server.on('error', console.error)
server.listen(TEST_PORT, function () {
cypress.run({
spec: resolve(__dirname, 'specs', 'the-example-app-spec.js'),
headed: !process.env.CI,
env: {
CF_SPACE, CF_ACCESS_TOKEN, CF_PREVIEW_ACCESS_TOKEN
}
})
.then(() => {
server.close()
process.exit(0)
}).catch(() => {
server.close()
process.exit(1)
})
})

View File

@@ -0,0 +1,11 @@
module.exports = {
'env': {
'node': true,
'mocha': true
},
'globals': {
'Cypress': true,
'cy': true,
'expect': true
}
}

View File

@@ -0,0 +1,252 @@
describe('The Example App', () => {
context('basics', () => {
beforeEach(() => {
cy.visit('/')
})
it('meta tags', () => {
cy.title().should('equals', 'Home — The Example App', 'Home page should have correct meta title')
cy.get('meta[name="description"]').should('attr', 'content', 'This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.')
cy.get('meta[name="twitter:card"]').should('attr', 'value', 'This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.')
cy.get('meta[property="og:title"]').should('attr', 'content', 'Home — The Example App')
cy.get('meta[property="og:type"]').should('attr', 'content', 'article')
cy.get('meta[property="og:url"]').should('exist')
cy.get('meta[property="og:image"]').should('exist')
cy.get('meta[property="og:image:type"]').should('attr', 'content', 'image/jpeg')
cy.get('meta[property="og:image:width"]').should('attr', 'content', '1200')
cy.get('meta[property="og:image:height"]').should('attr', 'content', '1200')
cy.get('meta[property="og:description"]').should('attr', 'content', 'This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.')
cy.get('link[rel="apple-touch-icon"]')
.should('attr', 'sizes', '120x120')
.should('attr', 'href', '/apple-touch-icon.png')
cy.get('link[rel="icon"]').should('have.length.gte', 2, 'containts at least 2 favicons')
cy.get('link[rel="manifest"]').should('attr', 'href', '/manifest.json')
cy.get('link[rel="mask-icon"]')
.should('attr', 'href', '/safari-pinned-tab.svg')
.should('attr', 'color', '#4a90e2')
cy.get('meta[name="theme-color"]').should('attr', 'content', '#ffffff')
})
it('global elements', () => {
cy.get('.header__upper')
.should('contain', 'What is this example app?')
.should('contain', 'View on Github')
cy.get('.main__footer .footer__lower')
.should('contain', 'Powered by Contentful')
.should('contain', 'View on Github')
.should('contain', 'Imprint')
})
it('about modal', () => {
cy.get('section.modal .modal__wrapper').should('hidden')
cy.get('.header__upper .header__title').click()
cy.get('section.modal .modal__wrapper').should('visible')
cy.get('section.modal .modal__title').should('contain', 'A referenceable example for developers using Contentful')
cy.get('section.modal .modal__content').should('contain', 'This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.')
// Close on background
cy.get('section.modal .modal__overlay').click({force: true})
cy.get('section.modal .modal__wrapper').should('hidden')
cy.get('.header__upper .header__title').click()
cy.get('section.modal .modal__wrapper').should('visible')
// Close on X
cy.get('section.modal .modal__close-button').click()
cy.get('section.modal .modal__wrapper').should('hidden')
cy.get('.header__upper .header__title').click()
cy.get('section.modal .modal__wrapper').should('visible')
// Close on "Got this" button
cy.get('section.modal .modal__cta').click()
cy.get('section.modal .modal__wrapper').should('hidden')
})
it('header dropdowns show and hide', () => {
cy.get('.header__controls > *:first-child .header__controls_dropdown').should('have.css', 'opacity').and('be', 0)
cy.get('.header__controls > *:first-child .header__controls_label').click()
cy.get('.header__controls > *:first-child .header__controls_dropdown').should('have.css', 'opacity').and('be', 1)
// Should hide dropdown after a while
cy.wait(300)
cy.get('.header__controls > *:first-child .header__controls_dropdown').should('have.css', 'opacity').and('be', 0)
cy.get('.header__controls > *:last-child .header__controls_dropdown').should('have.css', 'opacity').and('be', 0)
cy.get('.header__controls > *:last-child .header__controls_label').click()
cy.get('.header__controls > *:last-child .header__controls_dropdown').should('have.css', 'opacity').and('be', 1)
// Should hide dropdown after a while
cy.wait(300)
cy.get('.header__controls > *:last-child .header__controls_dropdown').should('have.css', 'opacity').and('be', 0)
})
it('header dropdowns change app context', () => {
cy.get('.header__controls > *:first-child .header__controls_label').click()
cy.get('.header__controls > *:first-child .header__controls_dropdown button:last-child').click()
cy.location('search').should('contain', 'api=cpa')
cy.get('.header__controls > *:last-child .header__controls_label').click()
cy.get('.header__controls > *:last-child .header__controls_dropdown button:last-child').click()
cy.location('search')
.should('contain', 'locale=de')
.should('contain', 'api=cpa')
})
})
context('Home', () => {
it('renders home page', () => {
cy.visit('/')
cy.get('main .module-higlighted-course').should('have.length.gte', 1, 'should have at least one highlighted course')
})
})
context('Courses', () => {
afterEach(() => {
cy.title().should('match', / — The Example App$/, 'Title has contextual suffix (appname)')
})
it('renders course overview', () => {
cy.visit('/courses')
cy.get('.course-card').should('have.length.gte', 3, 'renders at least 3 courses')
cy.get('.layout-sidebar__sidebar-header > h2').should('contain', 'Categories', 'Shows category title in sidebar')
cy.get('.sidebar-menu__list > .sidebar-menu__item:first-child').should('contain', 'All courses', 'Shows all courses link')
cy.get('.sidebar-menu__list > .sidebar-menu__item').should('have.length.gte', 2, 'renders at least one category selector')
cy.get('.sidebar-menu__list > .sidebar-menu__item:first-child > a').should('have.class', 'active', 'All courses is selected by default')
})
it('can filter course overview', () => {
cy.visit('/courses')
cy.get('.sidebar-menu__list > .sidebar-menu__item:nth-child(2) > a').click()
cy.get('.sidebar-menu__list > .sidebar-menu__item:nth-child(1) > a').should('not.have.class', 'active', 'All courses link is no more active')
cy.get('.sidebar-menu__list > .sidebar-menu__item:nth-child(2) > a').should('have.class', 'active', 'First category filter link should be active')
cy.get('main h1').invoke('text').then((text) => console.log('headline content:', text))
cy.get('.sidebar-menu__list > .sidebar-menu__item:nth-child(2) > a').invoke('text').then((firstCategoryTitle) => {
cy.get('main h1').invoke('text').then((headline) => {
expect(headline).to.match(new RegExp(`^${firstCategoryTitle} \\([0-9]+\\)$`), 'Title now contains the category name with number of courses')
})
})
cy.get('.course-card').should('have.length.gte', 1, 'filtered courses contain at least one course')
})
it('tracks the watched state of lessons', () => {
// Home
cy.visit('/')
// Navigate to courses
cy.get('header.header .main-navigation ul > li:last-child a').click()
// Click title of course card to open it
cy.get('.course-card .course-card__title a').first().click()
// Check that overview link is visited and active
cy.get('.table-of-contents .table-of-contents__list .table-of-contents__item:nth-child(1) a')
.should('have.class', 'active')
.should('have.class', 'visited')
// Check that lesson link is neither visited nor active
cy.get('.table-of-contents .table-of-contents__list .table-of-contents__item:nth-child(2) a')
.should('not.have.class', 'active')
.should('not.have.class', 'visited')
// Start first lesson
cy.get('.course__overview a.course__overview-cta').click()
// Check that overview link is visited but not active
cy.get('.table-of-contents .table-of-contents__list .table-of-contents__item:nth-child(1) a')
.should('not.have.class', 'active')
.should('have.class', 'visited')
// Check that lesson link is visited and active
cy.get('.table-of-contents .table-of-contents__list .table-of-contents__item:nth-child(2) a')
.should('have.class', 'active')
.should('have.class', 'visited')
})
})
context('Settings', () => {
beforeEach(() => {
cy.visit('/settings')
})
it('renders setting with default values', () => {
cy.title().should('equals', 'Settings — The Example App')
cy.get('main h1').should('have.text', 'Settings')
cy.get('.status-block')
.should('have.length', 1)
.invoke('text').then((text) => {
expect(text).to.match(/Connected to space “.+”/)
})
cy.get('input#input-space').should('have.value', Cypress.env('CF_SPACE'))
cy.get('input#input-cda').should('have.value', Cypress.env('CF_ACCESS_TOKEN'))
cy.get('input#input-cpa').should('have.value', Cypress.env('CF_PREVIEW_ACCESS_TOKEN'))
})
it('checks for required fields', () => {
cy.get('input#input-space').clear()
cy.get('input#input-cda').clear()
cy.get('input#input-cpa').clear()
cy.get('input[type=submit]').click()
cy.get('.status-block--info').should('not.exist')
cy.get('.status-block--success').should('not.exist')
cy.get('.status-block--error').should('exist')
cy.get('input#input-space').parent().children('.form-item__error-wrapper')
.should('exist')
.find('.form-item__error-message')
.should('have.text', 'This field is required')
cy.get('input#input-cda').parent().children('.form-item__error-wrapper')
.should('exist')
.find('.form-item__error-message')
.should('have.text', 'This field is required')
cy.get('input#input-cpa').parent().children('.form-item__error-wrapper')
.should('exist')
.find('.form-item__error-message')
.should('have.text', 'This field is required')
})
it('validates field with actual client', () => {
cy.get('input#input-space').clear().type(Math.random().toString(36).substring(12))
cy.get('input#input-cda').clear().type(Math.random().toString(36))
cy.get('input#input-cpa').clear().type(Math.random().toString(36))
cy.get('input[type=submit]').click()
cy.get('.status-block--info').should('not.exist')
cy.get('.status-block--success').should('not.exist')
cy.get('.status-block--error').should('exist')
cy.get('input#input-cda').parent().children('.form-item__error-wrapper')
.should('exist')
.find('.form-item__error-message')
.should('have.text', 'Your Delivery API key is invalid.')
})
it('shows success message when valid credentials are supplied', () => {
cy.get('input#input-space').clear().type(Cypress.env('CF_SPACE'))
cy.get('input#input-cda').clear().type(Cypress.env('CF_ACCESS_TOKEN'))
cy.get('input#input-cpa').clear().type(Cypress.env('CF_PREVIEW_ACCESS_TOKEN'))
cy.get('input[type=submit]').click()
cy.get('.status-block--info').should('exist')
cy.get('.status-block--success').should('exist')
cy.get('.status-block--error').should('not.exist')
cy.get('.form-item__error-wrapper').should('not.exist')
})
it('enables editorial features and displays them on home', () => {
cy.get('input#input-editorial-features').check()
cy.get('input[type=submit]').click()
cy.get('.status-block--info').should('exist')
cy.get('.status-block--success').should('exist')
cy.get('.status-block--error').should('not.exist')
cy.get('input#input-editorial-features').should('checked')
cy.visit('/')
cy.get('.editorial-features').should('exist')
})
})
})