diff --git a/superset-frontend/spec/helpers/reducerIndex.ts b/superset-frontend/spec/helpers/reducerIndex.ts
new file mode 100644
index 0000000000..41b5aff1b2
--- /dev/null
+++ b/superset-frontend/spec/helpers/reducerIndex.ts
@@ -0,0 +1,55 @@
+/**
+ * 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 charts from 'src/chart/chartReducer';
+import dashboardInfo from 'src/dashboard/reducers/dashboardInfo';
+import dashboardState from 'src/dashboard/reducers/dashboardState';
+import dashboardFilters from 'src/dashboard/reducers/dashboardFilters';
+import nativeFilters from 'src/dashboard/reducers/nativeFilters';
+import datasources from 'src/dashboard/reducers/datasources';
+import sliceEntities from 'src/dashboard/reducers/sliceEntities';
+import dashboardLayout from 'src/dashboard/reducers/undoableDashboardLayout';
+import messageToasts from 'src/messageToasts/reducers';
+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';
+
+const impressionId = (state = '') => state;
+
+const container = document.getElementById('app');
+const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}');
+const common = { ...bootstrap.common };
+
+export default {
+ charts,
+ datasources,
+ dashboardInfo,
+ dashboardFilters,
+ nativeFilters,
+ dashboardState,
+ dashboardLayout,
+ impressionId,
+ messageToasts,
+ sliceEntities,
+ saveModal,
+ explore,
+ sqlLab,
+ localStorageUsageInKilobytes,
+ common,
+};
diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx
index 2727679352..71672444dd 100644
--- a/superset-frontend/spec/helpers/testing-library.tsx
+++ b/superset-frontend/spec/helpers/testing-library.tsx
@@ -20,15 +20,41 @@ import '@testing-library/jest-dom/extend-expect';
import React, { ReactNode, ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
+import { Provider } from 'react-redux';
+import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
+import thunk from 'redux-thunk';
+import reducerIndex from 'spec/helpers/reducerIndex';
-function SupersetProviders({ children }: { children?: ReactNode }) {
- return {children};
+type Options = Omit & {
+ useRedux?: boolean;
+ initialState?: {};
+ reducers?: {};
+};
+
+function createWrapper(options?: Options) {
+ const { useRedux, initialState, reducers } = options || {};
+
+ if (useRedux) {
+ const store = createStore(
+ combineReducers(reducers || reducerIndex),
+ initialState || {},
+ compose(applyMiddleware(thunk)),
+ );
+
+ return ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ );
+ }
+
+ return ({ children }: { children?: ReactNode }) => (
+ {children}
+ );
}
-const customRender = (
- ui: ReactElement,
- options?: Omit,
-) => render(ui, { wrapper: SupersetProviders, ...options });
+const customRender = (ui: ReactElement, options?: Options) =>
+ render(ui, { wrapper: createWrapper(options), ...options });
export function sleep(time: number) {
return new Promise(resolve => {
diff --git a/superset-frontend/spec/javascripts/components/URLShortLinkButton_spec.jsx b/superset-frontend/spec/javascripts/components/URLShortLinkButton_spec.jsx
deleted file mode 100644
index fc6194248c..0000000000
--- a/superset-frontend/spec/javascripts/components/URLShortLinkButton_spec.jsx
+++ /dev/null
@@ -1,45 +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 configureStore from 'redux-mock-store';
-import { shallow } from 'enzyme';
-
-import Popover from 'src/common/components/Popover';
-import URLShortLinkButton from 'src/components/URLShortLinkButton';
-
-describe('URLShortLinkButton', () => {
- const defaultProps = {
- url: 'mockURL',
- emailSubject: 'Mock Subject',
- emailContent: 'mock content',
- };
-
- function setup() {
- const mockStore = configureStore([]);
- const store = mockStore({});
- return shallow(
- ,
- ).dive();
- }
-
- it('renders OverlayTrigger', () => {
- const wrapper = setup();
- expect(wrapper.find(Popover)).toExist();
- });
-});
diff --git a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
index b9e38c6ad2..78d5d637d8 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
@@ -134,7 +134,7 @@ describe('Tabs', () => {
const wrapper = setup({ editMode: true, onChangeTab });
wrapper
.find(
- '[data-test="dashboard-component-tabs"] .ant-tabs-tab [data-test="short-link-button"]',
+ '[data-test="dashboard-component-tabs"] .ant-tabs-tab [role="button"]',
)
.at(1) // will not call if it is already selected
.simulate('click');
diff --git a/superset-frontend/src/components/URLShortLinkButton/URLShortLinkButton.test.tsx b/superset-frontend/src/components/URLShortLinkButton/URLShortLinkButton.test.tsx
new file mode 100644
index 0000000000..6bba1dee41
--- /dev/null
+++ b/superset-frontend/src/components/URLShortLinkButton/URLShortLinkButton.test.tsx
@@ -0,0 +1,74 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
+import URLShortLinkButton from 'src/components/URLShortLinkButton';
+import ToastPresenter from 'src/messageToasts/containers/ToastPresenter';
+
+const fakeUrl = 'http://fakeurl.com';
+
+fetchMock.post('glob:*/r/shortner/', fakeUrl);
+
+test('renders with default props', () => {
+ render(, { useRedux: true });
+ expect(screen.getByRole('button')).toBeInTheDocument();
+});
+
+test('renders overlay on click', async () => {
+ render(, { useRedux: true });
+ userEvent.click(screen.getByRole('button'));
+ expect(await screen.findByRole('tooltip')).toBeInTheDocument();
+});
+
+test('obtains short url', async () => {
+ render(, { useRedux: true });
+ userEvent.click(screen.getByRole('button'));
+ expect(await screen.findByRole('tooltip')).toHaveTextContent(fakeUrl);
+});
+
+test('creates email anchor', async () => {
+ const subject = 'Subject';
+ const content = 'Content';
+
+ render(, {
+ useRedux: true,
+ });
+
+ const href = `mailto:?Subject=${subject}%20&Body=${content}${fakeUrl}`;
+ userEvent.click(screen.getByRole('button'));
+ expect(await screen.findByRole('link')).toHaveAttribute('href', href);
+});
+
+test('renders error message on short url error', async () => {
+ fetchMock.mock('glob:*/r/shortner/', 500, {
+ overwriteRoutes: true,
+ });
+
+ render(
+ <>
+
+
+ >,
+ { useRedux: true },
+ );
+ userEvent.click(screen.getByRole('button'));
+ expect(await screen.findByRole('alert')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/URLShortLinkButton.jsx b/superset-frontend/src/components/URLShortLinkButton/index.jsx
similarity index 93%
rename from superset-frontend/src/components/URLShortLinkButton.jsx
rename to superset-frontend/src/components/URLShortLinkButton/index.jsx
index 68728f6abe..fb9fa89d1c 100644
--- a/superset-frontend/src/components/URLShortLinkButton.jsx
+++ b/superset-frontend/src/components/URLShortLinkButton/index.jsx
@@ -20,9 +20,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@superset-ui/core';
import Popover from 'src/common/components/Popover';
-import CopyToClipboard from './CopyToClipboard';
-import { getShortUrl } from '../utils/urlUtils';
-import withToasts from '../messageToasts/enhancers/withToasts';
+import CopyToClipboard from 'src/components/CopyToClipboard';
+import { getShortUrl } from 'src/utils/urlUtils';
+import withToasts from 'src/messageToasts/enhancers/withToasts';
const propTypes = {
url: PropTypes.string,
@@ -85,7 +85,7 @@ class URLShortLinkButton extends React.Component {
>
diff --git a/superset-frontend/src/messageToasts/components/Toast.tsx b/superset-frontend/src/messageToasts/components/Toast.tsx
index a833b8ba0a..9018ed6ae2 100644
--- a/superset-frontend/src/messageToasts/components/Toast.tsx
+++ b/superset-frontend/src/messageToasts/components/Toast.tsx
@@ -93,6 +93,7 @@ export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {