diff --git a/.circleci/config.yml b/.circleci/config.yml
index de43422d1b..a33a302a6e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -54,6 +54,26 @@ jobs:
- run: npm install
- run: npm run bundle
- run: npm test
+ frontend-e2e-tests:
+ environment:
+ COMPOSE_FILE: .circleci/docker-compose.cypress.yml
+ COMPOSE_PROJECT_NAME: cypress
+ docker:
+ - image: circleci/node:8
+ steps:
+ - setup_remote_docker
+ - checkout
+ - run:
+ name: Install npm dependencies
+ command: npm install
+ - run:
+ name: Setup Redash server
+ command: |
+ npm run cypress:server start-ci
+ docker-compose run cypress node ./cypress/cypress-server.js setup
+ - run:
+ name: Execute Cypress tests
+ command: docker-compose run cypress ./node_modules/.bin/cypress run
build-tarball:
docker:
- image: circleci/node:8
@@ -77,64 +97,15 @@ jobs:
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t redash/redash:$(.circleci/docker_tag) .
- run: docker push redash/redash:$(.circleci/docker_tag)
- integration-tests:
- working_directory: ~/redash
- machine: true
- environment:
- REDASH_SERVER_URL : "http://127.0.0.1:5000/"
- DOCKER_IMAGE: mozilla/redash-ui-tests
- steps:
- - checkout
- - run:
- name: Install Docker Compose
- command: |
- set -x
- pip install --upgrade pip
- pip install docker-compose>=1.18
- docker-compose --version
- - run:
- name: Pull redash images
- command: |
- set -x
- docker-compose -f docker-compose.yml up --no-start
- sleep 10
- - run:
- name: Pull redash-ui-tests
- command: docker pull "${DOCKER_IMAGE}":latest
- - run:
- name: Setup redash instance
- command: |
- set -x
- docker-compose run --rm --user root server create_db
- docker-compose run --rm postgres psql -h postgres -U postgres -c "create database tests"
- docker-compose run --rm --user root server /app/manage.py users create_root root@example.com "rootuser" --password "IAMROOT" --org default
- docker-compose run --rm --user root server /app/manage.py ds new "ui-tests" --type "url" --options '{"title": "uitests"}'
- docker-compose run -d -p 5000:5000 --user root server
- docker-compose start postgres
- docker-compose run --rm --user root server npm install
- docker-compose run --rm --user root server npm run bundle
- docker-compose run --rm --user root server npm run build
- - run:
- name: Run tests
- command: |
- set -x
- docker run --net="host" --env REDASH_SERVER_URL="${REDASH_SERVER_URL}" "${DOCKER_IMAGE}"
- - store_artifacts:
- path: report.html
workflows:
version: 2
- # integration_tests:
- # jobs:
- # - integration-tests:
- # filters:
- # branches:
- # only: master
build:
jobs:
- python-flake8-tests
- legacy-python-flake8-tests
- backend-unit-tests
- frontend-unit-tests
+ - frontend-e2e-tests
- build-tarball:
requires:
- backend-unit-tests
diff --git a/.circleci/docker-compose.cypress.yml b/.circleci/docker-compose.cypress.yml
new file mode 100644
index 0000000000..ae06edb482
--- /dev/null
+++ b/.circleci/docker-compose.cypress.yml
@@ -0,0 +1,43 @@
+version: '2'
+services:
+ server:
+ build: ../
+ command: dev_server
+ depends_on:
+ - postgres
+ - redis
+ ports:
+ - "5000:5000"
+ environment:
+ PYTHONUNBUFFERED: 0
+ REDASH_LOG_LEVEL: "INFO"
+ REDASH_REDIS_URL: "redis://redis:6379/0"
+ REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
+ worker:
+ build: ../
+ command: scheduler
+ depends_on:
+ - server
+ environment:
+ PYTHONUNBUFFERED: 0
+ REDASH_LOG_LEVEL: "INFO"
+ REDASH_REDIS_URL: "redis://redis:6379/0"
+ REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
+ QUEUES: "queries,scheduled_queries,celery"
+ WORKERS_COUNT: 2
+ cypress:
+ build:
+ context: ../
+ dockerfile: Dockerfile.cypress
+ depends_on:
+ - server
+ - worker
+ environment:
+ CYPRESS_baseUrl: "http://server:5000"
+ redis:
+ image: redis:3.0-alpine
+ restart: unless-stopped
+ postgres:
+ image: postgres:9.5.6-alpine
+ command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
+ restart: unless-stopped
diff --git a/.gitignore b/.gitignore
index e405ce2850..7307021b73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,5 @@ node_modules
.sass-cache
npm-debug.log
+cypress/screenshots
+cypress/videos
diff --git a/Dockerfile.cypress b/Dockerfile.cypress
new file mode 100644
index 0000000000..31f619ba56
--- /dev/null
+++ b/Dockerfile.cypress
@@ -0,0 +1,10 @@
+FROM cypress/browsers:chrome67
+
+WORKDIR /usr/src/app
+
+RUN npm install cypress > /dev/null
+
+COPY cypress /usr/src/app/cypress
+COPY cypress.json /usr/src/app/cypress.json
+
+RUN ./node_modules/.bin/cypress verify
diff --git a/client/.eslintrc.js b/client/.eslintrc.js
index 759c24f53e..10fae2b4c6 100644
--- a/client/.eslintrc.js
+++ b/client/.eslintrc.js
@@ -1,13 +1,14 @@
module.exports = {
root: true,
extends: ["airbnb", "plugin:jest/recommended"],
- plugins: ["jest"],
+ plugins: ["jest", "cypress"],
settings: {
"import/resolver": "webpack"
},
parser: "babel-eslint",
env: {
"jest/globals": true,
+ "cypress/globals": true,
"browser": true,
"node": true
},
diff --git a/client/app/components/app-header/app-header.html b/client/app/components/app-header/app-header.html
index 184c84bac5..18ee1ea8fc 100644
--- a/client/app/components/app-header/app-header.html
+++ b/client/app/components/app-header/app-header.html
@@ -86,7 +86,7 @@
-
+
diff --git a/client/app/components/dynamic-form.html b/client/app/components/dynamic-form.html
index 27c545364d..0378ea4c22 100644
--- a/client/app/components/dynamic-form.html
+++ b/client/app/components/dynamic-form.html
@@ -8,11 +8,13 @@
+ ng-if="field.property.type !== 'file' && field.property.type !== 'checkbox'" accesskey="tab" placeholder="{{field.property.default}}"
+ data-cy="{{field.property.title || field.name | toHuman}}">
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000000..f152b0cf66
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,3 @@
+{
+ "baseUrl": "http://localhost:5000"
+}
diff --git a/cypress/cypress-server.js b/cypress/cypress-server.js
new file mode 100644
index 0000000000..1661de7275
--- /dev/null
+++ b/cypress/cypress-server.js
@@ -0,0 +1,52 @@
+/* eslint-disable import/no-extraneous-dependencies, no-console */
+const { execSync } = require('child_process');
+const { post } = require('request');
+
+function execSetup() {
+ console.log('Running setup...');
+
+ const setupData = {
+ name: 'Example Admin',
+ email: 'admin@redash.io',
+ password: 'password',
+ org_name: 'Redash',
+ };
+
+ const baseUrl = process.env.CYPRESS_baseUrl || 'http://localhost:5000';
+
+ post(baseUrl + '/setup', { formData: setupData });
+}
+
+function startServer() {
+ console.log('Starting the server...');
+
+ execSync('docker-compose -p cypress build --build-arg skip_ds_deps=true', { stdio: 'inherit' });
+ execSync('docker-compose -p cypress up -d', { stdio: 'inherit' });
+ execSync('docker-compose -p cypress run server create_db', { stdio: 'inherit' });
+}
+
+function stopServer() {
+ console.log('Stopping the server...');
+ execSync('docker-compose -p cypress down', { stdio: 'inherit' });
+}
+
+const command = process.argv[2];
+
+switch (command) {
+ case 'start':
+ startServer();
+ execSetup();
+ break;
+ case 'start-ci':
+ startServer();
+ break;
+ case 'setup':
+ execSetup();
+ break;
+ case 'stop':
+ stopServer();
+ break;
+ default:
+ console.log('Usage: npm run cypress:server start|stop');
+ break;
+}
diff --git a/cypress/integration/data-source/create_data_source_spec.js b/cypress/integration/data-source/create_data_source_spec.js
new file mode 100644
index 0000000000..a89835cb3e
--- /dev/null
+++ b/cypress/integration/data-source/create_data_source_spec.js
@@ -0,0 +1,19 @@
+describe('Create Data Source', () => {
+ beforeEach(() => {
+ cy.login();
+ cy.visit('/data_sources');
+ });
+
+ it('creates a new PostgreSQL data source', () => {
+ cy.contains('New Data Source').click();
+ cy.contains('PostgreSQL').click();
+
+ cy.get('[name=targetName]').type('Redash');
+ cy.get('[data-cy=Host]').type('{selectall}localhost');
+ cy.get('[data-cy=User]').type('postgres');
+ cy.get('[data-cy=Password]').type('postgres');
+ cy.get('[data-cy="Database Name"]').type('postgres{enter}');
+
+ cy.contains('Saved.');
+ });
+});
diff --git a/cypress/integration/user/login_spec.js b/cypress/integration/user/login_spec.js
new file mode 100644
index 0000000000..136b87ace5
--- /dev/null
+++ b/cypress/integration/user/login_spec.js
@@ -0,0 +1,24 @@
+describe('Login', () => {
+ beforeEach(() => {
+ cy.visit('/login');
+ });
+
+ it('greets the user', () => {
+ cy.contains('h3', 'Login to Redash');
+ });
+
+ it('shows message on failed login', () => {
+ cy.get('#inputEmail').type('admin@redash.io');
+ cy.get('#inputPassword').type('wrongpassword{enter}');
+
+ cy.get('.alert').should('contain', 'Wrong email or password.');
+ });
+
+ it('navigates to homepage with successful login', () => {
+ cy.get('#inputEmail').type('admin@redash.io');
+ cy.get('#inputPassword').type('password{enter}');
+
+ cy.title().should('eq', 'Redash');
+ cy.contains('Example Admin');
+ });
+});
diff --git a/cypress/integration/user/logout_spec.js b/cypress/integration/user/logout_spec.js
new file mode 100644
index 0000000000..8648935053
--- /dev/null
+++ b/cypress/integration/user/logout_spec.js
@@ -0,0 +1,13 @@
+describe('Logout', () => {
+ beforeEach(() => {
+ cy.login();
+ cy.visit('/');
+ });
+
+ it('shows login page after logout', () => {
+ cy.get('[data-cy=dropdown-profile]').click();
+ cy.contains('Log out').click();
+
+ cy.title().should('eq', 'Login to Redash');
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000000..cc40a4649c
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1 @@
+module.exports = () => {};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000000..1121e49624
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,15 @@
+Cypress.Commands.add('login', () => {
+ const users = {
+ admin: {
+ email: 'admin@redash.io',
+ password: 'password',
+ },
+ };
+
+ cy.request({
+ url: '/login',
+ method: 'POST',
+ form: true,
+ body: users.admin,
+ });
+});
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000000..1221b17e09
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1 @@
+import './commands';
diff --git a/package-lock.json b/package-lock.json
index bdb297fdd6..40e22e638a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "redash-client",
- "version": "5.0.0",
+ "version": "6.0.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -5362,6 +5362,23 @@
}
}
},
+ "eslint-plugin-cypress": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.0.1.tgz",
+ "integrity": "sha512-iC17zJhUxW4JMGQYyxq2lYl5vCUDqGnC2Qkmt1mMk3kn5XIu6ypbtAbREDTC+cFY6SG7USlsmURjTAJ4LtlM6A==",
+ "dev": true,
+ "requires": {
+ "globals": "^11.0.1"
+ },
+ "dependencies": {
+ "globals": {
+ "version": "11.8.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.8.0.tgz",
+ "integrity": "sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA==",
+ "dev": true
+ }
+ }
+ },
"eslint-plugin-import": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz",
@@ -6516,11 +6533,13 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6533,15 +6552,18 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -6644,7 +6666,8 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -6654,6 +6677,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -6666,17 +6690,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -6693,6 +6720,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -6765,7 +6793,8 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -6775,6 +6804,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -6880,6 +6910,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -15085,18 +15116,18 @@
"dev": true
},
"mime-db": {
- "version": "1.36.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
- "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==",
+ "version": "1.37.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
+ "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
"dev": true
},
"mime-types": {
- "version": "2.1.20",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz",
- "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==",
+ "version": "2.1.21",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
+ "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
"dev": true,
"requires": {
- "mime-db": "~1.36.0"
+ "mime-db": "~1.37.0"
}
},
"oauth-sign": {
diff --git a/package.json b/package.json
index f5b2f78be0..88e6cdd2e5 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,12 @@
"analyze": "npm run clean && BUNDLE_ANALYZER=on webpack",
"analyze:build": "npm run clean && NODE_ENV=production BUNDLE_ANALYZER=on webpack",
"test": "jest",
- "test:watch": "jest --watch"
+ "test:watch": "jest --watch",
+ "cypress:install": "npm install --no-save cypress",
+ "cypress:server": "node cypress/cypress-server.js",
+ "cypress:run": "cypress run",
+ "cypress:open": "cypress open",
+ "cypress": "npm run cypress:server start && cypress run && npm run cypress:server stop"
},
"repository": {
"type": "git",
@@ -98,6 +103,7 @@
"eslint-config-airbnb-base": "^12.0.1",
"eslint-import-resolver-webpack": "^0.8.3",
"eslint-loader": "^2.1.1",
+ "eslint-plugin-cypress": "^2.0.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jest": "^21.25.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
@@ -112,6 +118,7 @@
"mini-css-extract-plugin": "^0.4.4",
"raw-loader": "^0.5.1",
"react-test-renderer": "^16.5.2",
+ "request": "^2.88.0",
"url-loader": "^0.5.9",
"webpack": "^4.20.2",
"webpack-build-notifier": "^0.1.16",