mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
[wip] dashboard builder v2 (#4528)
* [dashboard builder] Add dir structure for dashboard/v2, simplified Header, split pane, Draggable side panel [grid] add <DashboardGrid />, <ResizableContainer />, and initial grid components. [grid] gridComponents/ directory, add fixtures/ directory and test layout, add <Column /> [grid] working grid with gutters [grid] design tweaks and polish, add <Tabs /> [header] add gradient header logo and favicon [dnd] begin adding dnd functionality [dnd] add util/isValidChild.js [react-beautiful-dnd] iterate on dnd until blocked [dnd] refactor to use react-dnd [react-dnd] refactor to use composable <DashboardComponent /> structure [dnd] factor out DashboardComponent, let components render dropInidcator and set draggableRef, add draggable tabs [dnd] refactor to use redux, add DashboardComponent and DashboardGrid containers [dragdroppable] rename horizontal/vertical => row/column [builder] refactor into HoverMenu, add WithPopoverMenu [builder] add editable header and disableDragDrop prop for Dragdroppable's [builder] make tabs editable [builder] add generic popover dropdown and header row style editability [builder] add hover rowStyle dropdown, make row styles editable [builder] add some new component icons, add popover with delete to charts [builder] add preview icons, add popover menu to rows. [builder] add IconButton and RowStyleDropdown [resizable] use ResizableContainer instead of DimensionProvider, fix resize and delete bugs [builder] fix bug with spacer [builder] clean up, header.size => header.headerSize [builder] support more drag/drop combinations by wrapping some components in rows upon drop. fix within list drop index. refactor some utils. [builder][tabs] fix broken add tab button [dashboard builder] don't pass dashboard layout to all dashboard components, improve drop indicator logic, fix delete component pure component bug [dnd] refactor drop position logic * fix rebase error, clean up css organization and use @less vars * [dashboard-builder] add top-level tabs + undo-redo (#4626) * [top-level-tabs] initial working version of top-level tabs * [top-level-tabs] simplify redux and disable ability to displace top-level tabs with other tabs * [top-level-tabs] improve tab drag and drop css * [undo-redo] add redux undo redo * [dnd] clean up dropResult shape, add new component source id + type, use css for drop indicator instead of styles and fix tab indicators. * [top-level-tabs] add 'Collapse tab content' to delete tabs button * [dnd] add depth validation to drag and drop logic * [dashboard-builder] add resize action, enforce minimum width of columns, column children inherit column size when necessary, meta.rowStyle => meta.background, add background to columns * [dashboard-builder] make sure getChildWidth returns a number * [dashboard builder] static layout + toasts (#4763) * [dashboard-builder] remove spacer component * [dashboard-builder] better transparent indicator, better grid gutter logic, no dragging top-level tabs, headers are multiples of grid unit, fix row height granularity, update redux state key dashboard => dashboardLayout * [dashboard-builder] don't blast column child dimensions on resize * [dashboard-builder] ResizableContainer min size can't be smaller than size, fix row style, role=none on WithPopoverMenu container * [edit mode] add edit mode to redux and propogate to all <DashboardComponent />s * [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers * [dashboard-builder] add info toast when dropResult overflows parent * [dashboard builder] git mv to src/ post-rebase * Dashboard builder rebased + linted (#4849) * define dashboard redux state * update dashboard state reducer * dashboard layout converter + grid render * builder pane + slice adder * Dashboard header + slice header controls * fix linting * 2nd code review comments * [dashboard builder] improve perf (#4855) * address major perf + css issues [dashboard builder] fix dashboard filters and some css [dashboard builder] use VIZ_TYPES, move stricter .eslintrc to dashboard/, more css fixes [builder] delete GridCell and GridLayout, remove some unused css. fix broken tabs. * [builder] fix errors post-rebase * [builder] add support for custom DragDroppable drag layer and add AddSliceDragPreview * [AddSliceDragPreview] fix type check * [dashboard builder] add prettier and update all files * [dashboard builder] merge v2/ directory int dashboard/ * [dashboard builder] move component/*Container => containers/* * add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893) * dashboard header, slice header UI improvement * add slider and sticky * dashboard header, slice header UI improvement * make builder pane floating * [dashboard builder] add sticky top-level tabs, refactor for performant tabs * [dashboard builder] visually distinct containers, icons for undo-redo, fix some isValidChild bugs * [dashboard builder] better undo redo <> save changes state, notify upon reaching undo limit * [dashboard builder] hook up edit + create component actions to saved-state pop. * [dashboard builder] visual refinement, refactor Dashboard header content and updates into layout for undo-redo, refactor save dashboard modal to use toasts instead of notify. * [dashboard builder] refactor chart name update logic to use layout for undo redo, save slice name changes on dashboard save * add slider and sticky * [dashboard builder] fix layout converter slice_id + chartId type casting, don't change grid size upon edit (perf) * [dashboard builder] don't set version key in getInitialState * [dashboard builder] make top level tabs addition/removal undoable, fix double sticky tabs + side panel. * [dashboard builder] fix sticky tabs offset bug * [dashboard builder] fix drag preview width, css polish, fix rebase issue * [dashboard builder] fix side pane labels and hove z-index * Markdown for dashboard (#4962) * fix dashboard server-side unit tests (#5009) * Dashboard save button (#4979) * save button * fix slices list height * save custom css * merge save-dash changes from dashboard v1 https://github.com/apache/incubator-superset/pull/4900 https://github.com/apache/incubator-superset/pull/5051 * [dashboard v2] check for default_filters before json_loads-ing them (#5064) [dashboard v2] check for default_filters before json-loads-ing them * [dashboard v2] fix bugs from rebase * [dashboard v2] tests! (#5066) * [dashboard v2][tests] add tests for newComponentFactory, isValidChild, dropOverflowsParent, and dnd-reorder * [dashboard v2][tests] add tests for componentIsResizable, findParentId, getChartIdsFromLayout, newEntitiesFromDrop, and getDropPosition * [dashboard v2][tests] add mockStore, mockState, and tests for DragDroppable, DashboardBuilder, DashboardGrid, ToastPresenter, and Toast * [dashboard builder][tests] separate files for state tree fixtures, add ChartHolder, Chart, Divider, Header, Row tests and WithDragDropContext helper * [dashboard v2][tests] fix dragdrop context with util/getDragDropManager, add test for menu/* and resizable/*, and new components * [dashboard v2][tests] fix and re-write Dashboard tests, add getFormDataWithExtraFilters_spec * [dashboard v2][tests] add reducer tests, fix lint error * [dashboard-v2][tests] add actions/dashboardLayout_spec * [dashboard v2] fix some prop bugs, open side pane on edit, fix slice name bug * [dashboard v2] fix slice name save bug * [dashboard v2] fix lint errors * [dashboard v2] fix filters bug and add test * [dashboard v2] fix getFormDataWithExtraFilters_spec * [dashboard v2] logging updates (#5087) * [dashboard v2] initial logging refactor * [dashboard v2] clean up logger * [logger] update explore with new log events, add refresh dashboard + refresh dashboard chart actions * [logging] add logger_spec.js, fix reducers/dashboardState_spec + gridComponents/Chart_spec * [dashboard v2][logging] refactor for bulk logging in python * [logging] tweak python, fix and remove dup start_offset entries * [dashboard v2][logging] add dashboard_first_load event * [dashboard v2][logging] add slice_ids to dashboard pane load event * [tests] fix npm test script * Fix: update slices list when add/remove multiple slices (#5138) * [dashboard v2] add v1 switch (#5126) * [dashboard] copy all dashboard v1 into working v1 switch * [dashboard] add functional v1 <> v2 switch with messaging * [dashboard] add v2 logging to v1 dashboard, add read-v2-changes link, add client logging to track v1 <> v2 switches * [dashboard] Remove default values for feedback url + v2 auto convert date * [dashboard v2] fix misc UI/UX issues * [dashboard v2] fix Markdown persistance issues and css, fix copy dash title, don't enforce shallow hovering with drop indicator * [dashboard v2] improve non-shallow drop target UX, fix Markdown drop indicator, clarify slice adder filter/sort * [dashboard v2] delete empty rows on drag or delete events that leave them without children, add test * [dashboard v2] improve v1<>v2 switch modals, add convert to v2 badge in v1, fix unsaved changes issue in preview mode, don't auto convert column child widths for now * [dashboard v2][dnd] add drop position cache to fix non-shallow drops * [dashboard] fix test script with glob instead of recurse, fix tests, add temp fix for tab nesting, ignore v1 lint errors * [dashboard] v2 badge style tweaks, add back v1 _set_dash_metadata for v1 editing * [dashboard] fix python linting and tests * [dashboard] lint tests * add slice from explore view (#5141) * Fix dashboard position row data (#5131) * add slice_name to markdown (cherry picked from commit 14b01f1) * set min grid width be 1 column * remove empty column * check total columns count <= 12 * scan position data and fix rows * fix dashboard url with default_filters * [dashboard v2] better grid drop ux, fix tab bugs 🐛 (#5151) * [dashboard v2] add empty droptarget to dashboard grid for better ux and update test * [dashboard] reset tab index upon top-level tab deletion, fix findparentid bug * [dashboard] update v1<>v2 modal link for tracking * Fix: Should pass slice_can_edit flag down (#5159) * [dash builder fix] combine markdown and slice name, slice picker height (#5165) * combine markdown code and markdown slice name * allow dynamic height for slice picker cell * add word break for long datasource name * [fix] new dashboard state (#5213) * [dashboard v2] ui + ux fixes (#5208) * [dashboard v2] use <Loading /> throughout, small loading gif, improve row/column visual hierarchy, add cached data pop * [dashboard v2] lots of polish * [dashboard v2] remove markdown padding on edit, more opaque slice drag preview, unsavedChanges=true upon moving a component, fix initial load logging. * [dashboard v2] gray loading.gif, sticky header, undo/redo keyboard shortcuts, fix move component saved changes update, v0 double scrollbar fix * [dashboard v2] move UndoRedoKeylisteners into Header, render only in edit mode, show visual feedback for keyboard shortcut, hide hover menu in top-level tabs * [dashboard v2] fix grid + sidepane height issues * [dashboard v2] add auto-resize functionality, update tests. cache findParentId results. * [dashboard v2][tests] add getDetailedComponentWidth_spec.js * [dashboard v2] fix lint * [fix] layout converter fix (#5218) * [fix] layout converter fix * add changed_on into initial sliceEntity data * add unit tests for SliceAdder component * remove old fixtures file * [dashboard v2] remove webpack-cli, fresh yarn.lock post-rebase * [dashboard v2] lint javascript * [dashboard v2] fix python tests * [Fix] import/export dash in V2 (#5273) * [dashboard v2] add markdown tests (#5275) * [dashboard v2] add Markdown tests * [dashboard v2][mocks] fix markdown mock
This commit is contained in:
@@ -20,7 +20,7 @@ describe('Chart', () => {
|
||||
};
|
||||
const mockedProps = {
|
||||
...chart,
|
||||
chartKey: 'slice_223',
|
||||
id: 223,
|
||||
containerId: 'slice-container-223',
|
||||
datasource: {},
|
||||
formData: {},
|
||||
|
||||
33
superset/assets/spec/javascripts/dashboard/.eslintrc
Normal file
33
superset/assets/spec/javascripts/dashboard/.eslintrc
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": "prettier",
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"prefer-template": 2,
|
||||
"new-cap": 2,
|
||||
"no-restricted-syntax": 2,
|
||||
"guard-for-in": 2,
|
||||
"prefer-arrow-callback": 2,
|
||||
"func-names": 2,
|
||||
"react/jsx-no-bind": 2,
|
||||
"no-confusing-arrow": 2,
|
||||
"jsx-a11y/no-static-element-interactions": 2,
|
||||
"jsx-a11y/anchor-has-content": 2,
|
||||
"react/require-default-props": 2,
|
||||
"no-plusplus": 2,
|
||||
"no-mixed-operators": 0,
|
||||
"no-continue": 2,
|
||||
"no-bitwise": 2,
|
||||
"no-undef": 2,
|
||||
"no-multi-assign": 2,
|
||||
"no-restricted-properties": 2,
|
||||
"no-prototype-builtins": 2,
|
||||
"jsx-a11y/href-no-hash": 2,
|
||||
"class-methods-use-this": 2,
|
||||
"import/no-named-as-default": 2,
|
||||
"import/prefer-default-export": 2,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"react/no-string-refs": 2,
|
||||
"react/jsx-indent": 0,
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
4
superset/assets/spec/javascripts/dashboard/.prettierrc
Normal file
4
superset/assets/spec/javascripts/dashboard/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import * as dashboardActions from '../../../src/dashboard/actions';
|
||||
import * as chartActions from '../../../src/chart/chartAction';
|
||||
import Dashboard from '../../../src/dashboard/components/Dashboard';
|
||||
import { defaultFilters, dashboard, charts } from './fixtures';
|
||||
|
||||
describe('Dashboard', () => {
|
||||
const mockedProps = {
|
||||
actions: { ...chartActions, ...dashboardActions },
|
||||
initMessages: [],
|
||||
dashboard: dashboard.dashboard,
|
||||
slices: charts,
|
||||
filters: dashboard.filters,
|
||||
datasources: dashboard.datasources,
|
||||
refresh: false,
|
||||
timeout: 60,
|
||||
isStarred: false,
|
||||
userId: dashboard.userId,
|
||||
};
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = shallow(<Dashboard {...mockedProps} />);
|
||||
expect(wrapper.find('#dashboard-container')).to.have.length(1);
|
||||
expect(wrapper.instance().getAllSlices()).to.have.length(3);
|
||||
});
|
||||
|
||||
it('should handle metadata default_filters', () => {
|
||||
const wrapper = shallow(<Dashboard {...mockedProps} />);
|
||||
expect(wrapper.instance().props.filters).deep.equal(defaultFilters);
|
||||
});
|
||||
|
||||
describe('getFormDataExtra', () => {
|
||||
let wrapper;
|
||||
let selectedSlice;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<Dashboard {...mockedProps} />);
|
||||
selectedSlice = wrapper.instance().props.dashboard.slices[1];
|
||||
});
|
||||
|
||||
it('should carry default_filters', () => {
|
||||
const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
|
||||
expect(extraFilters[0]).to.deep.equal({ col: 'region', op: 'in', val: [] });
|
||||
expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['United States'] });
|
||||
});
|
||||
|
||||
it('should carry updated filter', () => {
|
||||
wrapper.setProps({
|
||||
filters: {
|
||||
256: { region: [] },
|
||||
257: { country_name: ['France'] },
|
||||
},
|
||||
});
|
||||
const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
|
||||
expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['France'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshExcept', () => {
|
||||
let wrapper;
|
||||
let spy;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<Dashboard {...mockedProps} />);
|
||||
spy = sinon.spy(wrapper.instance(), 'fetchSlices');
|
||||
});
|
||||
afterEach(() => {
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('should not refresh filter slice', () => {
|
||||
const filterKey = Object.keys(defaultFilters)[1];
|
||||
wrapper.instance().refreshExcept(filterKey);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
expect(spy.getCall(0).args[0].length).to.equal(1);
|
||||
});
|
||||
|
||||
it('should refresh all slices', () => {
|
||||
wrapper.instance().refreshExcept();
|
||||
expect(spy.callCount).to.equal(1);
|
||||
expect(spy.getCall(0).args[0].length).to.equal(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentDidUpdate', () => {
|
||||
let wrapper;
|
||||
let refreshExceptSpy;
|
||||
let fetchSlicesStub;
|
||||
let prevProp;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<Dashboard {...mockedProps} />);
|
||||
prevProp = wrapper.instance().props;
|
||||
refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
fetchSlicesStub = sinon.stub(wrapper.instance(), 'fetchSlices');
|
||||
});
|
||||
afterEach(() => {
|
||||
fetchSlicesStub.restore();
|
||||
refreshExceptSpy.restore();
|
||||
});
|
||||
|
||||
describe('should check if filter has change', () => {
|
||||
beforeEach(() => {
|
||||
refreshExceptSpy.reset();
|
||||
});
|
||||
it('no change', () => {
|
||||
wrapper.setProps({
|
||||
refresh: true,
|
||||
filters: {
|
||||
256: { region: [] },
|
||||
257: { country_name: ['United States'] },
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProp);
|
||||
expect(refreshExceptSpy.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('remove filter', () => {
|
||||
wrapper.setProps({
|
||||
refresh: true,
|
||||
filters: {
|
||||
256: { region: [] },
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProp);
|
||||
expect(refreshExceptSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('change filter', () => {
|
||||
wrapper.setProps({
|
||||
refresh: true,
|
||||
filters: {
|
||||
256: { region: [] },
|
||||
257: { country_name: ['Canada'] },
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProp);
|
||||
expect(refreshExceptSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('add filter', () => {
|
||||
wrapper.setProps({
|
||||
refresh: true,
|
||||
filters: {
|
||||
256: { region: [] },
|
||||
257: { country_name: ['Canada'] },
|
||||
258: { another_filter: ['new'] },
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProp);
|
||||
expect(refreshExceptSpy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should refresh if refresh flag is true', () => {
|
||||
wrapper.setProps({
|
||||
refresh: true,
|
||||
filters: {
|
||||
256: { region: ['Asian'] },
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProp);
|
||||
const fetchArgs = fetchSlicesStub.lastCall.args[0];
|
||||
expect(fetchArgs).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should not refresh filter_immune_slices', () => {
|
||||
wrapper.setProps({
|
||||
refresh: true,
|
||||
filters: {
|
||||
256: { region: [] },
|
||||
257: { country_name: ['Canada'] },
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProp);
|
||||
const fetchArgs = fetchSlicesStub.lastCall.args[0];
|
||||
expect(fetchArgs).to.have.length(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,454 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { ActionCreators as UndoActionCreators } from 'redux-undo';
|
||||
|
||||
import {
|
||||
UPDATE_COMPONENTS,
|
||||
updateComponents,
|
||||
DELETE_COMPONENT,
|
||||
deleteComponent,
|
||||
CREATE_COMPONENT,
|
||||
CREATE_TOP_LEVEL_TABS,
|
||||
createTopLevelTabs,
|
||||
DELETE_TOP_LEVEL_TABS,
|
||||
deleteTopLevelTabs,
|
||||
resizeComponent,
|
||||
MOVE_COMPONENT,
|
||||
handleComponentDrop,
|
||||
updateDashboardTitle,
|
||||
undoLayoutAction,
|
||||
redoLayoutAction,
|
||||
} from '../../../../src/dashboard/actions/dashboardLayout';
|
||||
|
||||
import { setUnsavedChanges } from '../../../../src/dashboard/actions/dashboardState';
|
||||
import { addInfoToast } from '../../../../src/dashboard/actions/messageToasts';
|
||||
|
||||
import {
|
||||
DASHBOARD_GRID_TYPE,
|
||||
ROW_TYPE,
|
||||
CHART_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
import {
|
||||
DASHBOARD_HEADER_ID,
|
||||
DASHBOARD_GRID_ID,
|
||||
DASHBOARD_ROOT_ID,
|
||||
NEW_COMPONENTS_SOURCE_ID,
|
||||
NEW_ROW_ID,
|
||||
} from '../../../../src/dashboard/util/constants';
|
||||
|
||||
describe('dashboardLayout actions', () => {
|
||||
const mockState = {
|
||||
dashboardState: {
|
||||
hasUnsavedChanges: true, // don't dispatch setUnsavedChanges() after every action
|
||||
},
|
||||
dashboardInfo: {},
|
||||
dashboardLayout: {
|
||||
past: [],
|
||||
present: {},
|
||||
future: {},
|
||||
},
|
||||
};
|
||||
|
||||
function setup(stateOverrides) {
|
||||
const state = { ...mockState, ...stateOverrides };
|
||||
const getState = sinon.spy(() => state);
|
||||
const dispatch = sinon.spy();
|
||||
|
||||
return { getState, dispatch, state };
|
||||
}
|
||||
|
||||
describe('updateComponents', () => {
|
||||
it('should dispatch an updateLayout action', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const nextComponents = { 1: {} };
|
||||
const thunk = updateComponents(nextComponents);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
expect(dispatch.getCall(0).args[0]).to.deep.equal({
|
||||
type: UPDATE_COMPONENTS,
|
||||
payload: { nextComponents },
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardState: { hasUnsavedChanges: false },
|
||||
});
|
||||
const nextComponents = { 1: {} };
|
||||
const thunk = updateComponents(nextComponents);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal(
|
||||
setUnsavedChanges(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteComponents', () => {
|
||||
it('should dispatch an deleteComponent action', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const thunk = deleteComponent('id', 'parentId');
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
expect(dispatch.getCall(0).args[0]).to.deep.equal({
|
||||
type: DELETE_COMPONENT,
|
||||
payload: { id: 'id', parentId: 'parentId' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardState: { hasUnsavedChanges: false },
|
||||
});
|
||||
const thunk = deleteComponent('id', 'parentId');
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal(
|
||||
setUnsavedChanges(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDashboardTitle', () => {
|
||||
it('should dispatch an updateComponent action for the header component', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const thunk1 = updateDashboardTitle('new text');
|
||||
thunk1(dispatch, getState);
|
||||
|
||||
const thunk2 = dispatch.getCall(0).args[0];
|
||||
thunk2(dispatch, getState);
|
||||
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal({
|
||||
type: UPDATE_COMPONENTS,
|
||||
payload: {
|
||||
nextComponents: {
|
||||
[DASHBOARD_HEADER_ID]: {
|
||||
meta: { text: 'new text' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTopLevelTabs', () => {
|
||||
it('should dispatch a createTopLevelTabs action', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const dropResult = {};
|
||||
const thunk = createTopLevelTabs(dropResult);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
expect(dispatch.getCall(0).args[0]).to.deep.equal({
|
||||
type: CREATE_TOP_LEVEL_TABS,
|
||||
payload: { dropResult },
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardState: { hasUnsavedChanges: false },
|
||||
});
|
||||
const dropResult = {};
|
||||
const thunk = createTopLevelTabs(dropResult);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal(
|
||||
setUnsavedChanges(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTopLevelTabs', () => {
|
||||
it('should dispatch a deleteTopLevelTabs action', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const dropResult = {};
|
||||
const thunk = deleteTopLevelTabs(dropResult);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
expect(dispatch.getCall(0).args[0]).to.deep.equal({
|
||||
type: DELETE_TOP_LEVEL_TABS,
|
||||
payload: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardState: { hasUnsavedChanges: false },
|
||||
});
|
||||
const dropResult = {};
|
||||
const thunk = deleteTopLevelTabs(dropResult);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal(
|
||||
setUnsavedChanges(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resizeComponent', () => {
|
||||
const dashboardLayout = {
|
||||
...mockState.dashboardLayout,
|
||||
present: {
|
||||
1: {
|
||||
id: 1,
|
||||
children: [],
|
||||
meta: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should update the size of the component', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardLayout,
|
||||
});
|
||||
|
||||
const thunk1 = resizeComponent({ id: 1, width: 10, height: 3 });
|
||||
thunk1(dispatch, getState);
|
||||
|
||||
const thunk2 = dispatch.getCall(0).args[0];
|
||||
thunk2(dispatch, getState);
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal({
|
||||
type: UPDATE_COMPONENTS,
|
||||
payload: {
|
||||
nextComponents: {
|
||||
1: {
|
||||
id: 1,
|
||||
children: [],
|
||||
meta: {
|
||||
width: 10,
|
||||
height: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardState: { hasUnsavedChanges: false },
|
||||
dashboardLayout,
|
||||
});
|
||||
const thunk1 = resizeComponent({ id: 1, width: 10, height: 3 });
|
||||
thunk1(dispatch, getState);
|
||||
|
||||
const thunk2 = dispatch.getCall(0).args[0];
|
||||
thunk2(dispatch, getState);
|
||||
|
||||
expect(dispatch.callCount).to.equal(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleComponentDrop', () => {
|
||||
it('should create a component if it is new', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const dropResult = {
|
||||
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
||||
destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
|
||||
dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
|
||||
};
|
||||
|
||||
const handleComponentDropThunk = handleComponentDrop(dropResult);
|
||||
handleComponentDropThunk(dispatch, getState);
|
||||
|
||||
const createComponentThunk = dispatch.getCall(0).args[0];
|
||||
createComponentThunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal({
|
||||
type: CREATE_COMPONENT,
|
||||
payload: {
|
||||
dropResult,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
});
|
||||
|
||||
it('should move a component if the component is not new', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardLayout: {
|
||||
// if 'dragging' is not only child will dispatch deleteComponent thunk
|
||||
present: { id: { type: ROW_TYPE, children: ['_'] } },
|
||||
},
|
||||
});
|
||||
const dropResult = {
|
||||
source: { id: 'id', index: 0, type: ROW_TYPE },
|
||||
destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
|
||||
dragging: { id: 'dragging', type: ROW_TYPE },
|
||||
};
|
||||
|
||||
const handleComponentDropThunk = handleComponentDrop(dropResult);
|
||||
handleComponentDropThunk(dispatch, getState);
|
||||
|
||||
const moveComponentThunk = dispatch.getCall(0).args[0];
|
||||
moveComponentThunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal({
|
||||
type: MOVE_COMPONENT,
|
||||
payload: {
|
||||
dropResult,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
});
|
||||
|
||||
it('should dispatch a toast if the drop overflows the destination', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardLayout: {
|
||||
present: {
|
||||
source: { type: ROW_TYPE },
|
||||
destination: { type: ROW_TYPE, children: ['rowChild'] },
|
||||
dragging: { type: CHART_TYPE, meta: { width: 1 } },
|
||||
rowChild: { type: CHART_TYPE, meta: { width: 12 } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const dropResult = {
|
||||
source: { id: 'source', type: ROW_TYPE },
|
||||
destination: { id: 'destination', type: ROW_TYPE },
|
||||
dragging: { id: 'dragging', type: CHART_TYPE },
|
||||
};
|
||||
|
||||
const thunk = handleComponentDrop(dropResult);
|
||||
thunk(dispatch, getState);
|
||||
expect(dispatch.getCall(0).args[0].type).to.deep.equal(
|
||||
addInfoToast('').type,
|
||||
);
|
||||
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should delete a parent Row or Tabs if the moved child was the only child', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardLayout: {
|
||||
present: {
|
||||
parentId: { id: 'parentId', children: ['tabsId'] },
|
||||
tabsId: { id: 'tabsId', type: TABS_TYPE, children: [] },
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
},
|
||||
tabId: { id: 'tabId', type: TAB_TYPE },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dropResult = {
|
||||
source: { id: 'tabsId', type: TABS_TYPE },
|
||||
destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
|
||||
dragging: { id: 'tabId', type: TAB_TYPE },
|
||||
};
|
||||
|
||||
const moveThunk = handleComponentDrop(dropResult);
|
||||
moveThunk(dispatch, getState);
|
||||
|
||||
// first call is move action which is not a thunk
|
||||
const deleteThunk = dispatch.getCall(1).args[0];
|
||||
deleteThunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.getCall(2).args[0]).to.deep.equal({
|
||||
type: DELETE_COMPONENT,
|
||||
payload: {
|
||||
id: 'tabsId',
|
||||
parentId: 'parentId',
|
||||
},
|
||||
});
|
||||
|
||||
// move thunk, delete thunk, delete result actions
|
||||
expect(dispatch.callCount).to.equal(3);
|
||||
});
|
||||
|
||||
it('should create top-level tabs if dropped on root', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const dropResult = {
|
||||
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
||||
destination: { id: DASHBOARD_ROOT_ID },
|
||||
dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
|
||||
};
|
||||
|
||||
const thunk1 = handleComponentDrop(dropResult);
|
||||
thunk1(dispatch, getState);
|
||||
|
||||
const thunk2 = dispatch.getCall(0).args[0];
|
||||
thunk2(dispatch, getState);
|
||||
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal({
|
||||
type: CREATE_TOP_LEVEL_TABS,
|
||||
payload: {
|
||||
dropResult,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('undoLayoutAction', () => {
|
||||
it('should dispatch a redux-undo .undo() action ', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardLayout: { past: ['non-empty'] },
|
||||
});
|
||||
const thunk = undoLayoutAction();
|
||||
thunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
expect(dispatch.getCall(0).args[0]).to.deep.equal(
|
||||
UndoActionCreators.undo(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges(false) action history length is zero', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardLayout: { past: [] },
|
||||
});
|
||||
const thunk = undoLayoutAction();
|
||||
thunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal(
|
||||
setUnsavedChanges(false),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redoLayoutAction', () => {
|
||||
it('should dispatch a redux-undo .redo() action ', () => {
|
||||
const { getState, dispatch } = setup();
|
||||
const thunk = redoLayoutAction();
|
||||
thunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.callCount).to.equal(1);
|
||||
expect(dispatch.getCall(0).args[0]).to.deep.equal(
|
||||
UndoActionCreators.redo(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch a setUnsavedChanges(true) action if hasUnsavedChanges=false', () => {
|
||||
const { getState, dispatch } = setup({
|
||||
dashboardState: { hasUnsavedChanges: false },
|
||||
});
|
||||
const thunk = redoLayoutAction();
|
||||
thunk(dispatch, getState);
|
||||
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0]).to.deep.equal(
|
||||
setUnsavedChanges(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,16 +3,14 @@ import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import CodeModal from '../../../src/dashboard/components/CodeModal';
|
||||
import CodeModal from '../../../../src/dashboard/components/CodeModal';
|
||||
|
||||
describe('CodeModal', () => {
|
||||
const mockedProps = {
|
||||
triggerNode: <i className="fa fa-edit" />,
|
||||
};
|
||||
it('is valid', () => {
|
||||
expect(
|
||||
React.isValidElement(<CodeModal {...mockedProps} />),
|
||||
).to.equal(true);
|
||||
expect(React.isValidElement(<CodeModal {...mockedProps} />)).to.equal(true);
|
||||
});
|
||||
it('renders the trigger node', () => {
|
||||
const wrapper = mount(<CodeModal {...mockedProps} />);
|
||||
@@ -3,16 +3,14 @@ import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import CssEditor from '../../../src/dashboard/components/CssEditor';
|
||||
import CssEditor from '../../../../src/dashboard/components/CssEditor';
|
||||
|
||||
describe('CssEditor', () => {
|
||||
const mockedProps = {
|
||||
triggerNode: <i className="fa fa-edit" />,
|
||||
};
|
||||
it('is valid', () => {
|
||||
expect(
|
||||
React.isValidElement(<CssEditor {...mockedProps} />),
|
||||
).to.equal(true);
|
||||
expect(React.isValidElement(<CssEditor {...mockedProps} />)).to.equal(true);
|
||||
});
|
||||
it('renders the trigger node', () => {
|
||||
const wrapper = mount(<CssEditor {...mockedProps} />);
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import ParentSize from '@vx/responsive/build/components/ParentSize';
|
||||
import { Sticky, StickyContainer } from 'react-sticky';
|
||||
import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
|
||||
|
||||
import BuilderComponentPane from '../../../../src/dashboard/components/BuilderComponentPane';
|
||||
import DashboardBuilder from '../../../../src/dashboard/components/DashboardBuilder';
|
||||
import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent';
|
||||
import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader';
|
||||
import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid';
|
||||
import WithDragDropContext from '../helpers/WithDragDropContext';
|
||||
import {
|
||||
dashboardLayout as undoableDashboardLayout,
|
||||
dashboardLayoutWithTabs as undoableDashboardLayoutWithTabs,
|
||||
} from '../fixtures/mockDashboardLayout';
|
||||
|
||||
import { mockStore, mockStoreWithTabs } from '../fixtures/mockStore';
|
||||
|
||||
const dashboardLayout = undoableDashboardLayout.present;
|
||||
const layoutWithTabs = undoableDashboardLayoutWithTabs.present;
|
||||
|
||||
describe('DashboardBuilder', () => {
|
||||
const props = {
|
||||
dashboardLayout,
|
||||
deleteTopLevelTabs() {},
|
||||
editMode: false,
|
||||
showBuilderPane: false,
|
||||
handleComponentDrop() {},
|
||||
toggleBuilderPane() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps, useProvider = false, store = mockStore) {
|
||||
const builder = <DashboardBuilder {...props} {...overrideProps} />;
|
||||
return useProvider
|
||||
? mount(
|
||||
<Provider store={store}>
|
||||
<WithDragDropContext>{builder}</WithDragDropContext>
|
||||
</Provider>,
|
||||
)
|
||||
: shallow(builder);
|
||||
}
|
||||
|
||||
it('should render a StickyContainer with class "dashboard"', () => {
|
||||
const wrapper = setup();
|
||||
const stickyContainer = wrapper.find(StickyContainer);
|
||||
expect(stickyContainer).to.have.length(1);
|
||||
expect(stickyContainer.prop('className')).to.equal('dashboard');
|
||||
});
|
||||
|
||||
it('should add the "dashboard--editing" class if editMode=true', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
const stickyContainer = wrapper.find(StickyContainer);
|
||||
expect(stickyContainer.prop('className')).to.equal(
|
||||
'dashboard dashboard--editing',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a DragDroppable DashboardHeader', () => {
|
||||
const wrapper = setup(null, true);
|
||||
expect(wrapper.find(DashboardHeader)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a Sticky top-level Tabs if the dashboard has tabs', () => {
|
||||
const wrapper = setup(
|
||||
{ dashboardLayout: layoutWithTabs },
|
||||
true,
|
||||
mockStoreWithTabs,
|
||||
);
|
||||
const sticky = wrapper.find(Sticky);
|
||||
const dashboardComponent = sticky.find(DashboardComponent);
|
||||
|
||||
const tabChildren = layoutWithTabs.TABS_ID.children;
|
||||
expect(sticky).to.have.length(1);
|
||||
expect(dashboardComponent).to.have.length(1 + tabChildren.length); // tab + tabs
|
||||
expect(dashboardComponent.at(0).prop('id')).to.equal('TABS_ID');
|
||||
tabChildren.forEach((tabId, i) => {
|
||||
expect(dashboardComponent.at(i + 1).prop('id')).to.equal(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a TabContainer and TabContent', () => {
|
||||
const wrapper = setup({ dashboardLayout: layoutWithTabs });
|
||||
const parentSize = wrapper.find(ParentSize).dive();
|
||||
expect(parentSize.find(TabContainer)).to.have.length(1);
|
||||
expect(parentSize.find(TabContent)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on TabContainer for perf', () => {
|
||||
const wrapper = setup({ dashboardLayout: layoutWithTabs });
|
||||
const tabProps = wrapper
|
||||
.find(ParentSize)
|
||||
.dive()
|
||||
.find(TabContainer)
|
||||
.props();
|
||||
expect(tabProps.animation).to.equal(true);
|
||||
expect(tabProps.mountOnEnter).to.equal(true);
|
||||
expect(tabProps.unmountOnExit).to.equal(false);
|
||||
});
|
||||
|
||||
it('should render a TabPane and DashboardGrid for each Tab', () => {
|
||||
const wrapper = setup({ dashboardLayout: layoutWithTabs });
|
||||
const parentSize = wrapper.find(ParentSize).dive();
|
||||
|
||||
const expectedCount = layoutWithTabs.TABS_ID.children.length;
|
||||
expect(parentSize.find(TabPane)).to.have.length(expectedCount);
|
||||
expect(parentSize.find(DashboardGrid)).to.have.length(expectedCount);
|
||||
});
|
||||
|
||||
it('should render a BuilderComponentPane if editMode=showBuilderPane=true', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(BuilderComponentPane)).to.have.length(0);
|
||||
|
||||
wrapper.setProps({ ...props, editMode: true, showBuilderPane: true });
|
||||
expect(wrapper.find(BuilderComponentPane)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should change tabs if a top-level Tab is clicked', () => {
|
||||
const wrapper = setup(
|
||||
{ dashboardLayout: layoutWithTabs },
|
||||
true,
|
||||
mockStoreWithTabs,
|
||||
);
|
||||
|
||||
expect(wrapper.find(TabContainer).prop('activeKey')).to.equal(0);
|
||||
|
||||
wrapper
|
||||
.find('.dashboard-component-tabs .nav-tabs a')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find(TabContainer).prop('activeKey')).to.equal(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent';
|
||||
import DashboardGrid from '../../../../src/dashboard/components/DashboardGrid';
|
||||
import DragDroppable from '../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
|
||||
|
||||
import { DASHBOARD_GRID_TYPE } from '../../../../src/dashboard/util/componentTypes';
|
||||
import { GRID_COLUMN_COUNT } from '../../../../src/dashboard/util/constants';
|
||||
|
||||
describe('DashboardGrid', () => {
|
||||
const props = {
|
||||
depth: 1,
|
||||
editMode: false,
|
||||
gridComponent: {
|
||||
...newComponentFactory(DASHBOARD_GRID_TYPE),
|
||||
children: ['a'],
|
||||
},
|
||||
handleComponentDrop() {},
|
||||
resizeComponent() {},
|
||||
width: 500,
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
const wrapper = shallow(<DashboardGrid {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a div with class "dashboard-grid"', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.dashboard-grid')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render one DashboardComponent for each gridComponent child', () => {
|
||||
const wrapper = setup({
|
||||
gridComponent: { ...props.gridComponent, children: ['a', 'b'] },
|
||||
});
|
||||
expect(wrapper.find(DashboardComponent)).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should render two empty DragDroppables in editMode to increase the drop target zone', () => {
|
||||
const viewMode = setup({ editMode: false });
|
||||
const editMode = setup({ editMode: true });
|
||||
expect(viewMode.find(DragDroppable)).to.have.length(0);
|
||||
expect(editMode.find(DragDroppable)).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should render grid column guides when resizing', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find('.grid-column-guide')).to.have.length(0);
|
||||
|
||||
wrapper.setState({ isResizing: true });
|
||||
|
||||
expect(wrapper.find('.grid-column-guide')).to.have.length(
|
||||
GRID_COLUMN_COUNT,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a grid row guide when resizing', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.grid-row-guide')).to.have.length(0);
|
||||
wrapper.setState({ isResizing: true, rowGuideTop: 10 });
|
||||
expect(wrapper.find('.grid-row-guide')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call resizeComponent when a child DashboardComponent calls resizeStop', () => {
|
||||
const resizeComponent = sinon.spy();
|
||||
const args = { id: 'id', widthMultiple: 1, heightMultiple: 3 };
|
||||
const wrapper = setup({ resizeComponent });
|
||||
const dashboardComponent = wrapper.find(DashboardComponent).first();
|
||||
dashboardComponent.prop('onResizeStop')(args);
|
||||
|
||||
expect(resizeComponent.callCount).to.equal(1);
|
||||
expect(resizeComponent.getCall(0).args[0]).to.deep.equal({
|
||||
id: 'id',
|
||||
width: 1,
|
||||
height: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Dashboard from '../../../../src/dashboard/components/Dashboard';
|
||||
import DashboardBuilder from '../../../../src/dashboard/containers/DashboardBuilder';
|
||||
|
||||
// mock data
|
||||
import chartQueries, { sliceId as chartId } from '../fixtures/mockChartQueries';
|
||||
import datasources from '../fixtures/mockDatasource';
|
||||
import dashboardInfo from '../fixtures/mockDashboardInfo';
|
||||
import { dashboardLayout } from '../fixtures/mockDashboardLayout';
|
||||
import dashboardState from '../fixtures/mockDashboardState';
|
||||
import { sliceEntitiesForChart as sliceEntities } from '../fixtures/mockSliceEntities';
|
||||
|
||||
import { CHART_TYPE } from '../../../../src/dashboard/util/componentTypes';
|
||||
import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
|
||||
|
||||
describe('Dashboard', () => {
|
||||
const props = {
|
||||
actions: {
|
||||
addSliceToDashboard() {},
|
||||
removeSliceFromDashboard() {},
|
||||
runQuery() {},
|
||||
},
|
||||
initMessages: [],
|
||||
dashboardState,
|
||||
dashboardInfo,
|
||||
charts: chartQueries,
|
||||
slices: sliceEntities.slices,
|
||||
datasources,
|
||||
layout: dashboardLayout.present,
|
||||
timeout: 60,
|
||||
userId: dashboardInfo.userId,
|
||||
impressionId: 'id',
|
||||
loadStats: {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
const wrapper = shallow(<Dashboard {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DashboardBuilder', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DashboardBuilder)).to.have.length(1);
|
||||
});
|
||||
|
||||
describe('refreshExcept', () => {
|
||||
const overrideCharts = {
|
||||
...chartQueries,
|
||||
1001: {
|
||||
...chartQueries[chartId],
|
||||
id: 1001,
|
||||
},
|
||||
};
|
||||
|
||||
const overrideSlices = {
|
||||
...props.slices,
|
||||
1001: {
|
||||
...props.slices[chartId],
|
||||
slice_id: 1001,
|
||||
},
|
||||
};
|
||||
|
||||
it('should call runQuery for all non-exempt slices', () => {
|
||||
const wrapper = setup({ charts: overrideCharts, slices: overrideSlices });
|
||||
const spy = sinon.spy(props.actions, 'runQuery');
|
||||
wrapper.instance().refreshExcept('1001');
|
||||
spy.restore();
|
||||
expect(spy.callCount).to.equal(Object.keys(overrideCharts).length - 1);
|
||||
});
|
||||
|
||||
it('should not call runQuery for filter_immune_slices', () => {
|
||||
const wrapper = setup({
|
||||
charts: overrideCharts,
|
||||
dashboardInfo: {
|
||||
...dashboardInfo,
|
||||
metadata: {
|
||||
...dashboardInfo.metadata,
|
||||
filter_immune_slices: Object.keys(overrideCharts).map(id =>
|
||||
Number(id),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
const spy = sinon.spy(props.actions, 'runQuery');
|
||||
wrapper.instance().refreshExcept();
|
||||
spy.restore();
|
||||
expect(spy.callCount).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentWillReceiveProps', () => {
|
||||
const layoutWithExtraChart = {
|
||||
...props.layout,
|
||||
1001: newComponentFactory(CHART_TYPE, { chartId: 1001 }),
|
||||
};
|
||||
|
||||
it('should call addSliceToDashboard if a new slice is added to the layout', () => {
|
||||
const wrapper = setup();
|
||||
const spy = sinon.spy(props.actions, 'addSliceToDashboard');
|
||||
wrapper.instance().componentWillReceiveProps({
|
||||
...props,
|
||||
layout: layoutWithExtraChart,
|
||||
});
|
||||
spy.restore();
|
||||
expect(spy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call removeSliceFromDashboard if a slice is removed from the layout', () => {
|
||||
const wrapper = setup({ layout: layoutWithExtraChart });
|
||||
const spy = sinon.spy(props.actions, 'removeSliceFromDashboard');
|
||||
const nextLayout = { ...layoutWithExtraChart };
|
||||
delete nextLayout[1001];
|
||||
|
||||
wrapper.instance().componentWillReceiveProps({
|
||||
...props,
|
||||
layout: nextLayout,
|
||||
});
|
||||
spy.restore();
|
||||
expect(spy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentDidUpdate', () => {
|
||||
const overrideDashboardState = {
|
||||
...dashboardState,
|
||||
filters: {
|
||||
1: { region: [] },
|
||||
2: { country_name: ['USA'] },
|
||||
},
|
||||
refresh: true,
|
||||
};
|
||||
|
||||
it('should not call refresh when there is no change', () => {
|
||||
const wrapper = setup({ dashboardState: overrideDashboardState });
|
||||
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
const prevProps = wrapper.instance().props;
|
||||
wrapper.setProps({
|
||||
dashboardState: {
|
||||
...overrideDashboardState,
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProps);
|
||||
refreshExceptSpy.restore();
|
||||
expect(refreshExceptSpy.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is added', () => {
|
||||
const wrapper = setup({ dashboardState: overrideDashboardState });
|
||||
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
const prevProps = wrapper.instance().props;
|
||||
wrapper.setProps({
|
||||
dashboardState: {
|
||||
...overrideDashboardState,
|
||||
filters: {
|
||||
...overrideDashboardState.filters,
|
||||
3: { another_filter: ['please'] },
|
||||
},
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProps);
|
||||
refreshExceptSpy.restore();
|
||||
expect(refreshExceptSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is removed', () => {
|
||||
const wrapper = setup({ dashboardState: overrideDashboardState });
|
||||
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
const prevProps = wrapper.instance().props;
|
||||
wrapper.setProps({
|
||||
dashboardState: {
|
||||
...overrideDashboardState,
|
||||
filters: {},
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProps);
|
||||
refreshExceptSpy.restore();
|
||||
expect(refreshExceptSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call refresh if a filter is changed', () => {
|
||||
const wrapper = setup({ dashboardState: overrideDashboardState });
|
||||
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
const prevProps = wrapper.instance().props;
|
||||
wrapper.setProps({
|
||||
dashboardState: {
|
||||
...overrideDashboardState,
|
||||
filters: {
|
||||
...overrideDashboardState.filters,
|
||||
2: { country_name: ['Canada'] },
|
||||
},
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProps);
|
||||
refreshExceptSpy.restore();
|
||||
expect(refreshExceptSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should not call refresh if filters change and refresh is false', () => {
|
||||
const wrapper = setup({ dashboardState: overrideDashboardState });
|
||||
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
const prevProps = wrapper.instance().props;
|
||||
wrapper.setProps({
|
||||
dashboardState: {
|
||||
...overrideDashboardState,
|
||||
filters: {
|
||||
...overrideDashboardState.filters,
|
||||
2: { country_name: ['Canada'] },
|
||||
},
|
||||
refresh: false,
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProps);
|
||||
refreshExceptSpy.restore();
|
||||
expect(refreshExceptSpy.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should not refresh filter_immune_slices', () => {
|
||||
const wrapper = setup({
|
||||
dashboardState: overrideDashboardState,
|
||||
dashboardInfo: {
|
||||
...dashboardInfo,
|
||||
metadata: {
|
||||
...dashboardInfo.metadata,
|
||||
filter_immune_slices: [chartId],
|
||||
},
|
||||
},
|
||||
});
|
||||
const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
|
||||
const prevProps = wrapper.instance().props;
|
||||
wrapper.setProps({
|
||||
dashboardState: {
|
||||
...overrideDashboardState,
|
||||
filters: {
|
||||
...overrideDashboardState.filters,
|
||||
2: { country_name: ['Canada'] },
|
||||
},
|
||||
refresh: false,
|
||||
},
|
||||
});
|
||||
wrapper.instance().componentDidUpdate(prevProps);
|
||||
refreshExceptSpy.restore();
|
||||
expect(refreshExceptSpy.callCount).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import RefreshIntervalModal from '../../../src/dashboard/components/RefreshIntervalModal';
|
||||
import RefreshIntervalModal from '../../../../src/dashboard/components/RefreshIntervalModal';
|
||||
|
||||
describe('RefreshIntervalModal', () => {
|
||||
const mockedProps = {
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it, beforeEach, afterEach } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { List } from 'react-virtualized';
|
||||
|
||||
import SliceAdder from '../../../../src/dashboard/components/SliceAdder';
|
||||
import { sliceEntitiesForDashboard as mockSliceEntities } from '../fixtures/mockSliceEntities';
|
||||
|
||||
describe('SliceAdder', () => {
|
||||
const mockEvent = {
|
||||
key: 'Enter',
|
||||
target: {
|
||||
value: 'mock event target',
|
||||
},
|
||||
preventDefault: () => {},
|
||||
};
|
||||
const props = {
|
||||
...mockSliceEntities,
|
||||
fetchAllSlices: () => {},
|
||||
selectedSliceIds: [127, 128],
|
||||
userId: '1',
|
||||
height: 100,
|
||||
};
|
||||
const errorProps = {
|
||||
...props,
|
||||
errorMessage: 'this is error',
|
||||
};
|
||||
|
||||
describe('SliceAdder.sortByComparator', () => {
|
||||
it('should sort by timestamp descending', () => {
|
||||
const sortedTimestamps = Object.values(props.slices)
|
||||
.sort(SliceAdder.sortByComparator('changed_on'))
|
||||
.map(slice => slice.changed_on);
|
||||
expect(
|
||||
sortedTimestamps.every((currentTimestamp, index) => {
|
||||
if (index === 0) {
|
||||
return true;
|
||||
}
|
||||
return currentTimestamp < sortedTimestamps[index - 1];
|
||||
}),
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('should sort by slice_name', () => {
|
||||
const sortedNames = Object.values(props.slices)
|
||||
.sort(SliceAdder.sortByComparator('slice_name'))
|
||||
.map(slice => slice.slice_name);
|
||||
const expectedNames = Object.values(props.slices)
|
||||
.map(slice => slice.slice_name)
|
||||
.sort();
|
||||
expect(sortedNames).to.deep.equal(expectedNames);
|
||||
});
|
||||
});
|
||||
|
||||
it('render List', () => {
|
||||
const wrapper = shallow(<SliceAdder {...props} />);
|
||||
wrapper.setState({ filteredSlices: Object.values(props.slices) });
|
||||
expect(wrapper.find(List)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('render error', () => {
|
||||
const wrapper = shallow(<SliceAdder {...errorProps} />);
|
||||
wrapper.setState({ filteredSlices: Object.values(props.slices) });
|
||||
expect(wrapper.text()).to.have.string(errorProps.errorMessage);
|
||||
});
|
||||
|
||||
it('componentDidMount', () => {
|
||||
sinon.spy(SliceAdder.prototype, 'componentDidMount');
|
||||
sinon.spy(props, 'fetchAllSlices');
|
||||
|
||||
shallow(<SliceAdder {...props} />, {
|
||||
lifecycleExperimental: true,
|
||||
});
|
||||
expect(SliceAdder.prototype.componentDidMount.calledOnce).to.equal(true);
|
||||
expect(props.fetchAllSlices.calledOnce).to.equal(true);
|
||||
|
||||
SliceAdder.prototype.componentDidMount.restore();
|
||||
props.fetchAllSlices.restore();
|
||||
});
|
||||
|
||||
describe('componentWillReceiveProps', () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SliceAdder {...props} />);
|
||||
wrapper.setState({ filteredSlices: Object.values(props.slices) });
|
||||
sinon.spy(wrapper.instance(), 'setState');
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.instance().setState.restore();
|
||||
});
|
||||
|
||||
it('fetch slices should update state', () => {
|
||||
wrapper.instance().componentWillReceiveProps({
|
||||
...props,
|
||||
lastUpdated: new Date().getTime(),
|
||||
});
|
||||
expect(wrapper.instance().setState.calledOnce).to.equal(true);
|
||||
|
||||
const stateKeys = Object.keys(
|
||||
wrapper.instance().setState.lastCall.args[0],
|
||||
);
|
||||
expect(stateKeys).to.include('filteredSlices');
|
||||
});
|
||||
|
||||
it('select slices should update state', () => {
|
||||
wrapper.instance().componentWillReceiveProps({
|
||||
...props,
|
||||
selectedSliceIds: [127],
|
||||
});
|
||||
expect(wrapper.instance().setState.calledOnce).to.equal(true);
|
||||
|
||||
const stateKeys = Object.keys(
|
||||
wrapper.instance().setState.lastCall.args[0],
|
||||
);
|
||||
expect(stateKeys).to.include('selectedSliceIdsSet');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should rerun filter and sort', () => {
|
||||
let wrapper;
|
||||
let spy;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SliceAdder {...props} />);
|
||||
wrapper.setState({ filteredSlices: Object.values(props.slices) });
|
||||
spy = sinon.spy(wrapper.instance(), 'getFilteredSortedSlices');
|
||||
});
|
||||
afterEach(() => {
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('searchUpdated', () => {
|
||||
const newSearchTerm = 'new search term';
|
||||
wrapper.instance().searchUpdated(newSearchTerm);
|
||||
expect(spy.calledOnce).to.equal(true);
|
||||
expect(spy.lastCall.args[0]).to.equal(newSearchTerm);
|
||||
});
|
||||
|
||||
it('handleSelect', () => {
|
||||
const newSortBy = 1;
|
||||
wrapper.instance().handleSelect(newSortBy);
|
||||
expect(spy.calledOnce).to.equal(true);
|
||||
expect(spy.lastCall.args[1]).to.equal(newSortBy);
|
||||
});
|
||||
|
||||
it('handleKeyPress', () => {
|
||||
wrapper.instance().handleKeyPress(mockEvent);
|
||||
expect(spy.calledOnce).to.equal(true);
|
||||
expect(spy.lastCall.args[0]).to.equal(mockEvent.target.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import mockMessageToasts from '../fixtures/mockMessageToasts';
|
||||
import Toast from '../../../../src/dashboard/components/Toast';
|
||||
import ToastPresenter from '../../../../src/dashboard/components/ToastPresenter';
|
||||
|
||||
describe('ToastPresenter', () => {
|
||||
const props = {
|
||||
toasts: mockMessageToasts,
|
||||
removeToast() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
const wrapper = shallow(<ToastPresenter {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a div with class toast-presenter', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.toast-presenter')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a Toast for each toast object', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Toast)).to.have.length(props.toasts.length);
|
||||
});
|
||||
|
||||
it('should pass removeToast to the Toast component', () => {
|
||||
const removeToast = () => {};
|
||||
const wrapper = setup({ removeToast });
|
||||
expect(
|
||||
wrapper
|
||||
.find(Toast)
|
||||
.first()
|
||||
.prop('onCloseToast'),
|
||||
).to.equal(removeToast);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import mockMessageToasts from '../fixtures/mockMessageToasts';
|
||||
import Toast from '../../../../src/dashboard/components/Toast';
|
||||
|
||||
describe('Toast', () => {
|
||||
const props = {
|
||||
toast: mockMessageToasts[0],
|
||||
onCloseToast() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
const wrapper = shallow(<Toast {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render an Alert', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Alert)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render toastText within the alert', () => {
|
||||
const wrapper = setup();
|
||||
const alert = wrapper.find(Alert).dive();
|
||||
|
||||
expect(alert.childAt(1).text()).to.equal(props.toast.text);
|
||||
});
|
||||
|
||||
it('should call onCloseToast upon alert dismissal', done => {
|
||||
const onCloseToast = id => {
|
||||
expect(id).to.equal(props.toast.id);
|
||||
done();
|
||||
};
|
||||
const wrapper = setup({ onCloseToast });
|
||||
const handleClosePress = wrapper.instance().handleClosePress;
|
||||
expect(wrapper.find(Alert).prop('onDismiss')).to.equal(handleClosePress);
|
||||
handleClosePress(); // there is a timeout for onCloseToast to be called
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import newComponentFactory from '../../../../../src/dashboard/util/newComponentFactory';
|
||||
import {
|
||||
CHART_TYPE,
|
||||
ROW_TYPE,
|
||||
} from '../../../../../src/dashboard/util/componentTypes';
|
||||
import { UnwrappedDragDroppable as DragDroppable } from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
|
||||
describe('DragDroppable', () => {
|
||||
const props = {
|
||||
component: newComponentFactory(CHART_TYPE),
|
||||
parentComponent: newComponentFactory(ROW_TYPE),
|
||||
editMode: false,
|
||||
depth: 1,
|
||||
index: 0,
|
||||
isDragging: false,
|
||||
isDraggingOver: false,
|
||||
isDraggingOverShallow: false,
|
||||
droppableRef() {},
|
||||
dragSourceRef() {},
|
||||
dragPreviewRef() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps, shouldMount = false) {
|
||||
const method = shouldMount ? mount : shallow;
|
||||
const wrapper = method(<DragDroppable {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a div with class dragdroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.dragdroppable')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should add class dragdroppable--dragging when dragging', () => {
|
||||
const wrapper = setup({ isDragging: true });
|
||||
expect(wrapper.find('.dragdroppable')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call its child function', () => {
|
||||
const childrenSpy = sinon.spy();
|
||||
setup({ children: childrenSpy });
|
||||
expect(childrenSpy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call its child function with "dragSourceRef" if editMode=true', () => {
|
||||
const children = sinon.spy();
|
||||
const dragSourceRef = () => {};
|
||||
setup({ children, editMode: false, dragSourceRef });
|
||||
setup({ children, editMode: true, dragSourceRef });
|
||||
|
||||
expect(children.getCall(0).args[0].dragSourceRef).to.equal(undefined);
|
||||
expect(children.getCall(1).args[0].dragSourceRef).to.equal(dragSourceRef);
|
||||
});
|
||||
|
||||
it('should call its child function with "dropIndicatorProps" dependent on editMode, isDraggingOver, state.dropIndicator is set', () => {
|
||||
const children = sinon.spy();
|
||||
const wrapper = setup({ children, editMode: false, isDraggingOver: false });
|
||||
wrapper.setState({ dropIndicator: 'nonsense' });
|
||||
wrapper.setProps({ ...props, editMode: true, isDraggingOver: true });
|
||||
|
||||
expect(children.callCount).to.equal(3); // initial + setState + setProps
|
||||
expect(children.getCall(0).args[0].dropIndicatorProps).to.equal(undefined);
|
||||
expect(children.getCall(2).args[0].dropIndicatorProps).to.deep.equal({
|
||||
className: 'drop-indicator',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call props.dragPreviewRef and props.droppableRef on mount', () => {
|
||||
const dragPreviewRef = sinon.spy();
|
||||
const droppableRef = sinon.spy();
|
||||
|
||||
setup({ dragPreviewRef, droppableRef }, true);
|
||||
expect(dragPreviewRef.callCount).to.equal(1);
|
||||
expect(droppableRef.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should set this.mounted dependent on life cycle', () => {
|
||||
const wrapper = setup({}, true);
|
||||
const instance = wrapper.instance();
|
||||
expect(instance.mounted).to.equal(true);
|
||||
wrapper.unmount();
|
||||
expect(instance.mounted).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Chart from '../../../../../src/dashboard/containers/Chart';
|
||||
import ChartHolder from '../../../../../src/dashboard/components/gridComponents/ChartHolder';
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
|
||||
|
||||
import { mockStore } from '../../fixtures/mockStore';
|
||||
import { sliceId } from '../../fixtures/mockSliceEntities';
|
||||
import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
|
||||
describe('ChartHolder', () => {
|
||||
const props = {
|
||||
id: String(sliceId),
|
||||
parentId: 'ROW_ID',
|
||||
component: mockLayout.present.CHART_ID,
|
||||
depth: 2,
|
||||
parentComponent: mockLayout.present.ROW_ID,
|
||||
index: 0,
|
||||
editMode: false,
|
||||
availableColumnCount: 12,
|
||||
columnWidth: 50,
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
handleComponentDrop() {},
|
||||
updateComponents() {},
|
||||
deleteComponent() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<WithDragDropContext>
|
||||
<ChartHolder {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a ResizableContainer', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(ResizableContainer)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should only have an adjustableWidth if its parent is a Row', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(ResizableContainer).prop('adjustableWidth')).to.equal(
|
||||
true,
|
||||
);
|
||||
|
||||
wrapper = setup({ ...props, parentComponent: mockLayout.present.CHART_ID });
|
||||
expect(wrapper.find(ResizableContainer).prop('adjustableWidth')).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass correct props to ResizableContainer', () => {
|
||||
const wrapper = setup();
|
||||
const resizableProps = wrapper.find(ResizableContainer).props();
|
||||
expect(resizableProps.widthStep).to.equal(props.columnWidth);
|
||||
expect(resizableProps.widthMultiple).to.equal(props.component.meta.width);
|
||||
expect(resizableProps.heightMultiple).to.equal(props.component.meta.height);
|
||||
expect(resizableProps.maxWidthMultiple).to.equal(
|
||||
props.component.meta.width + props.availableColumnCount,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a div with class "dashboard-component-chart-holder"', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.dashboard-component-chart-holder')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a Chart', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Chart)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a HoverMenu with DeleteComponentButton in editMode', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(0);
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Divider because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(1);
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Chart from '../../../../../src/dashboard/components/gridComponents/Chart';
|
||||
import SliceHeader from '../../../../../src/dashboard/components/SliceHeader';
|
||||
import ChartContainer from '../../../../../src/chart/ChartContainer';
|
||||
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
import {
|
||||
sliceEntitiesForChart as sliceEntities,
|
||||
sliceId,
|
||||
} from '../../fixtures/mockSliceEntities';
|
||||
import chartQueries, {
|
||||
sliceId as queryId,
|
||||
} from '../../fixtures/mockChartQueries';
|
||||
|
||||
describe('Chart', () => {
|
||||
const props = {
|
||||
id: sliceId,
|
||||
width: 100,
|
||||
height: 100,
|
||||
updateSliceName() {},
|
||||
|
||||
// from redux
|
||||
chart: chartQueries[queryId],
|
||||
formData: chartQueries[queryId].formData,
|
||||
datasource: mockDatasource[sliceEntities.slices[sliceId].datasource],
|
||||
slice: {
|
||||
...sliceEntities.slices[sliceId],
|
||||
description_markeddown: 'markdown',
|
||||
},
|
||||
sliceName: sliceEntities.slices[sliceId].slice_name,
|
||||
timeout: 60,
|
||||
filters: {},
|
||||
refreshChart() {},
|
||||
toggleExpandSlice() {},
|
||||
addFilter() {},
|
||||
editMode: false,
|
||||
isExpanded: false,
|
||||
supersetCanExplore: false,
|
||||
sliceCanEdit: false,
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
const wrapper = shallow(<Chart {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a SliceHeader', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(SliceHeader)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a ChartContainer', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(ChartContainer)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a description if it has one and isExpanded=true', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.slice_description')).to.have.length(0);
|
||||
|
||||
wrapper.setProps({ ...props, isExpanded: true });
|
||||
expect(wrapper.find('.slice_description')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call refreshChart when SliceHeader calls forceRefresh', () => {
|
||||
const refreshChart = sinon.spy();
|
||||
const wrapper = setup({ refreshChart });
|
||||
wrapper.instance().forceRefresh();
|
||||
expect(refreshChart.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call addFilter when ChartContainer calls addFilter', () => {
|
||||
const addFilter = sinon.spy();
|
||||
const wrapper = setup({ addFilter });
|
||||
wrapper.instance().addFilter();
|
||||
expect(addFilter.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should return props.filters when its getFilters method is called', () => {
|
||||
const filters = { column: ['value'] };
|
||||
const wrapper = setup({ filters });
|
||||
expect(wrapper.instance().getFilters()).to.equal(filters);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import BackgroundStyleDropdown from '../../../../../src/dashboard/components/menu/BackgroundStyleDropdown';
|
||||
import Column from '../../../../../src/dashboard/components/gridComponents/Column';
|
||||
import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
import IconButton from '../../../../../src/dashboard/components/IconButton';
|
||||
import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
|
||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||
|
||||
import { mockStore } from '../../fixtures/mockStore';
|
||||
import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
|
||||
describe('Column', () => {
|
||||
const columnWithoutChildren = {
|
||||
...mockLayout.present.COLUMN_ID,
|
||||
children: [],
|
||||
};
|
||||
const props = {
|
||||
id: 'COLUMN_ID',
|
||||
parentId: 'ROW_ID',
|
||||
component: mockLayout.present.COLUMN_ID,
|
||||
parentComponent: mockLayout.present.ROW_ID,
|
||||
index: 0,
|
||||
depth: 2,
|
||||
editMode: false,
|
||||
availableColumnCount: 12,
|
||||
minColumnWidth: 2,
|
||||
columnWidth: 50,
|
||||
occupiedColumnCount: 6,
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
handleComponentDrop() {},
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<WithDragDropContext>
|
||||
<Column {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
// don't count child DragDroppables
|
||||
const wrapper = setup({ component: columnWithoutChildren });
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a WithPopoverMenu', () => {
|
||||
// don't count child DragDroppables
|
||||
const wrapper = setup({ component: columnWithoutChildren });
|
||||
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a ResizableContainer', () => {
|
||||
// don't count child DragDroppables
|
||||
const wrapper = setup({ component: columnWithoutChildren });
|
||||
expect(wrapper.find(ResizableContainer)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a HoverMenu in editMode', () => {
|
||||
let wrapper = setup({ component: columnWithoutChildren });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Row because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ component: columnWithoutChildren, editMode: true });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a DeleteComponentButton in editMode', () => {
|
||||
let wrapper = setup({ component: columnWithoutChildren });
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Row because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ component: columnWithoutChildren, editMode: true });
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a BackgroundStyleDropdown when focused', () => {
|
||||
let wrapper = setup({ component: columnWithoutChildren });
|
||||
expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Row because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ component: columnWithoutChildren, editMode: true });
|
||||
wrapper
|
||||
.find(IconButton)
|
||||
.at(1) // first one is delete button
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should pass its own width as availableColumnCount to children', () => {
|
||||
const wrapper = setup();
|
||||
const dashboardComponent = wrapper.find(DashboardComponent).first();
|
||||
expect(dashboardComponent.props().availableColumnCount).to.equal(
|
||||
props.component.meta.width,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass appropriate dimensions to ResizableContainer', () => {
|
||||
const wrapper = setup({ component: columnWithoutChildren });
|
||||
const columnWidth = columnWithoutChildren.meta.width;
|
||||
const resizableProps = wrapper.find(ResizableContainer).props();
|
||||
expect(resizableProps.adjustableWidth).to.equal(true);
|
||||
expect(resizableProps.adjustableHeight).to.equal(false);
|
||||
expect(resizableProps.widthStep).to.equal(props.columnWidth);
|
||||
expect(resizableProps.widthMultiple).to.equal(columnWidth);
|
||||
expect(resizableProps.minWidthMultiple).to.equal(props.minColumnWidth);
|
||||
expect(resizableProps.maxWidthMultiple).to.equal(
|
||||
props.availableColumnCount + columnWidth,
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment the depth of its children', () => {
|
||||
const wrapper = setup();
|
||||
const dashboardComponent = wrapper.find(DashboardComponent);
|
||||
expect(dashboardComponent.props().depth).to.equal(props.depth + 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import Divider from '../../../../../src/dashboard/components/gridComponents/Divider';
|
||||
import newComponentFactory from '../../../../../src/dashboard/util/newComponentFactory';
|
||||
import {
|
||||
DIVIDER_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
} from '../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
|
||||
describe('Divider', () => {
|
||||
const props = {
|
||||
id: 'id',
|
||||
parentId: 'parentId',
|
||||
component: newComponentFactory(DIVIDER_TYPE),
|
||||
depth: 1,
|
||||
parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
|
||||
index: 0,
|
||||
editMode: false,
|
||||
handleComponentDrop() {},
|
||||
deleteComponent() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<WithDragDropContext>
|
||||
<Divider {...props} {...overrideProps} />
|
||||
</WithDragDropContext>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a div with class "dashboard-component-divider"', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.dashboard-component-divider')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a HoverMenu with DeleteComponentButton in editMode', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(0);
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Divider because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(1);
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import EditableTitle from '../../../../../src/components/EditableTitle';
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import Header from '../../../../../src/dashboard/components/gridComponents/Header';
|
||||
import newComponentFactory from '../../../../../src/dashboard/util/newComponentFactory';
|
||||
import {
|
||||
HEADER_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
} from '../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
|
||||
describe('Header', () => {
|
||||
const props = {
|
||||
id: 'id',
|
||||
parentId: 'parentId',
|
||||
component: newComponentFactory(HEADER_TYPE),
|
||||
depth: 1,
|
||||
parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
|
||||
index: 0,
|
||||
editMode: false,
|
||||
handleComponentDrop() {},
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<WithDragDropContext>
|
||||
<Header {...props} {...overrideProps} />
|
||||
</WithDragDropContext>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a WithPopoverMenu', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a HoverMenu in editMode', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Header because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render an EditableTitle with meta.text', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(EditableTitle)).to.have.length(1);
|
||||
expect(wrapper.find('input').prop('value')).to.equal(
|
||||
props.component.meta.text,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call updateComponents when EditableTitle changes', () => {
|
||||
const updateComponents = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, updateComponents });
|
||||
wrapper.find(EditableTitle).prop('onSaveTitle')('New title');
|
||||
|
||||
const headerId = props.component.id;
|
||||
expect(updateComponents.callCount).to.equal(1);
|
||||
expect(updateComponents.getCall(0).args[0][headerId].meta.text).to.equal(
|
||||
'New title',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a DeleteComponentButton when focused in editMode', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
||||
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import AceEditor from 'react-ace';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
import Markdown from '../../../../../src/dashboard/components/gridComponents/Markdown';
|
||||
import MarkdownModeDropdown from '../../../../../src/dashboard/components/menu/MarkdownModeDropdown';
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||
import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
|
||||
|
||||
import { mockStore } from '../../fixtures/mockStore';
|
||||
import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
|
||||
describe('Markdown', () => {
|
||||
const props = {
|
||||
id: 'id',
|
||||
parentId: 'parentId',
|
||||
component: mockLayout.present.MARKDOWN_ID,
|
||||
depth: 2,
|
||||
parentComponent: mockLayout.present.ROW_ID,
|
||||
index: 0,
|
||||
editMode: false,
|
||||
availableColumnCount: 12,
|
||||
columnWidth: 50,
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
handleComponentDrop() {},
|
||||
updateComponents() {},
|
||||
deleteComponent() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<WithDragDropContext>
|
||||
<Markdown {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a WithPopoverMenu', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a ResizableContainer', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(ResizableContainer)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should only have an adjustableWidth if its parent is a Row', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(ResizableContainer).prop('adjustableWidth')).to.equal(
|
||||
true,
|
||||
);
|
||||
|
||||
wrapper = setup({ ...props, parentComponent: mockLayout.present.CHART_ID });
|
||||
expect(wrapper.find(ResizableContainer).prop('adjustableWidth')).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass correct props to ResizableContainer', () => {
|
||||
const wrapper = setup();
|
||||
const resizableProps = wrapper.find(ResizableContainer).props();
|
||||
expect(resizableProps.widthStep).to.equal(props.columnWidth);
|
||||
expect(resizableProps.widthMultiple).to.equal(props.component.meta.width);
|
||||
expect(resizableProps.heightMultiple).to.equal(props.component.meta.height);
|
||||
expect(resizableProps.maxWidthMultiple).to.equal(
|
||||
props.component.meta.width + props.availableColumnCount,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an Markdown when NOT focused', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(AceEditor)).to.have.length(0);
|
||||
expect(wrapper.find(ReactMarkdown)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render an AceEditor when focused and editMode=true and editorMode=edit', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(AceEditor)).to.have.length(0);
|
||||
expect(wrapper.find(ReactMarkdown)).to.have.length(1);
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||
expect(wrapper.find(AceEditor)).to.have.length(1);
|
||||
expect(wrapper.find(ReactMarkdown)).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should render a ReactMarkdown when focused and editMode=true and editorMode=preview', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||
expect(wrapper.find(AceEditor)).to.have.length(1);
|
||||
expect(wrapper.find(ReactMarkdown)).to.have.length(0);
|
||||
|
||||
// we can't call setState on Markdown bc it's not the root component, so call
|
||||
// the mode dropdown onchange instead
|
||||
const dropdown = wrapper.find(MarkdownModeDropdown);
|
||||
dropdown.prop('onChange')('preview');
|
||||
|
||||
expect(wrapper.find(AceEditor)).to.have.length(0);
|
||||
expect(wrapper.find(ReactMarkdown)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call updateComponents when editMode changes from edit => preview, and there are markdownSource changes', () => {
|
||||
const updateComponents = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, updateComponents });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus + edit
|
||||
|
||||
// we can't call setState on Markdown bc it's not the root component, so call
|
||||
// the mode dropdown onchange instead
|
||||
const dropdown = wrapper.find(MarkdownModeDropdown);
|
||||
dropdown.prop('onChange')('preview');
|
||||
expect(updateComponents.callCount).to.equal(0);
|
||||
|
||||
dropdown.prop('onChange')('edit');
|
||||
// because we can't call setState on Markdown, change it through the editor
|
||||
// then go back to preview mode to invoke updateComponents
|
||||
const editor = wrapper.find(AceEditor);
|
||||
editor.prop('onChange')('new markdown!');
|
||||
dropdown.prop('onChange')('preview');
|
||||
expect(updateComponents.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should render a DeleteComponentButton when focused in editMode', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
||||
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import BackgroundStyleDropdown from '../../../../../src/dashboard/components/menu/BackgroundStyleDropdown';
|
||||
import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
import IconButton from '../../../../../src/dashboard/components/IconButton';
|
||||
import Row from '../../../../../src/dashboard/components/gridComponents/Row';
|
||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||
|
||||
import { mockStore } from '../../fixtures/mockStore';
|
||||
import { DASHBOARD_GRID_ID } from '../../../../../src/dashboard/util/constants';
|
||||
import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
|
||||
describe('Row', () => {
|
||||
const rowWithoutChildren = { ...mockLayout.present.ROW_ID, children: [] };
|
||||
const props = {
|
||||
id: 'ROW_ID',
|
||||
parentId: DASHBOARD_GRID_ID,
|
||||
component: mockLayout.present.ROW_ID,
|
||||
parentComponent: mockLayout.present[DASHBOARD_GRID_ID],
|
||||
index: 0,
|
||||
depth: 2,
|
||||
editMode: false,
|
||||
availableColumnCount: 12,
|
||||
columnWidth: 50,
|
||||
occupiedColumnCount: 6,
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
handleComponentDrop() {},
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<WithDragDropContext>
|
||||
<Row {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
// don't count child DragDroppables
|
||||
const wrapper = setup({ component: rowWithoutChildren });
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a WithPopoverMenu', () => {
|
||||
// don't count child DragDroppables
|
||||
const wrapper = setup({ component: rowWithoutChildren });
|
||||
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a HoverMenu in editMode', () => {
|
||||
let wrapper = setup({ component: rowWithoutChildren });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Row because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ component: rowWithoutChildren, editMode: true });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a DeleteComponentButton in editMode', () => {
|
||||
let wrapper = setup({ component: rowWithoutChildren });
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Row because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ component: rowWithoutChildren, editMode: true });
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a BackgroundStyleDropdown when focused', () => {
|
||||
let wrapper = setup({ component: rowWithoutChildren });
|
||||
expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(0);
|
||||
|
||||
// we cannot set props on the Row because of the WithDragDropContext wrapper
|
||||
wrapper = setup({ component: rowWithoutChildren, editMode: true });
|
||||
wrapper
|
||||
.find(IconButton)
|
||||
.at(1) // first one is delete button
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should pass appropriate availableColumnCount to children', () => {
|
||||
const wrapper = setup();
|
||||
const dashboardComponent = wrapper.find(DashboardComponent).first();
|
||||
expect(dashboardComponent.props().availableColumnCount).to.equal(
|
||||
props.availableColumnCount - props.occupiedColumnCount,
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment the depth of its children', () => {
|
||||
const wrapper = setup();
|
||||
const dashboardComponent = wrapper.find(DashboardComponent).first();
|
||||
expect(dashboardComponent.props().depth).to.equal(props.depth + 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import EditableTitle from '../../../../../src/components/EditableTitle';
|
||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||
import Tab, {
|
||||
RENDER_TAB,
|
||||
RENDER_TAB_CONTENT,
|
||||
} from '../../../../../src/dashboard/components/gridComponents/Tab';
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
import { dashboardLayoutWithTabs } from '../../fixtures/mockDashboardLayout';
|
||||
import { mockStoreWithTabs } from '../../fixtures/mockStore';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const props = {
|
||||
id: 'TAB_ID',
|
||||
parentId: 'TABS_ID',
|
||||
component: dashboardLayoutWithTabs.present.TAB_ID,
|
||||
parentComponent: dashboardLayoutWithTabs.present.TABS_ID,
|
||||
index: 0,
|
||||
depth: 1,
|
||||
editMode: false,
|
||||
renderType: RENDER_TAB,
|
||||
onDropOnTab() {},
|
||||
onDeleteTab() {},
|
||||
availableColumnCount: 12,
|
||||
columnWidth: 50,
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
createComponent() {},
|
||||
handleComponentDrop() {},
|
||||
onChangeTab() {},
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStoreWithTabs}>
|
||||
<WithDragDropContext>
|
||||
<Tab {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe('renderType=RENDER_TAB', () => {
|
||||
it('should render a DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render an EditableTitle with meta.text', () => {
|
||||
const wrapper = setup();
|
||||
const title = wrapper.find(EditableTitle);
|
||||
expect(title).to.have.length(1);
|
||||
expect(title.find('input').prop('value')).to.equal(
|
||||
props.component.meta.text,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call updateComponents when EditableTitle changes', () => {
|
||||
const updateComponents = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, updateComponents });
|
||||
wrapper.find(EditableTitle).prop('onSaveTitle')('New title');
|
||||
|
||||
expect(updateComponents.callCount).to.equal(1);
|
||||
expect(updateComponents.getCall(0).args[0].TAB_ID.meta.text).to.equal(
|
||||
'New title',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a WithPopoverMenu', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a DeleteComponentButton when focused if its not the only tab', () => {
|
||||
let wrapper = setup();
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
|
||||
wrapper = setup({ editMode: true });
|
||||
wrapper.find(WithPopoverMenu).simulate('click');
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
|
||||
wrapper = setup({
|
||||
editMode: true,
|
||||
parentComponent: {
|
||||
...props.parentComponent,
|
||||
children: props.parentComponent.children.slice(0, 1),
|
||||
},
|
||||
});
|
||||
wrapper.find(WithPopoverMenu).simulate('click');
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderType=RENDER_TAB_CONTENT', () => {
|
||||
it('should render a DashboardComponent', () => {
|
||||
const wrapper = setup({ renderType: RENDER_TAB_CONTENT });
|
||||
// We expect 2 because this Tab has a Row child and the row has a Chart
|
||||
expect(wrapper.find(DashboardComponent)).to.have.length(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
|
||||
|
||||
import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
|
||||
import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import Tabs from '../../../../../src/dashboard/components/gridComponents/Tabs';
|
||||
import WithDragDropContext from '../../helpers/WithDragDropContext';
|
||||
import { dashboardLayoutWithTabs } from '../../fixtures/mockDashboardLayout';
|
||||
import { mockStoreWithTabs } from '../../fixtures/mockStore';
|
||||
import { DASHBOARD_ROOT_ID } from '../../../../../src/dashboard/util/constants';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const props = {
|
||||
id: 'TABS_ID',
|
||||
parentId: DASHBOARD_ROOT_ID,
|
||||
component: dashboardLayoutWithTabs.present.TABS_ID,
|
||||
parentComponent: dashboardLayoutWithTabs.present[DASHBOARD_ROOT_ID],
|
||||
index: 0,
|
||||
depth: 1,
|
||||
renderTabContent: true,
|
||||
editMode: false,
|
||||
availableColumnCount: 12,
|
||||
columnWidth: 50,
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
createComponent() {},
|
||||
handleComponentDrop() {},
|
||||
onChangeTab() {},
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStoreWithTabs}>
|
||||
<WithDragDropContext>
|
||||
<Tabs {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
// test just Tabs with no children DragDroppables
|
||||
const wrapper = setup({ component: { ...props.component, children: [] } });
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render BootstrapTabs', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(BootstrapTabs)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on BootstrapTabs for perf', () => {
|
||||
const wrapper = setup();
|
||||
const tabProps = wrapper.find(BootstrapTabs).props();
|
||||
expect(tabProps.animation).to.equal(true);
|
||||
expect(tabProps.mountOnEnter).to.equal(true);
|
||||
expect(tabProps.unmountOnExit).to.equal(false);
|
||||
});
|
||||
|
||||
it('should render a BootstrapTab for each child', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(BootstrapTab)).to.have.length(
|
||||
props.component.children.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an extra (+) BootstrapTab in editMode', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(BootstrapTab)).to.have.length(
|
||||
props.component.children.length + 1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a DashboardComponent for each child', () => {
|
||||
// note: this does not test Tab content
|
||||
const wrapper = setup({ renderTabContent: false });
|
||||
expect(wrapper.find(DashboardComponent)).to.have.length(
|
||||
props.component.children.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call createComponent if the (+) tab is clicked', () => {
|
||||
const createComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, createComponent });
|
||||
wrapper
|
||||
.find('.dashboard-component-tabs .nav-tabs a')
|
||||
.last()
|
||||
.simulate('click');
|
||||
|
||||
expect(createComponent.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call onChangeTab when a tab is clicked', () => {
|
||||
const onChangeTab = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, onChangeTab });
|
||||
wrapper
|
||||
.find('.dashboard-component-tabs .nav-tabs a')
|
||||
.at(1) // will not call if it is already selected
|
||||
.simulate('click');
|
||||
|
||||
expect(onChangeTab.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should render a HoverMenu in editMode', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(0);
|
||||
|
||||
wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(HoverMenu)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render a DeleteComponentButton in editMode', () => {
|
||||
let wrapper = setup();
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
|
||||
|
||||
wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should call deleteComponent when deleted', () => {
|
||||
const deleteComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, deleteComponent });
|
||||
wrapper.find(DeleteComponentButton).simulate('click');
|
||||
|
||||
expect(deleteComponent.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import DragDroppable from '../../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||
import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
|
||||
import WithDragDropContext from '../../../helpers/WithDragDropContext';
|
||||
|
||||
import { NEW_COMPONENTS_SOURCE_ID } from '../../../../../../src/dashboard/util/constants';
|
||||
import {
|
||||
NEW_COMPONENT_SOURCE_TYPE,
|
||||
CHART_TYPE,
|
||||
} from '../../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('DraggableNewComponent', () => {
|
||||
const props = {
|
||||
id: 'id',
|
||||
type: CHART_TYPE,
|
||||
label: 'label!',
|
||||
className: 'a_class',
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
// We have to wrap provide DragDropContext for the underlying DragDroppable
|
||||
// otherwise we cannot assert on DragDroppable children
|
||||
const wrapper = mount(
|
||||
<WithDragDropContext>
|
||||
<DraggableNewComponent {...props} {...overrideProps} />
|
||||
</WithDragDropContext>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DragDroppable)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should pass component={ type, id } to DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
const dragdroppable = wrapper.find(DragDroppable);
|
||||
expect(dragdroppable.prop('component')).to.deep.equal({
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass appropriate parent source and id to DragDroppable', () => {
|
||||
const wrapper = setup();
|
||||
const dragdroppable = wrapper.find(DragDroppable);
|
||||
expect(dragdroppable.prop('parentComponent')).to.deep.equal({
|
||||
id: NEW_COMPONENTS_SOURCE_ID,
|
||||
type: NEW_COMPONENT_SOURCE_TYPE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the passed label', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.new-component').text()).to.equal(props.label);
|
||||
});
|
||||
|
||||
it('should add the passed className', () => {
|
||||
const wrapper = setup();
|
||||
const className = `.new-component-placeholder.${props.className}`;
|
||||
expect(wrapper.find(className)).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
|
||||
import NewColumn from '../../../../../../src/dashboard/components/gridComponents/new/NewColumn';
|
||||
|
||||
import { NEW_COLUMN_ID } from '../../../../../../src/dashboard/util/constants';
|
||||
import { COLUMN_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('NewColumn', () => {
|
||||
function setup() {
|
||||
return shallow(<NewColumn />);
|
||||
}
|
||||
|
||||
it('should render a DraggableNewComponent', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set appropriate type and id', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent).props()).to.include({
|
||||
type: COLUMN_TYPE,
|
||||
id: NEW_COLUMN_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
|
||||
import NewDivider from '../../../../../../src/dashboard/components/gridComponents/new/NewDivider';
|
||||
|
||||
import { NEW_DIVIDER_ID } from '../../../../../../src/dashboard/util/constants';
|
||||
import { DIVIDER_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('NewDivider', () => {
|
||||
function setup() {
|
||||
return shallow(<NewDivider />);
|
||||
}
|
||||
|
||||
it('should render a DraggableNewComponent', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set appropriate type and id', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent).props()).to.include({
|
||||
type: DIVIDER_TYPE,
|
||||
id: NEW_DIVIDER_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
|
||||
import NewHeader from '../../../../../../src/dashboard/components/gridComponents/new/NewHeader';
|
||||
|
||||
import { NEW_HEADER_ID } from '../../../../../../src/dashboard/util/constants';
|
||||
import { HEADER_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('NewHeader', () => {
|
||||
function setup() {
|
||||
return shallow(<NewHeader />);
|
||||
}
|
||||
|
||||
it('should render a DraggableNewComponent', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set appropriate type and id', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent).props()).to.include({
|
||||
type: HEADER_TYPE,
|
||||
id: NEW_HEADER_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
|
||||
import NewRow from '../../../../../../src/dashboard/components/gridComponents/new/NewRow';
|
||||
|
||||
import { NEW_ROW_ID } from '../../../../../../src/dashboard/util/constants';
|
||||
import { ROW_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('NewRow', () => {
|
||||
function setup() {
|
||||
return shallow(<NewRow />);
|
||||
}
|
||||
|
||||
it('should render a DraggableNewComponent', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set appropriate type and id', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent).props()).to.include({
|
||||
type: ROW_TYPE,
|
||||
id: NEW_ROW_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
|
||||
import NewTabs from '../../../../../../src/dashboard/components/gridComponents/new/NewTabs';
|
||||
|
||||
import { NEW_TABS_ID } from '../../../../../../src/dashboard/util/constants';
|
||||
import { TABS_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('NewTabs', () => {
|
||||
function setup() {
|
||||
return shallow(<NewTabs />);
|
||||
}
|
||||
|
||||
it('should render a DraggableNewComponent', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should set appropriate type and id', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(DraggableNewComponent).props()).to.include({
|
||||
type: TABS_TYPE,
|
||||
id: NEW_TABS_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
|
||||
|
||||
describe('HoverMenu', () => {
|
||||
it('should render a div.hover-menu', () => {
|
||||
const wrapper = shallow(<HoverMenu />);
|
||||
expect(wrapper.find('.hover-menu')).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||
|
||||
describe('WithPopoverMenu', () => {
|
||||
const props = {
|
||||
children: <div id="child" />,
|
||||
disableClick: false,
|
||||
menuItems: [<div id="menu1" />, <div id="menu2" />],
|
||||
onChangeFocus() {},
|
||||
shouldFocus: () => true, // needed for mock
|
||||
isFocused: false,
|
||||
editMode: false,
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
const wrapper = shallow(<WithPopoverMenu {...props} {...overrideProps} />);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('should render a div with class "with-popover-menu"', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('.with-popover-menu')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should render the passed children', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('#child')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should focus on click in editMode', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.state('isFocused')).to.equal(false);
|
||||
|
||||
wrapper.simulate('click');
|
||||
expect(wrapper.state('isFocused')).to.equal(false);
|
||||
|
||||
wrapper.setProps({ ...props, editMode: true });
|
||||
wrapper.simulate('click');
|
||||
expect(wrapper.state('isFocused')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should render menuItems when focused', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find('#menu1')).to.have.length(0);
|
||||
expect(wrapper.find('#menu2')).to.have.length(0);
|
||||
|
||||
wrapper.simulate('click');
|
||||
expect(wrapper.find('#menu1')).to.have.length(1);
|
||||
expect(wrapper.find('#menu2')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should not focus when disableClick=true', () => {
|
||||
const wrapper = setup({ disableClick: true, editMode: true });
|
||||
expect(wrapper.state('isFocused')).to.equal(false);
|
||||
|
||||
wrapper.simulate('click');
|
||||
expect(wrapper.state('isFocused')).to.equal(false);
|
||||
});
|
||||
|
||||
it('should use the passed shouldFocus func to determine if it should focus', () => {
|
||||
const wrapper = setup({ editMode: true, shouldFocus: () => false });
|
||||
expect(wrapper.state('isFocused')).to.equal(false);
|
||||
|
||||
wrapper.simulate('click');
|
||||
expect(wrapper.state('isFocused')).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import Resizable from 're-resizable';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
|
||||
|
||||
describe('ResizableContainer', () => {
|
||||
const props = { editMode: false, id: 'id' };
|
||||
|
||||
function setup(propOverrides) {
|
||||
return shallow(<ResizableContainer {...props} {...propOverrides} />);
|
||||
}
|
||||
|
||||
it('should render a Resizable', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Resizable)).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import ResizableHandle from '../../../../../src/dashboard/components/resizable/ResizableHandle';
|
||||
|
||||
describe('ResizableHandle', () => {
|
||||
it('should render a right resize handle', () => {
|
||||
const wrapper = shallow(<ResizableHandle.right />);
|
||||
expect(wrapper.find('.resize-handle.resize-handle--right')).to.have.length(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a bottom resize handle', () => {
|
||||
const wrapper = shallow(<ResizableHandle.bottom />);
|
||||
expect(wrapper.find('.resize-handle.resize-handle--bottom')).to.have.length(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a bottomRight resize handle', () => {
|
||||
const wrapper = shallow(<ResizableHandle.bottomRight />);
|
||||
expect(
|
||||
wrapper.find('.resize-handle.resize-handle--bottom-right'),
|
||||
).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
import { getInitialState } from '../../../src/dashboard/reducers';
|
||||
|
||||
export const defaultFilters = {
|
||||
256: { region: [] },
|
||||
257: { country_name: ['United States'] },
|
||||
};
|
||||
export const regionFilter = {
|
||||
datasource: null,
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
edit_url: '/slicemodelview/edit/256',
|
||||
form_data: {
|
||||
datasource: '2__table',
|
||||
date_filter: false,
|
||||
filters: [{
|
||||
col: 'country_name',
|
||||
op: 'in',
|
||||
val: ['United States', 'France', 'Japan'],
|
||||
}],
|
||||
granularity_sqla: null,
|
||||
groupby: ['region', 'country_name'],
|
||||
having: '',
|
||||
instant_filtering: true,
|
||||
metric: 'sum__SP_POP_TOTL',
|
||||
show_druid_time_granularity: false,
|
||||
show_druid_time_origin: false,
|
||||
show_sqla_time_column: false,
|
||||
show_sqla_time_granularity: false,
|
||||
since: '100 years ago',
|
||||
slice_id: 256,
|
||||
time_grain_sqla: null,
|
||||
until: 'now',
|
||||
viz_type: 'filter_box',
|
||||
where: '',
|
||||
},
|
||||
slice_id: 256,
|
||||
slice_name: 'Region Filters',
|
||||
slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20256%7D',
|
||||
};
|
||||
export const countryFilter = {
|
||||
datasource: null,
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
edit_url: '/slicemodelview/edit/257',
|
||||
form_data: {
|
||||
datasource: '2__table',
|
||||
date_filter: false,
|
||||
filters: [],
|
||||
granularity_sqla: null,
|
||||
groupby: ['country_name'],
|
||||
having: '',
|
||||
instant_filtering: true,
|
||||
metric: 'sum__SP_POP_TOTL',
|
||||
show_druid_time_granularity: false,
|
||||
show_druid_time_origin: false,
|
||||
show_sqla_time_column: false,
|
||||
show_sqla_time_granularity: false,
|
||||
since: '100 years ago',
|
||||
slice_id: 257,
|
||||
time_grain_sqla: null,
|
||||
until: 'now',
|
||||
viz_type: 'filter_box',
|
||||
where: '',
|
||||
},
|
||||
slice_id: 257,
|
||||
slice_name: 'Country Filters',
|
||||
slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20257%7D',
|
||||
};
|
||||
export const slice = {
|
||||
datasource: null,
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
edit_url: '/slicemodelview/edit/248',
|
||||
form_data: {
|
||||
annotation_layers: [],
|
||||
bottom_margin: 'auto',
|
||||
color_scheme: 'bnbColors',
|
||||
contribution: false,
|
||||
datasource: '2__table',
|
||||
filters: [],
|
||||
granularity_sqla: null,
|
||||
groupby: [],
|
||||
having: '',
|
||||
left_margin: 'auto',
|
||||
limit: 50,
|
||||
line_interpolation: 'linear',
|
||||
metrics: ['sum__SP_POP_TOTL'],
|
||||
num_period_compare: '',
|
||||
order_desc: true,
|
||||
period_ratio_type: 'growth',
|
||||
resample_fillmethod: null,
|
||||
resample_how: null,
|
||||
resample_rule: null,
|
||||
rich_tooltip: true,
|
||||
rolling_type: 'None',
|
||||
show_brush: false,
|
||||
show_legend: true,
|
||||
show_markers: false,
|
||||
since: '1961-01-01T00:00:00',
|
||||
slice_id: 248,
|
||||
time_compare: null,
|
||||
time_grain_sqla: null,
|
||||
timeseries_limit_metric: null,
|
||||
until: '2014-12-31T00:00:00',
|
||||
viz_type: 'line',
|
||||
where: '',
|
||||
x_axis_format: 'smart_date',
|
||||
x_axis_label: '',
|
||||
x_axis_showminmax: true,
|
||||
y_axis_bounds: [null, null],
|
||||
y_axis_format: '.3s',
|
||||
y_axis_label: '',
|
||||
y_axis_showminmax: true,
|
||||
y_log_scale: false,
|
||||
},
|
||||
slice_id: 248,
|
||||
slice_name: 'Filtered Population',
|
||||
slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
|
||||
};
|
||||
|
||||
const datasources = {};
|
||||
const mockDashboardData = {
|
||||
css: '',
|
||||
dash_edit_perm: true,
|
||||
dash_save_perm: true,
|
||||
dashboard_title: 'Births',
|
||||
id: 2,
|
||||
metadata: {
|
||||
default_filters: JSON.stringify(defaultFilters),
|
||||
filter_immune_slices: [256],
|
||||
timed_refresh_immune_slices: [],
|
||||
filter_immune_slice_fields: {},
|
||||
expanded_slices: {},
|
||||
},
|
||||
position_json: [
|
||||
{
|
||||
size_x: 4,
|
||||
slice_id: '256',
|
||||
row: 0,
|
||||
size_y: 4,
|
||||
col: 5,
|
||||
},
|
||||
{
|
||||
size_x: 4,
|
||||
slice_id: '248',
|
||||
row: 0,
|
||||
size_y: 4,
|
||||
col: 1,
|
||||
},
|
||||
],
|
||||
slug: 'births',
|
||||
slices: [regionFilter, slice, countryFilter],
|
||||
standalone_mode: false,
|
||||
};
|
||||
export const { dashboard, charts } = getInitialState({
|
||||
common: {},
|
||||
dashboard_data: mockDashboardData,
|
||||
datasources,
|
||||
user_id: '1',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { datasourceId } from './mockDatasource';
|
||||
|
||||
export const sliceId = 18;
|
||||
|
||||
export default {
|
||||
[sliceId]: {
|
||||
id: sliceId,
|
||||
chartAlert: null,
|
||||
chartStatus: 'rendered',
|
||||
chartUpdateEndTime: 1525852456388,
|
||||
chartUpdateStartTime: 1525852454838,
|
||||
latestQueryFormData: {},
|
||||
queryRequest: {},
|
||||
queryResponse: {},
|
||||
triggerQuery: false,
|
||||
lastRendered: 0,
|
||||
form_data: {
|
||||
slice_id: sliceId,
|
||||
viz_type: 'pie',
|
||||
row_limit: 50000,
|
||||
metric: 'sum__num',
|
||||
since: '100 years ago',
|
||||
groupby: ['gender'],
|
||||
metrics: ['sum__num'],
|
||||
compare_lag: '10',
|
||||
limit: '25',
|
||||
until: 'now',
|
||||
granularity: 'ds',
|
||||
markup_type: 'markdown',
|
||||
where: '',
|
||||
compare_suffix: 'o10Y',
|
||||
datasource: datasourceId,
|
||||
},
|
||||
formData: {
|
||||
datasource: datasourceId,
|
||||
viz_type: 'pie',
|
||||
slice_id: sliceId,
|
||||
granularity_sqla: null,
|
||||
time_grain_sqla: null,
|
||||
since: '100 years ago',
|
||||
until: 'now',
|
||||
metrics: ['sum__num'],
|
||||
groupby: ['gender'],
|
||||
limit: '25',
|
||||
pie_label_type: 'key',
|
||||
donut: false,
|
||||
show_legend: true,
|
||||
labels_outside: true,
|
||||
color_scheme: 'bnbColors',
|
||||
where: '',
|
||||
having: '',
|
||||
filters: [],
|
||||
row_limit: 50000,
|
||||
metric: 'sum__num',
|
||||
compare_lag: '10',
|
||||
granularity: 'ds',
|
||||
markup_type: 'markdown',
|
||||
compare_suffix: 'o10Y',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
id: 1234,
|
||||
slug: 'dashboardSlug',
|
||||
metadata: {},
|
||||
userId: 'mock_user_id',
|
||||
dash_edit_perm: true,
|
||||
dash_save_perm: true,
|
||||
common: {
|
||||
flash_messages: [],
|
||||
conf: { ENABLE_JAVASCRIPT_CONTROLS: false, SUPERSET_WEBSERVER_TIMEOUT: 60 },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_HEADER_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
CHART_TYPE,
|
||||
ROW_TYPE,
|
||||
COLUMN_TYPE,
|
||||
MARKDOWN_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
import {
|
||||
DASHBOARD_ROOT_ID,
|
||||
DASHBOARD_HEADER_ID,
|
||||
DASHBOARD_GRID_ID,
|
||||
} from '../../../../src/dashboard/util/constants';
|
||||
|
||||
import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
|
||||
|
||||
import { sliceId as chartId } from './mockChartQueries';
|
||||
|
||||
export const sliceId = chartId;
|
||||
|
||||
export const dashboardLayout = {
|
||||
past: [],
|
||||
present: {
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
type: DASHBOARD_ROOT_TYPE,
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: [DASHBOARD_GRID_ID],
|
||||
},
|
||||
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: ['ROW_ID'],
|
||||
meta: {},
|
||||
},
|
||||
|
||||
[DASHBOARD_HEADER_ID]: {
|
||||
type: DASHBOARD_HEADER_TYPE,
|
||||
id: DASHBOARD_HEADER_ID,
|
||||
meta: {
|
||||
text: 'New dashboard',
|
||||
},
|
||||
},
|
||||
|
||||
ROW_ID: {
|
||||
...newComponentFactory(ROW_TYPE),
|
||||
id: 'ROW_ID',
|
||||
children: ['COLUMN_ID'],
|
||||
},
|
||||
|
||||
COLUMN_ID: {
|
||||
...newComponentFactory(COLUMN_TYPE),
|
||||
id: 'COLUMN_ID',
|
||||
children: ['CHART_ID'],
|
||||
},
|
||||
|
||||
CHART_ID: {
|
||||
...newComponentFactory(CHART_TYPE),
|
||||
id: 'CHART_ID',
|
||||
meta: {
|
||||
chartId,
|
||||
width: 3,
|
||||
height: 10,
|
||||
chartName: 'Mock chart name',
|
||||
},
|
||||
},
|
||||
|
||||
MARKDOWN_ID: {
|
||||
...newComponentFactory(MARKDOWN_TYPE),
|
||||
id: 'MARKDOWN_ID',
|
||||
},
|
||||
},
|
||||
future: [],
|
||||
};
|
||||
|
||||
export const dashboardLayoutWithTabs = {
|
||||
past: [],
|
||||
present: {
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
type: DASHBOARD_ROOT_TYPE,
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: ['TABS_ID'],
|
||||
},
|
||||
|
||||
TABS_ID: {
|
||||
id: 'TABS_ID',
|
||||
type: TABS_TYPE,
|
||||
children: ['TAB_ID', 'TAB_ID2'],
|
||||
},
|
||||
|
||||
TAB_ID: {
|
||||
id: 'TAB_ID',
|
||||
type: TAB_TYPE,
|
||||
children: ['ROW_ID'],
|
||||
meta: {
|
||||
text: 'tab1',
|
||||
},
|
||||
},
|
||||
|
||||
TAB_ID2: {
|
||||
id: 'TAB_ID2',
|
||||
type: TAB_TYPE,
|
||||
children: [],
|
||||
meta: {
|
||||
text: 'tab2',
|
||||
},
|
||||
},
|
||||
|
||||
CHART_ID: {
|
||||
...newComponentFactory(CHART_TYPE),
|
||||
id: 'CHART_ID',
|
||||
meta: {
|
||||
chartId,
|
||||
width: 3,
|
||||
height: 10,
|
||||
chartName: 'Mock chart name',
|
||||
},
|
||||
},
|
||||
|
||||
ROW_ID: {
|
||||
...newComponentFactory(ROW_TYPE),
|
||||
id: 'ROW_ID',
|
||||
children: ['CHART_ID'],
|
||||
},
|
||||
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: [],
|
||||
meta: {},
|
||||
},
|
||||
|
||||
[DASHBOARD_HEADER_ID]: {
|
||||
type: DASHBOARD_HEADER_TYPE,
|
||||
id: DASHBOARD_HEADER_ID,
|
||||
meta: {
|
||||
text: 'New dashboard',
|
||||
},
|
||||
},
|
||||
},
|
||||
future: [],
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { id as sliceId } from './mockChartQueries';
|
||||
|
||||
export default {
|
||||
sliceIds: [sliceId],
|
||||
refresh: false,
|
||||
filters: {},
|
||||
expandedSlices: {},
|
||||
editMode: false,
|
||||
showBuilderPane: false,
|
||||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
isStarred: true,
|
||||
css: '',
|
||||
isV2Preview: false, // @TODO remove upon v1 deprecation
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
export const id = 7;
|
||||
export const datasourceId = `${id}__table`;
|
||||
|
||||
export default {
|
||||
[datasourceId]: {
|
||||
verbose_map: {
|
||||
count: 'COUNT(*)',
|
||||
__timestamp: 'Time',
|
||||
sum__sum_girls: 'sum__sum_girls',
|
||||
name: 'name',
|
||||
avg__sum_girls: 'avg__sum_girls',
|
||||
gender: 'gender',
|
||||
sum_girls: 'sum_girls',
|
||||
ds: 'ds',
|
||||
sum__sum_boys: 'sum__sum_boys',
|
||||
state: 'state',
|
||||
num: 'num',
|
||||
sum__num: 'sum__num',
|
||||
sum_boys: 'sum_boys',
|
||||
avg__num: 'avg__num',
|
||||
avg__sum_boys: 'avg__sum_boys',
|
||||
},
|
||||
gb_cols: [['gender', 'gender'], ['name', 'name'], ['state', 'state']],
|
||||
metrics: [
|
||||
{
|
||||
expression: 'SUM(birth_names.num)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__num',
|
||||
metric_name: 'sum__num',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'AVG(birth_names.num)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__num',
|
||||
metric_name: 'avg__num',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'SUM(birth_names.sum_boys)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__sum_boys',
|
||||
metric_name: 'sum__sum_boys',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'AVG(birth_names.sum_boys)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__sum_boys',
|
||||
metric_name: 'avg__sum_boys',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'SUM(birth_names.sum_girls)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__sum_girls',
|
||||
metric_name: 'sum__sum_girls',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'AVG(birth_names.sum_girls)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__sum_girls',
|
||||
metric_name: 'avg__sum_girls',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'COUNT(*)',
|
||||
warning_text: null,
|
||||
verbose_name: 'COUNT(*)',
|
||||
metric_name: 'count',
|
||||
description: null,
|
||||
},
|
||||
],
|
||||
column_formats: {},
|
||||
columns: [
|
||||
{
|
||||
type: 'DATETIME',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: true,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'ds',
|
||||
},
|
||||
{
|
||||
type: 'VARCHAR(16)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'gender',
|
||||
},
|
||||
{
|
||||
type: 'VARCHAR(255)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'num',
|
||||
},
|
||||
{
|
||||
type: 'VARCHAR(10)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'state',
|
||||
},
|
||||
{
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'sum_boys',
|
||||
},
|
||||
{
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'sum_girls',
|
||||
},
|
||||
],
|
||||
id,
|
||||
granularity_sqla: [['ds', 'ds']],
|
||||
name: 'birth_names',
|
||||
database: {
|
||||
allow_multi_schema_metadata_fetch: null,
|
||||
name: 'main',
|
||||
backend: 'sqlite',
|
||||
},
|
||||
time_grain_sqla: [
|
||||
[null, 'Time Column'],
|
||||
['PT1H', 'hour'],
|
||||
['P1D', 'day'],
|
||||
['P1W', 'week'],
|
||||
['P1M', 'month'],
|
||||
],
|
||||
filterable_cols: [
|
||||
['gender', 'gender'],
|
||||
['name', 'name'],
|
||||
['state', 'state'],
|
||||
],
|
||||
all_cols: [
|
||||
['ds', 'ds'],
|
||||
['gender', 'gender'],
|
||||
['name', 'name'],
|
||||
['num', 'num'],
|
||||
['state', 'state'],
|
||||
['sum_boys', 'sum_boys'],
|
||||
['sum_girls', 'sum_girls'],
|
||||
],
|
||||
filter_select: true,
|
||||
order_by_choices: [
|
||||
['["ds", true]', 'ds [asc]'],
|
||||
['["ds", false]', 'ds [desc]'],
|
||||
['["gender", true]', 'gender [asc]'],
|
||||
['["gender", false]', 'gender [desc]'],
|
||||
['["name", true]', 'name [asc]'],
|
||||
['["name", false]', 'name [desc]'],
|
||||
['["num", true]', 'num [asc]'],
|
||||
['["num", false]', 'num [desc]'],
|
||||
['["state", true]', 'state [asc]'],
|
||||
['["state", false]', 'state [desc]'],
|
||||
['["sum_boys", true]', 'sum_boys [asc]'],
|
||||
['["sum_boys", false]', 'sum_boys [desc]'],
|
||||
['["sum_girls", true]', 'sum_girls [asc]'],
|
||||
['["sum_girls", false]', 'sum_girls [desc]'],
|
||||
],
|
||||
metrics_combo: [
|
||||
['count', 'COUNT(*)'],
|
||||
['avg__num', 'avg__num'],
|
||||
['avg__sum_boys', 'avg__sum_boys'],
|
||||
['avg__sum_girls', 'avg__sum_girls'],
|
||||
['sum__num', 'sum__num'],
|
||||
['sum__sum_boys', 'sum__sum_boys'],
|
||||
['sum__sum_girls', 'sum__sum_girls'],
|
||||
],
|
||||
type: 'table',
|
||||
edit_url: '/tablemodelview/edit/7',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
INFO_TOAST,
|
||||
DANGER_TOAST,
|
||||
} from '../../../../src/dashboard/util/constants';
|
||||
|
||||
export default [
|
||||
{ id: 'info_id', toastType: INFO_TOAST, text: 'info toast' },
|
||||
{ id: 'danger_id', toastType: DANGER_TOAST, text: 'danger toast' },
|
||||
];
|
||||
@@ -0,0 +1,177 @@
|
||||
import { sliceId as id } from './mockChartQueries';
|
||||
import { datasourceId } from './mockDatasource';
|
||||
|
||||
export const sliceId = id;
|
||||
|
||||
export const sliceEntitiesForChart = {
|
||||
slices: {
|
||||
[sliceId]: {
|
||||
slice_id: sliceId,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%2018%7D',
|
||||
slice_name: 'Genders',
|
||||
form_data: {
|
||||
slice_id: sliceId,
|
||||
viz_type: 'pie',
|
||||
row_limit: 50000,
|
||||
metric: 'sum__num',
|
||||
since: '100 years ago',
|
||||
groupby: ['gender'],
|
||||
metrics: ['sum__num'],
|
||||
compare_lag: '10',
|
||||
limit: '25',
|
||||
until: 'now',
|
||||
granularity: 'ds',
|
||||
markup_type: 'markdown',
|
||||
where: '',
|
||||
compare_suffix: 'o10Y',
|
||||
datasource: datasourceId,
|
||||
},
|
||||
edit_url: `/slicemodelview/edit/${sliceId}`,
|
||||
viz_type: 'pie',
|
||||
datasource: datasourceId,
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
lastUpdated: 0,
|
||||
};
|
||||
|
||||
export const sliceEntitiesForDashboard = {
|
||||
slices: {
|
||||
127: {
|
||||
slice_id: 127,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D',
|
||||
slice_name: 'Region Filter',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/127',
|
||||
viz_type: 'filter_box',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332615,
|
||||
},
|
||||
128: {
|
||||
slice_id: 128,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20128%7D',
|
||||
slice_name: "World's Population",
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/128',
|
||||
viz_type: 'big_number',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332628,
|
||||
},
|
||||
129: {
|
||||
slice_id: 129,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20129%7D',
|
||||
slice_name: 'Most Populated Countries',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/129',
|
||||
viz_type: 'table',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332637,
|
||||
},
|
||||
130: {
|
||||
slice_id: 130,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20130%7D',
|
||||
slice_name: 'Growth Rate',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/130',
|
||||
viz_type: 'line',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332645,
|
||||
},
|
||||
131: {
|
||||
slice_id: 131,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20131%7D',
|
||||
slice_name: '% Rural',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/131',
|
||||
viz_type: 'world_map',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332654,
|
||||
},
|
||||
132: {
|
||||
slice_id: 132,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20132%7D',
|
||||
slice_name: 'Life Expectancy VS Rural %',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/132',
|
||||
viz_type: 'bubble',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332663,
|
||||
},
|
||||
133: {
|
||||
slice_id: 133,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20133%7D',
|
||||
slice_name: 'Rural Breakdown',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/133',
|
||||
viz_type: 'sunburst',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332673,
|
||||
},
|
||||
134: {
|
||||
slice_id: 134,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20134%7D',
|
||||
slice_name: "World's Pop Growth",
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/134',
|
||||
viz_type: 'area',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332680,
|
||||
},
|
||||
135: {
|
||||
slice_id: 135,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20135%7D',
|
||||
slice_name: 'Box plot',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/135',
|
||||
viz_type: 'box_plot',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332688,
|
||||
},
|
||||
136: {
|
||||
slice_id: 136,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20136%7D',
|
||||
slice_name: 'Treemap',
|
||||
form_data: {},
|
||||
edit_url: '/slicemodelview/edit/136',
|
||||
viz_type: 'treemap',
|
||||
datasource: '2__table',
|
||||
description: null,
|
||||
description_markeddown: '',
|
||||
modified: '23 hours ago',
|
||||
changed_on: 1529453332700,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
lastUpdated: 0,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import chartQueries from './mockChartQueries';
|
||||
import { dashboardLayout } from './mockDashboardLayout';
|
||||
import dashboardInfo from './mockDashboardInfo';
|
||||
import dashboardState from './mockDashboardState';
|
||||
import messageToasts from './mockMessageToasts';
|
||||
import datasources from './mockDatasource';
|
||||
import sliceEntities from './mockSliceEntities';
|
||||
|
||||
export default {
|
||||
datasources,
|
||||
sliceEntities,
|
||||
charts: chartQueries,
|
||||
dashboardInfo,
|
||||
dashboardState,
|
||||
dashboardLayout,
|
||||
messageToasts,
|
||||
impressionId: 'mock_impression_id',
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import rootReducer from '../../../../src/dashboard/reducers/index';
|
||||
|
||||
import mockState from './mockState';
|
||||
import { dashboardLayoutWithTabs } from './mockDashboardLayout';
|
||||
|
||||
export const mockStore = createStore(
|
||||
rootReducer,
|
||||
mockState,
|
||||
compose(applyMiddleware(thunk)),
|
||||
);
|
||||
|
||||
export const mockStoreWithTabs = createStore(
|
||||
rootReducer,
|
||||
{
|
||||
...mockState,
|
||||
dashboardLayout: dashboardLayoutWithTabs,
|
||||
},
|
||||
compose(applyMiddleware(thunk)),
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import getDragDropManager from '../../../../src/dashboard/util/getDragDropManager';
|
||||
|
||||
// A helper component that provides a DragDropContext for components that require it
|
||||
class WithDragDropContext extends React.Component {
|
||||
getChildContext() {
|
||||
return {
|
||||
dragDropManager: this.context.dragDropManager || getDragDropManager(),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
WithDragDropContext.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
WithDragDropContext.childContextTypes = {
|
||||
dragDropManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default WithDragDropContext;
|
||||
@@ -0,0 +1,443 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import layoutReducer from '../../../../src/dashboard/reducers/dashboardLayout';
|
||||
|
||||
import {
|
||||
UPDATE_COMPONENTS,
|
||||
DELETE_COMPONENT,
|
||||
CREATE_COMPONENT,
|
||||
MOVE_COMPONENT,
|
||||
CREATE_TOP_LEVEL_TABS,
|
||||
DELETE_TOP_LEVEL_TABS,
|
||||
} from '../../../../src/dashboard/actions/dashboardLayout';
|
||||
|
||||
import {
|
||||
CHART_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
ROW_TYPE,
|
||||
TAB_TYPE,
|
||||
TABS_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
import {
|
||||
DASHBOARD_ROOT_ID,
|
||||
DASHBOARD_GRID_ID,
|
||||
NEW_COMPONENTS_SOURCE_ID,
|
||||
NEW_TABS_ID,
|
||||
NEW_ROW_ID,
|
||||
} from '../../../../src/dashboard/util/constants';
|
||||
|
||||
describe('dashboardLayout reducer', () => {
|
||||
it('should return initial state for unrecognized actions', () => {
|
||||
expect(layoutReducer(undefined, {})).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('should delete a component, remove its reference in its parent, and recursively all of its children', () => {
|
||||
expect(
|
||||
layoutReducer(
|
||||
{
|
||||
toDelete: {
|
||||
id: 'toDelete',
|
||||
children: ['child1'],
|
||||
},
|
||||
child1: {
|
||||
id: 'child1',
|
||||
children: ['child2'],
|
||||
},
|
||||
child2: {
|
||||
id: 'child2',
|
||||
children: [],
|
||||
},
|
||||
parentId: {
|
||||
id: 'parentId',
|
||||
type: ROW_TYPE,
|
||||
children: ['toDelete', 'anotherId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: DELETE_COMPONENT,
|
||||
payload: { id: 'toDelete', parentId: 'parentId' },
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
parentId: {
|
||||
id: 'parentId',
|
||||
children: ['anotherId'],
|
||||
type: ROW_TYPE,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a parent if the parent was a row and no longer has children', () => {
|
||||
expect(
|
||||
layoutReducer(
|
||||
{
|
||||
grandparentId: {
|
||||
id: 'grandparentId',
|
||||
children: ['parentId'],
|
||||
},
|
||||
parentId: {
|
||||
id: 'parentId',
|
||||
type: ROW_TYPE,
|
||||
children: ['toDelete'],
|
||||
},
|
||||
toDelete: {
|
||||
id: 'toDelete',
|
||||
children: ['child1'],
|
||||
},
|
||||
child1: {
|
||||
id: 'child1',
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: DELETE_COMPONENT,
|
||||
payload: { id: 'toDelete', parentId: 'parentId' },
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
grandparentId: {
|
||||
id: 'grandparentId',
|
||||
children: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update components', () => {
|
||||
expect(
|
||||
layoutReducer(
|
||||
{
|
||||
update: {
|
||||
id: 'update',
|
||||
children: [],
|
||||
},
|
||||
update2: {
|
||||
id: 'update2',
|
||||
children: [],
|
||||
},
|
||||
dontUpdate: {
|
||||
id: 'dontUpdate',
|
||||
something: 'something',
|
||||
children: ['abcd'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UPDATE_COMPONENTS,
|
||||
payload: {
|
||||
nextComponents: {
|
||||
update: {
|
||||
id: 'update',
|
||||
newField: 'newField',
|
||||
},
|
||||
update2: {
|
||||
id: 'update2',
|
||||
newField: 'newField',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
update: {
|
||||
id: 'update',
|
||||
newField: 'newField',
|
||||
},
|
||||
update2: {
|
||||
id: 'update2',
|
||||
newField: 'newField',
|
||||
},
|
||||
dontUpdate: {
|
||||
id: 'dontUpdate',
|
||||
something: 'something',
|
||||
children: ['abcd'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should move a component', () => {
|
||||
const layout = {
|
||||
source: {
|
||||
id: 'source',
|
||||
type: ROW_TYPE,
|
||||
children: ['dontMove', 'toMove'],
|
||||
},
|
||||
destination: {
|
||||
id: 'destination',
|
||||
type: ROW_TYPE,
|
||||
children: ['anotherChild'],
|
||||
},
|
||||
toMove: {
|
||||
id: 'toMove',
|
||||
type: CHART_TYPE,
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
|
||||
const dropResult = {
|
||||
source: { id: 'source', type: ROW_TYPE, index: 1 },
|
||||
destination: { id: 'destination', type: ROW_TYPE, index: 0 },
|
||||
dragging: { id: 'toMove', type: CHART_TYPE },
|
||||
};
|
||||
|
||||
expect(
|
||||
layoutReducer(layout, {
|
||||
type: MOVE_COMPONENT,
|
||||
payload: { dropResult },
|
||||
}),
|
||||
).to.deep.equal({
|
||||
source: {
|
||||
id: 'source',
|
||||
type: ROW_TYPE,
|
||||
children: ['dontMove'],
|
||||
},
|
||||
destination: {
|
||||
id: 'destination',
|
||||
type: ROW_TYPE,
|
||||
children: ['toMove', 'anotherChild'],
|
||||
},
|
||||
toMove: {
|
||||
id: 'toMove',
|
||||
type: CHART_TYPE,
|
||||
children: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap a moved component in a row if need be', () => {
|
||||
const layout = {
|
||||
source: {
|
||||
id: 'source',
|
||||
type: ROW_TYPE,
|
||||
children: ['dontMove', 'toMove'],
|
||||
},
|
||||
destination: {
|
||||
id: 'destination',
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
children: [],
|
||||
},
|
||||
toMove: {
|
||||
id: 'toMove',
|
||||
type: CHART_TYPE,
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
|
||||
const dropResult = {
|
||||
source: { id: 'source', type: ROW_TYPE, index: 1 },
|
||||
destination: { id: 'destination', type: DASHBOARD_GRID_TYPE, index: 0 },
|
||||
dragging: { id: 'toMove', type: CHART_TYPE },
|
||||
};
|
||||
|
||||
const result = layoutReducer(layout, {
|
||||
type: MOVE_COMPONENT,
|
||||
payload: { dropResult },
|
||||
});
|
||||
|
||||
const newRow = Object.values(result).find(
|
||||
component =>
|
||||
['source', 'destination', 'toMove'].indexOf(component.id) === -1,
|
||||
);
|
||||
|
||||
expect(newRow.children[0]).to.equal('toMove');
|
||||
expect(result.destination.children[0]).to.equal(newRow.id);
|
||||
expect(Object.keys(result)).to.have.length(4);
|
||||
});
|
||||
|
||||
it('should add top-level tabs from a new tabs component, moving grid children to new tab', () => {
|
||||
const layout = {
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: [DASHBOARD_GRID_ID],
|
||||
},
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: ['child'],
|
||||
},
|
||||
child: {
|
||||
id: 'child',
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
|
||||
const dropResult = {
|
||||
source: { id: NEW_COMPONENTS_SOURCE_ID, type: '' },
|
||||
destination: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
type: DASHBOARD_ROOT_TYPE,
|
||||
index: 0,
|
||||
},
|
||||
dragging: { id: NEW_TABS_ID, type: TABS_TYPE },
|
||||
};
|
||||
|
||||
const result = layoutReducer(layout, {
|
||||
type: CREATE_TOP_LEVEL_TABS,
|
||||
payload: { dropResult },
|
||||
});
|
||||
|
||||
const tabComponent = Object.values(result).find(
|
||||
component => component.type === TAB_TYPE,
|
||||
);
|
||||
|
||||
const tabsComponent = Object.values(result).find(
|
||||
component => component.type === TABS_TYPE,
|
||||
);
|
||||
|
||||
expect(Object.keys(result)).to.have.length(5); // initial + Tabs + Tab
|
||||
expect(result[DASHBOARD_ROOT_ID].children[0]).to.equal(tabsComponent.id);
|
||||
expect(result[tabsComponent.id].children[0]).to.equal(tabComponent.id);
|
||||
expect(result[tabComponent.id].children[0]).to.equal('child');
|
||||
expect(result[DASHBOARD_GRID_ID].children).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should add top-level tabs from an existing tabs component, moving grid children to new tab', () => {
|
||||
const layout = {
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: [DASHBOARD_GRID_ID],
|
||||
},
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: ['child', 'tabs', 'child2'],
|
||||
},
|
||||
child: {
|
||||
id: 'child',
|
||||
children: [],
|
||||
},
|
||||
child2: {
|
||||
id: 'child2',
|
||||
children: [],
|
||||
},
|
||||
tabs: {
|
||||
id: 'tabs',
|
||||
type: TABS_TYPE,
|
||||
children: ['tab'],
|
||||
},
|
||||
tab: {
|
||||
id: 'tab',
|
||||
type: TAB_TYPE,
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
|
||||
const dropResult = {
|
||||
source: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE, index: 1 },
|
||||
destination: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
type: DASHBOARD_ROOT_TYPE,
|
||||
index: 0,
|
||||
},
|
||||
dragging: { id: 'tabs', type: TABS_TYPE },
|
||||
};
|
||||
|
||||
const result = layoutReducer(layout, {
|
||||
type: CREATE_TOP_LEVEL_TABS,
|
||||
payload: { dropResult },
|
||||
});
|
||||
|
||||
expect(Object.keys(result)).to.have.length(Object.keys(layout).length);
|
||||
expect(result[DASHBOARD_ROOT_ID].children[0]).to.equal('tabs');
|
||||
expect(result.tabs.children[0]).to.equal('tab');
|
||||
expect(result.tab.children).to.deep.equal(['child', 'child2']);
|
||||
expect(result[DASHBOARD_GRID_ID].children).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should remove top-level tabs, moving children to the grid', () => {
|
||||
const layout = {
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: ['tabs'],
|
||||
},
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: [],
|
||||
},
|
||||
child: {
|
||||
id: 'child',
|
||||
children: [],
|
||||
},
|
||||
child2: {
|
||||
id: 'child2',
|
||||
children: [],
|
||||
},
|
||||
tabs: {
|
||||
id: 'tabs',
|
||||
type: TABS_TYPE,
|
||||
children: ['tab'],
|
||||
},
|
||||
tab: {
|
||||
id: 'tab',
|
||||
type: TAB_TYPE,
|
||||
children: ['child', 'child2'],
|
||||
},
|
||||
};
|
||||
|
||||
const dropResult = {
|
||||
source: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE, index: 1 },
|
||||
destination: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
type: DASHBOARD_ROOT_TYPE,
|
||||
index: 0,
|
||||
},
|
||||
dragging: { id: 'tabs', type: TABS_TYPE },
|
||||
};
|
||||
|
||||
const result = layoutReducer(layout, {
|
||||
type: DELETE_TOP_LEVEL_TABS,
|
||||
payload: { dropResult },
|
||||
});
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: [DASHBOARD_GRID_ID],
|
||||
},
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: ['child', 'child2'],
|
||||
},
|
||||
child: {
|
||||
id: 'child',
|
||||
children: [],
|
||||
},
|
||||
child2: {
|
||||
id: 'child2',
|
||||
children: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a component', () => {
|
||||
const layout = {
|
||||
[DASHBOARD_ROOT_ID]: {
|
||||
id: DASHBOARD_ROOT_ID,
|
||||
children: [DASHBOARD_GRID_ID],
|
||||
},
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
children: ['child'],
|
||||
},
|
||||
child: { id: 'child' },
|
||||
};
|
||||
|
||||
const dropResult = {
|
||||
source: { id: NEW_COMPONENTS_SOURCE_ID, type: '' },
|
||||
destination: {
|
||||
id: DASHBOARD_GRID_ID,
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
index: 1,
|
||||
},
|
||||
dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
|
||||
};
|
||||
|
||||
const result = layoutReducer(layout, {
|
||||
type: CREATE_COMPONENT,
|
||||
payload: { dropResult },
|
||||
});
|
||||
|
||||
const newId = result[DASHBOARD_GRID_ID].children[1];
|
||||
expect(result[DASHBOARD_GRID_ID].children).to.have.length(2);
|
||||
expect(result[newId].type).to.equal(ROW_TYPE);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import {
|
||||
ADD_SLICE,
|
||||
CHANGE_FILTER,
|
||||
ON_CHANGE,
|
||||
ON_SAVE,
|
||||
REMOVE_SLICE,
|
||||
SET_EDIT_MODE,
|
||||
SET_MAX_UNDO_HISTORY_EXCEEDED,
|
||||
SET_UNSAVED_CHANGES,
|
||||
TOGGLE_BUILDER_PANE,
|
||||
TOGGLE_EXPAND_SLICE,
|
||||
TOGGLE_FAVE_STAR,
|
||||
} from '../../../../src/dashboard/actions/dashboardState';
|
||||
|
||||
import dashboardStateReducer from '../../../../src/dashboard/reducers/dashboardState';
|
||||
|
||||
describe('dashboardState reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(dashboardStateReducer(undefined, {})).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('should add a slice', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ sliceIds: [1] },
|
||||
{ type: ADD_SLICE, slice: { slice_id: 2 } },
|
||||
),
|
||||
).to.deep.equal({ sliceIds: [1, 2] });
|
||||
});
|
||||
|
||||
it('should remove a slice', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ sliceIds: [1, 2], filters: {} },
|
||||
{ type: REMOVE_SLICE, sliceId: 2 },
|
||||
),
|
||||
).to.deep.equal({ sliceIds: [1], refresh: false, filters: {} });
|
||||
});
|
||||
|
||||
it('should reset filters if a removed slice is a filter', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ sliceIds: [1, 2], filters: { 2: {}, 1: {} } },
|
||||
{ type: REMOVE_SLICE, sliceId: 2 },
|
||||
),
|
||||
).to.deep.equal({ sliceIds: [1], filters: { 1: {} }, refresh: true });
|
||||
});
|
||||
|
||||
it('should toggle fav star', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ isStarred: false },
|
||||
{ type: TOGGLE_FAVE_STAR, isStarred: true },
|
||||
),
|
||||
).to.deep.equal({ isStarred: true });
|
||||
});
|
||||
|
||||
it('should toggle edit mode', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ editMode: false },
|
||||
{ type: SET_EDIT_MODE, editMode: true },
|
||||
),
|
||||
).to.deep.equal({ editMode: true, showBuilderPane: true });
|
||||
});
|
||||
|
||||
it('should toggle builder pane', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ showBuilderPane: false },
|
||||
{ type: TOGGLE_BUILDER_PANE },
|
||||
),
|
||||
).to.deep.equal({ showBuilderPane: true });
|
||||
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ showBuilderPane: true },
|
||||
{ type: TOGGLE_BUILDER_PANE },
|
||||
),
|
||||
).to.deep.equal({ showBuilderPane: false });
|
||||
});
|
||||
|
||||
it('should toggle expanded slices', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ expandedSlices: { 1: true, 2: false } },
|
||||
{ type: TOGGLE_EXPAND_SLICE, sliceId: 1 },
|
||||
),
|
||||
).to.deep.equal({ expandedSlices: { 2: false } });
|
||||
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{ expandedSlices: { 1: true, 2: false } },
|
||||
{ type: TOGGLE_EXPAND_SLICE, sliceId: 2 },
|
||||
),
|
||||
).to.deep.equal({ expandedSlices: { 1: true, 2: true } });
|
||||
});
|
||||
|
||||
it('should set hasUnsavedChanges', () => {
|
||||
expect(dashboardStateReducer({}, { type: ON_CHANGE })).to.deep.equal({
|
||||
hasUnsavedChanges: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{},
|
||||
{ type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges: false } },
|
||||
),
|
||||
).to.deep.equal({
|
||||
hasUnsavedChanges: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set maxUndoHistoryExceeded', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{},
|
||||
{
|
||||
type: SET_MAX_UNDO_HISTORY_EXCEEDED,
|
||||
payload: { maxUndoHistoryExceeded: true },
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
maxUndoHistoryExceeded: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set unsaved changes, max undo history, and editMode to false on save', () => {
|
||||
expect(
|
||||
dashboardStateReducer({ hasUnsavedChanges: true }, { type: ON_SAVE }),
|
||||
).to.deep.equal({
|
||||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
editMode: false,
|
||||
isV2Preview: false, // @TODO remove upon v1 deprecation
|
||||
});
|
||||
});
|
||||
|
||||
describe('change filter', () => {
|
||||
it('should add a new filter if it does not exist', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{
|
||||
filters: {},
|
||||
sliceIds: [1],
|
||||
},
|
||||
{
|
||||
type: CHANGE_FILTER,
|
||||
chart: { id: 1, formData: { groupby: 'column' } },
|
||||
col: 'column',
|
||||
vals: ['b', 'a'],
|
||||
refresh: true,
|
||||
merge: true,
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
filters: { 1: { column: ['b', 'a'] } },
|
||||
refresh: true,
|
||||
sliceIds: [1],
|
||||
});
|
||||
});
|
||||
|
||||
it('should overwrite a filter if merge is false', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{
|
||||
filters: {
|
||||
1: { column: ['z'] },
|
||||
},
|
||||
sliceIds: [1],
|
||||
},
|
||||
{
|
||||
type: CHANGE_FILTER,
|
||||
chart: { id: 1, formData: { groupby: 'column' } },
|
||||
col: 'column',
|
||||
vals: ['b', 'a'],
|
||||
refresh: true,
|
||||
merge: false,
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
filters: { 1: { column: ['b', 'a'] } },
|
||||
refresh: true,
|
||||
sliceIds: [1],
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge a filter if merge is true', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{
|
||||
filters: {
|
||||
1: { column: ['z'] },
|
||||
},
|
||||
sliceIds: [1],
|
||||
},
|
||||
{
|
||||
type: CHANGE_FILTER,
|
||||
chart: { id: 1, formData: { groupby: 'column' } },
|
||||
col: 'column',
|
||||
vals: ['b', 'a'],
|
||||
refresh: true,
|
||||
merge: true,
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
filters: { 1: { column: ['z', 'b', 'a'] } },
|
||||
refresh: true,
|
||||
sliceIds: [1],
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove the filter if values are empty', () => {
|
||||
expect(
|
||||
dashboardStateReducer(
|
||||
{
|
||||
filters: {
|
||||
1: { column: ['z'] },
|
||||
},
|
||||
sliceIds: [1],
|
||||
},
|
||||
{
|
||||
type: CHANGE_FILTER,
|
||||
chart: { id: 1, formData: { groupby: 'column' } },
|
||||
col: 'column',
|
||||
vals: [],
|
||||
refresh: true,
|
||||
merge: false,
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
filters: {},
|
||||
refresh: true,
|
||||
sliceIds: [1],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import {
|
||||
ADD_TOAST,
|
||||
REMOVE_TOAST,
|
||||
} from '../../../../src/dashboard/actions/messageToasts';
|
||||
import messageToastsReducer from '../../../../src/dashboard/reducers/messageToasts';
|
||||
|
||||
describe('messageToasts reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(messageToastsReducer(undefined, {})).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should add a toast', () => {
|
||||
expect(
|
||||
messageToastsReducer([], {
|
||||
type: ADD_TOAST,
|
||||
payload: { text: 'test', id: 'id', type: 'test_type' },
|
||||
}),
|
||||
).to.deep.equal([{ text: 'test', id: 'id', type: 'test_type' }]);
|
||||
});
|
||||
|
||||
it('should add a toast', () => {
|
||||
expect(
|
||||
messageToastsReducer([{ id: 'id' }, { id: 'id2' }], {
|
||||
type: REMOVE_TOAST,
|
||||
payload: { id: 'id' },
|
||||
}),
|
||||
).to.deep.equal([{ id: 'id2' }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import {
|
||||
FETCH_ALL_SLICES_FAILED,
|
||||
FETCH_ALL_SLICES_STARTED,
|
||||
SET_ALL_SLICES,
|
||||
} from '../../../../src/dashboard/actions/sliceEntities';
|
||||
|
||||
import sliceEntitiesReducer from '../../../../src/dashboard/reducers/sliceEntities';
|
||||
|
||||
describe('sliceEntities reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(sliceEntitiesReducer({}, {})).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('should set loading when fetching slices', () => {
|
||||
expect(
|
||||
sliceEntitiesReducer(
|
||||
{ isLoading: false },
|
||||
{ type: FETCH_ALL_SLICES_STARTED },
|
||||
).isLoading,
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('should set slices', () => {
|
||||
const result = sliceEntitiesReducer(
|
||||
{ slices: { a: {} } },
|
||||
{ type: SET_ALL_SLICES, slices: { 1: {}, 2: {} } },
|
||||
);
|
||||
|
||||
expect(result.slices).to.deep.equal({
|
||||
1: {},
|
||||
2: {},
|
||||
a: {},
|
||||
});
|
||||
expect(result.isLoading).to.equal(false);
|
||||
});
|
||||
|
||||
it('should set an error on error', () => {
|
||||
const result = sliceEntitiesReducer(
|
||||
{},
|
||||
{
|
||||
type: FETCH_ALL_SLICES_FAILED,
|
||||
error: { responseJSON: { message: 'errorrr' } },
|
||||
},
|
||||
);
|
||||
expect(result.isLoading).to.equal(false);
|
||||
expect(result.errorMessage.indexOf('errorrr')).to.be.above(-1);
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { dashboard as reducers } from '../../../src/dashboard/reducers';
|
||||
import * as actions from '../../../src/dashboard/actions';
|
||||
import { defaultFilters, dashboard as initState } from './fixtures';
|
||||
|
||||
describe('Dashboard reducers', () => {
|
||||
it('should remove slice', () => {
|
||||
const action = {
|
||||
type: actions.REMOVE_SLICE,
|
||||
slice: initState.dashboard.slices[1],
|
||||
};
|
||||
expect(initState.dashboard.slices).to.have.length(3);
|
||||
|
||||
const { dashboard, filters, refresh } = reducers(initState, action);
|
||||
expect(dashboard.slices).to.have.length(2);
|
||||
expect(filters).to.deep.equal(defaultFilters);
|
||||
expect(refresh).to.equal(false);
|
||||
});
|
||||
|
||||
it('should remove filter slice', () => {
|
||||
const action = {
|
||||
type: actions.REMOVE_SLICE,
|
||||
slice: initState.dashboard.slices[0],
|
||||
};
|
||||
const initFilters = Object.keys(initState.filters);
|
||||
expect(initFilters).to.have.length(2);
|
||||
|
||||
const { dashboard, filters, refresh } = reducers(initState, action);
|
||||
expect(dashboard.slices).to.have.length(2);
|
||||
expect(Object.keys(filters)).to.have.length(1);
|
||||
expect(refresh).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import componentIsResizable from '../../../../src/dashboard/util/componentIsResizable';
|
||||
import {
|
||||
CHART_TYPE,
|
||||
COLUMN_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
HEADER_TYPE,
|
||||
MARKDOWN_TYPE,
|
||||
ROW_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
const notResizable = [
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
HEADER_TYPE,
|
||||
ROW_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
];
|
||||
|
||||
const resizable = [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE];
|
||||
|
||||
describe('componentIsResizable', () => {
|
||||
resizable.forEach(type => {
|
||||
it(`should return true for ${type}`, () => {
|
||||
expect(componentIsResizable({ type })).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
notResizable.forEach(type => {
|
||||
it(`should return false for ${type}`, () => {
|
||||
expect(componentIsResizable({ type })).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import reorderItem from '../../../../src/dashboard/util/dnd-reorder';
|
||||
|
||||
describe('dnd-reorderItem', () => {
|
||||
it('should remove the item from its source entity and add it to its destination entity', () => {
|
||||
const result = reorderItem({
|
||||
entitiesMap: {
|
||||
a: {
|
||||
id: 'a',
|
||||
children: ['x', 'y', 'z'],
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
children: ['banana'],
|
||||
},
|
||||
},
|
||||
source: { id: 'a', index: 2 },
|
||||
destination: { id: 'b', index: 1 },
|
||||
});
|
||||
|
||||
expect(result.a.children).to.deep.equal(['x', 'y']);
|
||||
expect(result.b.children).to.deep.equal(['banana', 'z']);
|
||||
});
|
||||
|
||||
it('should correctly move elements within the same list', () => {
|
||||
const result = reorderItem({
|
||||
entitiesMap: {
|
||||
a: {
|
||||
id: 'a',
|
||||
children: ['x', 'y', 'z'],
|
||||
},
|
||||
},
|
||||
source: { id: 'a', index: 2 },
|
||||
destination: { id: 'a', index: 0 },
|
||||
});
|
||||
|
||||
expect(result.a.children).to.deep.equal(['z', 'x', 'y']);
|
||||
});
|
||||
|
||||
it('should copy items that do not move into the result', () => {
|
||||
const extraEntity = {};
|
||||
const result = reorderItem({
|
||||
entitiesMap: {
|
||||
a: {
|
||||
id: 'a',
|
||||
children: ['x', 'y', 'z'],
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
children: ['banana'],
|
||||
},
|
||||
iAmExtra: extraEntity,
|
||||
},
|
||||
source: { id: 'a', index: 2 },
|
||||
destination: { id: 'b', index: 1 },
|
||||
});
|
||||
|
||||
expect(result.iAmExtra === extraEntity).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import dropOverflowsParent from '../../../../src/dashboard/util/dropOverflowsParent';
|
||||
import { NEW_COMPONENTS_SOURCE_ID } from '../../../../src/dashboard/util/constants';
|
||||
import {
|
||||
CHART_TYPE,
|
||||
COLUMN_TYPE,
|
||||
ROW_TYPE,
|
||||
HEADER_TYPE,
|
||||
TAB_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('dropOverflowsParent', () => {
|
||||
it('returns true if a parent does NOT have adequate width for child', () => {
|
||||
const dropResult = {
|
||||
source: { id: '_' },
|
||||
destination: { id: 'a' },
|
||||
dragging: { id: 'z' },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: ROW_TYPE,
|
||||
children: ['b', 'b', 'b', 'b'], // width = 4x bs = 12
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
z: {
|
||||
id: 'z',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if a parent DOES have adequate width for child', () => {
|
||||
const dropResult = {
|
||||
source: { id: '_' },
|
||||
destination: { id: 'a' },
|
||||
dragging: { id: 'z' },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: ROW_TYPE,
|
||||
children: ['b', 'b'],
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
z: {
|
||||
id: 'z',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if a child CAN shrink to available parent space', () => {
|
||||
const dropResult = {
|
||||
source: { id: '_' },
|
||||
destination: { id: 'a' },
|
||||
dragging: { id: 'z' },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: ROW_TYPE,
|
||||
children: ['b', 'b'], // 2x b = 10
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 5,
|
||||
},
|
||||
},
|
||||
z: {
|
||||
id: 'z',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true if a child CANNOT shrink to available parent space', () => {
|
||||
const dropResult = {
|
||||
source: { id: '_' },
|
||||
destination: { id: 'a' },
|
||||
dragging: { id: 'b' },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: COLUMN_TYPE,
|
||||
meta: {
|
||||
width: 6,
|
||||
},
|
||||
},
|
||||
// rows with children cannot shrink
|
||||
b: {
|
||||
id: 'b',
|
||||
type: ROW_TYPE,
|
||||
children: ['bChild', 'bChild', 'bChild'],
|
||||
},
|
||||
bChild: {
|
||||
id: 'bChild',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if a column has children that CANNOT shrink to available parent space', () => {
|
||||
const dropResult = {
|
||||
source: { id: '_' },
|
||||
destination: { id: 'destination' },
|
||||
dragging: { id: 'dragging' },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
destination: {
|
||||
id: 'destination',
|
||||
type: ROW_TYPE,
|
||||
children: ['b', 'b'], // 2x b = 10, 2 available
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
type: CHART_TYPE,
|
||||
meta: {
|
||||
width: 5,
|
||||
},
|
||||
},
|
||||
dragging: {
|
||||
id: 'dragging',
|
||||
type: COLUMN_TYPE,
|
||||
meta: {
|
||||
width: 10,
|
||||
},
|
||||
children: ['rowWithChildren'], // 2x b = width 10
|
||||
},
|
||||
rowWithChildren: {
|
||||
id: 'rowWithChildren',
|
||||
type: ROW_TYPE,
|
||||
children: ['b', 'b'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
|
||||
// remove children
|
||||
expect(
|
||||
dropOverflowsParent(dropResult, {
|
||||
...layout,
|
||||
dragging: { ...layout.dragging, children: [] },
|
||||
}),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it('should work with new components that are not in the layout', () => {
|
||||
const dropResult = {
|
||||
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
||||
destination: { id: 'a' },
|
||||
dragging: { type: CHART_TYPE },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: ROW_TYPE,
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
|
||||
});
|
||||
|
||||
it('source/destination without widths should not overflow parent', () => {
|
||||
const dropResult = {
|
||||
source: { id: '_' },
|
||||
destination: { id: 'tab' },
|
||||
dragging: { id: 'header' },
|
||||
};
|
||||
|
||||
const layout = {
|
||||
tab: {
|
||||
id: 'tab',
|
||||
type: TAB_TYPE,
|
||||
},
|
||||
header: {
|
||||
id: 'header',
|
||||
type: HEADER_TYPE,
|
||||
},
|
||||
};
|
||||
|
||||
expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import findFirstParentContainerId from '../../../../src/dashboard/util/findFirstParentContainer';
|
||||
import {
|
||||
DASHBOARD_GRID_ID,
|
||||
DASHBOARD_ROOT_ID,
|
||||
} from '../../../../src/dashboard/util/constants';
|
||||
|
||||
describe('findFirstParentContainer', () => {
|
||||
const mockGridLayout = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
DASHBOARD_ROOT_ID: {
|
||||
type: 'DASHBOARD_ROOT_TYPE',
|
||||
id: 'DASHBOARD_ROOT_ID',
|
||||
children: ['DASHBOARD_GRID_ID'],
|
||||
},
|
||||
DASHBOARD_GRID_ID: {
|
||||
type: 'DASHBOARD_GRID_TYPE',
|
||||
id: 'DASHBOARD_GRID_ID',
|
||||
children: ['DASHBOARD_ROW_TYPE-Bk45URrlQ'],
|
||||
},
|
||||
'DASHBOARD_ROW_TYPE-Bk45URrlQ': {
|
||||
type: 'DASHBOARD_ROW_TYPE',
|
||||
id: 'DASHBOARD_ROW_TYPE-Bk45URrlQ',
|
||||
children: ['DASHBOARD_CHART_TYPE-ryxVc8RHlX'],
|
||||
},
|
||||
'DASHBOARD_CHART_TYPE-ryxVc8RHlX': {
|
||||
type: 'DASHBOARD_CHART_TYPE',
|
||||
id: 'DASHBOARD_CHART_TYPE-ryxVc8RHlX',
|
||||
children: [],
|
||||
},
|
||||
DASHBOARD_HEADER_ID: {
|
||||
id: 'DASHBOARD_HEADER_ID',
|
||||
type: 'DASHBOARD_HEADER_TYPE',
|
||||
},
|
||||
};
|
||||
const mockTabsLayout = {
|
||||
'DASHBOARD_CHART_TYPE-S1gilYABe7': {
|
||||
children: [],
|
||||
id: 'DASHBOARD_CHART_TYPE-S1gilYABe7',
|
||||
type: 'DASHBOARD_CHART_TYPE',
|
||||
},
|
||||
'DASHBOARD_CHART_TYPE-SJli5K0HlQ': {
|
||||
children: [],
|
||||
id: 'DASHBOARD_CHART_TYPE-SJli5K0HlQ',
|
||||
type: 'DASHBOARD_CHART_TYPE',
|
||||
},
|
||||
DASHBOARD_GRID_ID: {
|
||||
children: [],
|
||||
id: 'DASHBOARD_GRID_ID',
|
||||
type: 'DASHBOARD_GRID_TYPE',
|
||||
},
|
||||
DASHBOARD_HEADER_ID: {
|
||||
id: 'DASHBOARD_HEADER_ID',
|
||||
type: 'DASHBOARD_HEADER_TYPE',
|
||||
},
|
||||
DASHBOARD_ROOT_ID: {
|
||||
children: ['DASHBOARD_TABS_TYPE-SkgJ5t0Bem'],
|
||||
id: 'DASHBOARD_ROOT_ID',
|
||||
type: 'DASHBOARD_ROOT_TYPE',
|
||||
},
|
||||
'DASHBOARD_ROW_TYPE-S1B8-JLgX': {
|
||||
children: ['DASHBOARD_CHART_TYPE-SJli5K0HlQ'],
|
||||
id: 'DASHBOARD_ROW_TYPE-S1B8-JLgX',
|
||||
type: 'DASHBOARD_ROW_TYPE',
|
||||
},
|
||||
'DASHBOARD_ROW_TYPE-S1bUb1Ilm': {
|
||||
children: ['DASHBOARD_CHART_TYPE-S1gilYABe7'],
|
||||
id: 'DASHBOARD_ROW_TYPE-S1bUb1Ilm',
|
||||
type: 'DASHBOARD_ROW_TYPE',
|
||||
},
|
||||
'DASHBOARD_TABS_TYPE-ByeLSWyLe7': {
|
||||
children: ['DASHBOARD_TAB_TYPE-BJbLSZ1UeQ'],
|
||||
id: 'DASHBOARD_TABS_TYPE-ByeLSWyLe7',
|
||||
type: 'DASHBOARD_TABS_TYPE',
|
||||
},
|
||||
'DASHBOARD_TABS_TYPE-SkgJ5t0Bem': {
|
||||
children: [
|
||||
'DASHBOARD_TAB_TYPE-HkWJcFCHxQ',
|
||||
'DASHBOARD_TAB_TYPE-ByDBbkLlQ',
|
||||
],
|
||||
id: 'DASHBOARD_TABS_TYPE-SkgJ5t0Bem',
|
||||
meta: {},
|
||||
type: 'DASHBOARD_TABS_TYPE',
|
||||
},
|
||||
'DASHBOARD_TAB_TYPE-BJbLSZ1UeQ': {
|
||||
children: ['DASHBOARD_ROW_TYPE-S1bUb1Ilm'],
|
||||
id: 'DASHBOARD_TAB_TYPE-BJbLSZ1UeQ',
|
||||
type: 'DASHBOARD_TAB_TYPE',
|
||||
},
|
||||
'DASHBOARD_TAB_TYPE-ByDBbkLlQ': {
|
||||
children: ['DASHBOARD_ROW_TYPE-S1B8-JLgX'],
|
||||
id: 'DASHBOARD_TAB_TYPE-ByDBbkLlQ',
|
||||
type: 'DASHBOARD_TAB_TYPE',
|
||||
},
|
||||
'DASHBOARD_TAB_TYPE-HkWJcFCHxQ': {
|
||||
children: ['DASHBOARD_TABS_TYPE-ByeLSWyLe7'],
|
||||
id: 'DASHBOARD_TAB_TYPE-HkWJcFCHxQ',
|
||||
type: 'DASHBOARD_TAB_TYPE',
|
||||
},
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
};
|
||||
|
||||
it('should return grid root', () => {
|
||||
expect(findFirstParentContainerId(mockGridLayout)).to.equal(
|
||||
DASHBOARD_GRID_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return first tab', () => {
|
||||
const tabsId = mockTabsLayout[DASHBOARD_ROOT_ID].children[0];
|
||||
const firstTabId = mockTabsLayout[tabsId].children[0];
|
||||
expect(findFirstParentContainerId(mockTabsLayout)).to.equal(firstTabId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import findParentId from '../../../../src/dashboard/util/findParentId';
|
||||
|
||||
describe('findParentId', () => {
|
||||
const layout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
children: ['b', 'r', 'k'],
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
children: ['x', 'y', 'z'],
|
||||
},
|
||||
z: {
|
||||
id: 'z',
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
it('should return the correct parentId', () => {
|
||||
expect(findParentId({ childId: 'b', layout })).to.equal('a');
|
||||
expect(findParentId({ childId: 'z', layout })).to.equal('b');
|
||||
});
|
||||
|
||||
it('should return null if the parent cannot be found', () => {
|
||||
expect(findParentId({ childId: 'a', layout })).to.equal(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import getChartIdsFromLayout from '../../../../src/dashboard/util/getChartIdsFromLayout';
|
||||
import {
|
||||
ROW_TYPE,
|
||||
CHART_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('getChartIdsFromLayout', () => {
|
||||
const mockLayout = {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: CHART_TYPE,
|
||||
meta: { chartId: 'A' },
|
||||
},
|
||||
b: {
|
||||
id: 'b',
|
||||
type: CHART_TYPE,
|
||||
meta: { chartId: 'B' },
|
||||
},
|
||||
c: {
|
||||
id: 'c',
|
||||
type: ROW_TYPE,
|
||||
meta: { chartId: 'C' },
|
||||
},
|
||||
};
|
||||
|
||||
it('should return an array of chartIds', () => {
|
||||
const result = getChartIdsFromLayout(mockLayout);
|
||||
expect(Array.isArray(result)).to.equal(true);
|
||||
expect(result.includes('A')).to.equal(true);
|
||||
expect(result.includes('B')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should return ids only from CHART_TYPE components', () => {
|
||||
const result = getChartIdsFromLayout(mockLayout);
|
||||
expect(result.length).to.equal(2);
|
||||
expect(result.includes('C')).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import getDetailedComponentWidth from '../../../../src/dashboard/util/getDetailedComponentWidth';
|
||||
import * as types from '../../../../src/dashboard/util/componentTypes';
|
||||
import {
|
||||
GRID_COLUMN_COUNT,
|
||||
GRID_MIN_COLUMN_COUNT,
|
||||
} from '../../../../src/dashboard/util/constants';
|
||||
|
||||
describe('getDetailedComponentWidth', () => {
|
||||
it('should return an object with width, minimumWidth, and occupiedWidth', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({ id: '_', components: {} }),
|
||||
).to.have.all.keys(['minimumWidth', 'occupiedWidth', 'width']);
|
||||
});
|
||||
|
||||
describe('width', () => {
|
||||
it('should be undefined if the component is not resizable and has no defined width', () => {
|
||||
const empty = {
|
||||
width: undefined,
|
||||
occupiedWidth: undefined,
|
||||
minimumWidth: undefined,
|
||||
};
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.HEADER_TYPE },
|
||||
}),
|
||||
).to.deep.equal(empty);
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.DIVIDER_TYPE },
|
||||
}),
|
||||
).to.deep.equal(empty);
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.TAB_TYPE },
|
||||
}),
|
||||
).to.deep.equal(empty);
|
||||
});
|
||||
|
||||
it('should match component meta width for resizeable components', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.CHART_TYPE, meta: { width: 1 } },
|
||||
}),
|
||||
).to.deep.equal({ width: 1, occupiedWidth: 1, minimumWidth: 1 });
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.MARKDOWN_TYPE, meta: { width: 2 } },
|
||||
}),
|
||||
).to.deep.equal({ width: 2, occupiedWidth: 2, minimumWidth: 1 });
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.COLUMN_TYPE, meta: { width: 3 } },
|
||||
}),
|
||||
// note: occupiedWidth is zero for colunns/see test below
|
||||
).to.deep.equal({ width: 3, occupiedWidth: 0, minimumWidth: 1 });
|
||||
});
|
||||
|
||||
it('should be GRID_COLUMN_COUNT for row components WITHOUT parents', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
id: 'row',
|
||||
components: { row: { id: 'row', type: types.ROW_TYPE } },
|
||||
}),
|
||||
).to.deep.equal({
|
||||
width: GRID_COLUMN_COUNT,
|
||||
occupiedWidth: 0,
|
||||
minimumWidth: GRID_MIN_COLUMN_COUNT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match parent width for row components WITH parents', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
id: 'row',
|
||||
components: {
|
||||
row: { id: 'row', type: types.ROW_TYPE },
|
||||
parent: {
|
||||
id: 'parent',
|
||||
type: types.COLUMN_TYPE,
|
||||
children: ['row'],
|
||||
meta: { width: 7 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.deep.equal({
|
||||
width: 7,
|
||||
occupiedWidth: 0,
|
||||
minimumWidth: GRID_MIN_COLUMN_COUNT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use either id or component (to support new components)', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
id: 'id',
|
||||
components: {
|
||||
id: { id: 'id', type: types.CHART_TYPE, meta: { width: 6 } },
|
||||
},
|
||||
}).width,
|
||||
).to.equal(6);
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: 'id', type: types.CHART_TYPE, meta: { width: 6 } },
|
||||
}).width,
|
||||
).to.equal(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('occupiedWidth', () => {
|
||||
it('should reflect the sum of child widths for row components', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
id: 'row',
|
||||
components: {
|
||||
row: {
|
||||
id: 'row',
|
||||
type: types.ROW_TYPE,
|
||||
children: ['child', 'child'],
|
||||
},
|
||||
child: { id: 'child', meta: { width: 3.5 } },
|
||||
},
|
||||
}),
|
||||
).to.deep.equal({ width: 12, occupiedWidth: 7, minimumWidth: 7 });
|
||||
});
|
||||
|
||||
it('should always be zero for column components', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.COLUMN_TYPE, meta: { width: 2 } },
|
||||
}),
|
||||
).to.deep.equal({ width: 2, occupiedWidth: 0, minimumWidth: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('minimumWidth', () => {
|
||||
it('should equal GRID_MIN_COLUMN_COUNT for resizable components', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.CHART_TYPE, meta: { width: 1 } },
|
||||
}),
|
||||
).to.deep.equal({
|
||||
width: 1,
|
||||
minimumWidth: GRID_MIN_COLUMN_COUNT,
|
||||
occupiedWidth: 1,
|
||||
});
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.MARKDOWN_TYPE, meta: { width: 2 } },
|
||||
}),
|
||||
).to.deep.equal({
|
||||
width: 2,
|
||||
minimumWidth: GRID_MIN_COLUMN_COUNT,
|
||||
occupiedWidth: 2,
|
||||
});
|
||||
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
component: { id: '', type: types.COLUMN_TYPE, meta: { width: 3 } },
|
||||
}),
|
||||
).to.deep.equal({
|
||||
width: 3,
|
||||
minimumWidth: GRID_MIN_COLUMN_COUNT,
|
||||
occupiedWidth: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should equal the width of row children for column components with row children', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
id: 'column',
|
||||
components: {
|
||||
column: {
|
||||
id: 'column',
|
||||
type: types.COLUMN_TYPE,
|
||||
children: ['rowChild', 'ignoredChartChild'],
|
||||
meta: { width: 12 },
|
||||
},
|
||||
rowChild: {
|
||||
id: 'rowChild',
|
||||
type: types.ROW_TYPE,
|
||||
children: ['rowChildChild', 'rowChildChild'],
|
||||
},
|
||||
rowChildChild: {
|
||||
id: 'rowChildChild',
|
||||
meta: { width: 3.5 },
|
||||
},
|
||||
ignoredChartChild: {
|
||||
id: 'ignoredChartChild',
|
||||
meta: { width: 100 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
// occupiedWidth is zero for colunns/see test below
|
||||
).to.deep.equal({ width: 12, occupiedWidth: 0, minimumWidth: 7 });
|
||||
});
|
||||
|
||||
it('should equal occupiedWidth for row components', () => {
|
||||
expect(
|
||||
getDetailedComponentWidth({
|
||||
id: 'row',
|
||||
components: {
|
||||
row: {
|
||||
id: 'row',
|
||||
type: types.ROW_TYPE,
|
||||
children: ['child', 'child'],
|
||||
},
|
||||
child: { id: 'child', meta: { width: 3.5 } },
|
||||
},
|
||||
}),
|
||||
).to.deep.equal({ width: 12, occupiedWidth: 7, minimumWidth: 7 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,422 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import getDropPosition, {
|
||||
DROP_TOP,
|
||||
DROP_RIGHT,
|
||||
DROP_BOTTOM,
|
||||
DROP_LEFT,
|
||||
} from '../../../../src/dashboard/util/getDropPosition';
|
||||
|
||||
import {
|
||||
CHART_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
HEADER_TYPE,
|
||||
ROW_TYPE,
|
||||
TAB_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('getDropPosition', () => {
|
||||
// helper to easily configure test
|
||||
function getMocks({
|
||||
parentType,
|
||||
componentType,
|
||||
draggingType,
|
||||
depth = 1,
|
||||
hasChildren = false,
|
||||
orientation = 'row',
|
||||
clientOffset = { x: 0, y: 0 },
|
||||
boundingClientRect = {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
isDraggingOverShallow = true,
|
||||
}) {
|
||||
const monitorMock = {
|
||||
getItem: () => ({
|
||||
id: 'id',
|
||||
type: draggingType,
|
||||
}),
|
||||
getClientOffset: () => clientOffset,
|
||||
};
|
||||
|
||||
const ComponentMock = {
|
||||
props: {
|
||||
depth,
|
||||
parentComponent: {
|
||||
type: parentType,
|
||||
},
|
||||
component: {
|
||||
type: componentType,
|
||||
children: hasChildren ? [''] : [],
|
||||
},
|
||||
orientation,
|
||||
isDraggingOverShallow,
|
||||
},
|
||||
ref: {
|
||||
getBoundingClientRect: () => boundingClientRect,
|
||||
},
|
||||
};
|
||||
|
||||
return [monitorMock, ComponentMock];
|
||||
}
|
||||
|
||||
describe('invalid child + invalid sibling', () => {
|
||||
it('should return null', () => {
|
||||
const result = getDropPosition(
|
||||
// TAB is an invalid child + sibling of GRID > ROW
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: TAB_TYPE,
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid child + invalid sibling', () => {
|
||||
it('should return DROP_LEFT if component has NO children, and orientation is "row"', () => {
|
||||
// HEADER is a valid child + invalid sibling of ROOT > GRID
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_ROOT_TYPE,
|
||||
componentType: DASHBOARD_GRID_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_LEFT);
|
||||
});
|
||||
|
||||
it('should return DROP_RIGHT if component HAS children, and orientation is "row"', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_ROOT_TYPE,
|
||||
componentType: DASHBOARD_GRID_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
hasChildren: true,
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_RIGHT);
|
||||
});
|
||||
|
||||
it('should return DROP_TOP if component has NO children, and orientation is "column"', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_ROOT_TYPE,
|
||||
componentType: DASHBOARD_GRID_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
orientation: 'column',
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_TOP);
|
||||
});
|
||||
|
||||
it('should return DROP_BOTTOM if component HAS children, and orientation is "column"', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_ROOT_TYPE,
|
||||
componentType: DASHBOARD_GRID_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
orientation: 'column',
|
||||
hasChildren: true,
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_BOTTOM);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid child + valid sibling', () => {
|
||||
it('should return DROP_TOP if orientation="row" and clientOffset is closer to component top than bottom', () => {
|
||||
const result = getDropPosition(
|
||||
// HEADER is an invalid child but valid sibling of GRID > ROW
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
clientOffset: { y: 10 },
|
||||
boundingClientRect: {
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_TOP);
|
||||
});
|
||||
|
||||
it('should return DROP_BOTTOM if orientation="row" and clientOffset is closer to component bottom than top', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
clientOffset: { y: 55 },
|
||||
boundingClientRect: {
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_BOTTOM);
|
||||
});
|
||||
|
||||
it('should return DROP_LEFT if orientation="column" and clientOffset is closer to component left than right', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
orientation: 'column',
|
||||
clientOffset: { x: 45 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_LEFT);
|
||||
});
|
||||
|
||||
it('should return DROP_RIGHT if orientation="column" and clientOffset is closer to component right than left', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: HEADER_TYPE,
|
||||
orientation: 'column',
|
||||
clientOffset: { x: 55 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_RIGHT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('child + valid sibling (row orientation)', () => {
|
||||
it('should return DROP_LEFT if component has NO children, and clientOffset is NOT near top/bottom sibling boundary', () => {
|
||||
const result = getDropPosition(
|
||||
// CHART is a valid child + sibling of GRID > ROW
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
clientOffset: { x: 10, y: 50 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_LEFT);
|
||||
});
|
||||
|
||||
it('should return DROP_RIGHT if component HAS children, and clientOffset is NOT near top/bottom sibling boundary', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
hasChildren: true,
|
||||
clientOffset: { x: 10, y: 50 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_RIGHT);
|
||||
});
|
||||
|
||||
it('should return DROP_TOP regardless of component children if clientOffset IS near top sibling boundary', () => {
|
||||
const noChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
clientOffset: { x: 10, y: 2 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const withChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
hasChildren: true,
|
||||
clientOffset: { x: 10, y: 2 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(noChildren).to.equal(DROP_TOP);
|
||||
expect(withChildren).to.equal(DROP_TOP);
|
||||
});
|
||||
|
||||
it('should return DROP_BOTTOM regardless of component children if clientOffset IS near bottom sibling boundary', () => {
|
||||
const noChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
clientOffset: { x: 10, y: 95 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const withChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
hasChildren: true,
|
||||
clientOffset: { x: 10, y: 95 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(noChildren).to.equal(DROP_BOTTOM);
|
||||
expect(withChildren).to.equal(DROP_BOTTOM);
|
||||
});
|
||||
});
|
||||
|
||||
describe('child + valid sibling (column orientation)', () => {
|
||||
it('should return DROP_TOP if component has NO children, and clientOffset is NOT near left/right sibling boundary', () => {
|
||||
const result = getDropPosition(
|
||||
// CHART is a valid child + sibling of GRID > ROW
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
orientation: 'column',
|
||||
clientOffset: { x: 50, y: 0 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_TOP);
|
||||
});
|
||||
|
||||
it('should return DROP_BOTTOM if component HAS children, and clientOffset is NOT near left/right sibling boundary', () => {
|
||||
const result = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
orientation: 'column',
|
||||
hasChildren: true,
|
||||
clientOffset: { x: 50, y: 0 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).to.equal(DROP_BOTTOM);
|
||||
});
|
||||
|
||||
it('should return DROP_LEFT regardless of component children if clientOffset IS near left sibling boundary', () => {
|
||||
const noChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
orientation: 'column',
|
||||
clientOffset: { x: 10, y: 2 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const withChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
orientation: 'column',
|
||||
hasChildren: true,
|
||||
clientOffset: { x: 10, y: 2 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(noChildren).to.equal(DROP_LEFT);
|
||||
expect(withChildren).to.equal(DROP_LEFT);
|
||||
});
|
||||
|
||||
it('should return DROP_RIGHT regardless of component children if clientOffset IS near right sibling boundary', () => {
|
||||
const noChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
orientation: 'column',
|
||||
clientOffset: { x: 90, y: 95 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const withChildren = getDropPosition(
|
||||
...getMocks({
|
||||
parentType: DASHBOARD_GRID_TYPE,
|
||||
componentType: ROW_TYPE,
|
||||
draggingType: CHART_TYPE,
|
||||
orientation: 'column',
|
||||
hasChildren: true,
|
||||
clientOffset: { x: 90, y: 95 },
|
||||
boundingClientRect: {
|
||||
left: 0,
|
||||
right: 100,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(noChildren).to.equal(DROP_RIGHT);
|
||||
expect(withChildren).to.equal(DROP_RIGHT);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import getFormDataWithExtraFilters from '../../../../src/dashboard/util/charts/getFormDataWithExtraFilters';
|
||||
|
||||
describe('getFormDataWithExtraFilters', () => {
|
||||
const chartId = 'chartId';
|
||||
const mockArgs = {
|
||||
chart: {
|
||||
id: chartId,
|
||||
formData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'country_name',
|
||||
op: 'in',
|
||||
val: ['United States'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dashboardMetadata: {
|
||||
filter_immune_slices: [],
|
||||
filter_immune_slice_fields: {},
|
||||
},
|
||||
filters: {
|
||||
filterId: {
|
||||
region: ['Spain'],
|
||||
color: ['pink', 'purple'],
|
||||
},
|
||||
},
|
||||
sliceId: chartId,
|
||||
};
|
||||
|
||||
it('should include filters from the passed filters', () => {
|
||||
const result = getFormDataWithExtraFilters(mockArgs);
|
||||
expect(result.extra_filters).to.have.length(2);
|
||||
expect(result.extra_filters[0]).to.deep.equal({
|
||||
col: 'region',
|
||||
op: 'in',
|
||||
val: ['Spain'],
|
||||
});
|
||||
expect(result.extra_filters[1]).to.deep.equal({
|
||||
col: 'color',
|
||||
op: 'in',
|
||||
val: ['pink', 'purple'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add additional filters if the slice is immune to them', () => {
|
||||
const result = getFormDataWithExtraFilters({
|
||||
...mockArgs,
|
||||
dashboardMetadata: {
|
||||
filter_immune_slices: [chartId],
|
||||
},
|
||||
});
|
||||
expect(result.extra_filters).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not add additional filters for fields to which the slice is immune', () => {
|
||||
const result = getFormDataWithExtraFilters({
|
||||
...mockArgs,
|
||||
dashboardMetadata: {
|
||||
filter_immune_slice_fields: {
|
||||
[chartId]: ['region'],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.extra_filters).to.have.length(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import isValidChild from '../../../../src/dashboard/util/isValidChild';
|
||||
|
||||
import {
|
||||
CHART_TYPE as CHART,
|
||||
COLUMN_TYPE as COLUMN,
|
||||
DASHBOARD_GRID_TYPE as GRID,
|
||||
DASHBOARD_ROOT_TYPE as ROOT,
|
||||
DIVIDER_TYPE as DIVIDER,
|
||||
HEADER_TYPE as HEADER,
|
||||
MARKDOWN_TYPE as MARKDOWN,
|
||||
ROW_TYPE as ROW,
|
||||
TABS_TYPE as TABS,
|
||||
TAB_TYPE as TAB,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
const getIndentation = depth =>
|
||||
Array(depth * 3)
|
||||
.fill('')
|
||||
.join('-');
|
||||
|
||||
describe('isValidChild', () => {
|
||||
describe('valid calls', () => {
|
||||
// these are representations of nested structures for easy testing
|
||||
// [ROOT (depth 0) > GRID (depth 1) > HEADER (depth 2)]
|
||||
// every unique parent > child relationship is tested, but because this
|
||||
// test representation WILL result in duplicates, we hash each test
|
||||
// to keep track of which we've run
|
||||
const didTest = {};
|
||||
const validExamples = [
|
||||
[ROOT, GRID, CHART], // chart is valid because it is wrapped in a row
|
||||
[ROOT, GRID, MARKDOWN], // markdown is valid because it is wrapped in a row
|
||||
[ROOT, GRID, COLUMN], // column is valid because it is wrapped in a row
|
||||
[ROOT, GRID, HEADER],
|
||||
[ROOT, GRID, ROW, MARKDOWN],
|
||||
[ROOT, GRID, ROW, CHART],
|
||||
|
||||
[ROOT, GRID, ROW, COLUMN, HEADER],
|
||||
[ROOT, GRID, ROW, COLUMN, DIVIDER],
|
||||
[ROOT, GRID, ROW, COLUMN, CHART],
|
||||
[ROOT, GRID, ROW, COLUMN, MARKDOWN],
|
||||
|
||||
[ROOT, GRID, ROW, COLUMN, ROW, CHART],
|
||||
[ROOT, GRID, ROW, COLUMN, ROW, MARKDOWN],
|
||||
|
||||
[ROOT, GRID, ROW, COLUMN, ROW, COLUMN, CHART],
|
||||
[ROOT, GRID, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
|
||||
[ROOT, GRID, TABS, TAB, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
|
||||
|
||||
// tab equivalents
|
||||
[ROOT, TABS, TAB, CHART],
|
||||
[ROOT, TABS, TAB, MARKDOWN],
|
||||
[ROOT, TABS, TAB, COLUMN],
|
||||
[ROOT, TABS, TAB, HEADER],
|
||||
[ROOT, TABS, TAB, ROW, MARKDOWN],
|
||||
[ROOT, TABS, TAB, ROW, CHART],
|
||||
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, HEADER],
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, DIVIDER],
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, CHART],
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, MARKDOWN],
|
||||
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, ROW, CHART],
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, ROW, MARKDOWN],
|
||||
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, ROW, COLUMN, CHART],
|
||||
[ROOT, TABS, TAB, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
|
||||
[ROOT, TABS, TAB, TABS, TAB, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
|
||||
];
|
||||
|
||||
validExamples.forEach((example, exampleIdx) => {
|
||||
let childDepth = 0;
|
||||
example.forEach((childType, i) => {
|
||||
const parentDepth = childDepth - 1;
|
||||
const parentType = example[i - 1];
|
||||
const testKey = `${parentType}-${childType}-${parentDepth}`;
|
||||
|
||||
if (i > 0 && !didTest[testKey]) {
|
||||
didTest[testKey] = true;
|
||||
|
||||
it(`(${exampleIdx})${getIndentation(
|
||||
childDepth,
|
||||
)}${parentType} (depth ${parentDepth}) > ${childType} ✅`, () => {
|
||||
expect(
|
||||
isValidChild({
|
||||
parentDepth,
|
||||
parentType,
|
||||
childType,
|
||||
}),
|
||||
).to.equal(true);
|
||||
});
|
||||
}
|
||||
// see isValidChild.js for why tabs do not increment the depth of their children
|
||||
childDepth += childType !== TABS && childType !== TAB ? 1 : 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid calls', () => {
|
||||
// In order to assert that a parent > child hierarchy at a given depth is invalid
|
||||
// we also define some valid hierarchies in doing so. we indicate which
|
||||
// parent > [child] relationships should be asserted as invalid using a nested array
|
||||
const invalidExamples = [
|
||||
[ROOT, [DIVIDER]],
|
||||
[ROOT, [CHART]],
|
||||
[ROOT, [MARKDOWN]],
|
||||
[ROOT, GRID, [TAB]],
|
||||
[ROOT, GRID, TABS, [ROW]],
|
||||
// [ROOT, GRID, TABS, TAB, [TABS]], // @TODO this needs to be fixed
|
||||
[ROOT, GRID, ROW, [TABS]],
|
||||
[ROOT, GRID, ROW, [TAB]],
|
||||
[ROOT, GRID, ROW, [DIVIDER]],
|
||||
[ROOT, GRID, ROW, COLUMN, [TABS]],
|
||||
[ROOT, GRID, ROW, COLUMN, [TAB]],
|
||||
[ROOT, GRID, ROW, COLUMN, ROW, [DIVIDER]],
|
||||
[ROOT, GRID, ROW, COLUMN, ROW, COLUMN, [ROW]], // too nested
|
||||
];
|
||||
|
||||
invalidExamples.forEach((example, exampleIdx) => {
|
||||
let childDepth = 0;
|
||||
example.forEach((childType, i) => {
|
||||
const shouldTestChild = Array.isArray(childType);
|
||||
|
||||
if (i > 0 && shouldTestChild) {
|
||||
const parentDepth = childDepth - 1;
|
||||
const parentType = example[i - 1];
|
||||
|
||||
it(`(${exampleIdx})${getIndentation(
|
||||
childDepth,
|
||||
)}${parentType} (depth ${parentDepth}) > ${childType} ❌`, () => {
|
||||
expect(
|
||||
isValidChild({
|
||||
parentDepth,
|
||||
parentType,
|
||||
childType,
|
||||
}),
|
||||
).to.equal(false);
|
||||
});
|
||||
}
|
||||
// see isValidChild.js for why tabs do not increment the depth of their children
|
||||
childDepth += childType !== TABS && childType !== TAB ? 1 : 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
|
||||
|
||||
import {
|
||||
CHART_TYPE,
|
||||
COLUMN_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
HEADER_TYPE,
|
||||
MARKDOWN_TYPE,
|
||||
NEW_COMPONENT_SOURCE_TYPE,
|
||||
ROW_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
const types = [
|
||||
CHART_TYPE,
|
||||
COLUMN_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
DASHBOARD_ROOT_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
HEADER_TYPE,
|
||||
MARKDOWN_TYPE,
|
||||
NEW_COMPONENT_SOURCE_TYPE,
|
||||
ROW_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
];
|
||||
|
||||
describe('newEntityFactory', () => {
|
||||
types.forEach(type => {
|
||||
it(`returns a new ${type}`, () => {
|
||||
const result = newComponentFactory(type);
|
||||
|
||||
expect(result.type).to.equal(type);
|
||||
expect(typeof result.id).to.equal('string');
|
||||
expect(typeof result.meta).to.equal('object');
|
||||
expect(Array.isArray(result.children)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds passed meta data to the entity', () => {
|
||||
const banana = 'banana';
|
||||
const result = newComponentFactory(CHART_TYPE, { banana });
|
||||
expect(result.meta.banana).to.equal(banana);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import newEntitiesFromDrop from '../../../../src/dashboard/util/newEntitiesFromDrop';
|
||||
import {
|
||||
CHART_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
ROW_TYPE,
|
||||
TABS_TYPE,
|
||||
TAB_TYPE,
|
||||
} from '../../../../src/dashboard/util/componentTypes';
|
||||
|
||||
describe('newEntitiesFromDrop', () => {
|
||||
it('should return a new Entity of appropriate type, and add it to the drop target children', () => {
|
||||
const result = newEntitiesFromDrop({
|
||||
dropResult: {
|
||||
destination: { id: 'a', index: 0 },
|
||||
dragging: { type: CHART_TYPE },
|
||||
source: { id: 'b', index: 0 },
|
||||
},
|
||||
layout: {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: ROW_TYPE,
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newId = result.a.children[0];
|
||||
expect(result.a.children.length).to.equal(1);
|
||||
expect(Object.keys(result).length).to.equal(2);
|
||||
expect(result[newId].type).to.equal(CHART_TYPE);
|
||||
});
|
||||
|
||||
it('should create Tab AND Tabs components if the drag entity is Tabs', () => {
|
||||
const result = newEntitiesFromDrop({
|
||||
dropResult: {
|
||||
destination: { id: 'a', index: 0 },
|
||||
dragging: { type: TABS_TYPE },
|
||||
source: { id: 'b', index: 0 },
|
||||
},
|
||||
layout: {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newTabsId = result.a.children[0];
|
||||
const newTabId = result[newTabsId].children[0];
|
||||
|
||||
expect(result.a.children.length).to.equal(1);
|
||||
expect(Object.keys(result).length).to.equal(3);
|
||||
expect(result[newTabsId].type).to.equal(TABS_TYPE);
|
||||
expect(result[newTabId].type).to.equal(TAB_TYPE);
|
||||
});
|
||||
|
||||
it('should create a Row if the drag entity should be wrapped in a row', () => {
|
||||
const result = newEntitiesFromDrop({
|
||||
dropResult: {
|
||||
destination: { id: 'a', index: 0 },
|
||||
dragging: { type: CHART_TYPE },
|
||||
source: { id: 'b', index: 0 },
|
||||
},
|
||||
layout: {
|
||||
a: {
|
||||
id: 'a',
|
||||
type: DASHBOARD_GRID_TYPE,
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newRowId = result.a.children[0];
|
||||
const newChartId = result[newRowId].children[0];
|
||||
|
||||
expect(result.a.children.length).to.equal(1);
|
||||
expect(Object.keys(result).length).to.equal(3);
|
||||
expect(result[newRowId].type).to.equal(ROW_TYPE);
|
||||
expect(result[newChartId].type).to.equal(CHART_TYPE);
|
||||
});
|
||||
});
|
||||
143
superset/assets/spec/javascripts/logger_spec.js
Normal file
143
superset/assets/spec/javascripts/logger_spec.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import $ from 'jquery';
|
||||
import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { Logger, ActionLog } from '../../src/logger';
|
||||
|
||||
describe('ActionLog', () => {
|
||||
it('should be a constructor', () => {
|
||||
const newLogger = new ActionLog({});
|
||||
expect(newLogger instanceof ActionLog).to.equal(true);
|
||||
});
|
||||
|
||||
it('should set the eventNames, impressionId, source, sourceId, and sendNow init parameters', () => {
|
||||
const eventNames = [];
|
||||
const impressionId = 'impressionId';
|
||||
const source = 'source';
|
||||
const sourceId = 'sourceId';
|
||||
const sendNow = true;
|
||||
|
||||
const log = new ActionLog({ eventNames, impressionId, source, sourceId, sendNow });
|
||||
expect(log.eventNames).to.equal(eventNames);
|
||||
expect(log.impressionId).to.equal(impressionId);
|
||||
expect(log.source).to.equal(source);
|
||||
expect(log.sourceId).to.equal(sourceId);
|
||||
expect(log.sendNow).to.equal(sendNow);
|
||||
});
|
||||
|
||||
it('should set attributes with the setAttribute method', () => {
|
||||
const log = new ActionLog({});
|
||||
expect(log.test).to.equal(undefined);
|
||||
log.setAttribute('test', 'testValue');
|
||||
expect(log.test).to.equal('testValue');
|
||||
});
|
||||
|
||||
it('should track added events', () => {
|
||||
const log = new ActionLog({});
|
||||
const eventName = 'myEventName';
|
||||
const eventBody = { test: 'event' };
|
||||
expect(log.events[eventName]).to.equal(undefined);
|
||||
|
||||
log.addEvent(eventName, eventBody);
|
||||
expect(log.events[eventName]).to.have.length(1);
|
||||
expect(log.events[eventName][0]).to.deep.include(eventBody);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logger', () => {
|
||||
it('should add events when .append(eventName, eventBody) is called', () => {
|
||||
const eventName = 'testEvent';
|
||||
const eventBody = { test: 'event' };
|
||||
const log = new ActionLog({ eventNames: [eventName] });
|
||||
Logger.start(log);
|
||||
Logger.append(eventName, eventBody);
|
||||
expect(log.events[eventName]).to.have.length(1);
|
||||
expect(log.events[eventName][0]).to.deep.include(eventBody);
|
||||
Logger.end(log);
|
||||
});
|
||||
|
||||
describe('.send()', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy($, 'ajax');
|
||||
});
|
||||
afterEach(() => {
|
||||
$.ajax.restore();
|
||||
});
|
||||
|
||||
const eventNames = ['test'];
|
||||
|
||||
function setup(overrides = {}) {
|
||||
const log = new ActionLog({ eventNames, ...overrides });
|
||||
return log;
|
||||
}
|
||||
|
||||
it('should POST an event to /superset/log/ when called', () => {
|
||||
const log = setup();
|
||||
Logger.start(log);
|
||||
Logger.append(eventNames[0], { test: 'event' });
|
||||
expect(log.events[eventNames[0]]).to.have.length(1);
|
||||
Logger.end(log);
|
||||
expect($.ajax.calledOnce).to.equal(true);
|
||||
const args = $.ajax.getCall(0).args[0];
|
||||
expect(args.url).to.equal('/superset/log/');
|
||||
expect(args.method).to.equal('POST');
|
||||
});
|
||||
|
||||
it("should flush the log's events", () => {
|
||||
const log = setup();
|
||||
Logger.start(log);
|
||||
Logger.append(eventNames[0], { test: 'event' });
|
||||
const event = log.events[eventNames[0]][0];
|
||||
expect(event).to.deep.include({ test: 'event' });
|
||||
Logger.end(log);
|
||||
expect(log.events).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('should include ts, start_offset, event_name, impression_id, source, and source_id in every event', () => {
|
||||
const config = {
|
||||
eventNames: ['event1', 'event2'],
|
||||
impressionId: 'impress_me',
|
||||
source: 'superset',
|
||||
sourceId: 'lolz',
|
||||
};
|
||||
const log = setup(config);
|
||||
|
||||
Logger.start(log);
|
||||
Logger.append('event1', { key: 'value' });
|
||||
Logger.append('event2', { foo: 'bar' });
|
||||
Logger.end(log);
|
||||
|
||||
const args = $.ajax.getCall(0).args[0];
|
||||
const events = JSON.parse(args.data.events);
|
||||
|
||||
expect(events).to.have.length(2);
|
||||
expect(events[0]).to.deep.include({
|
||||
key: 'value',
|
||||
event_name: 'event1',
|
||||
impression_id: config.impressionId,
|
||||
source: config.source,
|
||||
source_id: config.sourceId,
|
||||
});
|
||||
expect(events[1]).to.deep.include({
|
||||
foo: 'bar',
|
||||
event_name: 'event2',
|
||||
impression_id: config.impressionId,
|
||||
source: config.source,
|
||||
source_id: config.sourceId,
|
||||
});
|
||||
expect(typeof events[0].ts).to.equal('number');
|
||||
expect(typeof events[1].ts).to.equal('number');
|
||||
expect(typeof events[0].start_offset).to.equal('number');
|
||||
expect(typeof events[1].start_offset).to.equal('number');
|
||||
});
|
||||
|
||||
it('should send() a log immediately if .append() is called with sendNow=true', () => {
|
||||
const log = setup();
|
||||
Logger.start(log);
|
||||
Logger.append(eventNames[0], { test: 'event' }, true);
|
||||
expect($.ajax.calledOnce).to.equal(true);
|
||||
Logger.end(log);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user