diff --git a/.gitignore b/.gitignore index ab7f2d288..ba193fa8d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ storybook-static/ coverage/ .vscode yarn-error.log +cypress/screenshots/ +cypress/plugins/ +cypress/fixtures/ +cypress/videos/ diff --git a/README.md b/README.md index f3d17fbef..4cd7c5801 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ docker build . --tag=nexus-web The following concern Plugins. [See how to manage plugin deployments](./docs/plugins.md) - `PLUGINS_MANIFEST_PATH`: Remote end point where plugins and manifest can be found. for example, `https://bbp-nexus.epfl.ch/plugins` -- `PLUGINS_CONFIG_PATH`: A full file path where a plugins configuration can be found. +- `PLUGINS_CONFIG_PATH`: A full file path where a plugins configuration can be found. ## Deployment @@ -126,3 +126,23 @@ There are several channels provided to address different issues: ### When building URLs inside the App - Don't add the basePath in a URL, it will be added automatically by react-router. + +### UI Testing + +UI tests are implemented with [Cypress]('https://www.cypress.io/'). + +To launch Cypress window in order to test UI locally, run: + +```sh +yarn test-ui +``` + +Make sure Nexus Web app is running locally and you use staging API, otherwise there won't be data available, and tests are likely to fail. + +To run in a headless mode, use the following command: + +```sh +yarn run cypress run --headless --browser chrome +``` + +It will run all the test scripts. diff --git a/cypress.json b/cypress.json new file mode 100644 index 000000000..9ec4dbfc0 --- /dev/null +++ b/cypress.json @@ -0,0 +1,4 @@ +{ + "viewportWidth": 1200, + "video": false +} diff --git a/cypress/integration/e2e/e2e_create_studio.spec.js b/cypress/integration/e2e/e2e_create_studio.spec.js new file mode 100644 index 000000000..76b73e1d6 --- /dev/null +++ b/cypress/integration/e2e/e2e_create_studio.spec.js @@ -0,0 +1,36 @@ +import { homePage } from '../../support'; +import { createInput, createLongerInput } from '../../support/utils'; + +describe('User is not logged in', () => { + const studioLabel = createInput(); + const description = createLongerInput(); + const workspaceLabel = createInput(); + const workspaceDescription = createLongerInput(); + const dashboardLabel = createInput(); + const dashboardDescpription = createLongerInput(); + + it('Creates Studio with Workspace and Dashboard', () => { + cy.visit(homePage); + cy.contains('Organizations'); + cy.get('.ListItem').click(); + cy.get('.ListItem').click(); + cy.contains('Studios').click(); + cy.contains('Create Studio').click(); + cy.get('.ui-studio-label-input').type(studioLabel); + cy.get('.ui-studio-description-input').type(description); + cy.get('form').submit(); + cy.wait(3000); + cy.contains(studioLabel); + cy.contains(description); + cy.contains('Add Workspace').click(); + cy.get('.ui-workspace-label-input').type(workspaceLabel); + cy.get('.ui-workspace-description-input').type(workspaceDescription); + cy.get('form').submit(); + cy.contains(workspaceLabel); + cy.contains('Add Dashboard').click(); + cy.contains('Create Dashboard'); + cy.get('.ui-dashboard-label-input').type(dashboardLabel); + cy.get('.ui-dashboard-description-input').type(dashboardDescpription); + cy.get('form').submit(); + }); +}); diff --git a/cypress/integration/elements/header.spec.js b/cypress/integration/elements/header.spec.js new file mode 100644 index 000000000..a4a2870e2 --- /dev/null +++ b/cypress/integration/elements/header.spec.js @@ -0,0 +1,35 @@ +import { homePage, docLink, reportIssueLink } from '../../support'; + +describe('Header', () => { + beforeEach(() => { + cy.visit(homePage); + }); + + it('contains the title', () => { + cy.contains('h1', 'Nexus'); + }); + + it('contains the link to the homepage', () => { + cy.contains('a', 'Nexus').should('have.attr', 'href', ''); + }); + + it('has the login link', () => { + cy.contains('a', 'login'); + }); + + it('contains the link to docs', () => { + cy.contains('a', 'Documentation').should('have.attr', 'href', docLink); + }); + + it('contains the link to open a github issue', () => { + cy.contains('a', 'Report Issue').should( + 'have.attr', + 'href', + reportIssueLink + ); + }); + + it('contains the information button', () => { + cy.get('.ui-header-info-button').should('be.visible'); + }); +}); diff --git a/cypress/integration/pages/home.spec.js b/cypress/integration/pages/home.spec.js new file mode 100644 index 000000000..c10f295c1 --- /dev/null +++ b/cypress/integration/pages/home.spec.js @@ -0,0 +1,24 @@ +import { homePage } from '../../support'; + +describe('Homepage', () => { + beforeEach(() => { + cy.visit(homePage); + }); + + it('has a header', () => { + cy.get('header.Header').should('be.visible'); + }); + + it('show a list of organisations', () => { + cy.contains('h1', 'Organizations'); + }); + + it('loads organizations', () => { + cy.server(); + cy.route('GET', '/orgs?deprecated=false&size=20'); + }); + + it('allows to search for an organization', () => { + cy.get('input').type('bbp'); + }); +}); diff --git a/cypress/integration/pages/login.spec.js b/cypress/integration/pages/login.spec.js new file mode 100644 index 000000000..5e45abe72 --- /dev/null +++ b/cypress/integration/pages/login.spec.js @@ -0,0 +1,19 @@ +import { loginPage } from '../../support'; + +describe('Login page', () => { + beforeEach(() => { + cy.visit(loginPage); + }); + + it('has a header', () => { + cy.get('header.Header').should('be.visible'); + }); + + it('contains the Nexus logo', () => { + cy.get('.logo').should('be.visible'); + }); + + it('has the login button', () => { + cy.contains('button', 'Log in'); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 000000000..ca4d256f3 --- /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 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 000000000..c3994403e --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,28 @@ +// *********************************************************** +// 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: + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import './commands'; + +export const homePage = 'http://localhost:8000'; +export const loginPage = 'http://localhost:8000/login'; + +export const docLink = 'https://bluebrainnexus.io/docs'; +export const reportIssueLink = + 'https://github.com/BlueBrain/nexus/issues/new?labels=frontend,nexus-web'; diff --git a/cypress/support/utils.js b/cypress/support/utils.js new file mode 100644 index 000000000..21adf34ce --- /dev/null +++ b/cypress/support/utils.js @@ -0,0 +1,51 @@ +const inputs = [ + 'thalamus', + 'neurotransmitters', + 'angiography', + 'cerebrospinal', + 'mapping', + 'cortex', + 'synaptic', + 'vestibular', + 'optogenetics', + 'neuron', + 'microglia', + 'insula', + 'expression', + 'phenotyping', + 'connectome', + 'sensory', + 'spinal', + 'cord', + 'data', + 'ion', + 'potential', + 'stem', + 'integrative', + 'project', + 'cognition', + 'neocortical', + 'biomedical', + 'molecular', + 'polyphenols', + 'trans-resveratrol', + 'angiographic', + 'multimerization', + 'derivatives', + 'randomized', + 'vivarium', + 'monophosphate', + 'protein', + 'measurements', + 'procedure', +]; + +const randomValue = array => array[Math.floor(Math.random() * array.length)]; + +export const createInput = () => { + return randomValue(inputs) + '-cy-test'; +}; + +export const createLongerInput = () => { + return randomValue(inputs) + ' ' + randomValue(inputs) + 'cypress test'; +}; diff --git a/package.json b/package.json index 94fb83e04..b861328cf 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "start": "NODE_ENV=development DEBUG=* webpack --mode development --config-name server && node dist/server.js", "build": "NODE_ENV=production webpack --mode production", "test": "jest", + "test-ui": "yarn run cypress open", "codecov": "codecov", "lint": "tslint --project tsconfig.json", "style": "prettier --check \"./src/**/*.{ts,tsx,js,jsx,less}\"", @@ -27,6 +28,7 @@ "codemirror": "^5.44.0", "connected-react-router": "^6.3.2", "cookie-parser": "^1.4.4", + "cypress": "^4.1.0", "cytoscape": "^3.12.0", "cytoscape-cola": "^2.3.0", "deep-object-diff": "^1.1.0", @@ -133,7 +135,10 @@ "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/__mocks__/fileMock.js", "\\.(css|less)$": "/src/__mocks__/styleMock.js" - } + }, + "testPathIgnorePatterns": [ + "/cypress/" + ] }, "prettier": { "singleQuote": true, diff --git a/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx b/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx index b80fed561..8e62ab97c 100644 --- a/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx +++ b/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx @@ -78,7 +78,7 @@ const DashboardConfigEditorComponent: React.FunctionComponent)} + })()} )} + })(