| /** |
| * 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, userEvent } from 'spec/helpers/testing-library'; |
| import setupCodeOverrides from 'src/setup/setupCodeOverrides'; |
| import { getExtensionsRegistry } from '@superset-ui/core'; |
| import { Menu } from './Menu'; |
| |
| const dropdownItems = [ |
| { |
| label: 'Data', |
| icon: 'fa-database', |
| childs: [ |
| { |
| label: 'Connect Database', |
| name: 'dbconnect', |
| perm: true, |
| }, |
| { |
| label: 'Connect Google Sheet', |
| name: 'gsheets', |
| perm: true, |
| }, |
| { |
| label: 'Upload a CSV', |
| name: 'Upload a CSV', |
| url: '#', |
| perm: true, |
| }, |
| { |
| label: 'Upload a Columnar File', |
| name: 'Upload a Columnar file', |
| url: '#', |
| perm: true, |
| }, |
| { |
| label: 'Upload Excel', |
| name: 'Upload Excel', |
| 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 user = { |
| createdOn: '2021-04-27T18:12:38.952304', |
| email: 'admin', |
| firstName: 'admin', |
| isActive: true, |
| lastName: 'admin', |
| permissions: {}, |
| roles: { |
| Admin: [ |
| ['can_sqllab', 'Superset'], |
| ['can_write', 'Dashboard'], |
| ['can_write', 'Chart'], |
| ], |
| }, |
| userId: 1, |
| username: 'admin', |
| }; |
| |
| const mockedProps = { |
| user, |
| data: { |
| menu: [ |
| { |
| name: 'Home', |
| icon: '', |
| label: 'Home', |
| url: '/superset/welcome', |
| index: 1, |
| }, |
| { |
| name: 'Sources', |
| icon: 'fa-table', |
| label: 'Sources', |
| index: 2, |
| childs: [ |
| { |
| name: 'Datasets', |
| icon: 'fa-table', |
| label: 'Datasets', |
| url: '/tablemodelview/list/', |
| index: 1, |
| }, |
| '-', |
| { |
| name: 'Databases', |
| icon: 'fa-database', |
| label: 'Databases', |
| url: '/databaseview/list/', |
| index: 2, |
| }, |
| ], |
| }, |
| { |
| name: 'Charts', |
| icon: 'fa-bar-chart', |
| label: 'Charts', |
| url: '/chart/list/', |
| index: 3, |
| }, |
| { |
| name: 'Dashboards', |
| icon: 'fa-dashboard', |
| label: 'Dashboards', |
| url: '/dashboard/list/', |
| index: 4, |
| }, |
| { |
| name: 'Data', |
| icon: 'fa-database', |
| label: 'Data', |
| childs: [ |
| { |
| name: 'Databases', |
| icon: 'fa-database', |
| label: 'Databases', |
| url: '/databaseview/list/', |
| }, |
| { |
| name: 'Datasets', |
| icon: 'fa-table', |
| label: 'Datasets', |
| url: '/tablemodelview/list/', |
| }, |
| '-', |
| ], |
| }, |
| ], |
| brand: { |
| path: '/superset/welcome/', |
| icon: '/static/assets/images/superset-logo-horiz.png', |
| alt: 'Apache Superset', |
| width: '126', |
| tooltip: '', |
| text: '', |
| }, |
| environment_tag: { |
| text: 'Production', |
| color: '#000', |
| }, |
| navbar_right: { |
| show_watermark: false, |
| bug_report_url: '/report/', |
| documentation_url: '/docs/', |
| languages: { |
| en: { |
| flag: 'us', |
| name: 'English', |
| url: '/lang/en', |
| }, |
| it: { |
| flag: 'it', |
| name: 'Italian', |
| url: '/lang/it', |
| }, |
| }, |
| show_language_picker: true, |
| user_is_anonymous: true, |
| 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: [ |
| { |
| name: 'Security', |
| icon: 'fa-cogs', |
| label: 'Security', |
| index: 1, |
| childs: [ |
| { |
| name: 'List Users', |
| icon: 'fa-user', |
| label: 'List Users', |
| url: '/users/list/', |
| index: 1, |
| }, |
| ], |
| }, |
| ], |
| }, |
| }; |
| |
| const notanonProps = { |
| ...mockedProps, |
| data: { |
| ...mockedProps.data, |
| navbar_right: { |
| ...mockedProps.data.navbar_right, |
| user_is_anonymous: false, |
| }, |
| }, |
| }; |
| |
| const useSelectorMock = jest.spyOn(reactRedux, 'useSelector'); |
| |
| fetchMock.get( |
| 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', |
| {}, |
| ); |
| |
| beforeEach(() => { |
| // setup a DOM element as a render target |
| useSelectorMock.mockClear(); |
| }); |
| |
| test('should render', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { container } = render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(/sources/i)).toBeInTheDocument(); |
| expect(container).toBeInTheDocument(); |
| }); |
| |
| test('should render the navigation', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByRole('navigation')).toBeInTheDocument(); |
| }); |
| |
| test('should render the brand', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { |
| brand: { alt, icon }, |
| }, |
| } = mockedProps; |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByAltText(alt)).toBeInTheDocument(); |
| const image = screen.getByAltText(alt); |
| expect(image).toHaveAttribute('src', icon); |
| }); |
| |
| test('should render the environment tag', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { environment_tag }, |
| } = mockedProps; |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(environment_tag.text)).toBeInTheDocument(); |
| }); |
| |
| test('should render all the top navbar menu items', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { menu }, |
| } = mockedProps; |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(menu[0].label)).toBeInTheDocument(); |
| menu.forEach(item => { |
| expect(screen.getByText(item.label)).toBeInTheDocument(); |
| }); |
| }); |
| |
| test('should render the top navbar child menu items', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { menu }, |
| } = mockedProps; |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| const sources = await screen.findByText('Sources'); |
| userEvent.hover(sources); |
| |
| const datasets = await screen.findByText('Datasets'); |
| const databases = await screen.findByText('Databases'); |
| const dataset = menu[1].childs![0] as { url: string }; |
| const database = menu[1].childs![2] as { url: string }; |
| |
| expect(datasets).toHaveAttribute('href', dataset.url); |
| expect(databases).toHaveAttribute('href', database.url); |
| }); |
| |
| test('should render the dropdown items', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...notanonProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| const dropdown = screen.getByTestId('new-dropdown-icon'); |
| userEvent.hover(dropdown); |
| // todo (philip): test data submenu |
| expect(await screen.findByText(dropdownItems[1].label)).toHaveAttribute( |
| 'href', |
| dropdownItems[1].url, |
| ); |
| expect(await screen.findByText(dropdownItems[1].label)).toHaveAttribute( |
| 'href', |
| dropdownItems[1].url, |
| ); |
| expect( |
| screen.getByTestId(`menu-item-${dropdownItems[1].label}`), |
| ).toBeInTheDocument(); |
| expect(await screen.findByText(dropdownItems[2].label)).toHaveAttribute( |
| 'href', |
| dropdownItems[2].url, |
| ); |
| expect( |
| screen.getByTestId(`menu-item-${dropdownItems[2].label}`), |
| ).toBeInTheDocument(); |
| }); |
| |
| test('should render the Settings', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| const settings = await screen.findByText('Settings'); |
| expect(settings).toBeInTheDocument(); |
| }); |
| |
| test('should render the Settings menu item', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| userEvent.hover(screen.getByText('Settings')); |
| const label = await screen.findByText('Security'); |
| expect(label).toBeInTheDocument(); |
| }); |
| |
| test('should render the Settings dropdown child menu items', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { settings }, |
| } = mockedProps; |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| userEvent.hover(screen.getByText('Settings')); |
| const listUsers = await screen.findByText('List Users'); |
| expect(listUsers).toHaveAttribute('href', settings[0].childs[0].url); |
| }); |
| |
| test('should render the plus menu (+) when user is not anonymous', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...notanonProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByTestId('new-dropdown')).toBeInTheDocument(); |
| }); |
| |
| test('should NOT render the plus menu (+) when user is anonymous', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(/sources/i)).toBeInTheDocument(); |
| expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); |
| }); |
| |
| test('should render the user actions when user is not anonymous', async () => { |
| useSelectorMock.mockReturnValue({ roles: mockedProps.user.roles }); |
| const { |
| data: { |
| navbar_right: { user_info_url, user_logout_url }, |
| }, |
| } = mockedProps; |
| |
| render(<Menu {...notanonProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| userEvent.hover(screen.getByText('Settings')); |
| const user = await screen.findByText('User'); |
| expect(user).toBeInTheDocument(); |
| |
| const info = await screen.findByText('Info'); |
| const logout = await screen.findByText('Logout'); |
| |
| expect(info).toHaveAttribute('href', user_info_url); |
| expect(logout).toHaveAttribute('href', user_logout_url); |
| }); |
| |
| test('should NOT render the user actions when user is anonymous', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(/sources/i)).toBeInTheDocument(); |
| expect(screen.queryByText('User')).not.toBeInTheDocument(); |
| }); |
| |
| test('should render the About section and version_string, sha or build_number when available', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { |
| navbar_right: { version_sha, version_string, build_number }, |
| }, |
| } = mockedProps; |
| |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| userEvent.hover(screen.getByText('Settings')); |
| const about = await screen.findByText('About'); |
| |
| // The version information is rendered as combined text in a single element |
| // Use getAllByText to get all matching elements and check the first one |
| const versionTexts = await screen.findAllByText( |
| (_, element) => |
| element?.textContent?.includes(`Version: ${version_string}`) ?? false, |
| ); |
| const shaTexts = await screen.findAllByText( |
| (_, element) => |
| element?.textContent?.includes(`SHA: ${version_sha}`) ?? false, |
| ); |
| const buildTexts = await screen.findAllByText( |
| (_, element) => |
| element?.textContent?.includes(`Build: ${build_number}`) ?? false, |
| ); |
| |
| expect(about).toBeInTheDocument(); |
| expect(versionTexts[0]).toBeInTheDocument(); |
| expect(shaTexts[0]).toBeInTheDocument(); |
| expect(buildTexts[0]).toBeInTheDocument(); |
| }); |
| |
| test('should render the Documentation link when available', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { |
| navbar_right: { documentation_url }, |
| }, |
| } = mockedProps; |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| userEvent.hover(screen.getByText('Settings')); |
| const doc = await screen.findByTitle('Documentation'); |
| expect(doc).toHaveAttribute('href', documentation_url); |
| }); |
| |
| test('should render the Bug Report link when available', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { |
| navbar_right: { bug_report_url }, |
| }, |
| } = mockedProps; |
| |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| const bugReport = await screen.findByTitle('Report a bug'); |
| expect(bugReport).toHaveAttribute('href', bug_report_url); |
| }); |
| |
| test('should render the Login link when user is anonymous', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| const { |
| data: { |
| navbar_right: { user_login_url }, |
| }, |
| } = mockedProps; |
| |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| const login = await screen.findByText('Login'); |
| expect(login).toHaveAttribute('href', user_login_url); |
| }); |
| |
| test('should render the Language Picker', async () => { |
| useSelectorMock.mockReturnValue({ roles: user.roles }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByLabelText('Languages')).toBeInTheDocument(); |
| }); |
| |
| test('should hide create button without proper roles', async () => { |
| useSelectorMock.mockReturnValue({ roles: [] }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useQueryParams: true, |
| useRouter: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(/sources/i)).toBeInTheDocument(); |
| expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); |
| }); |
| |
| test('should render without QueryParamProvider', async () => { |
| useSelectorMock.mockReturnValue({ roles: [] }); |
| render(<Menu {...mockedProps} />, { |
| useRedux: true, |
| useRouter: true, |
| useQueryParams: true, |
| useTheme: true, |
| }); |
| expect(await screen.findByText(/sources/i)).toBeInTheDocument(); |
| expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); |
| }); |
| |
| test('should render an extension component if one is supplied', async () => { |
| const extensionsRegistry = getExtensionsRegistry(); |
| |
| extensionsRegistry.set('navbar.right', () => ( |
| <>navbar.right extension component</> |
| )); |
| |
| setupCodeOverrides(); |
| |
| render(<Menu {...mockedProps} />, { |
| useRouter: true, |
| useQueryParams: true, |
| useRedux: true, |
| useTheme: true, |
| }); |
| |
| const extension = await screen.findAllByText( |
| 'navbar.right extension component', |
| ); |
| |
| expect(extension[0]).toBeInTheDocument(); |
| }); |
| |
| test('should render the brand text if available', async () => { |
| useSelectorMock.mockReturnValue({ roles: [] }); |
| |
| const modifiedProps = { |
| ...mockedProps, |
| data: { |
| ...mockedProps.data, |
| brand: { |
| ...mockedProps.data.brand, |
| text: 'Welcome to Superset', |
| }, |
| }, |
| }; |
| |
| render(<Menu {...modifiedProps} />, { |
| useRouter: true, |
| useQueryParams: true, |
| useRedux: true, |
| useTheme: true, |
| }); |
| |
| const brandText = await screen.findByText('Welcome to Superset'); |
| expect(brandText).toBeInTheDocument(); |
| }); |
| |
| test('should not render the brand text if not available', async () => { |
| useSelectorMock.mockReturnValue({ roles: [] }); |
| const text = 'Welcome to Superset'; |
| render(<Menu {...mockedProps} />, { |
| useRouter: true, |
| useQueryParams: true, |
| useRedux: true, |
| useTheme: true, |
| }); |
| |
| const brandText = screen.queryByText(text); |
| expect(brandText).not.toBeInTheDocument(); |
| }); |