From d1b1d507bcd616af116ccf34bb553aa409db2e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Mon, 11 Dec 2017 16:30:46 +0100 Subject: [PATCH] test(e2e): replace integrated tests with standalone repository --- .gitignore | 3 + circle.yml | 6 +- package.json | 4 +- test/e2e/run-e2e-test.js | 37 ---- test/e2e/specs/.eslintrc.js | 11 -- test/e2e/specs/the-example-app-spec.js | 253 ------------------------- test/run-e2e-test.js | 44 +++++ 7 files changed, 51 insertions(+), 307 deletions(-) delete mode 100644 test/e2e/run-e2e-test.js delete mode 100644 test/e2e/specs/.eslintrc.js delete mode 100644 test/e2e/specs/the-example-app-spec.js create mode 100644 test/run-e2e-test.js diff --git a/.gitignore b/.gitignore index ef0f335..b29653e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ package-lock.json # cypress test result files cypress + +# e2e test directory +test/e2e diff --git a/circle.yml b/circle.yml index e7c1136..7da0572 100644 --- a/circle.yml +++ b/circle.yml @@ -3,10 +3,8 @@ machine: version: "8" dependencies: pre: - - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - - sudo dpkg -i google-chrome.deb - - sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome - - rm google-chrome.deb + - git clone https://github.com/contentful/the-example-app-e2e-tests.git ./test/e2e + - cd ./test/e2e && npm install test: post: - tar -czf $CIRCLE_ARTIFACTS/cypress-result_`date +%Y-%m-%d_%H-%M-%S`.tar.gz ./cypress diff --git a/package.json b/package.json index f3970f8..4efd13e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "format": "eslint --fix . bin --ignore public node_modules", "pretest": "npm run lint", "test": "npm run test:unit && npm run test:integration && npm run test:e2e", - "test:e2e": "node test/e2e/run-e2e-test.js", + "test:e2e": "node test/run-e2e-test.js", "test:integration": "jest test/integration", "test:integration:watch": "jest test/integration --watch", "test:unit": "jest test/unit", @@ -27,6 +27,7 @@ "contentful": "^5.0.2", "cookie-parser": "~1.4.3", "dotenv": "^4.0.0", + "execa": "^0.8.0", "express": "~4.14.0", "helmet": "^3.9.0", "loadash": "^1.0.0", @@ -37,7 +38,6 @@ "devDependencies": { "cheerio": "^1.0.0-rc.2", "cookie": "^0.3.1", - "cypress": "^1.0.3", "eslint": "^3.16.0", "eslint-config-standard": "^6.2.1", "eslint-plugin-promise": "^3.4.2", diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js deleted file mode 100644 index 6873122..0000000 --- a/test/e2e/run-e2e-test.js +++ /dev/null @@ -1,37 +0,0 @@ -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 { CONTENTFUL_SPACE_ID, CONTENTFUL_DELIVERY_TOKEN, CONTENTFUL_PREVIEW_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: { - CONTENTFUL_SPACE_ID, CONTENTFUL_DELIVERY_TOKEN, CONTENTFUL_PREVIEW_TOKEN - } - }) - .then((result) => { - server.close() - if (result.failures > 0) { - process.exit(1) - return - } - process.exit(0) - }).catch(() => { - server.close() - process.exit(1) - }) -}) diff --git a/test/e2e/specs/.eslintrc.js b/test/e2e/specs/.eslintrc.js deleted file mode 100644 index d104be1..0000000 --- a/test/e2e/specs/.eslintrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - 'env': { - 'node': true, - 'mocha': true - }, - 'globals': { - 'Cypress': true, - 'cy': true, - 'expect': true - } -} diff --git a/test/e2e/specs/the-example-app-spec.js b/test/e2e/specs/the-example-app-spec.js deleted file mode 100644 index ec548db..0000000 --- a/test/e2e/specs/the-example-app-spec.js +++ /dev/null @@ -1,253 +0,0 @@ -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", a reference for building your own applications using Contentful.') - - cy.get('meta[name="twitter:card"]').should('attr', 'content', 'This is "The Example App", a reference for 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", a reference for 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', 'Help') - .should('contain', 'GitHub') - - cy.get('.main__footer .footer__lower') - .should('contain', 'Powered by Contentful') - .should('contain', 'GitHub') - .should('contain', 'Imprint') - .should('contain', 'Contact us') - }) - - it('about modal', () => { - cy.get('section.modal .modal__wrapper').should('hidden') - cy.get('.header__upper-title a').click() - cy.get('section.modal .modal__wrapper').should('visible') - cy.get('section.modal .modal__title').should('contain', 'A reference for developers using Contentful') - cy.get('section.modal .modal__content').should('contain', 'This is "The Example App" in node.js. While building your own apps with 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-title a').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-title a').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-highlighted-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', 2, 'renders at least 2 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-id').should('have.value', Cypress.env('CONTENTFUL_SPACE_ID')) - cy.get('input#input-delivery-token').should('have.value', Cypress.env('CONTENTFUL_DELIVERY_TOKEN')) - cy.get('input#input-preview-token').should('have.value', Cypress.env('CONTENTFUL_PREVIEW_TOKEN')) - }) - - it('checks for required fields', () => { - cy.get('input#input-space-id').clear() - cy.get('input#input-delivery-token').clear() - cy.get('input#input-preview-token').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-id').parent().children('.form-item__error-wrapper') - .should('exist') - .find('.form-item__error-message') - .should('have.text', 'This field is required') - cy.get('input#input-delivery-token').parent().children('.form-item__error-wrapper') - .should('exist') - .find('.form-item__error-message') - .should('have.text', 'This field is required') - cy.get('input#input-preview-token').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-id').clear().type(Math.random().toString(36).substring(12)) - cy.get('input#input-delivery-token').clear().type(Math.random().toString(36)) - cy.get('input#input-preview-token').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-delivery-token').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-id').clear().type(Cypress.env('CONTENTFUL_SPACE_ID')) - cy.get('input#input-delivery-token').clear().type(Cypress.env('CONTENTFUL_DELIVERY_TOKEN')) - cy.get('input#input-preview-token').clear().type(Cypress.env('CONTENTFUL_PREVIEW_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') - }) - }) -}) diff --git a/test/run-e2e-test.js b/test/run-e2e-test.js new file mode 100644 index 0000000..afe798a --- /dev/null +++ b/test/run-e2e-test.js @@ -0,0 +1,44 @@ +const http = require('http') +const { resolve } = require('path') +const execa = require('execa') + +require('dotenv').config({ path: 'variables.env' }) + +const app = require('../app') + +const TEST_PORT = 3007 + +app.set('port', TEST_PORT) + +const server = http.createServer(app) + +const { CONTENTFUL_SPACE_ID, CONTENTFUL_DELIVERY_TOKEN, CONTENTFUL_PREVIEW_TOKEN } = process.env + +server.on('error', console.error) +server.listen(TEST_PORT, function () { + const cypressBin = resolve(__dirname, 'e2e', 'node_modules', '.bin', 'cypress') + const spec = resolve(__dirname, 'e2e', 'specs', 'the-example-app-spec.js') + execa(cypressBin, [ + 'run', + '--spec', + spec, + !process.env.CI ? '--headed' : null, + '--env', + `CONTENTFUL_SPACE_ID=${CONTENTFUL_SPACE_ID},CONTENTFUL_DELIVERY_TOKEN=${CONTENTFUL_DELIVERY_TOKEN},CONTENTFUL_PREVIEW_TOKEN=${CONTENTFUL_PREVIEW_TOKEN}` + ].filter(Boolean)) + .then((result) => { + console.log('✔ e2e test succeeded:') + console.log(result.cmd) + console.log(result.stdout) + server.close() + process.exit(0) + }) + .catch((error) => { + console.log(`✖ e2e test failed with status code ${error.code}:`) + console.error(error.cmd) + console.error(error.stdout) + console.error(error.stderr) + server.close() + process.exit(1) + }) +})