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';