From d98db6415be876ed828107d1106cbdd6299e00f9 Mon Sep 17 00:00:00 2001 From: soupette Date: Thu, 22 Nov 2018 19:19:04 +0100 Subject: [PATCH] Add cypress in monorepo and split by package --- .gitignore | 2 + cypress.json | 11 + cypress/fixtures/api/category.json | 23 + cypress/fixtures/api/product.json | 59 +++ cypress/fixtures/api/tag.json | 14 + cypress/fixtures/seeds/category.json | 17 + cypress/fixtures/seeds/product.json | 18 + cypress/fixtures/seeds/tag.json | 14 + cypress/plugins/index.js | 17 + cypress/support/commands.js | 184 ++++++++ cypress/support/index.js | 33 ++ package.json | 4 +- .../cypress/integration/createPage_specs.js | 443 ++++++++++++++++++ .../cypress/integration/init_specs.js | 104 ++++ .../cypress/integration/listPage_specs.js | 174 +++++++ .../cypress/integration/createApi_specs.js | 235 ++++++++++ .../cypress/integration/login_specs.js | 49 ++ .../cypress/integration/register_specs.js | 56 +++ scripts/lint.js | 1 + test/start.js | 13 +- 20 files changed, 1468 insertions(+), 3 deletions(-) create mode 100644 cypress.json create mode 100644 cypress/fixtures/api/category.json create mode 100644 cypress/fixtures/api/product.json create mode 100644 cypress/fixtures/api/tag.json create mode 100644 cypress/fixtures/seeds/category.json create mode 100644 cypress/fixtures/seeds/product.json create mode 100644 cypress/fixtures/seeds/tag.json create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js create mode 100644 packages/strapi-plugin-content-manager/cypress/integration/createPage_specs.js create mode 100644 packages/strapi-plugin-content-manager/cypress/integration/init_specs.js create mode 100644 packages/strapi-plugin-content-manager/cypress/integration/listPage_specs.js create mode 100644 packages/strapi-plugin-content-type-builder/cypress/integration/createApi_specs.js create mode 100644 packages/strapi-plugin-users-permissions/cypress/integration/login_specs.js create mode 100644 packages/strapi-plugin-users-permissions/cypress/integration/register_specs.js diff --git a/.gitignore b/.gitignore index d5dbc29340..d210ab5aae 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,8 @@ package-lock.json testApp coverage +cypress/screenshots +cypress/videos ############################ diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000000..df33e38129 --- /dev/null +++ b/cypress.json @@ -0,0 +1,11 @@ +{ + "chromeWebSecurity": false, + "backendUrl": "http://localhost:1337", + "baseUrl": "http://localhost:1337", + "frontLoadingDelay": 3000, + "animDelay": 1000, + "serverRestartDelay": 11000, + "viewportHeight": 900, + "viewportWidth": 1440, + "integrationFolder": "./packages" +} diff --git a/cypress/fixtures/api/category.json b/cypress/fixtures/api/category.json new file mode 100644 index 0000000000..8b9fb1b6e0 --- /dev/null +++ b/cypress/fixtures/api/category.json @@ -0,0 +1,23 @@ +{ + "name": "category", + "description": "", + "connection": "default", + "attributes": [ + { + "name": "name", + "params": { + "required": true, + "unique": true, + "type": "string" + } + }, + { + "name": "products", + "params": { + "key": "category", + "nature": "oneToMany", + "target": "product" + } + } + ] +} \ No newline at end of file diff --git a/cypress/fixtures/api/product.json b/cypress/fixtures/api/product.json new file mode 100644 index 0000000000..f18b5cb2ba --- /dev/null +++ b/cypress/fixtures/api/product.json @@ -0,0 +1,59 @@ +{ + "name": "product", + "description": "", + "connection": "default", + "attributes": [ + { + "name": "name", + "params": { + "required": true, + "type": "string" + } + }, + { + "name": "description", + "params": { + "type": "text", + "appearance": { + "WYSIWYG": false + } + } + }, + { + "name": "price", + "params": { + "type": "integer", + "default": 0 + } + }, + { + "name": "bool", + "params": { + "type": "boolean", + "default": false + } + }, + { + "name": "bool1", + "params": { + "type": "boolean", + "default": true + } + }, + { + "name": "email", + "params": { + "type": "email" + } + }, + { + "name": "tags", + "params": { + "dominant": true, + "key": "products", + "nature": "manyToMany", + "target": "tag" + } + } + ] +} \ No newline at end of file diff --git a/cypress/fixtures/api/tag.json b/cypress/fixtures/api/tag.json new file mode 100644 index 0000000000..75d470ae28 --- /dev/null +++ b/cypress/fixtures/api/tag.json @@ -0,0 +1,14 @@ +{ + "name": "tag", + "description": "", + "connection": "default", + "attributes": [ + { + "name": "name", + "params": { + "required": true, + "type": "string" + } + } + ] +} \ No newline at end of file diff --git a/cypress/fixtures/seeds/category.json b/cypress/fixtures/seeds/category.json new file mode 100644 index 0000000000..e618bfe278 --- /dev/null +++ b/cypress/fixtures/seeds/category.json @@ -0,0 +1,17 @@ +[ + { + "name": "cat1" + }, + { + "name": "cat2" + }, + { + "name": "cat3" + }, + { + "name": "drinks" + }, + { + "name": "french food" + } +] \ No newline at end of file diff --git a/cypress/fixtures/seeds/product.json b/cypress/fixtures/seeds/product.json new file mode 100644 index 0000000000..46b5347075 --- /dev/null +++ b/cypress/fixtures/seeds/product.json @@ -0,0 +1,18 @@ +[ + { + "name": "name", + "description": "This is a super description", + "price": 1337, + "bool": true, + "bool1": false, + "email": "hi@strapi.io" + }, + { + "name": "name1", + "description": "This description is not cool", + "price": 4000, + "bool": false, + "bool1": true, + "email": "yo@strapi.io" + } +] \ No newline at end of file diff --git a/cypress/fixtures/seeds/tag.json b/cypress/fixtures/seeds/tag.json new file mode 100644 index 0000000000..66c6c1c1a8 --- /dev/null +++ b/cypress/fixtures/seeds/tag.json @@ -0,0 +1,14 @@ +[ + { + "name": "tag1" + }, + { + "name": "tag2" + }, + { + "name": "tag3" + }, + { + "name": "special tag" + } +] \ No newline at end of file diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000000..fd170fba69 --- /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 0000000000..2d66871cbe --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,184 @@ +// *********************************************** +// 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) => { ... }) +const stringify = JSON.stringify; +const backendUrl = Cypress.config('backendUrl'); +const serverRestartDelay = Cypress.config('serverRestartDelay'); + +Cypress.Commands.add('createUser', () => { + const user = { + username: 'soup', + email: 'hi@strapi.io', + password: 'coucou123', + }; + + return cy.request({ url: `${backendUrl}/users-permissions/init`, method: 'GET' }) + .then(response => { + const { body: { hasAdmin } } = response; + + if (!hasAdmin) { + // Create one + cy.request({ url: `${backendUrl}/auth/local/register`, method: 'POST', body: user }); + } + }); +}); + +Cypress.Commands.add('checkModalOpening', () => { + return cy.get('.modal').invoke('show'); +}) + +Cypress.Commands.add('deleteUser', (id, jwt) => { + cy.request({ + url: `${backendUrl}/users/${id}`, + method: 'DELETE', + headers: { + Authorization: `Bearer ${jwt}` + } + }); +}); + +Cypress.Commands.add('createProductAndTagApis', (jwt = null) => { + return cy + .fixture('api/tag.json') + .then(body => { + return cy.request({ + url: `${backendUrl}/content-type-builder/models`, + method: 'POST', + headers: { + Authorization: `Bearer ${jwt}` + }, + body, + }) + .wait(serverRestartDelay) + .fixture('api/product.json') + .then(body => { + return cy.request({ + url: `${backendUrl}/content-type-builder/models`, + method: 'POST', + headers: { + Authorization: `Bearer ${jwt}` + }, + body, + }) + .wait(serverRestartDelay); + }); + }); +}); + +Cypress.Commands.add('createCTMApis', (jwt = null) => { + return cy + .createProductAndTagApis(jwt) + .wait(serverRestartDelay) + .fixture('api/category.json') + .then(body => { + return cy + .request({ + url: `${backendUrl}/content-type-builder/models`, + method: 'POST', + headers: { + Authorization: `Bearer ${jwt}`, + }, + body, + }) + .wait(serverRestartDelay); + }); +}); + +Cypress.Commands.add('deleteAllModelData', (model, jwt, source = null) => { + // GET all data; + cy.request({ + url: `${backendUrl}/content-manager/explorer/${model}`, + method: 'GET', + headers: { + Authorization: `Bearer ${jwt}`, + }, + }) + .then(data => { + const entriesToDelete = data.body.reduce((acc, curr) => { + return acc.concat(curr.id); + }, []); + + const qs = Object.assign(entriesToDelete, source ? { source } : {}); + + return cy.request({ + url: `${backendUrl}/content-manager/explorer/deleteAll/${model}`, + method: 'DELETE', + headers: { + Authorization: `Bearer ${jwt}`, + }, + qs, + }); + }); +}); + +Cypress.Commands.add('deleteApi', (model, jwt) => { + return cy.request({ + url: `${backendUrl}/content-type-builder/models/${model}`, + method: 'DELETE', + headers: { + Authorization: `Bearer ${jwt}` + }, + }) + .wait(serverRestartDelay); +}); + +Cypress.Commands.add('login', () => { + cy.createUser() + return cy.request({ + url: `${backendUrl}/auth/local`, + method: 'POST', + body: { + identifier: 'soup', + password: 'coucou123', + }, + }) + .then(response => { + window.localStorage.setItem('jwtToken', stringify(response.body.jwt)); + window.localStorage.setItem('userInfo', stringify(response.body.user)); + + return response.body; + }) +}); + +Cypress.Commands.add('seedData', (model, jwt, source = null) => { + return cy + .fixture(`seeds/${model}.json`) + .then(seed => { + seed.forEach(body => { + cy.request({ + method: 'POST', + url: `${backendUrl}/content-manager/explorer/${model}?source='content-manager`, + headers: { + Authorization: `Bearer ${jwt}`, + }, + body, + }); + }); + }); +}); + +Cypress.Commands.add('submitForm', () => { + return cy.get('form').submit(); +}); \ No newline at end of file diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000000..de90bec1e8 --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,33 @@ +// *********************************************************** +// 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') + +// Temporary workaround for fetch see: https://github.com/cypress-io/cypress/issues/95 +Cypress.on('window:before:load', win => { + win.fetch = null; +}); + +Cypress.on('before:browser:launch', (browser = {}, args) => { + if (browser.name === 'chrome') { + args.push('--disable-site-isolation-trials'); + + return args + } +}) \ No newline at end of file diff --git a/package.json b/package.json index 10b573923e..4ce2f22a90 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "private": true, "version": "3.0.0-alpha.14.5", - "dependencies": {}, + "dependencies": { + "cypress": "^3.1.2" + }, "devDependencies": { "assert": "~1.3.0", "axios": "^0.18.0", diff --git a/packages/strapi-plugin-content-manager/cypress/integration/createPage_specs.js b/packages/strapi-plugin-content-manager/cypress/integration/createPage_specs.js new file mode 100644 index 0000000000..0a757df1ef --- /dev/null +++ b/packages/strapi-plugin-content-manager/cypress/integration/createPage_specs.js @@ -0,0 +1,443 @@ +let jwt; +let userId; +const animDelay = Cypress.config('animDelay'); +const frontEndUrl = Cypress.config('baseUrl'); +const frontLoadingDelay = Cypress.config('frontLoadingDelay'); +const backendUrl = Cypress.config('backendUrl'); +const pluginUrl = `${frontEndUrl}/admin/plugins/content-manager`; +const getCreateRedirectUrl = (model, sort = '_id') => { + return `${frontEndUrl}/admin/plugins/content-manager/${model}/create?redirectUrl=/plugins/content-manager/${model}?_limit=10&_page=1&_sort=${sort}&source=content-manager`; +}; +const getRequest = (model, sort = '_id') => { + return `${backendUrl}/content-manager/explorer/${model}?_limit=10&_start=0&_sort=${sort}:ASC&source=content-manager`; +}; + +describe('Testing Content Manager createPages', function() { + before(() => { + cy.login() + .then(data => { + jwt = data.jwt; + + return cy + .createCTMApis(data.jwt) + .then(() => jwt); + }) + .wait(1000); + + Cypress.Commands.add('ctmTagLink', () => { + return cy.get('a[href="/admin/plugins/content-manager/tag?source=content-manager"]'); + }); + Cypress.Commands.add('ctmProductLink', () => { + return cy.get('a[href="/admin/plugins/content-manager/product?source=content-manager"]'); + }); + Cypress.Commands.add('ctmCategoryLink', () => { + return cy.get('a[href="/admin/plugins/content-manager/category?source=content-manager"]'); + }); + Cypress.Commands.add('ctmAddButton', () => { + return cy.get('button#addEntry'); + }); + Cypress.Commands.add('inputError', (name) => { + return cy.get(`#errorOf${name} > span`); + }); + Cypress.Commands.add('getListTagsOrderedByName', () => { + return cy.ctmTagLink() + .click() + .get('tr > th:nth-child(3) > span') + .click(); + }); + Cypress.Commands.add('fillProductForm', (product) => { + Object.keys(product) + .forEach(key => { + if (key === 'description') { + cy.get(`textarea[name="${key}"]`) + .type(product[key]); + } else { + cy.get(`input[name="${key}"]`) + .type(product[key]); + } + }) + }); + Cypress.Commands.add('getProduct', (index) => { + return cy + .ctmProductLink() + .click() + .wait(1000) + .get(`tbody > tr:nth-child(${index})`) + .click() + .wait(1000) + .window() + .its('__store__') + .its('content-manager') + }); + }); + + after(() => { + cy.deleteApi('tag', jwt) + .deleteApi('category', jwt) + .deleteApi('product', jwt) + .wait(11000); + }); + + context('Creating data with no relation', () => { + beforeEach(() => { + cy.server(); + cy.route(`${backendUrl}/content-manager/models`).as('initContentManager'); + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + }) + .visit('/admin') + .wait(frontLoadingDelay) + .wait('@initContentManager'); + }); + + after(() => { + cy.deleteAllModelData('tag', jwt) + .deleteAllModelData('category', jwt) + .deleteAllModelData('product', jwt); + }); + + it('Should create a tag with no relation', () => { + cy.server(); + cy.route(getRequest('tag')).as('getTags'); + cy.ctmTagLink() + .click() + .ctmAddButton() + .click(); + const tagsToCreate = ['tag1', 'tag2', 'tag3', 'superTag', 'badTag', 'I\'m running out of idea tag']; + // Check redirect url + cy.url() + .should('equal', getCreateRedirectUrl('tag')); + + // Try to save empty data + cy.submitForm() + .get('input#name') + .invoke('attr', 'class') + .should('include', 'form-control is-invalid'); + + tagsToCreate.forEach((tagName, index) => { + cy.get('input#name') + .type(tagName) + .submitForm() + .wait('@getTags') + .get('tbody') + .children() + .should('have.length', index + 1); + + if (index < tagsToCreate.length -1) { + cy.ctmAddButton() + .click(); + } + }); + }); + + it('Should create a category with no relation', () => { + cy.server(); + cy.route(getRequest('category', 'name')).as('getCategories'); + cy.ctmCategoryLink() + .click() + .get('tr > th:nth-child(3) > span') + .click() + .ctmAddButton() + .click(); + const catsToCreate = ['drinks', 'food', 'junk food', 'french food', 'good french food', 'greasy', 'you don\'t want to eat that']; + // Check redirect url + cy.url() + .should('equal', getCreateRedirectUrl('category', 'name')); + + catsToCreate.forEach((catName, index) => { + cy.get('input#name') + .type(catName) + .submitForm() + .wait('@getCategories') + .get('tbody') + .children() + .should('have.length', index + 1); + + if (index < catsToCreate.length -1) { + cy.ctmAddButton() + .click(); + } + }); + }); + + it('Should display an error for unique fields for categories', () => { + cy.ctmCategoryLink() + .click() + .ctmAddButton() + .click() + .get('input#name') + .type('drinks') + .submitForm() + .get('input#name') + .invoke('attr', 'class') + .should('includes', 'form-control is-invalid') + .get('input#name') + .inputError('name') + .should('have.text', 'This name is already taken '); + }); + + it('Should delete all data using the UI', () => { + cy.server(); + cy.route(getRequest('tag')).as('getTags'); + cy.route(getRequest('category', 'name')).as('getCategories'); + + cy.ctmTagLink() + .click() + .wait('@getTags') + .wait(1000) + .get('thead > tr > th:first-child') + .click() + .get('span#deleteAllData') + .click() + .get('button#ctaConfirm') + .click() + .wait(2000) + .window() + .its('__store__') + .its('content-manager') + .then(pluginStore => { + const records = pluginStore + .getState() + .getIn(['listPage', 'records', 'tag']) + .toJS(); + + expect(records).to.have.length(0); + }); + }); + }); + + context('Creating and updating data with relation', () => { + before(() => { + cy.server(); + cy.route(`${backendUrl}/content-manager/models`).as('initContentManager'); + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + + return data.jwt; + }) + .then(jwt => { + return cy.seedData('tag', jwt) + .then(() => jwt); + + }) + .then(jwt => { + return cy.seedData('category', jwt); + }); + }); + + beforeEach(() => { + cy.server(); + cy.route(`${backendUrl}/content-manager/models`).as('initContentManager'); + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + + return data.jwt; + }) + .visit('/admin') + .wait(frontLoadingDelay) + .wait('@initContentManager'); + }) + + it('Should create a product and link several tags and 1 category', () => { + cy.server(); + cy.route(`${backendUrl}/content-manager/explorer/tag?_limit=10&_start=0&_sort=name:ASC&source=content-manager`).as('getTags'); + cy.ctmProductLink() + .click() + .ctmAddButton() + .click(); + + // Test default value + cy.get('button#__OFF__bool') + .invoke('attr', 'class') + .should('includes', 'gradientOff') + .get('button#__ON__bool1') + .invoke('attr', 'class') + .should('includes', 'gradientOn'); + + // Create a product + const product = { + name: 'product1', + description: 'This is a super description', + price: 1337, + email: 'hi@strapi.io', + }; + + Object.keys(product) + .forEach(key => { + if (key === 'description') { + cy.get(`textarea[name="${key}"]`) + .type(product[key]); + } else { + cy.get(`input[name="${key}"]`) + .type(product[key]); + } + }); + + cy.get('button#__ON__bool') + .click() + .get('button#__OFF__bool1') + .click(); + + cy.get('input#tags') + .type('special t', { force: true }) + .type('{enter}', { force: true }) + .type('ta', { force: true }) + .type('{enter}', { force: true }) + .get('ul#sortableListOftags') + .children('li') + .should((children) => { + expect(children[0].innerText.trim()).to.equal('special tag'); + expect(children[1].innerText.trim()).to.equal('tag1'); + }) + .get('input#category') + .type('french food', { force: true }) + .type('{enter}') + .invoke('attr', 'value') + .should('equal', 'french food') + .submitForm(); + + cy.getListTagsOrderedByName() + .wait('@getTags') + .wait(1000) + .get('tbody > tr:first-child') + .click() + .get('ul#sortableListOfproducts') + .children() + .should((children) => { + expect(children).to.have.length(1); + expect(children[0].innerText.trim()).to.equal('product1'); + }); + + cy.getListTagsOrderedByName() + .wait('@getTags') + .wait(2000) + .get('tbody > tr:nth-child(2)') + .click() + .get('ul#sortableListOfproducts') + .children() + .should((children) => { + expect(children).to.have.length(1); + expect(children[0].innerText.trim()).to.equal('product1'); + }); + }); + + it('Should delete a product in tag1', () => { + cy.getListTagsOrderedByName() + .wait(frontLoadingDelay) + .get('tbody > tr:nth-child(2)') + .click() + .wait(1000) + .get('ul#sortableListOfproducts > li:nth-child(1) > div:nth-child(2)') + .click() + .submitForm() + .ctmProductLink() + .click() + .wait(1000) + .get('tbody > tr:nth-child(1)') + .click() + .wait(frontLoadingDelay) + .get('ul#sortableListOftags') + .children() + .should((children) => { + expect(children).to.have.length(1); + expect(children[0].innerText.trim()).to.equal('special tag'); + }); + }); + + it('Should add several products to category french food', () => { + cy.server(); + cy.route(`${backendUrl}/content-manager/explorer/category?_limit=10&_start=0&_sort=_id:ASC&source=content-manager`).as('getCategories'); + cy.route(`${backendUrl}/content-manager/explorer/product?_limit=10&_start=0&_sort=_id:ASC&source=content-manager`).as('getProducts'); + const product = { + name: 'MacBook', + description: 'A laptop', + price: 2000, + email: 'john@strapi.io', + }; + const product2 = { + name: 'Dell', + description: 'Not a mac', + price: 4, + email: 'bob@strapi.io', + }; + + cy.ctmProductLink() + .click() + .ctmAddButton() + .click(); + + cy.fillProductForm(product) + .submitForm() + .ctmAddButton() + .click() + .fillProductForm(product2) + .submitForm(); + + cy.ctmCategoryLink() + .click() + .wait('@getCategories') + .wait(1000) + .get('tbody > tr:nth-child(5)') + .click() + .get('ul#sortableListOfproducts').as('relations') + .children() + .should(children => { + expect(children).to.have.length(1); + expect(children[0].innerText.trim()).to.equal('product1'); + }) + .get('ul#sortableListOfproducts > li:nth-child(1) > div:nth-child(2)') + .click() + .get('input#products') + .type('mac', { force: true }) + .type('{enter}', { force: true }) + .type('dell', { force: true }) + .type('{enter}', { force: true }) + .get('@relations') + .children() + .should(children => { + expect(children).to.have.length(2); + expect(children[0].innerText.trim()).to.equal('MacBook'); + expect(children[1].innerText.trim()).to.equal('Dell'); + }) + .submitForm(); + + cy.getProduct(1) + .then(pluginStore => { + const category = pluginStore + .getState() + .getIn(['editPage', 'record', 'category']) + + expect(category).to.equal(null); + }); + + cy.getProduct(2) + .then(pluginStore => { + const category = pluginStore + .getState() + .getIn(['editPage', 'record', 'category', 'name']) + + expect(category).to.equal('french food'); + }) + .getProduct(3) + .then(pluginStore => { + const category = pluginStore + .getState() + .getIn(['editPage', 'record', 'category', 'name']) + + expect(category).to.equal('french food'); + }); + }); + + after(() => { + cy.deleteAllModelData('tag', jwt) + .deleteAllModelData('category', jwt) + .deleteAllModelData('product', jwt); + }); + }); +}); \ No newline at end of file diff --git a/packages/strapi-plugin-content-manager/cypress/integration/init_specs.js b/packages/strapi-plugin-content-manager/cypress/integration/init_specs.js new file mode 100644 index 0000000000..2ac0b9c8da --- /dev/null +++ b/packages/strapi-plugin-content-manager/cypress/integration/init_specs.js @@ -0,0 +1,104 @@ +let jwt; +let userId; +const animDelay = Cypress.config('animDelay'); +const backendUrl = Cypress.config('backendUrl'); +const frontEndUrl = Cypress.config('baseUrl'); +const frontLoadingDelay = Cypress.config('frontLoadingDelay'); +const links = { + Category: '/admin/plugins/content-manager/category?source=content-manager', + Product: '/admin/plugins/content-manager/product?source=content-manager', + settings: '/admin/plugins/content-manager/ctm-configurations', + Tag: '/admin/plugins/content-manager/tag?source=content-manager', + User: '/admin/plugins/content-manager/user?source=users-permissions', +}; +const pluginUrl = `${frontEndUrl}/admin/plugins/content-manager`; + +describe('Testing build and schema core_store', () => { + before(() => { + cy.login() + .then(data => { + jwt = data.jwt; + return cy.createCTMApis(data.jwt); + }) + .wait(1000); + }); + + after(() => { + cy.deleteApi('tag', jwt) + .deleteApi('category', jwt) + .deleteApi('product', jwt); + }); + + context('Testing views', () => { + beforeEach(() => { + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + }) + .visit('/admin') + .wait(frontLoadingDelay); + }); + + it('Should visit all list pages without any errors', () => { + cy.server(); + cy.route(`${backendUrl}/content-manager/models`).as('initCTM'); + cy.get(`a[href="${links.settings}"]`) + .click() + .wait('@initCTM'); + + // Check all list views are rendered without any error + for (let i = 0; i < 4; i++) { + Object.keys(links).forEach(link => { + const name = link === 'settings' ? 'Content Manager' : link; + + cy.get(`a[href="${links[link]}"]`) + .click() + .get('h1') + .should('have', name); + }); + } + + }); + + it('Should visit all views once without any errors', () => { + cy.server(); + cy.route(`${backendUrl}/content-manager/models`).as('initCTM'); + cy.get(`a[href="${links.settings}"]`) + .click() + .wait('@initCTM'); + + // Testing errors related to reactstrap + cy.get('#cancelChanges') + .click() + .wait(animDelay) + .checkModalOpening() + .should('be.visible') + .type('{esc}'); + + + // Test setting view + Object.keys(links).forEach(link => { + if (link !== 'settings') { + cy.get(`#${link.toLowerCase()}`) + .click() + .get('h1') + .should('have', `Content Manager - ${link}`) + .get(`a[href="${links.settings}"]`) + .click(); + } + }); + + Object.keys(links).forEach(link => { + if (link !== 'settings') { + cy.get(`a[href="${links[link]}"]`) + .click() + .get('button#addEntry') + .click() + .get('h1') + .should('have', 'New Entry'); + } + }); + }); + }); +}); diff --git a/packages/strapi-plugin-content-manager/cypress/integration/listPage_specs.js b/packages/strapi-plugin-content-manager/cypress/integration/listPage_specs.js new file mode 100644 index 0000000000..cad573a2d3 --- /dev/null +++ b/packages/strapi-plugin-content-manager/cypress/integration/listPage_specs.js @@ -0,0 +1,174 @@ +let jwt; +let userId; +const animDelay = Cypress.config('animDelay'); +const frontEndUrl = Cypress.config('baseUrl'); +const frontLoadingDelay = Cypress.config('frontLoadingDelay'); +const backendUrl = Cypress.config('backendUrl'); +const pluginUrl = `${frontEndUrl}/admin/plugins/content-manager`; + +describe('Testing Content Manager ListPages', function() { + before(() => { + cy.login() + .then(data => { + jwt = data.jwt; + + return cy + .createCTMApis(data.jwt) + .then(() => jwt); + }) + .then(jwt => { + cy.seedData('product', jwt); + }) + .wait(1000); + }); + + after(() => { + cy.deleteAllModelData('product', jwt); + cy.deleteApi('tag', jwt) + .deleteApi('category', jwt) + .deleteApi('product', jwt); + }); + + context('Testing sorting options', () => { + beforeEach(() => { + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + }) + .visit('/admin') + .wait(frontLoadingDelay); + }); + + it('Should have the Id default sort', () => { + cy.get(`a[href="/admin/plugins/content-manager/product?source=content-manager"]`) + .click() + .wait(frontLoadingDelay); + + cy.get('tr > th:nth-child(2) > span') + .children('i') + .should('be.visible') + .invoke('attr', 'class') + .should('includes', 'fa-sort-asc'); + }); + + it('Should change the default sort of product to name ASC then name DESC', () => { + cy.server(); + cy.route(`${backendUrl}/content-manager/explorer/product?_limit=10&_start=0&_sort=_id:ASC&source=content-manager`).as('getProduct'); + cy.route(`${backendUrl}/content-manager/explorer/product?_limit=10&_start=0&_sort=name:ASC&source=content-manager`).as('getSortByNameASC'); + cy.route(`${backendUrl}/content-manager/explorer/product?_limit=10&_start=0&_sort=name:DESC&source=content-manager`).as('getSortByNameDESC'); + + cy.get('a[href="/admin/plugins/content-manager/product?source=content-manager"]') + .click() + .wait('@getProduct') + .get('tr > th:nth-child(3) > span').as('getName') + .click(); + + cy.wait('@getSortByNameASC') + .get('@getName') + .children('i') + .should('be.visible') + .invoke('attr', 'class') + .should('includes', 'iconAsc') + .get('tbody > tr:nth-child(1) > td:nth-child(3)').as('firstResult') + .should('have.text', 'name'); + + + cy.get('@getName') + .click() + .wait('@getSortByNameDESC') + .get('@getName') + .children('i') + .should('be.visible') + .invoke('attr', 'class') + .should('includes', 'iconDesc') + .get('@firstResult') + .should('have.text', 'name1'); + }); + + it('Should set the product default sort to name', () => { + cy.get('a[href="/admin/plugins/content-manager/ctm-configurations"]') + .click() + .get('#product') + .click() + .get('select[name="product\.defaultSort"]').as('defaultSort') + .select('name') + .should('have.value', 'name') + .get('select[name="product\.sort"]').as('sortOption') + .select('DESC') + .should('have.value', 'DESC') + .submitForm() + .get('#ctaConfirm') + .click() + .wait(frontLoadingDelay) + .get('a[href="/admin/plugins/content-manager/product?source=content-manager"]') + .click() + .wait(frontLoadingDelay) + .get('tr > th:nth-child(3) > span').as('getName') + .children('i') + .invoke('attr', 'class') + .should('includes', 'iconDesc') + .get('tbody > tr:nth-child(1) > td:nth-child(3)') + .should('have.text', 'name1'); + + // Set it back to normal + cy.get('a[href="/admin/plugins/content-manager/ctm-configurations"]') + .click() + .get('#product') + .click() + .get('@defaultSort') + .select('_id') + .should('have.value', '_id') + .get('@sortOption') + .select('ASC') + .should('have.value', 'ASC') + .submitForm() + .get('#ctaConfirm') + .click() + .wait(frontLoadingDelay) + .get('a[href="/admin/plugins/content-manager/product?source=content-manager"]') + .click() + .wait(frontLoadingDelay) + .get('tr > th:nth-child(2) > span') + .children('i') + .invoke('attr', 'class') + .should('includes', 'iconAsc'); + }); + }); + + context('Testing filters', () => { + beforeEach(() => { + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + }) + .visit('/admin') + .wait(frontLoadingDelay); + }); + + it('Should apply filters for product data', () => { + cy.get(`a[href="/admin/plugins/content-manager/product?source=content-manager"]`) + .click() + .wait(frontLoadingDelay); + cy.get('button#addFilterCTA').as('toggleFilter') + .click() + .wait(animDelay) + .get('div#filterPickWrapper').as('filterWrapper') + .children('div') + .should('have.length', 1); + + cy.get('input[name="0\.value"]') + .type('name') + .get('button#newFilter') + .click() + .get('select[name="1\.attr"]') + .select('bool') + .get('button[label="content-manager.components.FiltersPickWrapper.PluginHeader.actions.apply"]') + .click() + .wait(animDelay) + .get('tbody > tr') + .should('have.length', 1); + }); + }); +}); diff --git a/packages/strapi-plugin-content-type-builder/cypress/integration/createApi_specs.js b/packages/strapi-plugin-content-type-builder/cypress/integration/createApi_specs.js new file mode 100644 index 0000000000..ed03dfab4f --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/cypress/integration/createApi_specs.js @@ -0,0 +1,235 @@ +// import 'whatwg-fetch'; + +let jwt; +let userId; +const animDelay = Cypress.config('animDelay'); +const frontEndUrl = Cypress.config('baseUrl'); +const frontLoadingDelay = Cypress.config('frontLoadingDelay'); +const backendUrl = Cypress.config('backendUrl'); +const pluginUrl = `${frontEndUrl}/admin/plugins/content-type-builder`; +const TAG_API = { + name: 'tag', + description: 'This is a super tag \nwith multi \nlines description.', +}; + +describe('Test CTB', () => { + context('Check create and update API', () => { + beforeEach(() => { + cy.server(); + cy.route(`${backendUrl}/content-type-builder/autoReload`).as('initContentTypeBuilder'); + cy.login() + .then(data => { + jwt = data.jwt; + userId = data.user._id || data.user.id; + }); + cy.visit('/admin'); + cy.wait(frontLoadingDelay); + cy.wait('@initContentTypeBuilder'); + }); + + it('Should visit the content type builder', () => { + cy.get('a[href="/admin/plugins/content-type-builder"') + .click(); + cy.url() + .should('equal', pluginUrl); + }); + + it('Should prevent the user from creating a camelCase api', () => { + cy.server(); + cy.route('GET', `${backendUrl}/content-type-builder/models`).as('models'); + + cy.get('a[href="/admin/plugins/content-type-builder"') + .click() + .wait('@models') + .get('#openAddCT') + .click() + .get('#name') + .type('camelCase') + .get('#description') + .type('\n') + .get('#name') + .should('have.value', 'camelcase') + .get('#name') + .type('{selectall}') + .type('not camel-case') + .get('#description') + .type('{backspace}') + .get('#name') + .should('have.value', 'notcamelcase'); + }); + + it('Should create a TAG API', function() { + cy.server(); + cy.route('GET', `${backendUrl}/content-type-builder/models`).as('models'); + cy.route('POST', `${backendUrl}/content-type-builder/models`).as('createModel'); + cy.route('DELETE', `${backendUrl}/content-type-builder/models/tag`).as('deleteTag'); + + cy.get('a[href="/admin/plugins/content-type-builder"') + .click() + .wait('@models'); + + // Open modal + cy.get('#openAddCT') + .click() + .wait(animDelay); + + // Check the modal is opened this will tell is if we have a build issue + cy.checkModalOpening(); + cy.get('.modal') + .invoke('show'); + + // Fill the form + Object.keys(TAG_API) + .map(key => { + cy.log(key); + cy.get(`#${key}`) + .type(TAG_API[key]); + }); + + // Submit the form and navigate to product page + cy.submitForm() + .url() + .should('equal', `${pluginUrl}/models/tag`); + + // Open the attributes's modal + cy.get('#openAddAttr') + .click() + .wait(animDelay); + + // Check that we don't have a build error from reacstrap + cy.checkModalOpening() + .should('be.visible'); + + // Ensure the modal is opened to get #attrCardstring + cy.wait(1000) + .get('button#attrCardstring') + .click() + .get('input[name="name"]') + .type('name') + .get('#continue') + .click(); + + cy.get('button#saveData') + .should('contain', 'Save') + .click() + .wait('@createModel') + .wait(frontLoadingDelay); + + cy.get('#attributesList li') + .first() + .should('contain', 'name'); + + // Delete tag API + cy.get('a[href="/admin/plugins/content-type-builder"]') + .click() + .wait(frontLoadingDelay) + .wait(frontLoadingDelay) + .get('#deletetag') + .click() + .checkModalOpening() + .should('be.visible') + .get('#ctaConfirm') + .click() + .wait('@deleteTag') + .wait(frontLoadingDelay) + .get('#ctbModelsList li') + .should('have.length', 4); + }); + + it('Should update PRODUCT API field and visit the create product page', () => { + cy.server(); + cy.createProductAndTagApis(jwt); + cy.route(`${backendUrl}/content-type-builder/models/product?`).as('getProductModel'); + cy.route('PUT', `${backendUrl}/content-type-builder/models/product`).as('updateProductModel'); + + cy.visit('/admin/plugins/content-type-builder/models/product#editproduct::attributestring::baseSettings::0'); + cy.wait('@getProductModel'); + cy.wait(frontLoadingDelay); + + // Open the modal via url + cy.checkModalOpening() + .should('be.visible') + .get('input[name="name"]') + .type('{selectall}') + .type('updatedName') + .get('#continue') + .click(); + + cy.get('#attributesList li') + .first() + .should('have.text', 'updatedNameString'); + + cy.get('button#saveData') + .click() + .wait('@updateProductModel') + .wait(frontLoadingDelay); + + // Check that we can still go to the create page + cy.get('a[href="/admin/plugins/content-manager/product?source=content-manager"') + .click() + .get('button[label="content-manager.containers.List.addAnEntry"') + .click(); + + cy.window() + .its('__store__') + .its('content-manager') + .then(pluginStore => { + const displayedFields = pluginStore + .getState() + .getIn(['global', 'schema', 'models', 'product', 'editDisplay', 'fields']) + .toJS(); + + expect(displayedFields).to.include.members(['description', 'price', 'updatedName', 'bool', 'bool1', 'email']); + }); + }); + + it('Should update PRODUCT API name and visit the create product page', () => { + cy.server(); + // cy.createProductAndTagApis(jwt); + cy.route(`${backendUrl}/content-type-builder/models/product?`).as('getProductModel'); + cy.route('PUT', `${backendUrl}/content-type-builder/models/product`).as('updateProductModel'); + + cy.visit('/admin/plugins/content-type-builder/models/product#editproduct::contentType::baseSettings'); + cy.wait('@getProductModel'); + cy.wait(frontLoadingDelay); + + // Open the modal via url + cy.checkModalOpening() + .should('be.visible') + .get('input[name="name"]') + .type('{selectall}') + .type('produit') + .submitForm() + .wait('@updateProductModel') + .wait(frontLoadingDelay); + + // Check that we can still go to the create page + cy.get('a[href="/admin/plugins/content-manager/produit?source=content-manager"') + .click() + .wait(frontLoadingDelay) + .get('button[label="content-manager.containers.List.addAnEntry"') + .click() + .get('h1') + .should('have', 'New Entry'); + + // cy.window() + // .its('__store__') + // .its('content-manager') + // .then(pluginStore => { + // const displayedFields = pluginStore + // .getState() + // .getIn(['global', 'schema', 'models', 'product', 'editDisplay', 'fields']) + // .toJS(); + + // expect(displayedFields).to.include.members(['description', 'price', 'updatedName', 'bool', 'bool1', 'email']); + // }); + }); + }); + + after(() => { + cy.wait(10000) + .deleteApi('tag', jwt) + .deleteApi('produit', jwt) + .deleteUser(userId, jwt); + }); +}); \ No newline at end of file diff --git a/packages/strapi-plugin-users-permissions/cypress/integration/login_specs.js b/packages/strapi-plugin-users-permissions/cypress/integration/login_specs.js new file mode 100644 index 0000000000..1fbe0e39b9 --- /dev/null +++ b/packages/strapi-plugin-users-permissions/cypress/integration/login_specs.js @@ -0,0 +1,49 @@ +const frontLoadingDelay = Cypress.config('frontLoadingDelay'); +const userData = { + identifier: 'soup', + password: 'coucou123', +}; + +describe('Test login', () => { + let userId; + let jwt; + + // Create a user if there's none + before(() => { + cy.createUser(); + }); + + // Delete the user to test other features + after(() => { + if (userId) { + cy.deleteUser(userId, jwt); + } + }); + + + it('Should login the user', () => { + cy.visit('/admin/users-permissions/auth/login') + .wait(frontLoadingDelay); + + Object + .keys(userData) + .map(key => { + return cy + .get(`#${key}`) + .type(userData[key]); + }); + + cy.submitForm() + .window() + .should(win => { + const userInfo = JSON.parse(win.localStorage.getItem('userInfo')); + + jwt = JSON.parse(win.localStorage.getItem('jwtToken')); + userId = userInfo._id || userInfo.id; + expect(win.localStorage.getItem('jwtToken')).to.be.ok; + }); + + cy.url() + .should('equal', `${Cypress.config('baseUrl')}/admin/`); + }); +}); \ No newline at end of file diff --git a/packages/strapi-plugin-users-permissions/cypress/integration/register_specs.js b/packages/strapi-plugin-users-permissions/cypress/integration/register_specs.js new file mode 100644 index 0000000000..969381af9b --- /dev/null +++ b/packages/strapi-plugin-users-permissions/cypress/integration/register_specs.js @@ -0,0 +1,56 @@ +const frontLoadingDelay = Cypress.config('frontLoadingDelay'); +const registerData = { + username: 'soup', + email: 'hi@strapi.io', + password: 'coucou123', + confirmPassword: 'coucou123', +}; +let jwt; +let userId; +const frontEndUrl = Cypress.config('baseUrl'); + +describe('Test register page', () => { + after(() => { + if (userId) { + cy.deleteUser(userId, jwt); + } + }) + + it('Visits /admin and should be redirected to register page', () => { + cy.visit('/admin') + .wait(frontLoadingDelay); + + // Check if the user is being redirected to /register + cy.url() + .should('include', '/users-permissions/auth/register'); + }); + + it('Should redirect to /register when trying to hit /login', () => { + cy.visit('/admin/plugins/users-permissions/auth/login') + .wait(frontLoadingDelay); + + cy.url() + .should('include', '/users-permissions/auth/register'); + }); + + it('Should register the admin user', () => { + Object.keys(registerData).map(key => { + return cy + .get(`#${key}`) + .type(registerData[key]); + }); + + // Submit form + cy.submitForm() + .window() + .should(win => { + const userInfo = JSON.parse(win.sessionStorage.getItem('userInfo')); + + jwt = JSON.parse(win.sessionStorage.getItem('jwtToken')); + userId = userInfo._id || userInfo.id; + expect(win.sessionStorage.getItem('jwtToken')).to.be.ok; + }); + cy.url() + .should('equal', `${frontEndUrl}/admin/`); + }); +}); \ No newline at end of file diff --git a/scripts/lint.js b/scripts/lint.js index 899eb73be2..d8ea9df882 100644 --- a/scripts/lint.js +++ b/scripts/lint.js @@ -38,6 +38,7 @@ const except = [ 'strapi-middleware-views', 'strapi-plugin-settings-manager', 'test', + 'cypress', ]; const changedDirs = [...changedFiles] diff --git a/test/start.js b/test/start.js index ac907c823c..ef1acc65e9 100644 --- a/test/start.js +++ b/test/start.js @@ -2,6 +2,7 @@ const spawn = require('child_process').spawn; const fs = require('fs'); const path = require('path'); const shell = require('shelljs'); +const cypress = require('cypress') const { deleteApp } = require('./helpers/deleteFolder'); const strapiBin = path.resolve('./packages/strapi/bin/strapi.js'); @@ -65,6 +66,7 @@ const main = async () => { }); } catch (e) { + console.log(e) if (typeof appStart !== 'undefined') { process.kill(appStart.pid); } @@ -73,6 +75,7 @@ const main = async () => { }); }; + const test = () => { return new Promise(async (resolve) => { // Run setup tests to generate the app. @@ -98,12 +101,18 @@ const main = async () => { }); }; + const cypressTest = () => { + return cypress + .run({ spec: './packages/**/cypress/integration/*' }); + } + const testProcess = async (database) => { try { await clean(); await generate(database); await start(); - await test(); + await cypressTest(); + // await test(); process.kill(appStart.pid); } catch (e) { console.error(e.message); @@ -114,7 +123,7 @@ const main = async () => { await testProcess(databases.mongo); // await testProcess(databases.postgres); // await testProcess(databases.mysql); - process.exit(testExitCode); + // process.exit(testExitCode); }; main();