blob: bb3445de6151d57d49635ff9f5d3e1dfb7647f48 [file] [log] [blame]
/**
* 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();
});
});