blob: 2ae633e728233f293fb758cfb60239aaf205797c [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 { fireEvent, screen, waitFor } from 'spec/helpers/testing-library';
import { isFeatureEnabled } from '@superset-ui/core';
import {
mockCharts,
mockHandleResourceExport,
renderChartList,
setupMocks,
} from './ChartList.testHelpers';
jest.setTimeout(30000);
// Mock the feature flag
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(),
}));
// Mock the export utility
jest.mock('src/utils/export', () => ({
__esModule: true,
default: jest.fn(),
}));
const mockUser = {
userId: 1,
firstName: 'Test',
lastName: 'User',
roles: {
Admin: [
['can_sqllab', 'Superset'],
['can_write', 'Dashboard'],
['can_write', 'Chart'],
['can_export', 'Chart'],
],
},
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ChartList Card View Tests', () => {
beforeEach(() => {
setupMocks();
// Enable card view as default
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation(
(feature: string) => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
);
});
afterEach(() => {
fetchMock.resetHistory();
fetchMock.restore();
});
test('renders ChartList in card view', async () => {
renderChartList(mockUser);
// Wait for chart list to load
await screen.findByTestId('chart-list-view');
// Verify we're in card view by default (no table should be present)
expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
// Verify basic card view elements are present
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
// Verify card view toggle is active (appstore icon should have active class)
const cardViewToggle = screen.getByRole('img', { name: 'appstore' });
const cardViewButton = cardViewToggle.closest('[role="button"]');
expect(cardViewButton).toHaveClass('active');
// Verify list view toggle is not active
const listViewToggle = screen.getByRole('img', { name: 'unordered-list' });
const listViewButton = listViewToggle.closest('[role="button"]');
expect(listViewButton).not.toHaveClass('active');
});
test('switches from card view to list view', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
// Verify starting in card view
expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
// Switch to list view
const listViewToggle = screen.getByRole('img', { name: 'unordered-list' });
const listViewButton = listViewToggle.closest('[role="button"]');
expect(listViewButton).not.toBeNull();
fireEvent.click(listViewButton!);
// Verify table is now rendered (indicating list view)
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
});
test('renders ChartList in card view with thumbnails enabled', async () => {
// Enable thumbnails feature flag
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation(
(feature: string) =>
feature === 'LISTVIEWS_DEFAULT_CARD_VIEW' || feature === 'THUMBNAILS',
);
renderChartList(mockUser);
// Wait for chart list to load
await screen.findByTestId('chart-list-view');
// Wait for chart metadata section to load
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Should show images (thumbnails) in card view when feature is enabled
const allImages = await screen.findAllByTestId('image-loader');
expect(allImages).toHaveLength(mockCharts.length);
});
test('displays chart data correctly', async () => {
renderChartList(mockUser);
// Wait for chart list to load
await screen.findByTestId('chart-list-view');
// Wait for cards to render
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
const testChart = mockCharts[0];
// 1. Verify chart name appears
expect(screen.getByText(testChart.slice_name)).toBeInTheDocument();
// 2. Verify favorite stars exist (one per chart)
const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(mockCharts.length);
// 3. Verify last modified date appears (rendered with "Modified" prefix)
const modifiedText = `Modified ${testChart.changed_on_delta_humanized}`;
expect(screen.getByText(modifiedText)).toBeInTheDocument();
// 4. Verify action menu exists (more button for each card)
const moreButtons = screen.getAllByLabelText('more');
expect(moreButtons).toHaveLength(mockCharts.length);
// 5. Verify menu items appear on click
fireEvent.click(moreButtons[0]);
await waitFor(() => {
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Export')).toBeInTheDocument();
expect(screen.getByText('Delete')).toBeInTheDocument();
});
});
test('export chart api called when export button is clicked', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Find and click the more actions button on the first card
const moreButtons = screen.getAllByLabelText('more');
fireEvent.click(moreButtons[0]);
// Wait for dropdown menu and click export
const exportOption = await screen.findByText('Export');
fireEvent.click(exportOption);
// Verify export was called with correct chart ID
expect(mockHandleResourceExport).toHaveBeenCalledWith(
'chart',
[mockCharts[0].id],
expect.any(Function),
);
});
test('opens edit properties modal when edit button is clicked', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Find and click the more actions button on the first card
const moreButtons = screen.getAllByLabelText('more');
fireEvent.click(moreButtons[0]);
// Wait for dropdown menu and click edit
const editOption = await screen.findByText('Edit');
fireEvent.click(editOption);
// Verify edit modal appears (look for edit form elements)
await waitFor(() => {
expect(screen.getByText('Chart properties')).toBeInTheDocument();
});
});
test('opens delete confirmation when delete button is clicked', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Find and click the more actions button on the first card
const moreButtons = screen.getAllByLabelText('more');
fireEvent.click(moreButtons[0]);
// Wait for dropdown menu and click delete
const deleteOption = await screen.findByText('Delete');
fireEvent.click(deleteOption);
// Verify delete confirmation modal appears
await waitFor(() => {
const deleteModal = screen.getByRole('dialog');
expect(deleteModal).toBeInTheDocument();
expect(deleteModal).toHaveTextContent(/delete/i);
});
});
test('displays certified badge only for certified charts', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Test certified charts (mockCharts[1] and mockCharts[3] have certified_by)
const certifiedBadges = screen.getAllByLabelText('certified');
// Should have exactly 2 certified badges (for charts 1 and 3)
expect(certifiedBadges).toHaveLength(2);
// Verify specific certified charts show badges
// mockCharts[1] is certified by 'Data Team'
expect(screen.getByText(mockCharts[1].slice_name)).toBeInTheDocument();
// mockCharts[3] is certified by 'QA Team'
expect(screen.getByText(mockCharts[3].slice_name)).toBeInTheDocument();
});
test('can bulk deselect all charts', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls to appear
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// In card view, click on individual cards to select them (not checkboxes)
// Find the first chart name and click on it to select the card
const firstChartName = screen.getByText(mockCharts[0].slice_name);
fireEvent.click(firstChartName);
// Verify first chart is selected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
'1 Selected',
);
});
// Click on second chart to add to selection
const secondChartName = screen.getByText(mockCharts[1].slice_name);
fireEvent.click(secondChartName);
// Verify both charts are selected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
'2 Selected',
);
});
// Click deselect all
const deselectAllButton = screen.getByTestId('bulk-select-deselect-all');
fireEvent.click(deselectAllButton);
// Verify all charts are deselected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
'0 Selected',
);
});
});
test('can bulk export selected charts', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// Select charts by clicking on each card (no "Select all" in card view)
for (let i = 0; i < mockCharts.length; i += 1) {
const chartName = screen.getByText(mockCharts[i].slice_name);
fireEvent.click(chartName);
}
// Wait for all charts to be selected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
`${mockCharts.length} Selected`,
);
});
// Click bulk export button (find by text since there are multiple bulk-select-action buttons)
const bulkExportButton = screen.getByText('Export');
fireEvent.click(bulkExportButton);
// Verify export was called with all chart IDs
expect(mockHandleResourceExport).toHaveBeenCalledWith(
'chart',
mockCharts.map(chart => chart.id),
expect.any(Function),
);
});
test('can bulk delete selected charts', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// Select charts by clicking on each card (no "Select all" in card view)
for (let i = 0; i < mockCharts.length; i += 1) {
const chartName = screen.getByText(mockCharts[i].slice_name);
fireEvent.click(chartName);
}
// Wait for all charts to be selected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
`${mockCharts.length} Selected`,
);
});
// Click bulk delete button (find by text since there are multiple bulk-select-action buttons)
const bulkDeleteButton = screen.getByText('Delete');
fireEvent.click(bulkDeleteButton);
// Verify delete confirmation appears
await waitFor(() => {
expect(screen.getByText('Please confirm')).toBeInTheDocument();
});
});
test('can bulk add tags to selected charts', async () => {
// Enable tagging system for this test
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation(
(feature: string) =>
feature === 'LISTVIEWS_DEFAULT_CARD_VIEW' ||
feature === 'TAGGING_SYSTEM',
);
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// Select charts by clicking on each card (no "Select all" in card view)
for (let i = 0; i < mockCharts.length; i += 1) {
const chartName = screen.getByText(mockCharts[i].slice_name);
fireEvent.click(chartName);
}
// Wait for all charts to be selected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
`${mockCharts.length} Selected`,
);
});
// Since TAGGING_SYSTEM is enabled, the tag button should be present
const bulkTagButton = screen.getByTestId('bulk-select-tag-btn');
expect(bulkTagButton).toBeInTheDocument();
fireEvent.click(bulkTagButton);
// Verify tag modal appears
await waitFor(() => {
expect(screen.getByText('Add Tag')).toBeInTheDocument();
});
});
test('exit bulk select by hitting x on bulk select bar', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// Click the X button to close bulk select (look for close icon in bulk select bar)
const closeButton = document.querySelector(
'.ant-alert-close-icon',
) as HTMLButtonElement;
fireEvent.click(closeButton);
// Verify bulk select controls are gone
await waitFor(() => {
expect(
screen.queryByTestId('bulk-select-controls'),
).not.toBeInTheDocument();
});
});
test('exit bulk select by clicking bulk select button again', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// Click bulk select button again to exit
fireEvent.click(bulkSelectButton);
// Verify bulk select controls are gone
await waitFor(() => {
expect(
screen.queryByTestId('bulk-select-controls'),
).not.toBeInTheDocument();
});
});
test('card click behavior changes in bulk select mode', async () => {
renderChartList(mockUser);
// Wait for cards to load
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
});
// In normal mode, clicking card should navigate (but we can't test navigation in this setup)
// Instead, verify bulk select is not active initially
expect(
screen.queryByTestId('bulk-select-controls'),
).not.toBeInTheDocument();
// Enable bulk select mode
const bulkSelectButton = screen.getByTestId('bulk-select');
fireEvent.click(bulkSelectButton);
// Wait for bulk select controls
await waitFor(() => {
expect(screen.getByTestId('bulk-select-controls')).toBeInTheDocument();
});
// Now clicking on cards should select them instead of navigating
const firstChartName = screen.getByText(mockCharts[0].slice_name);
fireEvent.click(firstChartName);
// Verify chart was selected (not navigated)
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
'1 Selected',
);
});
// Clicking the same card again should deselect it
fireEvent.click(firstChartName);
// Verify chart was deselected
await waitFor(() => {
expect(screen.getByTestId('bulk-select-copy')).toHaveTextContent(
'0 Selected',
);
});
});
test('renders sort dropdown in card view', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
// Wait for the component to switch to card view (due to feature flag)
await waitFor(() => {
expect(screen.queryByTestId('listview-table')).not.toBeInTheDocument();
});
// Verify basic card view elements are present
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
// Find Sort dropdown using its data-test attribute (CardSortSelect component)
const sortFilter = screen.getByTestId('card-sort-select');
expect(sortFilter).toBeInTheDocument();
expect(sortFilter).toBeVisible();
expect(sortFilter).toBeEnabled();
});
});