From b68084b9ac76c943e55f44b1b5ae22d79fc19d6f Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Thu, 10 Aug 2017 17:04:44 -0700 Subject: [PATCH] Explore view save modal spec (#3110) * split reducer logic for ExploreViewContainer * fix saveModal component and unit tests * revert changes in SaveModal_spec. will make another commit just to improve test coverage for SaveModal component. * improve test coverage for explore view components: - SaveModal component - URLShortLinkButton * remove comment-out code * [bugfix] wrong 'Cant have overlap between Series and Breakdowns' (#3254) * [explore] make edit datasource a basic link (#3244) * Relying on FAB for font-awesome.min.css (#3261) * Modernize SQLA pessimistic handling (#3256) Looks like SQLAlchemy has redefined the best practice around pessimistic connection handling. * [webpack] break CSS and JS files while webpackin' (#3262) * [webpack] break CSS and JS files while webpackin' * cleaning up some templates * Fix pylint issue * import logging (#3264) * [bugfix] preserve order in groupby (#3268) Recently in https://github.com/apache/incubator-superset/commit/4c3313b01cb508ced8519a68f6479db423974929 I introduced an issue where the order of groupby fields might change. This addresses this issue and will preserve ordering. * Explicitly add Flask as dependancy (#3252) * Use sane Celery defaults to prevent tasks from being delayed (#3267) * Improve the chart type of Visualize in sqllab (#3241) * Improve the chart type of Visualize in sqllab & Add some css & Fix the link address in the navbar * add vizTypes filter * Set default ports Druid (#3266) For Druid set the default port for the broker and coordinator. * [explore] Split large reducer logic in ExploreViewContainer (#3088) * split reducer logic for ExploreViewContainer * fix saveModal component and unit tests * revert changes in SaveModal_spec. will make another commit just to improve test coverage for SaveModal component. * remove comment-out code * fix merge confilicts --- .../explore/actions/exploreActions.js | 38 +++ .../components/CheckboxControl_spec.jsx | 12 +- .../explore/components/SaveModal_spec.jsx | 221 +++++++++++++++--- .../components/URLShortLinkButton_spec.jsx | 6 + 4 files changed, 238 insertions(+), 39 deletions(-) diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js index 32fa3c5b4745f..f539aa11e644a 100644 --- a/superset/assets/javascripts/explore/actions/exploreActions.js +++ b/superset/assets/javascripts/explore/actions/exploreActions.js @@ -14,6 +14,11 @@ export function setDatasource(datasource) { return { type: SET_DATASOURCE, datasource }; } +export const SET_DATASOURCES = 'SET_DATASOURCES'; +export function setDatasources(datasources) { + return { type: SET_DATASOURCES, datasources }; +} + export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED'; export function fetchDatasourceStarted() { return { type: FETCH_DATASOURCE_STARTED }; @@ -29,6 +34,21 @@ export function fetchDatasourceFailed(error) { return { type: FETCH_DATASOURCE_FAILED, error }; } +export const FETCH_DATASOURCES_STARTED = 'FETCH_DATASOURCES_STARTED'; +export function fetchDatasourcesStarted() { + return { type: FETCH_DATASOURCES_STARTED }; +} + +export const FETCH_DATASOURCES_SUCCEEDED = 'FETCH_DATASOURCES_SUCCEEDED'; +export function fetchDatasourcesSucceeded() { + return { type: FETCH_DATASOURCES_SUCCEEDED }; +} + +export const FETCH_DATASOURCES_FAILED = 'FETCH_DATASOURCES_FAILED'; +export function fetchDatasourcesFailed(error) { + return { type: FETCH_DATASOURCES_FAILED, error }; +} + export const RESET_FIELDS = 'RESET_FIELDS'; export function resetControls() { return { type: RESET_FIELDS }; @@ -61,6 +81,24 @@ export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) }; } +export function fetchDatasources() { + return function (dispatch) { + dispatch(fetchDatasourcesStarted()); + const url = '/superset/datasources/'; + $.ajax({ + type: 'GET', + url, + success: (data) => { + dispatch(setDatasources(data)); + dispatch(fetchDatasourcesSucceeded()); + }, + error(error) { + dispatch(fetchDatasourcesFailed(error.responseJSON.error)); + }, + }); + }; +} + export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR'; export function toggleFaveStar(isStarred) { return { type: TOGGLE_FAVE_STAR, isStarred }; diff --git a/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx index 5512b96722a58..0b0839778a238 100644 --- a/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx @@ -4,24 +4,30 @@ import { Checkbox } from 'react-bootstrap'; import sinon from 'sinon'; import { expect } from 'chai'; import { describe, it, beforeEach } from 'mocha'; -import { mount } from 'enzyme'; +import { shallow } from 'enzyme'; import CheckboxControl from '../../../../javascripts/explore/components/controls/CheckboxControl'; +import ControlHeader from '../../../../javascripts/explore/components/ControlHeader'; const defaultProps = { name: 'show_legend', onChange: sinon.spy(), value: false, + label: 'checkbox label', }; describe('CheckboxControl', () => { let wrapper; beforeEach(() => { - wrapper = mount(); + wrapper = shallow(); }); it('renders a Checkbox', () => { - expect(wrapper.find(Checkbox)).to.have.lengthOf(1); + const controlHeader = wrapper.find(ControlHeader); + expect(controlHeader).to.have.lengthOf(1); + + const headerWrapper = controlHeader.shallow(); + expect(headerWrapper.find(Checkbox)).to.have.length(1); }); }); diff --git a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx index e0a1a845130d4..e548d21a60a0a 100644 --- a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx @@ -1,33 +1,57 @@ import React from 'react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + import { expect } from 'chai'; import { describe, it, beforeEach } from 'mocha'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { Modal, Button, Radio } from 'react-bootstrap'; import sinon from 'sinon'; -import { defaultFormData } from '../../../../javascripts/explore/stores/store'; -import { SaveModal } from '../../../../javascripts/explore/components/SaveModal'; - -const defaultProps = { - can_edit: true, - onHide: () => ({}), - actions: { - saveSlice: sinon.spy(), - }, - form_data: defaultFormData, - user_id: '1', - dashboards: [], - slice: {}, -}; +import * as exploreUtils from '../../../../javascripts/explore/exploreUtils'; +import * as saveModalActions from '../../../../javascripts/explore/actions/saveModalActions'; +import SaveModal from '../../../../javascripts/explore/components/SaveModal'; + +const $ = window.$ = require('jquery'); describe('SaveModal', () => { - let wrapper; + const middlewares = [thunk]; + const mockStore = configureStore(middlewares); + const initialState = { + chart: {}, + saveModal: { + dashboards: [], + }, + explore: { + can_overwrite: true, + user_id: '1', + datasource: {}, + slice: { + slice_id: 1, + slice_name: 'title', + }, + alert: null, + }, + }; + const store = mockStore(initialState); - beforeEach(() => { - wrapper = shallow(); - }); + const defaultProps = { + onHide: () => ({}), + actions: saveModalActions, + form_data: {}, + }; + const mockEvent = { + target: { + value: 'mock event target', + }, + value: 'mock value', + }; + const getWrapper = () => (shallow(, { + context: { store }, + }).dive()); it('renders a Modal with 7 inputs and 2 buttons', () => { + const wrapper = getWrapper(); expect(wrapper.find(Modal)).to.have.lengthOf(1); expect(wrapper.find('input')).to.have.lengthOf(2); expect(wrapper.find(Button)).to.have.lengthOf(2); @@ -35,42 +59,167 @@ describe('SaveModal', () => { }); it('does not show overwrite option for new slice', () => { - defaultProps.slice = null; - const wrapperNewSlice = shallow(); + const wrapperNewSlice = getWrapper(); + wrapperNewSlice.setProps({ slice: null }); expect(wrapperNewSlice.find('#overwrite-radio')).to.have.lengthOf(0); expect(wrapperNewSlice.find('#saveas-radio')).to.have.lengthOf(1); }); it('disable overwrite option for non-owner', () => { - defaultProps.slice = {}; - defaultProps.can_overwrite = false; - const wrapperForNonOwner = shallow(); + const wrapperForNonOwner = getWrapper(); + wrapperForNonOwner.setProps({ can_overwrite: false }); const overwriteRadio = wrapperForNonOwner.find('#overwrite-radio'); expect(overwriteRadio).to.have.lengthOf(1); expect(overwriteRadio.prop('disabled')).to.equal(true); }); it('saves a new slice', () => { - defaultProps.slice = { - slice_id: 1, - slice_name: 'title', - }; - defaultProps.can_overwrite = false; - const wrapperForNewSlice = shallow(); + const wrapperForNewSlice = getWrapper(); + wrapperForNewSlice.setProps({ can_overwrite: false }); + wrapperForNewSlice.instance().changeAction('saveas'); const saveasRadio = wrapperForNewSlice.find('#saveas-radio'); saveasRadio.simulate('click'); expect(wrapperForNewSlice.state().action).to.equal('saveas'); }); it('overwrite a slice', () => { - defaultProps.slice = { - slice_id: 1, - slice_name: 'title', - }; - defaultProps.can_overwrite = true; - const wrapperForOverwrite = shallow(); + const wrapperForOverwrite = getWrapper(); const overwriteRadio = wrapperForOverwrite.find('#overwrite-radio'); overwriteRadio.simulate('click'); expect(wrapperForOverwrite.state().action).to.equal('overwrite'); }); + + it('componentDidMount', () => { + sinon.spy(SaveModal.prototype, 'componentDidMount'); + sinon.spy(saveModalActions, 'fetchDashboards'); + mount(, { + context: { store }, + }); + expect(SaveModal.prototype.componentDidMount.calledOnce).to.equal(true); + expect(saveModalActions.fetchDashboards.calledOnce).to.equal(true); + + SaveModal.prototype.componentDidMount.restore(); + saveModalActions.fetchDashboards.restore(); + }); + + it('onChange', () => { + const wrapper = getWrapper(); + + wrapper.instance().onChange('newSliceName', mockEvent); + expect(wrapper.state().newSliceName).to.equal(mockEvent.target.value); + + wrapper.instance().onChange('saveToDashboardId', mockEvent); + expect(wrapper.state().saveToDashboardId).to.equal(mockEvent.value); + + wrapper.instance().onChange('newDashboardName', mockEvent); + expect(wrapper.state().newDashboardName).to.equal(mockEvent.target.value); + }); + + describe('saveOrOverwrite', () => { + beforeEach(() => { + sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL')); + sinon.stub(saveModalActions, 'saveSlice').callsFake(() => { + const d = $.Deferred(); + d.resolve('done'); + return d.promise(); + }); + }); + afterEach(() => { + exploreUtils.getExploreUrl.restore(); + saveModalActions.saveSlice.restore(); + }); + + it('should save slice', () => { + const wrapper = getWrapper(); + wrapper.instance().saveOrOverwrite(true); + expect(saveModalActions.saveSlice.getCall(0).args[0]).to.equal('mockURL'); + }); + it('existing dashboard', () => { + const wrapper = getWrapper(); + const saveToDashboardId = 100; + + wrapper.setState({ addToDash: 'existing' }); + wrapper.instance().saveOrOverwrite(true); + expect(wrapper.state().alert).to.equal('Please select a dashboard'); + + wrapper.setState({ saveToDashboardId }); + wrapper.instance().saveOrOverwrite(true); + const args = exploreUtils.getExploreUrl.getCall(0).args; + expect(args[4].save_to_dashboard_id).to.equal(saveToDashboardId); + }); + it('new dashboard', () => { + const wrapper = getWrapper(); + const newDashboardName = 'new dashboard name'; + + wrapper.setState({ addToDash: 'new' }); + wrapper.instance().saveOrOverwrite(true); + expect(wrapper.state().alert).to.equal('Please enter a dashboard name'); + + wrapper.setState({ newDashboardName }); + wrapper.instance().saveOrOverwrite(true); + const args = exploreUtils.getExploreUrl.getCall(0).args; + expect(args[4].new_dashboard_name).to.equal(newDashboardName); + }); + }); + + describe('should fetchDashboards', () => { + let dispatch; + let request; + let ajaxStub; + const userID = 1; + beforeEach(() => { + dispatch = sinon.spy(); + ajaxStub = sinon.stub($, 'ajax'); + }); + afterEach(() => { + ajaxStub.restore(); + }); + const mockDashboardData = { + pks: ['value'], + result: [ + { dashboard_title: 'dashboard title' }, + ], + }; + const makeRequest = () => { + request = saveModalActions.fetchDashboards(userID); + request(dispatch); + }; + + it('makes the ajax request', () => { + makeRequest(); + expect(ajaxStub.callCount).to.equal(1); + }); + + it('calls correct url', () => { + const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userID; + makeRequest(); + expect(ajaxStub.getCall(0).args[0].url).to.be.equal(url); + }); + + it('calls correct actions on error', () => { + ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } }); + makeRequest(); + expect(dispatch.callCount).to.equal(1); + expect(dispatch.getCall(0).args[0].type).to.equal(saveModalActions.FETCH_DASHBOARDS_FAILED); + }); + + it('calls correct actions on success', () => { + ajaxStub.yieldsTo('success', mockDashboardData); + makeRequest(); + expect(dispatch.callCount).to.equal(1); + expect(dispatch.getCall(0).args[0].type) + .to.equal(saveModalActions.FETCH_DASHBOARDS_SUCCEEDED); + }); + }); + + it('removeAlert', () => { + sinon.spy(saveModalActions, 'removeSaveModalAlert'); + const wrapper = getWrapper(); + wrapper.setProps({ alert: 'old alert' }); + + wrapper.instance().removeAlert(); + expect(saveModalActions.removeSaveModalAlert.callCount).to.equal(1); + expect(wrapper.state().alert).to.be.a('null'); + saveModalActions.removeSaveModalAlert.restore(); + }); }); diff --git a/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx b/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx index f2729db1163f2..74d0d041a87bb 100644 --- a/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx @@ -1,7 +1,9 @@ import React from 'react'; import { expect } from 'chai'; import { describe, it } from 'mocha'; +import { shallow } from 'enzyme'; +import { OverlayTrigger } from 'react-bootstrap'; import URLShortLinkButton from '../../../../javascripts/explore/components/URLShortLinkButton'; describe('URLShortLinkButton', () => { @@ -14,4 +16,8 @@ describe('URLShortLinkButton', () => { it('renders', () => { expect(React.isValidElement()).to.equal(true); }); + it('renders OverlayTrigger', () => { + const wrapper = shallow(); + expect(wrapper.find(OverlayTrigger)).have.length(1); + }); });