Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore view save modal spec #3110

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8c3411c
split reducer logic for ExploreViewContainer
Jul 5, 2017
f129f53
fix saveModal component and unit tests
Jul 5, 2017
fa3df04
revert changes in SaveModal_spec.
Jul 7, 2017
dfdc2d4
improve test coverage for explore view components:
Jul 7, 2017
90502f6
remove comment-out code
Jul 11, 2017
cf66782
Merge remote-tracking branch 'upstream/master' into gg-ExploreViewSpl…
Aug 7, 2017
b6da30f
Merge remote-tracking branch 'supercat/gg-ExploreViewSplitReducerLogi…
Aug 7, 2017
bfbee82
[bugfix] wrong 'Cant have overlap between Series and Breakdowns' (#3254)
mistercrunch Aug 8, 2017
367a4da
[explore] make edit datasource a basic link (#3244)
mistercrunch Aug 8, 2017
ab19f1f
import logging (#3264)
Aug 9, 2017
de1ab25
Relying on FAB for font-awesome.min.css (#3261)
mistercrunch Aug 9, 2017
c2f3c48
Explicitly add Flask as dependancy (#3252)
Fokko Aug 9, 2017
479b319
Modernize SQLA pessimistic handling (#3256)
mistercrunch Aug 9, 2017
976ed1c
Improve the chart type of Visualize in sqllab (#3241)
eeve Aug 9, 2017
bff35d9
[webpack] break CSS and JS files while webpackin' (#3262)
mistercrunch Aug 9, 2017
3266749
Set default ports Druid (#3266)
Fokko Aug 10, 2017
5877883
[bugfix] preserve order in groupby (#3268)
mistercrunch Aug 10, 2017
3d78940
Use sane Celery defaults to prevent tasks from being delayed (#3267)
saguziel Aug 10, 2017
707ee37
[explore] Split large reducer logic in ExploreViewContainer (#3088)
Aug 10, 2017
8fa7db8
Merge remote-tracking branch 'upstream/master' into gg-ExploreViewSav…
Aug 10, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions superset/assets/javascripts/explore/actions/exploreActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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 };
Expand Down Expand Up @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CheckboxControl {...defaultProps} />);
wrapper = shallow(<CheckboxControl {...defaultProps} />);
});

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);
});
});
221 changes: 185 additions & 36 deletions superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,225 @@
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(<SaveModal {...defaultProps} />);
});
const defaultProps = {
onHide: () => ({}),
actions: saveModalActions,
form_data: {},
};
const mockEvent = {
target: {
value: 'mock event target',
},
value: 'mock value',
};
const getWrapper = () => (shallow(<SaveModal {...defaultProps} />, {
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);
expect(wrapper.find(Radio)).to.have.lengthOf(5);
});

it('does not show overwrite option for new slice', () => {
defaultProps.slice = null;
const wrapperNewSlice = shallow(<SaveModal {...defaultProps} />);
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(<SaveModal {...defaultProps} />);
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(<SaveModal {...defaultProps} />);
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(<SaveModal {...defaultProps} />);
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(<SaveModal {...defaultProps} />, {
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();
});
});
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -14,4 +16,8 @@ describe('URLShortLinkButton', () => {
it('renders', () => {
expect(React.isValidElement(<URLShortLinkButton {...defaultProps} />)).to.equal(true);
});
it('renders OverlayTrigger', () => {
const wrapper = shallow(<URLShortLinkButton {...defaultProps} />);
expect(wrapper.find(OverlayTrigger)).have.length(1);
});
});