From b79c042e9e19a12af61c408507dd39ef9db34af2 Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 3 Aug 2018 19:23:43 -0400 Subject: [PATCH] chore(e2e): add cypress integration test of editorial workflow (#1573) --- .gitignore | 1 + cypress.json | 4 + cypress/fixtures/example.json | 5 + .../integration/editorial_workflow_spec.js | 231 +++++ cypress/plugins/index.js | 17 + cypress/support/commands.js | 25 + cypress/support/index.js | 20 + dev-test/index.html | 1 - package.json | 34 +- packages/netlify-cms/package.json | 2 +- scripts/cache.js | 15 + website/package.json | 2 +- yarn.lock | 800 ++++++++++++++++-- 13 files changed, 1072 insertions(+), 85 deletions(-) create mode 100644 cypress.json create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/integration/editorial_workflow_spec.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js create mode 100644 scripts/cache.js diff --git a/.gitignore b/.gitignore index d18a9647..59920ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ yarn-error.log manifest.yml .imdone/ website/data/contributors.json +cypress/videos /coverage/ .cache *.log diff --git a/cypress.json b/cypress.json new file mode 100644 index 00000000..081f6554 --- /dev/null +++ b/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:8080", + "projectId": "dzqjxb" +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 00000000..da18d935 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/editorial_workflow_spec.js b/cypress/integration/editorial_workflow_spec.js new file mode 100644 index 00000000..25237de9 --- /dev/null +++ b/cypress/integration/editorial_workflow_spec.js @@ -0,0 +1,231 @@ +describe('Editorial Workflow', () => { + const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' } + const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' } + const entry1 = { title: 'first title', body: 'first body' } + const entry2 = { title: 'second title', body: 'second body' } + const entry3 = { title: 'third title', body: 'third body' } + const notifications = { + saved: 'Entry saved', + published: 'Entry published', + updated: 'Entry status updated', + deletedUnpublished: 'Unpublished changes deleted', + } + + describe('Test Backend', () => { + function login() { + cy.viewport(1200, 1200) + cy.visit('/') + cy.contains('button', 'Login').click() + } + + function createPost({ title, body }) { + cy.contains('a', 'New Post').click() + cy.get('input').first().type(title) + cy.get('[data-slate-editor]').click().type(body) + cy.get('input').first().click() + cy.contains('button', 'Save').click() + assertNotification(notifications.saved) + } + + function exitEditor() { + cy.contains('a[href^="#/collections/"]', 'Writing in').click() + } + + function deleteEntryInEditor() { + cy.contains('button', 'Delete').click() + assertNotification(notifications.deletedUnpublished) + } + + function assertEntryDeleted(entry) { + if (Array.isArray(entry)) { + const titles = entry.map(e => e.title) + cy.get('a h2').each((el, idx) => { + expect(titles).not.to.include(el.text()) + }) + } else { + cy.get('a h2').each((el, idx) => { + expect(entry.title).not.to.equal(el.text()) + }) + } + } + + function createAndDeletePost(entry) { + createPost(entry) + deleteEntryInEditor() + } + + function createPostAndExit(entry) { + createPost(entry) + exitEditor() + } + + function createPublishedPost(entry) { + createPost(entry) + updateWorkflowStatusInEditor(editorStatus.ready) + publishInEditor() + } + + function createPublishedPostAndExit(entry) { + createPublishedPost(entry) + exitEditor() + } + + function goToWorkflow() { + cy.contains('a', 'Workflow').click() + } + + function goToCollections() { + cy.contains('a', 'Content').click() + } + + function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) { + cy.contains('h2', fromColumnHeading) + .parent() + .contains('a', title) + .trigger('dragstart', { + dataTransfer: {}, + force: true, + }) + cy.contains('h2', toColumnHeading) + .parent() + .trigger('drop', { + dataTransfer: {}, + force: true, + }) + assertNotification(notifications.updated) + } + + function assertWorkflowStatus({ title }, status) { + cy.contains('h2', status) + .parent() + .contains('a', title) + } + + function updateWorkflowStatusInEditor(newStatus) { + cy.contains('[role="button"]', 'Set status').as('setStatusButton') + cy.get('@setStatusButton').parent().within(() => { + cy.get('@setStatusButton').click() + cy.contains('[role="menuitem"] span', newStatus).click() + }) + assertNotification(notifications.updated) + } + + function assertWorkflowStatusInEditor(status) { + cy.contains('[role="button"]', 'Set status').as('setStatusButton') + cy.get('@setStatusButton').parent().within(() => { + cy.get('@setStatusButton').click() + cy.contains('[role="menuitem"] span', status).parent().within(() => { + cy.get('svg') + }) + cy.get('@setStatusButton').click() + }) + } + + function publishWorkflowEntry({ title }) { + cy.contains('h2', workflowStatus.ready).parent().within(() => { + cy.contains('a', title).parent().within(() => { + cy.contains('button', 'Publish new entry').click({ force: true }) + }) + }) + assertNotification(notifications.published) + } + + function assertPublishedEntry(entry) { + if (Array.isArray(entry)) { + const entries = entry.reverse() + cy.get('a h2').then(els => { + cy.wrap(els.slice(0, entries.length)).each((el, idx) => { + cy.wrap(el).contains(entries[idx].title) + }) + }) + } else { + cy.get('a h2').first().contains(entry.title) + } + } + + function assertNotification(message) { + if (Array.isArray(message)) { + const messages = message.reverse() + cy.get('.notif__container div') + .should('have.length.of', messages.length, ) + .each((el, idx) => { + cy.wrap(el).contains(messages[idx]).invoke('hide') + }) + } else { + cy.get('.notif__container').within(() => { + cy.contains(message).invoke('hide') + }) + } + } + + function assertOnCollectionsPage() { + cy.url().should('contain', '/#/collections/posts') + cy.contains('h1', 'Collections') + } + + it('successfully loads', () => { + login() + }) + + it('can create an entry', () => { + login() + createPostAndExit(entry1) + }) + + it('can publish an editorial workflow entry', () => { + login() + createPostAndExit(entry1) + goToWorkflow() + updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready) + publishWorkflowEntry(entry1) + }) + + it('can change workflow status', () => { + login() + createPostAndExit(entry1) + goToWorkflow() + updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review) + updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready) + updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review) + updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft) + updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready) + }) + + it('can change status on and publish multiple entries', () => { + login() + createPostAndExit(entry1) + createPostAndExit(entry2) + createPostAndExit(entry3) + goToWorkflow() + updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready) + updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready) + updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready) + publishWorkflowEntry(entry3) + publishWorkflowEntry(entry2) + publishWorkflowEntry(entry1) + goToCollections() + assertPublishedEntry([entry3, entry2, entry1]) + }) + + it('can delete an entry', () => { + login() + createPost(entry1) + deleteEntryInEditor() + assertOnCollectionsPage() + assertEntryDeleted(entry1) + }) + + it('can update workflow status from within the editor', () => { + login() + createPost(entry1) + assertWorkflowStatusInEditor(editorStatus.draft) + updateWorkflowStatusInEditor(editorStatus.review) + assertWorkflowStatusInEditor(editorStatus.review) + updateWorkflowStatusInEditor(editorStatus.ready) + assertWorkflowStatusInEditor(editorStatus.ready) + exitEditor() + goToWorkflow() + assertWorkflowStatus(entry1, workflowStatus.ready) + }) + }) +}) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 00000000..fd170fba --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000..c1f5a772 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 00000000..d68db96d --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/dev-test/index.html b/dev-test/index.html index 25d31dbc..d1fd3baf 100644 --- a/dev-test/index.html +++ b/dev-test/index.html @@ -4,7 +4,6 @@ Netlify CMS Development Test -