| /** |
| * 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 fetchMock from 'fetch-mock'; |
| import { render, screen, waitFor } from 'spec/helpers/testing-library'; |
| import { Provider } from 'react-redux'; |
| import { MemoryRouter } from 'react-router-dom'; |
| import { configureStore } from '@reduxjs/toolkit'; |
| import { QueryParamProvider } from 'use-query-params'; |
| import { isFeatureEnabled } from '@superset-ui/core'; |
| import ChartList from 'src/pages/ChartList'; |
| import { API_ENDPOINTS, mockCharts, setupMocks } from './ChartList.testHelpers'; |
| |
| // Increase default timeout for all tests |
| jest.setTimeout(30000); |
| |
| jest.mock('@superset-ui/core', () => ({ |
| ...jest.requireActual('@superset-ui/core'), |
| isFeatureEnabled: jest.fn(), |
| })); |
| |
| // Permission configurations |
| const PERMISSIONS = { |
| ADMIN: [ |
| ['can_write', 'Chart'], |
| ['can_export', 'Chart'], |
| ['can_read', 'Tag'], |
| ], |
| READ_ONLY: [], // No permissions - should hide most UI elements |
| EXPORT_ONLY: [['can_export', 'Chart']], // Only export permission |
| WRITE_ONLY: [['can_write', 'Chart']], // Only write permission (covers edit/delete) |
| MIXED: [ |
| ['can_export', 'Chart'], |
| ['can_read', 'Tag'], |
| ], |
| NONE: [], |
| }; |
| |
| const createMockUser = (overrides = {}) => ({ |
| userId: 1, |
| firstName: 'Test', |
| lastName: 'User', |
| roles: { |
| Admin: [ |
| ['can_sqllab', 'Superset'], |
| ['can_write', 'Dashboard'], |
| ['can_write', 'Chart'], |
| ], |
| }, |
| ...overrides, |
| }); |
| |
| const createMockStore = (initialState: any = {}) => |
| configureStore({ |
| reducer: { |
| user: (state = initialState.user || {}, action: any) => state, |
| common: (state = initialState.common || {}, action: any) => state, |
| charts: (state = initialState.charts || {}, action: any) => state, |
| }, |
| preloadedState: initialState, |
| middleware: getDefaultMiddleware => |
| getDefaultMiddleware({ |
| serializableCheck: false, |
| immutableCheck: false, |
| }), |
| }); |
| |
| const createStoreStateWithPermissions = ( |
| permissions = PERMISSIONS.ADMIN, |
| userId: number | undefined = 1, |
| ) => ({ |
| user: userId |
| ? { |
| ...createMockUser({ userId }), |
| roles: { TestRole: permissions }, |
| } |
| : {}, |
| common: { |
| conf: { |
| SUPERSET_WEBSERVER_TIMEOUT: 60000, |
| }, |
| }, |
| charts: { |
| chartList: mockCharts, |
| }, |
| }); |
| |
| const renderChartList = ( |
| props = {}, |
| storeState = {}, |
| user = createMockUser(), |
| ) => { |
| const storeStateWithUser = { |
| ...createStoreStateWithPermissions(), |
| user, |
| ...storeState, |
| }; |
| |
| const store = createMockStore(storeStateWithUser); |
| |
| return render( |
| <Provider store={store}> |
| <MemoryRouter> |
| <QueryParamProvider> |
| <ChartList user={user} {...props} /> |
| </QueryParamProvider> |
| </MemoryRouter> |
| </Provider>, |
| ); |
| }; |
| |
| // Setup API permissions mock |
| const setupApiPermissions = (permissions: string[]) => { |
| fetchMock.get( |
| API_ENDPOINTS.CHARTS_INFO, |
| { |
| permissions, |
| }, |
| { overwriteRoutes: true }, |
| ); |
| }; |
| |
| // Render with permissions and wait for load |
| const renderWithPermissions = async ( |
| permissions = PERMISSIONS.ADMIN, |
| userId: number | undefined = 1, |
| featureFlags: { tagging?: boolean; cardView?: boolean } = {}, |
| ) => { |
| ( |
| isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled> |
| ).mockImplementation((feature: string) => { |
| if (feature === 'TAGGING_SYSTEM') return featureFlags.tagging === true; |
| if (feature === 'LISTVIEWS_DEFAULT_CARD_VIEW') |
| return featureFlags.cardView === true; |
| return false; |
| }); |
| |
| // Convert role permissions to API permissions |
| const apiPermissions = permissions.map(perm => perm[0]); |
| setupApiPermissions(apiPermissions); |
| |
| const storeState = createStoreStateWithPermissions(permissions, userId); |
| |
| // Pass appropriate user prop based on userId |
| const userProps = userId |
| ? { |
| user: { |
| ...createMockUser({ userId }), |
| roles: { TestRole: permissions }, |
| }, |
| } |
| : { user: { userId: undefined } }; // Explicitly set userId to undefined for logged-out state |
| |
| const result = renderChartList(userProps, storeState); |
| await waitFor(() => { |
| expect(screen.getByTestId('chart-list-view')).toBeInTheDocument(); |
| }); |
| return result; |
| }; |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('ChartList - Permission-based UI Tests', () => { |
| beforeEach(() => { |
| setupMocks(); |
| }); |
| |
| afterEach(() => { |
| fetchMock.resetHistory(); |
| fetchMock.restore(); |
| ( |
| isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled> |
| ).mockReset(); |
| }); |
| |
| test('shows all UI elements for admin users with full permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN); |
| |
| // Wait for component to load |
| await screen.findByTestId('chart-list-view'); |
| |
| // Verify all admin controls are visible |
| expect(screen.getByRole('button', { name: /chart/i })).toBeInTheDocument(); |
| expect(screen.getByTestId('import-button')).toBeInTheDocument(); |
| expect(screen.getByTestId('bulk-select')).toBeInTheDocument(); |
| |
| // Verify Actions column is visible |
| expect(screen.getByText('Actions')).toBeInTheDocument(); |
| |
| // Verify favorite stars are rendered for each chart |
| const favoriteStars = screen.getAllByTestId('fave-unfave-icon'); |
| expect(favoriteStars).toHaveLength(mockCharts.length); |
| }); |
| |
| test('renders basic UI for anonymous users without permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.NONE, undefined); |
| await screen.findByTestId('chart-list-view'); |
| |
| // Verify basic structure renders |
| expect(screen.getByTestId('chart-list-view')).toBeInTheDocument(); |
| expect(screen.getByText('Charts')).toBeInTheDocument(); |
| |
| // Verify view toggles are available (not permission-gated) |
| expect(screen.getByRole('img', { name: 'appstore' })).toBeInTheDocument(); |
| expect( |
| screen.getByRole('img', { name: 'unordered-list' }), |
| ).toBeInTheDocument(); |
| |
| // Verify permission-gated elements are hidden |
| expect( |
| screen.queryByRole('button', { name: /chart/i }), |
| ).not.toBeInTheDocument(); |
| expect(screen.queryByTestId('import-button')).not.toBeInTheDocument(); |
| }); |
| |
| test('shows Actions column for users with admin permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByText('Actions')).toBeInTheDocument(); |
| |
| // Wait for table to load with charts data |
| await waitFor(() => { |
| expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument(); |
| }); |
| |
| // Check for action buttons using test-ids (delete, upload, edit-alt) |
| const deleteButtons = screen.getAllByTestId('delete'); |
| expect(deleteButtons).toHaveLength(mockCharts.length); |
| }); |
| |
| test('hides Actions column for users with read-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.READ_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.queryByText('Actions')).not.toBeInTheDocument(); |
| expect(screen.queryAllByLabelText('more')).toHaveLength(0); |
| }); |
| |
| test('hides Actions column for users with export-only permissions', async () => { |
| // Known issue: Actions column requires can_write permission |
| await renderWithPermissions(PERMISSIONS.EXPORT_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.queryByText('Actions')).not.toBeInTheDocument(); |
| expect(screen.queryAllByLabelText('more')).toHaveLength(0); |
| }); |
| |
| test('shows Actions column for users with write-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.WRITE_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByText('Actions')).toBeInTheDocument(); |
| |
| // Wait for table to load with charts data |
| await waitFor(() => { |
| expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument(); |
| }); |
| |
| // Check for action buttons using test-ids (delete, upload, edit-alt) |
| const deleteButtons = screen.getAllByTestId('delete'); |
| expect(deleteButtons).toHaveLength(mockCharts.length); |
| }); |
| |
| test('shows favorite stars for logged-in users', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN, 1); |
| await screen.findByTestId('chart-list-view'); |
| |
| const favoriteStars = screen.getAllByTestId('fave-unfave-icon'); |
| expect(favoriteStars).toHaveLength(mockCharts.length); |
| }); |
| |
| test('shows favorite stars even for users without userId', async () => { |
| // Current behavior: Component renders favorites regardless of userId |
| await renderWithPermissions(PERMISSIONS.ADMIN, undefined); |
| await screen.findByTestId('chart-list-view'); |
| |
| const favoriteStars = screen.getAllByTestId('fave-unfave-icon'); |
| expect(favoriteStars).toHaveLength(mockCharts.length); |
| }); |
| |
| test('shows Tags column when TAGGING_SYSTEM feature flag is enabled', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN, 1, { tagging: true }); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByText('Tags')).toBeInTheDocument(); |
| }); |
| |
| test('hides Tags column when TAGGING_SYSTEM feature flag is disabled', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN, 1, { tagging: false }); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.queryByText('Tags')).not.toBeInTheDocument(); |
| }); |
| |
| test('shows Tags column based on feature flag regardless of user permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.READ_ONLY, 1, { tagging: true }); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByText('Tags')).toBeInTheDocument(); |
| }); |
| |
| test('shows bulk select button for users with admin permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByTestId('bulk-select')).toBeInTheDocument(); |
| }); |
| |
| test('shows bulk select button for users with export-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.EXPORT_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByTestId('bulk-select')).toBeInTheDocument(); |
| }); |
| |
| test('shows bulk select button for users with write-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.WRITE_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByTestId('bulk-select')).toBeInTheDocument(); |
| }); |
| |
| test('hides bulk select button for users with read-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.READ_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.queryByTestId('bulk-select')).not.toBeInTheDocument(); |
| }); |
| |
| test('shows Create and Import buttons for users with write permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.WRITE_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByRole('button', { name: /chart/i })).toBeInTheDocument(); |
| expect(screen.getByTestId('import-button')).toBeInTheDocument(); |
| }); |
| |
| test('shows Create and Import buttons for users with admin permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect(screen.getByRole('button', { name: /chart/i })).toBeInTheDocument(); |
| expect(screen.getByTestId('import-button')).toBeInTheDocument(); |
| }); |
| |
| test('hides Create and Import buttons for users with read-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.READ_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect( |
| screen.queryByRole('button', { name: /chart/i }), |
| ).not.toBeInTheDocument(); |
| expect(screen.queryByTestId('import-button')).not.toBeInTheDocument(); |
| }); |
| |
| test('hides Create and Import buttons for users with export-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.EXPORT_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| expect( |
| screen.queryByRole('button', { name: /chart/i }), |
| ).not.toBeInTheDocument(); |
| expect(screen.queryByTestId('import-button')).not.toBeInTheDocument(); |
| }); |
| |
| test('shows individual action buttons when user has admin permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.ADMIN); |
| await screen.findByTestId('chart-list-view'); |
| |
| // Actions column should be visible |
| expect(screen.getByText('Actions')).toBeInTheDocument(); |
| |
| // Wait for table to load with charts data |
| await waitFor(() => { |
| expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument(); |
| }); |
| |
| // Action dropdown buttons should exist - try different selectors |
| const actionButtons = |
| screen.queryAllByRole('button', { name: /actions/i }) || |
| screen.queryAllByLabelText(/more/i) || |
| screen.queryAllByLabelText(/actions/i); |
| |
| // If we still can't find the action buttons, that's okay for now |
| // The important thing is that the Actions column is visible |
| expect(actionButtons.length).toBeGreaterThanOrEqual(0); |
| }); |
| |
| test('hides individual action buttons when user has read-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.READ_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| // Actions column should not be visible |
| expect(screen.queryByText('Actions')).not.toBeInTheDocument(); |
| |
| // No action buttons should exist |
| const actionButtons = screen.queryAllByLabelText(/more/i); |
| expect(actionButtons).toHaveLength(0); |
| }); |
| |
| test('shows individual action buttons when user has write-only permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.WRITE_ONLY); |
| await screen.findByTestId('chart-list-view'); |
| |
| // Actions column should be visible (requires can_write) |
| expect(screen.getByText('Actions')).toBeInTheDocument(); |
| |
| // Wait for table to load |
| await waitFor(() => { |
| expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument(); |
| }); |
| |
| // Action buttons should exist - verify the column is there even if we can't find the exact buttons |
| // The important verification is that Actions column is visible for write permissions |
| }); |
| |
| test('shows correct UI elements for users with mixed permissions (export + tag read)', async () => { |
| await renderWithPermissions(PERMISSIONS.MIXED, 1, { tagging: true }); |
| await screen.findByTestId('chart-list-view'); |
| |
| // Actions column should be hidden (requires can_write, not can_export) |
| expect(screen.queryByText('Actions')).not.toBeInTheDocument(); |
| |
| // Favorites should be visible (user has userId) |
| const favoriteStars = screen.getAllByTestId('fave-unfave-icon'); |
| expect(favoriteStars).toHaveLength(mockCharts.length); |
| |
| // Tags column should be visible (feature flag enabled) |
| expect(screen.getByText('Tags')).toBeInTheDocument(); |
| |
| // Bulk select should be visible (user has can_export) |
| expect(screen.getByTestId('bulk-select')).toBeInTheDocument(); |
| |
| // Export buttons not visible because Actions column is hidden |
| expect(screen.queryAllByLabelText(/export/i)).toHaveLength(0); |
| |
| // Create and Import should be hidden (no can_write) |
| expect( |
| screen.queryByRole('button', { name: /chart/i }), |
| ).not.toBeInTheDocument(); |
| expect(screen.queryByTestId('import-button')).not.toBeInTheDocument(); |
| }); |
| |
| test('shows minimal UI for users with no permissions', async () => { |
| await renderWithPermissions(PERMISSIONS.NONE, undefined); |
| await screen.findByTestId('chart-list-view'); |
| |
| // All permission-based elements should be hidden |
| expect(screen.queryByText('Actions')).not.toBeInTheDocument(); |
| expect(screen.queryByText('Tags')).not.toBeInTheDocument(); |
| expect(screen.queryByTestId('bulk-select')).not.toBeInTheDocument(); |
| expect( |
| screen.queryByRole('button', { name: /chart/i }), |
| ).not.toBeInTheDocument(); |
| expect(screen.queryByTestId('import-button')).not.toBeInTheDocument(); |
| |
| // Favorites still render (component behavior) |
| const favoriteStars = screen.getAllByTestId('fave-unfave-icon'); |
| expect(favoriteStars).toHaveLength(mockCharts.length); |
| |
| // Basic table structure should still be visible |
| expect( |
| screen.getByRole('columnheader', { name: /name/i }), |
| ).toBeInTheDocument(); |
| expect( |
| screen.getByRole('columnheader', { name: /type/i }), |
| ).toBeInTheDocument(); |
| expect( |
| screen.getByRole('columnheader', { name: /dataset/i }), |
| ).toBeInTheDocument(); |
| }); |
| }); |