diff --git a/superset-frontend/spec/fixtures/mockReportState.js b/superset-frontend/spec/fixtures/mockReportState.js
new file mode 100644
index 0000000000000..075af8bfe0962
--- /dev/null
+++ b/superset-frontend/spec/fixtures/mockReportState.js
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import dashboardInfo from './mockDashboardInfo';
+import { user } from '../javascripts/sqllab/fixtures';
+
+export default {
+ active: true,
+ creation_method: 'dashboards',
+ crontab: '0 12 * * 1',
+ dashboard: dashboardInfo.id,
+ name: 'Weekly Report',
+ owners: [user.userId],
+ recipients: [
+ {
+ recipient_config_json: {
+ target: user.email,
+ },
+ type: 'Email',
+ },
+ ],
+ type: 'Report',
+};
diff --git a/superset-frontend/spec/fixtures/mockStateWithoutUser.tsx b/superset-frontend/spec/fixtures/mockStateWithoutUser.tsx
new file mode 100644
index 0000000000000..bc92df4df75d0
--- /dev/null
+++ b/superset-frontend/spec/fixtures/mockStateWithoutUser.tsx
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import datasources from 'spec/fixtures/mockDatasource';
+import messageToasts from 'spec/javascripts/messageToasts/mockMessageToasts';
+import {
+ nativeFiltersInfo,
+ mockDataMaskInfo,
+} from 'spec/javascripts/dashboard/fixtures/mockNativeFilters';
+import chartQueries from 'spec/fixtures/mockChartQueries';
+import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
+import dashboardInfo from 'spec/fixtures/mockDashboardInfo';
+import { emptyFilters } from 'spec/fixtures/mockDashboardFilters';
+import dashboardState from 'spec/fixtures/mockDashboardState';
+import { sliceEntitiesForChart } from 'spec/fixtures/mockSliceEntities';
+import reports from 'spec/fixtures/mockReportState';
+
+export default {
+ datasources,
+ sliceEntities: sliceEntitiesForChart,
+ charts: chartQueries,
+ nativeFilters: nativeFiltersInfo,
+ dataMask: mockDataMaskInfo,
+ dashboardInfo,
+ dashboardFilters: emptyFilters,
+ dashboardState,
+ dashboardLayout,
+ messageToasts,
+ impressionId: 'mock_impression_id',
+ reports,
+};
diff --git a/superset-frontend/spec/helpers/reducerIndex.ts b/superset-frontend/spec/helpers/reducerIndex.ts
index e84b7f6f5119a..113368389509a 100644
--- a/superset-frontend/spec/helpers/reducerIndex.ts
+++ b/superset-frontend/spec/helpers/reducerIndex.ts
@@ -30,6 +30,7 @@ import saveModal from 'src/explore/reducers/saveModalReducer';
import explore from 'src/explore/reducers/exploreReducer';
import sqlLab from 'src/SqlLab/reducers/sqlLab';
import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage';
+import reports from 'src/reports/reducers/reports';
const impressionId = (state = '') => state;
@@ -53,5 +54,6 @@ export default {
explore,
sqlLab,
localStorageUsageInKilobytes,
+ reports,
common: () => common,
};
diff --git a/superset-frontend/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx
deleted file mode 100644
index c6eda704e16ec..0000000000000
--- a/superset-frontend/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import { ExploreChartHeader } from 'src/explore/components/ExploreChartHeader';
-import ExploreActionButtons from 'src/explore/components/ExploreActionButtons';
-import EditableTitle from 'src/components/EditableTitle';
-
-const saveSliceStub = jest.fn();
-const updateChartTitleStub = jest.fn();
-const fetchUISpecificReportStub = jest.fn();
-const mockProps = {
- actions: {
- saveSlice: saveSliceStub,
- updateChartTitle: updateChartTitleStub,
- },
- can_overwrite: true,
- can_download: true,
- isStarred: true,
- slice: {
- form_data: {
- viz_type: 'line',
- },
- },
- table_name: 'foo',
- form_data: {
- viz_type: 'table',
- },
- user: {
- createdOn: '2021-04-27T18:12:38.952304',
- email: 'admin',
- firstName: 'admin',
- isActive: true,
- lastName: 'admin',
- permissions: {},
- roles: { Admin: Array(173) },
- userId: 1,
- username: 'admin',
- },
- timeout: 1000,
- chart: {
- id: 0,
- queryResponse: {},
- },
- fetchUISpecificReport: fetchUISpecificReportStub,
- chartHeight: '30px',
-};
-
-describe('ExploreChartHeader', () => {
- let wrapper;
- beforeEach(() => {
- wrapper = shallow();
- });
-
- it('is valid', () => {
- expect(React.isValidElement()).toBe(
- true,
- );
- });
-
- it('renders', () => {
- expect(wrapper.find(EditableTitle)).toExist();
- expect(wrapper.find(ExploreActionButtons)).toExist();
- });
-
- it('should update title but not save', () => {
- const editableTitle = wrapper.find(EditableTitle);
- expect(editableTitle.props().onSaveTitle).toBe(updateChartTitleStub);
- });
-});
diff --git a/superset-frontend/src/components/ReportModal/index.test.tsx b/superset-frontend/src/components/ReportModal/index.test.tsx
index 99b1eadcc4970..44e3d0ef65c51 100644
--- a/superset-frontend/src/components/ReportModal/index.test.tsx
+++ b/superset-frontend/src/components/ReportModal/index.test.tsx
@@ -55,13 +55,17 @@ describe('Email Report Modal', () => {
(featureFlag: FeatureFlag) => featureFlag === FeatureFlag.ALERT_REPORTS,
);
});
+
+ beforeEach(() => {
+ render(, { useRedux: true });
+ });
+
afterAll(() => {
// @ts-ignore
isFeatureEnabledMock.restore();
});
- it('inputs respond correctly', () => {
- render(, { useRedux: true });
+ it('inputs respond correctly', () => {
// ----- Report name textbox
// Initial value
const reportNameTextbox = screen.getByTestId('report-name-test');
@@ -86,4 +90,21 @@ describe('Email Report Modal', () => {
const crontabInputs = screen.getAllByRole('combobox');
expect(crontabInputs).toHaveLength(5);
});
+
+ it('does not allow user to create a report without a name', () => {
+ // Grab name textbox and add button
+ const reportNameTextbox = screen.getByTestId('report-name-test');
+ const addButton = screen.getByRole('button', { name: /add/i });
+
+ // Add button should be enabled while name textbox has text
+ expect(reportNameTextbox).toHaveDisplayValue('Weekly Report');
+ expect(addButton).toBeEnabled();
+
+ // Clear the text from the name textbox
+ userEvent.clear(reportNameTextbox);
+
+ // Add button should now be disabled, blocking user from creation
+ expect(reportNameTextbox).toHaveDisplayValue('');
+ expect(addButton).toBeDisabled();
+ });
});
diff --git a/superset-frontend/src/components/ReportModal/index.tsx b/superset-frontend/src/components/ReportModal/index.tsx
index 292d3b7414b2e..e24d1d756c63f 100644
--- a/superset-frontend/src/components/ReportModal/index.tsx
+++ b/superset-frontend/src/components/ReportModal/index.tsx
@@ -53,7 +53,7 @@ import {
StyledRadioGroup,
} from './styles';
-interface ReportObject {
+export interface ReportObject {
id?: number;
active: boolean;
crontab: string;
diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
index 68af9380e9b42..8a9ecdb5be46f 100644
--- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
@@ -19,7 +19,12 @@
import React from 'react';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
+import sinon from 'sinon';
import fetchMock from 'fetch-mock';
+import * as actions from 'src/reports/actions/reports';
+import * as featureFlags from 'src/featureFlags';
+import { ReportObject } from 'src/components/ReportModal';
+import mockState from 'spec/fixtures/mockStateWithoutUser';
import { HeaderProps } from './types';
import Header from '.';
@@ -40,15 +45,16 @@ const createProps = () => ({
},
user: {
createdOn: '2021-04-27T18:12:38.952304',
- email: 'admin',
+ email: 'admin@test.com',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
- roles: { Admin: Array(173) },
+ roles: { Admin: [['menu_access', 'Manage']] },
userId: 1,
username: 'admin',
},
+ reports: {},
dashboardTitle: 'Dashboard Title',
charts: {},
layout: {},
@@ -107,8 +113,10 @@ const redoProps = {
redoLength: 1,
};
+const REPORT_ENDPOINT = 'glob:*/api/v1/report*';
+
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
-fetchMock.get('glob:*/api/v1/report*', {});
+fetchMock.get(REPORT_ENDPOINT, {});
function setup(props: HeaderProps) {
return (
@@ -315,3 +323,144 @@ test('should refresh the charts', async () => {
userEvent.click(screen.getByText('Refresh dashboard'));
expect(mockedProps.onRefresh).toHaveBeenCalledTimes(1);
});
+
+describe('Email Report Modal', () => {
+ let isFeatureEnabledMock: any;
+ let dispatch: any;
+
+ beforeEach(async () => {
+ isFeatureEnabledMock = jest
+ .spyOn(featureFlags, 'isFeatureEnabled')
+ .mockImplementation(() => true);
+ dispatch = sinon.spy();
+ });
+
+ afterAll(() => {
+ isFeatureEnabledMock.mockRestore();
+ });
+
+ it('creates a new email report', async () => {
+ // ---------- Render/value setup ----------
+ const mockedProps = createProps();
+ render(setup(mockedProps), { useRedux: true });
+
+ const reportValues = {
+ active: true,
+ creation_method: 'dashboards',
+ crontab: '0 12 * * 1',
+ dashboard: mockedProps.dashboardInfo.id,
+ name: 'Weekly Report',
+ owners: [mockedProps.user.userId],
+ recipients: [
+ {
+ recipient_config_json: {
+ target: mockedProps.user.email,
+ },
+ type: 'Email',
+ },
+ ],
+ type: 'Report',
+ };
+ // This is needed to structure the reportValues to match the fetchMock return
+ const stringyReportValues = `{"active":true,"creation_method":"dashboards","crontab":"0 12 * * 1","dashboard":${mockedProps.dashboardInfo.id},"name":"Weekly Report","owners":[${mockedProps.user.userId}],"recipients":[{"recipient_config_json":{"target":"${mockedProps.user.email}"},"type":"Email"}],"type":"Report"}`;
+ // Watch for report POST
+ fetchMock.post(REPORT_ENDPOINT, reportValues);
+
+ screen.logTestingPlaygroundURL();
+ // ---------- Begin tests ----------
+ // Click calendar icon to open email report modal
+ const emailReportModalButton = screen.getByRole('button', {
+ name: /schedule email report/i,
+ });
+ userEvent.click(emailReportModalButton);
+
+ // Click "Add" button to create a new email report
+ const addButton = screen.getByRole('button', { name: /add/i });
+ userEvent.click(addButton);
+
+ // Mock addReport from Redux
+ const makeRequest = () => {
+ const request = actions.addReport(reportValues as ReportObject);
+ return request(dispatch);
+ };
+
+ return makeRequest().then(() => {
+ // 🐞 ----- There are 2 POST calls at this point ----- 🐞
+
+ // addReport's mocked POST return should match the mocked values
+ expect(fetchMock.lastOptions()?.body).toEqual(stringyReportValues);
+ // Dispatch should be called once for addReport
+ expect(dispatch.callCount).toBe(2);
+ const reportCalls = fetchMock.calls(REPORT_ENDPOINT);
+ expect(reportCalls).toHaveLength(2);
+ });
+ });
+
+ it('edits an existing email report', async () => {
+ // TODO (lyndsiWilliams): This currently does not work, see TODOs below
+ // The modal does appear with the edit title, but the PUT call is not registering
+
+ // ---------- Render/value setup ----------
+ const mockedProps = createProps();
+ const editedReportValues = {
+ active: true,
+ creation_method: 'dashboards',
+ crontab: '0 12 * * 1',
+ dashboard: mockedProps.dashboardInfo.id,
+ name: 'Weekly Report edit',
+ owners: [mockedProps.user.userId],
+ recipients: [
+ {
+ recipient_config_json: {
+ target: mockedProps.user.email,
+ },
+ type: 'Email',
+ },
+ ],
+ type: 'Report',
+ };
+
+ // getMockStore({ reports: reportValues });
+ render(setup(mockedProps), {
+ useRedux: true,
+ initialState: mockState,
+ });
+ // TODO (lyndsiWilliams): currently fetchMock detects this PUT
+ // address as 'glob:*/api/v1/report/undefined', is not detected
+ // on fetchMock.calls()
+ fetchMock.put(`glob:*/api/v1/report*`, editedReportValues);
+
+ // Mock fetchUISpecificReport from Redux
+ // const makeFetchRequest = () => {
+ // const request = actions.fetchUISpecificReport(
+ // mockedProps.user.userId,
+ // 'dashboard_id',
+ // 'dashboards',
+ // mockedProps.dashboardInfo.id,
+ // );
+ // return request(dispatch);
+ // };
+
+ // makeFetchRequest();
+
+ dispatch(actions.setReport(editedReportValues));
+
+ // ---------- Begin tests ----------
+ // Click calendar icon to open email report modal
+ const emailReportModalButton = screen.getByRole('button', {
+ name: /schedule email report/i,
+ });
+ userEvent.click(emailReportModalButton);
+
+ const nameTextbox = screen.getByTestId('report-name-test');
+ userEvent.type(nameTextbox, ' edit');
+
+ const saveButton = screen.getByRole('button', { name: /save/i });
+ userEvent.click(saveButton);
+
+ // TODO (lyndsiWilliams): There should be a report in state at this porint,
+ // which would render the HeaderReportActionsDropDown under the calendar icon
+ // BLOCKER: I cannot get report to populate, as its data is handled through redux
+ expect.anything();
+ });
+});
diff --git a/superset-frontend/src/reports/actions/reports.js b/superset-frontend/src/reports/actions/reports.js
index 55cea9dbaa7c9..7b3bc814ca0e8 100644
--- a/superset-frontend/src/reports/actions/reports.js
+++ b/superset-frontend/src/reports/actions/reports.js
@@ -98,7 +98,7 @@ const structureFetchAction = (dispatch, getState) => {
export const ADD_REPORT = 'ADD_REPORT';
-export const addReport = report => dispatch => {
+export const addReport = report => dispatch =>
SupersetClient.post({
endpoint: `/api/v1/report/`,
jsonPayload: report,
@@ -118,7 +118,6 @@ export const addReport = report => dispatch => {
),
);
});
-};
export const EDIT_REPORT = 'EDIT_REPORT';