superset-frontend/src/features/home/RightMenu.test.tsx
/**
* 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 * as reactRedux from 'react-redux';
import fetchMock from 'fetch-mock';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import RightMenu from './RightMenu';
import { GlobalMenuDataOptions, RightMenuProps } from './types';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));
jest.mock('src/features/databases/DatabaseModal', () => () => <span />);
const dropdownItems = [
{
label: 'Data',
icon: 'fa-database',
childs: [
{
label: 'Connect database',
name: GlobalMenuDataOptions.DbConnection,
perm: true,
},
{
label: 'Create dataset',
name: GlobalMenuDataOptions.DatasetCreation,
perm: true,
},
{
label: 'Connect Google Sheet',
name: GlobalMenuDataOptions.GoogleSheets,
perm: true,
},
{
label: 'Upload CSV to database',
name: 'Upload a CSV',
url: '#',
perm: true,
},
{
label: 'Upload columnar file to database',
name: 'Upload a Columnar file',
url: '#',
perm: true,
},
],
},
{
label: 'SQL query',
url: '/sqllab?new=true',
icon: 'fa-fw fa-search',
perm: 'can_sqllab',
view: 'Superset',
},
{
label: 'Chart',
url: '/chart/add',
icon: 'fa-fw fa-bar-chart',
perm: 'can_write',
view: 'Chart',
},
{
label: 'Dashboard',
url: '/dashboard/new',
icon: 'fa-fw fa-dashboard',
perm: 'can_write',
view: 'Dashboard',
},
];
const createProps = (): RightMenuProps => ({
align: 'flex-end',
navbarRight: {
show_watermark: false,
bug_report_url: undefined,
documentation_url: undefined,
languages: {
en: {
flag: 'us',
name: 'English',
url: '/lang/en',
},
it: {
flag: 'it',
name: 'Italian',
url: '/lang/it',
},
},
show_language_picker: false,
user_is_anonymous: false,
user_info_url: '/users/userinfo/',
user_logout_url: '/logout/',
user_login_url: '/login/',
locale: 'en',
version_string: '1.0.0',
version_sha: 'randomSHA',
build_number: 'randomBuildNumber',
},
settings: [],
isFrontendRoute: () => true,
environmentTag: {
color: 'error.base',
text: 'Development2',
},
});
const mockNonExamplesDB = [...new Array(2)].map((_, i) => ({
changed_by: {
first_name: `user`,
last_name: `${i}`,
},
database_name: `db ${i}`,
backend: 'postgresql',
allow_run_async: true,
allow_dml: false,
allow_file_upload: true,
expose_in_sqllab: false,
changed_on_delta_humanized: `${i} day(s) ago`,
changed_on: new Date().toISOString,
id: i,
engine_information: {
supports_file_upload: true,
},
}));
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
beforeEach(async () => {
useSelectorMock.mockReset();
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
{ result: [], count: 0 },
);
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
{ result: [], count: 0 },
);
});
afterEach(fetchMock.restore);
const resetUseSelectorMock = () => {
useSelectorMock.mockReturnValueOnce({
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: {
Admin: [
['can_csv_upload', 'Database'], // So we can upload CSV
['can_excel_upload', 'Database'], // So we can upload CSV
['can_write', 'Database'], // So we can write DBs
['can_write', 'Dataset'], // So we can write Datasets
['can_write', 'Chart'], // So we can write Datasets
],
},
userId: 1,
username: 'admin',
});
// By default we get file extensions to be uploaded
useSelectorMock.mockReturnValueOnce('1');
// By default we get file extensions to be uploaded
useSelectorMock.mockReturnValueOnce({
CSV_EXTENSIONS: ['csv'],
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
});
};
test('renders', async () => {
const mockedProps = createProps();
// Initial Load
resetUseSelectorMock();
const { container } = render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
});
// expect(await screen.findByText(/Settings/i)).toBeInTheDocument();
await waitFor(() => expect(container).toBeInTheDocument());
});
test('If user has permission to upload files AND connect DBs we query existing DBs that has allow_file_upload as True and DBs that are not examples', async () => {
const mockedProps = createProps();
// Initial Load
resetUseSelectorMock();
const { container } = render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
});
await waitFor(() => expect(container).toBeVisible());
const callsD = fetchMock.calls(/database\/\?q/);
expect(callsD).toHaveLength(2);
expect(callsD[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))"`,
);
expect(callsD[1][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))"`,
);
});
test('If only examples DB exist we must show the Connect Database option', async () => {
const mockedProps = createProps();
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
{ result: [...mockNonExamplesDB], count: 2 },
{ overwriteRoutes: true },
);
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
{ result: [], count: 0 },
{ overwriteRoutes: true },
);
// Initial Load
resetUseSelectorMock();
// setAllowUploads called
resetUseSelectorMock();
render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
});
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
const dataMenu = await screen.findByText(dropdownItems[0].label);
userEvent.hover(dataMenu);
expect(await screen.findByText('Connect database')).toBeInTheDocument();
expect(screen.queryByText('Create dataset')).not.toBeInTheDocument();
});
test('If more than just examples DB exist we must show the Create dataset option', async () => {
const mockedProps = createProps();
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
{ result: [...mockNonExamplesDB], count: 2 },
{ overwriteRoutes: true },
);
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
{ result: [...mockNonExamplesDB], count: 2 },
{ overwriteRoutes: true },
);
// Initial Load
resetUseSelectorMock();
// setAllowUploads called
resetUseSelectorMock();
// setNonExamplesDBConnected called
resetUseSelectorMock();
render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
});
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
const dataMenu = await screen.findByText(dropdownItems[0].label);
userEvent.hover(dataMenu);
expect(await screen.findByText('Create dataset')).toBeInTheDocument();
expect(screen.queryByText('Connect database')).not.toBeInTheDocument();
});
test('If there is a DB with allow_file_upload set as True the option should be enabled', async () => {
const mockedProps = createProps();
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
{ result: [...mockNonExamplesDB], count: 2 },
{ overwriteRoutes: true },
);
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
{ result: [...mockNonExamplesDB], count: 2 },
{ overwriteRoutes: true },
);
// Initial load
resetUseSelectorMock();
// setAllowUploads called
resetUseSelectorMock();
// setNonExamplesDBConnected called
resetUseSelectorMock();
render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
});
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
const dataMenu = await screen.findByText(dropdownItems[0].label);
userEvent.hover(dataMenu);
const csvMenu = await screen.findByText('Upload CSV to database');
expect(csvMenu).toBeInTheDocument();
expect(
await screen.findByText('Upload Excel to database'),
).toBeInTheDocument();
expect(csvMenu).not.toHaveAttribute('aria-disabled', 'true');
});
test('If there is NOT a DB with allow_file_upload set as True the option should be disabled', async () => {
const mockedProps = createProps();
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
{ result: [], count: 0 },
{ overwriteRoutes: true },
);
fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
{ result: [...mockNonExamplesDB], count: 2 },
{ overwriteRoutes: true },
);
// Initial load
resetUseSelectorMock();
// setAllowUploads called
resetUseSelectorMock();
// setNonExamplesDBConnected called
resetUseSelectorMock();
render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
});
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
const dataMenu = await screen.findByText(dropdownItems[0].label);
userEvent.hover(dataMenu);
const csvMenu = await screen.findByRole('menuitem', {
name: 'Upload CSV to database',
});
expect(csvMenu).toBeInTheDocument();
expect(csvMenu).toHaveAttribute('aria-disabled', 'true');
});
test('Logs out and clears local storage item redux', async () => {
const mockedProps = createProps();
resetUseSelectorMock();
render(<RightMenu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
});
// Set an item in local storage to test if it gets cleared
localStorage.setItem('redux', JSON.stringify({ test: 'test' }));
expect(localStorage.getItem('redux')).not.toBeNull();
userEvent.hover(await screen.findByText(/Settings/i));
// Simulate user clicking the logout button
await waitFor(() => {
const logoutButton = screen.getByText('Logout');
userEvent.click(logoutButton);
});
// Wait for local storage to be cleared
await waitFor(() => {
expect(localStorage.getItem('redux')).toBeNull();
});
});