blob: ff01207c8e7a3839bd4cd6091fa1c786c3094b98 [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 { screen, waitFor, fireEvent } from 'spec/helpers/testing-library';
import { isFeatureEnabled } from '@superset-ui/core';
import {
API_ENDPOINTS,
mockCharts,
renderChartList,
setupMocks,
} from './ChartList.testHelpers';
const mockPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({ push: mockPush }),
}));
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(),
}));
// Increase default timeout for all tests
jest.setTimeout(30000);
const mockUser = {
userId: 1,
firstName: 'Test',
lastName: 'User',
roles: {
Admin: [
['can_sqllab', 'Superset'],
['can_write', 'Dashboard'],
['can_write', 'Chart'],
['can_export', 'Chart'],
],
},
};
// Filter utilities
const findFilterByLabel = (labelText: string) => {
const containers = screen.getAllByTestId('select-filter-container');
for (const container of containers) {
const label = container.querySelector('label');
if (label?.textContent === labelText) {
return container.querySelector('[role="combobox"], .ant-select');
}
}
return null;
};
describe('ChartList', () => {
beforeEach(() => {
setupMocks();
mockPush.mockClear();
});
afterEach(() => {
fetchMock.resetHistory();
fetchMock.restore();
// Reset feature flag mock
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockReset();
});
it('renders component with basic structure', async () => {
renderChartList(mockUser);
expect(await screen.findByTestId('chart-list-view')).toBeInTheDocument();
expect(screen.getByText('Charts')).toBeInTheDocument();
});
it('verify New Chart button existence and functionality', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
// Verify New Chart button exists
const newChartButton = screen.getByRole('button', { name: /chart/i });
expect(newChartButton).toBeInTheDocument();
expect(screen.getByTestId('plus')).toBeInTheDocument();
// Click the New Chart button
fireEvent.click(newChartButton);
// Verify it triggers navigation to chart creation
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/chart/add');
});
});
it('verify Import button existence and functionality', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
// Verify Import button exists
const importButton = screen.getByTestId('import-button');
expect(importButton).toBeInTheDocument();
// Click the Import button
fireEvent.click(importButton);
// Verify import modal opens
await waitFor(() => {
const importModal = screen.getByRole('dialog');
expect(importModal).toBeInTheDocument();
expect(importModal).toHaveTextContent(/import/i);
});
});
it('shows loading state during initial data fetch', async () => {
// Delay the chart data response to test loading state
fetchMock.get(
API_ENDPOINTS.CHARTS,
new Promise(resolve =>
setTimeout(() => resolve({ result: mockCharts, chart_count: 3 }), 200),
),
{ overwriteRoutes: true },
);
renderChartList(mockUser);
// Component should render immediately with loading state
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
// Wait for data to eventually load
await waitFor(
() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
},
{ timeout: 1000 },
);
});
it('makes correct API calls on initial load', async () => {
renderChartList(mockUser);
await waitFor(() => {
const infoCalls = fetchMock.calls(/chart\/_info/);
const dataCalls = fetchMock.calls(/chart\/\?q/);
expect(infoCalls).toHaveLength(1);
expect(dataCalls).toHaveLength(1);
expect(dataCalls[0][0]).toContain(
'order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25',
);
});
});
it('shows loading state while API calls are in progress', async () => {
// Mock delayed API responses
fetchMock.get(
API_ENDPOINTS.CHARTS_INFO,
new Promise(resolve =>
setTimeout(
() => resolve({ permissions: ['can_read', 'can_write'] }),
100,
),
),
{ overwriteRoutes: true },
);
fetchMock.get(
API_ENDPOINTS.CHARTS,
new Promise(resolve =>
setTimeout(() => resolve({ result: mockCharts, chart_count: 3 }), 150),
),
{ overwriteRoutes: true },
);
renderChartList(mockUser);
// Main container should render immediately
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
// Eventually data should load
await waitFor(
() => {
const infoCalls = fetchMock.calls(/chart\/_info/);
const dataCalls = fetchMock.calls(/chart\/\?q/);
expect(infoCalls).toHaveLength(1);
expect(dataCalls).toHaveLength(1);
},
{ timeout: 1000 },
);
});
it('maintains component structure during loading', async () => {
// Only delay data loading, not permissions
fetchMock.get(
API_ENDPOINTS.CHARTS,
new Promise(resolve =>
setTimeout(() => resolve({ result: mockCharts, chart_count: 3 }), 200),
),
{ overwriteRoutes: true },
);
renderChartList(mockUser);
// Core structure should be available immediately
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
expect(screen.getByText('Charts')).toBeInTheDocument();
// View toggles should be available during loading
expect(screen.getByRole('img', { name: 'appstore' })).toBeInTheDocument();
expect(
screen.getByRole('img', { name: 'unordered-list' }),
).toBeInTheDocument();
// Wait for permissions to load, then action buttons should appear
await waitFor(
() => {
expect(
screen.getByRole('button', { name: 'Bulk select' }),
).toBeInTheDocument();
},
{ timeout: 500 },
);
// Wait for data to eventually load
await waitFor(
() => {
expect(screen.getByText(mockCharts[0].slice_name)).toBeInTheDocument();
},
{ timeout: 1000 },
);
});
it('handles API errors gracefully', async () => {
// Mock API failure
fetchMock.get(
API_ENDPOINTS.CHARTS_INFO,
{ throws: new Error('API Error') },
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
// Should handle error gracefully and still render component
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
it('handles empty results', async () => {
// Mock empty chart data (not permissions)
fetchMock.get(
API_ENDPOINTS.CHARTS,
{ result: [], chart_count: 0 },
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
// Should render component even with no data
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
// Global controls should still be functional with no data
expect(screen.getByRole('img', { name: 'appstore' })).toBeInTheDocument();
expect(
screen.getByRole('img', { name: 'unordered-list' }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Bulk select' }),
).toBeInTheDocument();
});
});
describe('ChartList - Global Filter Interactions', () => {
beforeEach(() => {
setupMocks();
});
afterEach(() => {
fetchMock.resetHistory();
fetchMock.restore();
// Reset feature flag mock
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockReset();
});
it('renders search filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
// Verify search filter renders correctly
expect(screen.getByTestId('filters-search')).toBeInTheDocument();
expect(screen.getByPlaceholderText(/type a value/i)).toBeInTheDocument();
});
it('renders Type filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const typeFilter = findFilterByLabel('Type');
expect(typeFilter).toBeVisible();
expect(typeFilter).toBeEnabled();
});
it('renders Dataset filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const datasetFilter = findFilterByLabel('Dataset');
expect(datasetFilter).toBeVisible();
expect(datasetFilter).toBeEnabled();
});
it('renders Owner filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const ownerFilter = findFilterByLabel('Owner');
expect(ownerFilter).toBeVisible();
expect(ownerFilter).toBeEnabled();
});
it('renders Certified filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const certifiedFilter = findFilterByLabel('Certified');
expect(certifiedFilter).toBeVisible();
expect(certifiedFilter).toBeEnabled();
});
it('renders Favorite filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const favoriteFilter = findFilterByLabel('Favorite');
expect(favoriteFilter).toBeVisible();
expect(favoriteFilter).toBeEnabled();
});
it('renders Dashboard filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const dashboardFilter = findFilterByLabel('Dashboard');
expect(dashboardFilter).toBeVisible();
expect(dashboardFilter).toBeEnabled();
});
it('renders Modified by filter correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
const modifiedByFilter = findFilterByLabel('Modified by');
expect(modifiedByFilter).toBeVisible();
expect(modifiedByFilter).toBeEnabled();
});
it('renders Tags filter when TAGGING_SYSTEM is enabled', async () => {
// Mock feature flag to enable tags
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation(
(feature: string) =>
feature === 'TAGGING_SYSTEM' ||
feature !== 'LISTVIEWS_DEFAULT_CARD_VIEW',
);
// Render with tag permissions
const userWithTagPerms = {
...mockUser,
roles: {
Admin: [
['can_sqllab', 'Superset'],
['can_write', 'Dashboard'],
['can_write', 'Chart'],
['can_read', 'Tag'],
['can_write', 'Tag'],
],
},
};
renderChartList(userWithTagPerms);
const tagsFilter = findFilterByLabel('Tag');
expect(tagsFilter).toBeVisible();
expect(tagsFilter).toBeEnabled();
});
it('does not render Tags filter when TAGGING_SYSTEM is disabled', async () => {
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation(
(feature: string) =>
feature !== 'LISTVIEWS_DEFAULT_CARD_VIEW' &&
feature !== 'TAGGING_SYSTEM',
);
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await screen.findByTestId('listview-table');
// Check that Tag filter is not present in filter containers
const containers = screen.getAllByTestId('select-filter-container');
const filterLabels = containers
.map(container => {
const label = container.querySelector('label');
return label?.textContent;
})
.filter(Boolean);
expect(filterLabels).not.toContain('Tag');
});
it('allows filters to be reset correctly', async () => {
renderChartList(mockUser);
await screen.findByTestId('chart-list-view');
await waitFor(() => {
expect(screen.getByTestId('listview-table')).toBeInTheDocument();
});
// Apply search filter
const searchInput = screen.getByTestId('filters-search');
fireEvent.change(searchInput, { target: { value: 'test' } });
// Clear search
fireEvent.change(searchInput, { target: { value: '' } });
// Verify filter UI is reset
expect((searchInput as HTMLInputElement).value).toBe('');
});
});