import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { Modal } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
import $ from 'jquery';
import shortid from 'shortid';
import { queries } from './fixtures';
import { sqlLabReducer } from '../../../javascripts/SqlLab/reducers';
import * as actions from '../../../javascripts/SqlLab/actions';
import { VISUALIZE_VALIDATION_ERRORS } from '../../../javascripts/SqlLab/constants';
import VisualizeModal from '../../../javascripts/SqlLab/components/VisualizeModal';
import * as exploreUtils from '../../../javascripts/explore/exploreUtils';
global.notify = {
info: () => {},
error: () => {},
};
describe('VisualizeModal', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const initialState = sqlLabReducer(undefined, {});
const store = mockStore(initialState);
const mockedProps = {
show: true,
query: queries[0],
};
const mockColumns = {
ds: {
is_date: true,
is_dim: false,
name: 'ds',
type: 'STRING',
},
gender: {
is_date: false,
is_dim: true,
name: 'gender',
type: 'STRING',
},
};
const mockChartTypeBarChart = {
label: 'Distribution - Bar Chart',
requiresTime: false,
value: 'dist_bar',
};
const mockChartTypeTB = {
label: 'Time Series - Bar Chart',
requiresTime: true,
value: 'bar',
};
const mockEvent = {
target: {
value: 'mock event value',
},
};
const getVisualizeModalWrapper = () => (
shallow(, {
context: { store },
}).dive());
it('renders', () => {
expect(React.isValidElement()).to.equal(true);
});
it('renders with props', () => {
expect(
React.isValidElement(),
).to.equal(true);
});
it('renders a Modal', () => {
const wrapper = getVisualizeModalWrapper();
expect(wrapper.find(Modal)).to.have.length(1);
});
describe('getColumnFromProps', () => {
it('should require valid query parameter in props', () => {
const emptyQuery = {
show: true,
query: {},
};
const wrapper = shallow(, {
context: { store },
}).dive();
expect(wrapper.state().columns).to.deep.equal({});
});
it('should set columns state', () => {
const wrapper = getVisualizeModalWrapper();
expect(wrapper.state().columns).to.deep.equal(mockColumns);
});
it('should not change columns state when closing Modal', () => {
const wrapper = getVisualizeModalWrapper();
expect(wrapper.state().columns).to.deep.equal(mockColumns);
// first change columns state
const newColumns = {
ds: {
is_date: true,
is_dim: false,
name: 'ds',
type: 'STRING',
},
name: {
is_date: false,
is_dim: true,
name: 'name',
type: 'STRING',
},
};
wrapper.instance().setState({ columns: newColumns });
// then close Modal
wrapper.setProps({ show: false });
expect(wrapper.state().columns).to.deep.equal(newColumns);
});
});
describe('datasourceName', () => {
const wrapper = getVisualizeModalWrapper();
let stub;
beforeEach(() => {
stub = sinon.stub(shortid, 'generate').returns('abcd');
});
afterEach(() => {
stub.restore();
});
it('should generate data source name from query', () => {
const sampleQuery = queries[0];
const name = wrapper.instance().datasourceName();
expect(name).to.equal(`${sampleQuery.user}-${sampleQuery.db}-${sampleQuery.tab}-abcd`);
});
it('should generate data source name with empty query', () => {
wrapper.setProps({ query: {} });
const name = wrapper.instance().datasourceName();
expect(name).to.equal('undefined-abcd');
});
});
describe('mergedColumns', () => {
const wrapper = getVisualizeModalWrapper();
const oldColumns = {
ds: 1,
gender: 2,
};
it('should merge by column name', () => {
wrapper.setState({ columns: {} });
const mc = wrapper.instance().mergedColumns();
expect(mc).to.deep.equal(mockColumns);
});
it('should not override current state', () => {
wrapper.setState({ columns: oldColumns });
const mc = wrapper.instance().mergedColumns();
expect(mc.ds).to.equal(oldColumns.ds);
expect(mc.gender).to.equal(oldColumns.gender);
});
});
describe('validate', () => {
const wrapper = getVisualizeModalWrapper();
let columnsStub;
beforeEach(() => {
columnsStub = sinon.stub(wrapper.instance(), 'mergedColumns');
});
afterEach(() => {
columnsStub.restore();
});
it('should validate column name', () => {
columnsStub.returns(mockColumns);
wrapper.instance().validate();
expect(wrapper.state().hints).to.have.length(0);
wrapper.instance().mergedColumns.restore();
});
it('should hint invalid column name', () => {
columnsStub.returns({
'&': 1,
});
wrapper.instance().validate();
expect(wrapper.state().hints).to.have.length(1);
wrapper.instance().mergedColumns.restore();
});
it('should hint empty chartType', () => {
columnsStub.returns(mockColumns);
wrapper.setState({ chartType: null });
wrapper.instance().validate();
expect(wrapper.state().hints).to.have.length(1);
expect(wrapper.state().hints[0])
.to.have.string(VISUALIZE_VALIDATION_ERRORS.REQUIRE_CHART_TYPE);
});
it('should check time series', () => {
columnsStub.returns(mockColumns);
wrapper.setState({ chartType: mockChartTypeTB });
wrapper.instance().validate();
expect(wrapper.state().hints).to.have.length(0);
// no is_date columns
columnsStub.returns({
ds: {
is_date: false,
is_dim: false,
name: 'ds',
type: 'STRING',
},
gender: {
is_date: false,
is_dim: true,
name: 'gender',
type: 'STRING',
},
});
wrapper.setState({ chartType: mockChartTypeTB });
wrapper.instance().validate();
expect(wrapper.state().hints).to.have.length(1);
expect(wrapper.state().hints[0]).to.have.string(VISUALIZE_VALIDATION_ERRORS.REQUIRE_TIME);
});
it('should validate after change checkbox', () => {
const spy = sinon.spy(wrapper.instance(), 'validate');
columnsStub.returns(mockColumns);
wrapper.instance().changeCheckbox('is_dim', 'gender', mockEvent);
expect(spy.callCount).to.equal(1);
spy.restore();
});
it('should validate after change Agg function', () => {
const spy = sinon.spy(wrapper.instance(), 'validate');
columnsStub.returns(mockColumns);
wrapper.instance().changeAggFunction('num', { label: 'MIN(x)', value: 'min' });
expect(spy.callCount).to.equal(1);
spy.restore();
});
});
it('should validate after change chart type', () => {
const wrapper = getVisualizeModalWrapper();
wrapper.setState({ chartType: mockChartTypeTB });
const spy = sinon.spy(wrapper.instance(), 'validate');
wrapper.instance().changeChartType(mockChartTypeBarChart);
expect(spy.callCount).to.equal(1);
expect(wrapper.state().chartType).to.equal(mockChartTypeBarChart);
});
it('should validate after change datasource name', () => {
const wrapper = getVisualizeModalWrapper();
const spy = sinon.spy(wrapper.instance(), 'validate');
wrapper.instance().changeDatasourceName(mockEvent);
expect(spy.callCount).to.equal(1);
expect(wrapper.state().datasourceName).to.equal(mockEvent.target.value);
});
it('should build viz options', () => {
const wrapper = getVisualizeModalWrapper();
wrapper.setState({ chartType: mockChartTypeTB });
const spy = sinon.spy(wrapper.instance(), 'buildVizOptions');
wrapper.instance().buildVizOptions();
expect(spy.returnValues[0]).to.deep.equal({
chartType: wrapper.state().chartType.value,
datasourceName: wrapper.state().datasourceName,
columns: wrapper.state().columns,
sql: wrapper.instance().props.query.sql,
dbId: wrapper.instance().props.query.dbId,
});
});
it('should build visualize advise for long query', () => {
const longQuery = Object.assign({}, queries[0], { endDttm: 1476910666798 });
const props = {
show: true,
query: longQuery,
};
const longQueryWrapper = shallow(, {
context: { store },
}).dive();
const alertWrapper = shallow(longQueryWrapper.instance().buildVisualizeAdvise());
expect(alertWrapper.hasClass('alert')).to.equal(true);
expect(alertWrapper.text()).to.contain(
'This query took 101 seconds to run, and the explore view times out at 45 seconds');
});
it('should not build visualize advise', () => {
const wrapper = getVisualizeModalWrapper();
expect(wrapper.instance().buildVisualizeAdvise()).to.be.a('undefined');
});
describe('visualize', () => {
const wrapper = getVisualizeModalWrapper();
const mockOptions = { attr: 'mockOptions' };
wrapper.setState({
chartType: mockChartTypeBarChart,
columns: mockColumns,
datasourceName: 'mockDatasourceName',
});
let ajaxSpy;
let datasourceSpy;
beforeEach(() => {
ajaxSpy = sinon.spy($, 'ajax');
sinon.stub(JSON, 'parse').callsFake(() => ({ table_id: 107 }));
sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL'));
sinon.stub(wrapper.instance(), 'buildVizOptions').callsFake(() => (mockOptions));
sinon.spy(window, 'open');
datasourceSpy = sinon.stub(actions, 'createDatasource');
});
afterEach(() => {
ajaxSpy.restore();
JSON.parse.restore();
exploreUtils.getExploreUrl.restore();
wrapper.instance().buildVizOptions.restore();
window.open.restore();
datasourceSpy.restore();
});
it('should build request', () => {
wrapper.instance().visualize();
expect(ajaxSpy.callCount).to.equal(1);
const spyCall = ajaxSpy.getCall(0);
expect(spyCall.args[0].type).to.equal('POST');
expect(spyCall.args[0].url).to.equal('/superset/sqllab_viz/');
expect(spyCall.args[0].data.data).to.equal(JSON.stringify(mockOptions));
});
it('should open new window', () => {
datasourceSpy.callsFake(() => {
const d = $.Deferred();
d.resolve('done');
return d.promise();
});
wrapper.setProps({ actions: { createDatasource: datasourceSpy } });
wrapper.instance().visualize();
expect(window.open.callCount).to.equal(1);
});
it('should notify error', () => {
datasourceSpy.callsFake(() => {
const d = $.Deferred();
d.reject('error message');
return d.promise();
});
wrapper.setProps({ actions: { createDatasource: datasourceSpy } });
sinon.spy(notify, 'error');
wrapper.instance().visualize();
expect(window.open.callCount).to.equal(0);
expect(notify.error.callCount).to.equal(1);
});
});
});