diff --git a/.travis.yml b/.travis.yml
index 16cdbb35bf3a5..a02d009f85252 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,6 +28,8 @@ matrix:
env: TOXENV=py36-sqlite
- python: 3.6
env: TOXENV=pylint
+ - python: 3.6
+ env: TOXENV=cypress
exclude:
- python: 2.7
- python: 3.6
diff --git a/superset/assets/cypress.json b/superset/assets/cypress.json
new file mode 100644
index 0000000000000..c84e05d6d3211
--- /dev/null
+++ b/superset/assets/cypress.json
@@ -0,0 +1,3 @@
+{
+ "baseUrl": "http://localhost:8081"
+}
diff --git a/superset/assets/cypress/integration/dashboard/dashboard_tests.js b/superset/assets/cypress/integration/dashboard/dashboard_tests.js
new file mode 100644
index 0000000000000..10e4f1132f395
--- /dev/null
+++ b/superset/assets/cypress/integration/dashboard/dashboard_tests.js
@@ -0,0 +1,26 @@
+describe('Load dashboard', function () {
+ it('Load birth names dashboard', function () {
+ cy.server();
+ cy.login();
+
+ cy.visit('/superset/dashboard/births');
+
+ cy.route('POST', '/superset/explore_json/**').as('getJson');
+ cy.wait(10000, ['@getJson']);
+
+ let sliceData;
+
+ cy.get('@getJson.all').then((xhrs) => {
+ sliceData = xhrs;
+ xhrs.forEach((data) => {
+ expect(data.status).to.eq(200);
+ expect(data.response.body).to.have.property('error', null);
+ cy.get(`#slice-container-${data.response.body.form_data.slice_id}`);
+ });
+ cy.get('#app').then((data) => {
+ const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
+ expect(bootstrapData.dashboard_data.slices.length).to.eq(sliceData.length);
+ });
+ });
+ });
+});
diff --git a/superset/assets/cypress/integration/explore/control_tests.js b/superset/assets/cypress/integration/explore/control_tests.js
new file mode 100644
index 0000000000000..d4c5e4cfb657e
--- /dev/null
+++ b/superset/assets/cypress/integration/explore/control_tests.js
@@ -0,0 +1,59 @@
+// ***********************************************
+// Tests for setting controls in the UI
+// ***********************************************
+
+describe('Groupby', function () {
+ it('Set groupby', function () {
+ cy.server();
+ cy.login();
+
+ cy.route('POST', '/superset/explore_json/**').as('getJson');
+ cy.visitChartByName('Num Births Trend');
+ cy.verifySliceSuccess('@getJson');
+
+ cy.get('[data-test=groupby]').within(() => {
+ cy.get('.Select-control').click();
+ cy.get('input.select-input').type('state', { force: true });
+ cy.get('.VirtualizedSelectFocusedOption').click();
+ });
+ cy.get('button.query').click();
+ cy.verifySliceSuccess('@getJson');
+ });
+});
+
+describe('SimpleAdhocMetric', function () {
+ it('Clear metric and set simple adhoc metric', function () {
+ cy.server();
+ cy.login();
+
+ const metricName = 'Girl Births';
+
+ cy.route('POST', '/superset/explore_json/**').as('getJson');
+ cy.visitChartByName('Num Births Trend');
+ cy.verifySliceSuccess('@getJson');
+
+ cy.get('[data-test=metrics]').within(() => {
+ cy.get('.select-clear').click();
+ cy.get('.Select-control').click({ force: true });
+ cy.get('input').type('sum_girls', { force: true });
+ cy.get('.VirtualizedSelectFocusedOption').trigger('mousedown').click();
+ });
+
+ cy.get('#metrics-edit-popover').within(() => {
+ cy.get('.popover-title').within(() => {
+ cy.get('span').click();
+ cy.get('input').type(metricName);
+ });
+ cy.get('button').contains('Save').click();
+ });
+
+ cy.get('button.query').click();
+ cy.wait(['@getJson']).then((data) => {
+ expect(data.status).to.eq(200);
+ expect(data.response.body).to.have.property('error', null);
+ expect(data.response.body.data[0].key).to.equal(metricName);
+ cy.get('.slice_container');
+ });
+ });
+});
+
diff --git a/superset/assets/cypress/integration/explore/visualization_tests.js b/superset/assets/cypress/integration/explore/visualization_tests.js
new file mode 100644
index 0000000000000..50c331dbb468c
--- /dev/null
+++ b/superset/assets/cypress/integration/explore/visualization_tests.js
@@ -0,0 +1,54 @@
+// ***********************************************
+// Tests for visualization types
+// ***********************************************
+
+const FORM_DATA_DEFAULTS = {
+ datasource: '3__table',
+ viz_type: 'line',
+ granularity_sqla: 'ds',
+ time_grain_sqla: null,
+ time_range: '100+years+ago+:+now',
+ adhoc_filters: [],
+ groupby: [],
+ limit: null,
+ timeseries_limit_metric: null,
+ order_desc: false,
+ contribution: false,
+};
+
+describe('Line', function () {
+ it('Test line chart with adhoc metric', function () {
+ cy.server();
+ cy.login();
+
+ const metrics = [{
+ expressionType: 'SIMPLE',
+ column: {
+ id: 336,
+ column_name: 'num',
+ verbose_name: null,
+ description: null,
+ expression: '',
+ filterable: false,
+ groupby: false,
+ is_dttm: false,
+ type: 'BIGINT',
+ database_expression: null,
+ python_date_format: null,
+ optionName: '_col_num',
+ },
+ aggregate: 'SUM',
+ sqlExpression: null,
+ hasCustomLabel: false,
+ fromFormData: false,
+ label: 'SUM(num)',
+ optionName: 'metric_1de0s4viy5d_ly7y8k6ghvk',
+ }];
+
+ const formData = { ...FORM_DATA_DEFAULTS, metrics };
+
+ cy.route('POST', '/superset/explore_json/**').as('getJson');
+ cy.visitChartByParams(JSON.stringify(formData));
+ cy.verifySliceSuccess('@getJson');
+ });
+});
diff --git a/superset/assets/cypress/plugins/index.js b/superset/assets/cypress/plugins/index.js
new file mode 100644
index 0000000000000..df3a5aeeaf1ec
--- /dev/null
+++ b/superset/assets/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/superset/assets/cypress/support/commands.js b/superset/assets/cypress/support/commands.js
new file mode 100644
index 0000000000000..41c64a7ab1eaf
--- /dev/null
+++ b/superset/assets/cypress/support/commands.js
@@ -0,0 +1,59 @@
+// ***********************************************
+// 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 BASE_EXPLORE_URL = '/superset/explore/?form_data=';
+
+Cypress.Commands.add('login', () => {
+ cy.request({
+ method: 'POST',
+ url: 'http://localhost:8081/login/',
+ body: { username: 'admin', password: 'general' },
+ }).then((response) => {
+ expect(response.status).to.eq(200);
+ });
+});
+
+Cypress.Commands.add('visitChartByName', (name) => {
+ cy.request(`http://localhost:8081/chart/api/read?_flt_3_slice_name=${name}`).then((response) => {
+ cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${response.body.pks[0]}}`);
+ });
+});
+
+Cypress.Commands.add('visitChartById', (chartId) => {
+ cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`);
+});
+
+Cypress.Commands.add('visitChartByParams', (params) => {
+ cy.visit(`${BASE_EXPLORE_URL}${params}`);
+});
+
+Cypress.Commands.add('verifySliceSuccess', (waitAlias) => {
+ cy.wait([waitAlias]).then((data) => {
+ expect(data.status).to.eq(200);
+ expect(data.response.body).to.have.property('error', null);
+ cy.get('.slice_container');
+ });
+});
diff --git a/superset/assets/cypress/support/index.js b/superset/assets/cypress/support/index.js
new file mode 100644
index 0000000000000..37a498fb5bf39
--- /dev/null
+++ b/superset/assets/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/superset/assets/cypress_build.sh b/superset/assets/cypress_build.sh
new file mode 100755
index 0000000000000..dd80e873880a2
--- /dev/null
+++ b/superset/assets/cypress_build.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+set -e
+
+superset/bin/superset db upgrade
+superset/bin/superset load_test_users
+superset/bin/superset load_examples
+superset/bin/superset init
+superset/bin/superset runserver &
+
+cd "$(dirname "$0")"
+
+npm install -g yarn
+yarn
+npm run build
+npm run cypress run
+kill %1
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 13122feefcc83..f36ffdeb5db44 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -17,7 +17,8 @@
"build": "webpack --mode=production --colors --progress",
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx .",
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx .",
- "sync-backend": "babel-node --presets env src/syncBackend.js"
+ "sync-backend": "babel-node --presets env src/syncBackend.js",
+ "cypress": "cypress"
},
"repository": {
"type": "git",
@@ -141,6 +142,7 @@
"chai": "^4.0.2",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.0",
+ "cypress": "^3.0.3",
"enzyme": "^2.0.0",
"eslint": "^4.19.0",
"eslint-config-airbnb": "^15.0.1",
diff --git a/superset/assets/src/explore/components/Control.jsx b/superset/assets/src/explore/components/Control.jsx
index 52682dee00222..bc58ec2b0edd1 100644
--- a/superset/assets/src/explore/components/Control.jsx
+++ b/superset/assets/src/explore/components/Control.jsx
@@ -84,6 +84,7 @@ export default class Control extends React.PureComponent {
const divStyle = this.props.hidden ? { display: 'none' } : null;
return (
=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@@ -8537,6 +8785,15 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
+ora@^0.2.3:
+ version "0.2.3"
+ resolved "http://registry.npmjs.org/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+ dependencies:
+ chalk "^1.1.1"
+ cli-cursor "^1.0.2"
+ cli-spinners "^0.1.2"
+ object-assign "^4.0.1"
+
original@>=0.0.5:
version "1.0.2"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
@@ -8547,7 +8804,7 @@ os-browserify@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
-os-homedir@^1.0.0:
+os-homedir@^1.0.0, os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@@ -8559,7 +8816,7 @@ os-locale@^2.0.0:
lcid "^1.0.0"
mem "^1.1.0"
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -8851,6 +9108,10 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+
performance-now@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
@@ -8863,7 +9124,7 @@ phin@^2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.1.tgz#0de9059b1a9bd56fcb1bd8a374344a06f25f1901"
-pify@^2.0.0, pify@^2.3.0:
+pify@^2.0.0, pify@^2.2.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -9468,6 +9729,10 @@ process@~0.5.1:
version "0.5.2"
resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
+progress@1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
progress@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
@@ -9648,6 +9913,10 @@ raf@^3.3.0:
dependencies:
performance-now "^2.1.0"
+ramda@0.24.1:
+ version "0.24.1"
+ resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857"
+
randomatic@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.0.tgz#36f2ca708e9e567f5ed2ec01949026d50aa10116"
@@ -10218,7 +10487,7 @@ readable-stream@~2.1.5:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
+readdir-scoped-modules@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
dependencies:
@@ -10461,30 +10730,36 @@ replace-ext@1.0.0, replace-ext@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
-request@2, request@^2.65.0, request@^2.74.0, request@^2.79.0, request@^2.85.0:
- version "2.88.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+request-progress@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.3.1.tgz#0721c105d8a96ac6b2ce8b2c89ae2d5ecfcf6b3a"
+ dependencies:
+ throttleit "~0.0.2"
+
+request@2, request@2.87.0, request@^2.74.0, request@^2.79.0:
+ version "2.87.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
dependencies:
aws-sign2 "~0.7.0"
- aws4 "^1.8.0"
+ aws4 "^1.6.0"
caseless "~0.12.0"
- combined-stream "~1.0.6"
- extend "~3.0.2"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
forever-agent "~0.6.1"
- form-data "~2.3.2"
- har-validator "~5.1.0"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
- mime-types "~2.1.19"
- oauth-sign "~0.9.0"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
performance-now "^2.1.0"
- qs "~6.5.2"
- safe-buffer "^5.1.2"
- tough-cookie "~2.4.3"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ tough-cookie "~2.3.3"
tunnel-agent "^0.6.0"
- uuid "^3.3.2"
+ uuid "^3.1.0"
request@2.81.0:
version "2.81.0"
@@ -10513,6 +10788,31 @@ request@2.81.0:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
+request@^2.65.0, request@^2.85.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
request@~2.22.0:
version "2.22.0"
resolved "http://registry.npmjs.org/request/-/request-2.22.0.tgz#b883a769cc4a909571eb5004b344c43cf7e51592"
@@ -10717,9 +11017,9 @@ rx-lite@*, rx-lite@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
-rxjs@^5.5.2:
- version "5.5.12"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc"
+rxjs@^5.0.0-beta.11, rxjs@^5.5.2:
+ version "5.5.11"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87"
dependencies:
symbol-observable "1.0.1"
@@ -11342,6 +11642,10 @@ stream-to-buffer@^0.1.0:
dependencies:
stream-to "~0.2.0"
+stream-to-observable@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe"
+
stream-to@~0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d"
@@ -11475,6 +11779,12 @@ supports-color@3.1.2, supports-color@3.1.x:
dependencies:
has-flag "^1.0.0"
+supports-color@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
+ dependencies:
+ has-flag "^2.0.0"
+
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -11600,6 +11910,10 @@ textextensions@2:
version "2.2.0"
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
+throttleit@~0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
+
through2@^2.0.0, through2@^2.0.3, through2@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
@@ -11648,6 +11962,12 @@ tinyqueue@^1.1.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d"
+tmp@0.0.31:
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -11941,6 +12261,10 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.3.0:
dependencies:
unist-util-visit-parents "^2.0.0"
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -12038,7 +12362,7 @@ url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
-url@^0.11.0:
+url@0.11.0, url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
dependencies:
@@ -12098,7 +12422,7 @@ v8flags@^2.1.1:
dependencies:
user-home "^1.1.1"
-validate-npm-package-license@*, validate-npm-package-license@^3.0.1:
+validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
dependencies:
@@ -12628,6 +12952,19 @@ yargs@~1.2.6:
dependencies:
minimist "^0.1.0"
+yauzl@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+ dependencies:
+ fd-slicer "~1.0.1"
+
+yauzl@2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2"
+ dependencies:
+ buffer-crc32 "~0.2.3"
+ fd-slicer "~1.0.1"
+
yeoman-environment@^2.0.5, yeoman-environment@^2.1.1:
version "2.3.3"
resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.3.3.tgz#1bd9720714cc49036e901503a789d809df8f51bf"
diff --git a/superset/cli.py b/superset/cli.py
index 074e12f963102..68e3de7c159a6 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -20,6 +20,7 @@
from superset import (
app, data, db, dict_import_export_util, security_manager, utils,
)
+from tests.utils import get_main_database
config = app.config
celery_app = utils.get_celery_app(config)
@@ -365,3 +366,69 @@ def flower(port, address):
print(Fore.YELLOW + cmd)
print(Fore.BLUE + '-=' * 40)
Popen(cmd, shell=True).wait()
+
+
+@app.cli.command()
+def load_test_users():
+ """
+ Loads admin, alpha, and gamma user for testing purposes
+
+ Syncs permissions for those users/roles
+ """
+ load_test_users_run()
+
+
+def load_test_users_run():
+ """
+ Loads admin, alpha, and gamma user for testing purposes
+
+ Syncs permissions for those users/roles
+ """
+ if config.get('TESTING'):
+ security_manager.sync_role_definitions()
+ gamma_sqllab_role = security_manager.add_role('gamma_sqllab')
+ for perm in security_manager.find_role('Gamma').permissions:
+ security_manager.add_permission_role(gamma_sqllab_role, perm)
+ utils.get_or_create_main_db()
+ db_perm = get_main_database(security_manager.get_session).perm
+ security_manager.merge_perm('database_access', db_perm)
+ db_pvm = security_manager.find_permission_view_menu(
+ view_menu_name=db_perm, permission_name='database_access')
+ gamma_sqllab_role.permissions.append(db_pvm)
+ for perm in security_manager.find_role('sql_lab').permissions:
+ security_manager.add_permission_role(gamma_sqllab_role, perm)
+
+ admin = security_manager.find_user('admin')
+ if not admin:
+ security_manager.add_user(
+ 'admin', 'admin', ' user', 'admin@fab.org',
+ security_manager.find_role('Admin'),
+ password='general')
+
+ gamma = security_manager.find_user('gamma')
+ if not gamma:
+ security_manager.add_user(
+ 'gamma', 'gamma', 'user', 'gamma@fab.org',
+ security_manager.find_role('Gamma'),
+ password='general')
+
+ gamma2 = security_manager.find_user('gamma2')
+ if not gamma2:
+ security_manager.add_user(
+ 'gamma2', 'gamma2', 'user', 'gamma2@fab.org',
+ security_manager.find_role('Gamma'),
+ password='general')
+
+ gamma_sqllab_user = security_manager.find_user('gamma_sqllab')
+ if not gamma_sqllab_user:
+ security_manager.add_user(
+ 'gamma_sqllab', 'gamma_sqllab', 'user', 'gamma_sqllab@fab.org',
+ gamma_sqllab_role, password='general')
+
+ alpha = security_manager.find_user('alpha')
+ if not alpha:
+ security_manager.add_user(
+ 'alpha', 'alpha', 'user', 'alpha@fab.org',
+ security_manager.find_role('Alpha'),
+ password='general')
+ security_manager.get_session.commit()
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index 0b232f751c017..56af4a6423b98 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -955,6 +955,14 @@ def load_birth_names():
'aggregate': 'SUM',
'label': 'SUM(num_california)',
})),
+ Slice(
+ slice_name="Num Births Trend",
+ viz_type='line',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(
+ defaults,
+ viz_type="line")),
]
for slc in slices:
merge_slice(slc)
diff --git a/tests/base_tests.py b/tests/base_tests.py
index 782cedd0a19e3..f89aaf76b95c4 100644
--- a/tests/base_tests.py
+++ b/tests/base_tests.py
@@ -14,10 +14,11 @@
from mock import Mock
import pandas as pd
-from superset import app, cli, db, security_manager, utils
+from superset import app, cli, db, security_manager
from superset.connectors.druid.models import DruidCluster, DruidDatasource
from superset.connectors.sqla.models import SqlaTable
from superset.models import core as models
+from .utils import get_main_database
BASE_DIR = app.config.get('BASE_DIR')
@@ -43,52 +44,7 @@ def __init__(self, *args, **kwargs):
self.client = app.test_client()
self.maxDiff = None
- gamma_sqllab_role = security_manager.add_role('gamma_sqllab')
- for perm in security_manager.find_role('Gamma').permissions:
- security_manager.add_permission_role(gamma_sqllab_role, perm)
- utils.get_or_create_main_db()
- db_perm = self.get_main_database(security_manager.get_session).perm
- security_manager.merge_perm('database_access', db_perm)
- db_pvm = security_manager.find_permission_view_menu(
- view_menu_name=db_perm, permission_name='database_access')
- gamma_sqllab_role.permissions.append(db_pvm)
- for perm in security_manager.find_role('sql_lab').permissions:
- security_manager.add_permission_role(gamma_sqllab_role, perm)
-
- admin = security_manager.find_user('admin')
- if not admin:
- security_manager.add_user(
- 'admin', 'admin', ' user', 'admin@fab.org',
- security_manager.find_role('Admin'),
- password='general')
-
- gamma = security_manager.find_user('gamma')
- if not gamma:
- security_manager.add_user(
- 'gamma', 'gamma', 'user', 'gamma@fab.org',
- security_manager.find_role('Gamma'),
- password='general')
-
- gamma2 = security_manager.find_user('gamma2')
- if not gamma2:
- security_manager.add_user(
- 'gamma2', 'gamma2', 'user', 'gamma2@fab.org',
- security_manager.find_role('Gamma'),
- password='general')
-
- gamma_sqllab_user = security_manager.find_user('gamma_sqllab')
- if not gamma_sqllab_user:
- security_manager.add_user(
- 'gamma_sqllab', 'gamma_sqllab', 'user', 'gamma_sqllab@fab.org',
- gamma_sqllab_role, password='general')
-
- alpha = security_manager.find_user('alpha')
- if not alpha:
- security_manager.add_user(
- 'alpha', 'alpha', 'user', 'alpha@fab.org',
- security_manager.find_role('Alpha'),
- password='general')
- security_manager.get_session.commit()
+ cli.load_test_users_run()
# create druid cluster and druid datasources
session = db.session
cluster = (
@@ -185,13 +141,6 @@ def get_json_resp(
resp = self.get_resp(url, data, follow_redirects, raise_on_error)
return json.loads(resp)
- def get_main_database(self, session):
- return (
- db.session.query(models.Database)
- .filter_by(database_name='main')
- .first()
- )
-
def get_access_requests(self, username, ds_type, ds_id):
DAR = models.DatasourceAccessRequest
return (
@@ -227,7 +176,7 @@ def run_sql(self, sql, client_id, user_name=None, raise_on_error=False):
if user_name:
self.logout()
self.login(username=(user_name if user_name else 'admin'))
- dbid = self.get_main_database(db.session).id
+ dbid = get_main_database(db.session).id
resp = self.get_json_resp(
'/superset/sql_json/',
raise_on_error=False,
diff --git a/tests/celery_tests.py b/tests/celery_tests.py
index 243aacef4586d..7542abeb00376 100644
--- a/tests/celery_tests.py
+++ b/tests/celery_tests.py
@@ -19,6 +19,7 @@
from superset.models.sql_lab import Query
from superset.sql_parse import SupersetQuery
from .base_tests import SupersetTestCase
+from .utils import get_main_database
BASE_DIR = app.config.get('BASE_DIR')
@@ -140,14 +141,14 @@ def run_sql(self, db_id, sql, client_id, cta='false', tmp_table='tmp',
return json.loads(resp.data.decode('utf-8'))
def test_run_sync_query_dont_exist(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
db_id = main_db.id
sql_dont_exist = 'SELECT name FROM table_dont_exist'
result1 = self.run_sql(db_id, sql_dont_exist, '1', cta='true')
self.assertTrue('error' in result1)
def test_run_sync_query_cta(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
db_id = main_db.id
eng = main_db.get_sqla_engine()
perm_name = 'can_sql_json'
@@ -166,7 +167,7 @@ def test_run_sync_query_cta(self):
self.assertEqual([{'name': perm_name}], data2)
def test_run_sync_query_cta_no_data(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
db_id = main_db.id
sql_empty_result = 'SELECT * FROM ab_user WHERE id=666'
result3 = self.run_sql(
@@ -179,7 +180,7 @@ def test_run_sync_query_cta_no_data(self):
self.assertEqual(QueryStatus.SUCCESS, query3.status)
def test_run_async_query(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
eng = main_db.get_sqla_engine()
sql_where = "SELECT name FROM ab_role WHERE name='Admin'"
result = self.run_sql(
@@ -207,7 +208,7 @@ def test_run_async_query(self):
self.assertEqual(True, query.select_as_cta_used)
def test_run_async_query_with_lower_limit(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
eng = main_db.get_sqla_engine()
sql_where = "SELECT name FROM ab_role WHERE name='Alpha' LIMIT 1"
result = self.run_sql(
diff --git a/tests/core_tests.py b/tests/core_tests.py
index f03c51f2b392c..d4aeeceff28d7 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -29,6 +29,7 @@
from superset.models.sql_lab import Query
from superset.views.core import DatabaseView
from .base_tests import SupersetTestCase
+from .utils import get_main_database
class CoreTests(SupersetTestCase):
@@ -312,7 +313,7 @@ def test_misc(self):
def test_testconn(self, username='admin'):
self.login(username=username)
- database = self.get_main_database(db.session)
+ database = get_main_database(db.session)
# validate that the endpoint works with the password-masked sqlalchemy uri
data = json.dumps({
@@ -341,7 +342,7 @@ def test_testconn(self, username='admin'):
assert response.headers['Content-Type'] == 'application/json'
def test_custom_password_store(self):
- database = self.get_main_database(db.session)
+ database = get_main_database(db.session)
conn_pre = sqla.engine.url.make_url(database.sqlalchemy_uri_decrypted)
def custom_password_store(uri):
@@ -359,13 +360,13 @@ def test_databaseview_edit(self, username='admin'):
# validate that sending a password-masked uri does not over-write the decrypted
# uri
self.login(username=username)
- database = self.get_main_database(db.session)
+ database = get_main_database(db.session)
sqlalchemy_uri_decrypted = database.sqlalchemy_uri_decrypted
url = 'databaseview/edit/{}'.format(database.id)
data = {k: database.__getattribute__(k) for k in DatabaseView.add_columns}
data['sqlalchemy_uri'] = database.safe_sqlalchemy_uri()
self.client.post(url, data=data)
- database = self.get_main_database(db.session)
+ database = get_main_database(db.session)
self.assertEqual(sqlalchemy_uri_decrypted, database.sqlalchemy_uri_decrypted)
def test_warm_up_cache(self):
@@ -452,27 +453,27 @@ def test_csv_endpoint(self):
def test_extra_table_metadata(self):
self.login('admin')
- dbid = self.get_main_database(db.session).id
+ dbid = get_main_database(db.session).id
self.get_json_resp(
'/superset/extra_table_metadata/{dbid}/'
'ab_permission_view/panoramix/'.format(**locals()))
def test_process_template(self):
- maindb = self.get_main_database(db.session)
+ maindb = get_main_database(db.session)
sql = "SELECT '{{ datetime(2017, 1, 1).isoformat() }}'"
tp = jinja_context.get_template_processor(database=maindb)
rendered = tp.process_template(sql)
self.assertEqual("SELECT '2017-01-01T00:00:00'", rendered)
def test_get_template_kwarg(self):
- maindb = self.get_main_database(db.session)
+ maindb = get_main_database(db.session)
s = '{{ foo }}'
tp = jinja_context.get_template_processor(database=maindb, foo='bar')
rendered = tp.process_template(s)
self.assertEqual('bar', rendered)
def test_template_kwarg(self):
- maindb = self.get_main_database(db.session)
+ maindb = get_main_database(db.session)
s = '{{ foo }}'
tp = jinja_context.get_template_processor(database=maindb)
rendered = tp.process_template(s, foo='bar')
@@ -485,7 +486,7 @@ def test_templated_sql_json(self):
self.assertEqual(data['data'][0]['test'], '2017-01-01T00:00:00')
def test_table_metadata(self):
- maindb = self.get_main_database(db.session)
+ maindb = get_main_database(db.session)
backend = maindb.backend
data = self.get_json_resp(
'/superset/table/{}/ab_user/null/'.format(maindb.id))
diff --git a/tests/dict_import_export_tests.py b/tests/dict_import_export_tests.py
index cbe8aa2ea240f..21a46ee9e8fcd 100644
--- a/tests/dict_import_export_tests.py
+++ b/tests/dict_import_export_tests.py
@@ -16,6 +16,7 @@
)
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from .base_tests import SupersetTestCase
+from .utils import get_main_database
DBREF = 'dict_import__export_test'
NAME_PREFIX = 'dict_'
@@ -55,7 +56,7 @@ def create_table(
params = {DBREF: id, 'database_name': database_name}
dict_rep = {
- 'database_id': self.get_main_database(db.session).id,
+ 'database_id': get_main_database(db.session).id,
'table_name': name,
'schema': schema,
'id': id,
diff --git a/tests/form_tests.py b/tests/form_tests.py
index 82178a213eb09..93bef66bcdba4 100644
--- a/tests/form_tests.py
+++ b/tests/form_tests.py
@@ -4,11 +4,11 @@
from __future__ import print_function
from __future__ import unicode_literals
-from tests.base_tests import SupersetTestCase
from wtforms.form import Form
from superset.forms import (
CommaSeparatedListField, filter_not_empty_values)
+from tests.base_tests import SupersetTestCase
class FormTestCase(SupersetTestCase):
diff --git a/tests/model_tests.py b/tests/model_tests.py
index 74dc822645d00..565791a8e357f 100644
--- a/tests/model_tests.py
+++ b/tests/model_tests.py
@@ -11,6 +11,7 @@
from superset import app, db
from superset.models.core import Database
from .base_tests import SupersetTestCase
+from .utils import get_main_database
class DatabaseModelTestCase(SupersetTestCase):
@@ -77,7 +78,7 @@ def test_database_impersonate_user(self):
self.assertNotEquals(example_user, user_name)
def test_select_star(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
table_name = 'bart_lines'
sql = main_db.select_star(
table_name, show_cols=False, latest_partition=False)
@@ -107,7 +108,7 @@ def test_grains_dict(self):
self.assertEquals(d.get('Time Column').function, '{col}')
def test_single_statement(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
if main_db.backend == 'mysql':
df = main_db.get_df('SELECT 1', None)
@@ -117,7 +118,7 @@ def test_single_statement(self):
self.assertEquals(df.iat[0, 0], 1)
def test_multi_statement(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
if main_db.backend == 'mysql':
df = main_db.get_df('USE superset; SELECT 1', None)
diff --git a/tests/sqllab_tests.py b/tests/sqllab_tests.py
index c3fd4045f7191..1159c4e8b3635 100644
--- a/tests/sqllab_tests.py
+++ b/tests/sqllab_tests.py
@@ -16,6 +16,7 @@
from superset.db_engine_specs import BaseEngineSpec
from superset.models.sql_lab import Query
from .base_tests import SupersetTestCase
+from .utils import get_main_database
class SqlLabTests(SupersetTestCase):
@@ -62,7 +63,7 @@ def test_explain(self):
self.assertLess(0, len(data['data']))
def test_sql_json_has_access(self):
- main_db = self.get_main_database(db.session)
+ main_db = get_main_database(db.session)
security_manager.add_permission_view_menu('database_access', main_db.perm)
db.session.commit()
main_db_permission_view = (
diff --git a/tests/utils.py b/tests/utils.py
index d1a5adb01febb..a0ab45652295d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -7,9 +7,20 @@
import json
from os import path
+from superset import db
+from superset.models import core as models
+
FIXTURES_DIR = 'tests/fixtures'
def load_fixture(fixture_file_name):
with open(path.join(FIXTURES_DIR, fixture_file_name)) as fixture_file:
return json.load(fixture_file)
+
+
+def get_main_database(session):
+ return (
+ db.session.query(models.Database)
+ .filter_by(database_name='main')
+ .first()
+ )
diff --git a/tox.ini b/tox.ini
index b73d276841681..039075a8af8a3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,8 @@
[flake8]
accept-encodings = utf-8
-application-import-names = superset
+application-import-names =
+ superset
+ tests
exclude =
.tox
build
@@ -43,6 +45,17 @@ setenv =
whitelist_externals =
npm
+[testenv:cypress]
+commands =
+ {toxinidir}/superset/assets/cypress_build.sh
+setenv =
+ PYTHONPATH = {toxinidir}
+ SUPERSET_CONFIG = tests.superset_test_config
+ SUPERSET_HOME = {envtmpdir}
+deps =
+ -rrequirements.txt
+ -rrequirements-dev.txt
+
[testenv:eslint]
changedir = {toxinidir}/superset/assets
commands =
@@ -70,6 +83,7 @@ deps =
[tox]
envlist =
+ cypress
eslint
flake8
javascript