Compare commits

...

2 Commits

Author SHA1 Message Date
Elizabeth Thompson
c2355c7162 test: add comprehensive tests for favorite column hiding behavior
Add test suites to verify that the favorite column is properly hidden
when no items are favorited and shown when favorites exist.

Tests cover:
- Column hiding when no items are favorited
- Column showing when at least one item is favorited
- Column showing when all items are favorited
- Column hiding for unauthenticated users
- Column hiding for empty lists
- Graceful handling of partial favorite status loading

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 10:34:44 -07:00
Elizabeth Thompson
320cc6831c feat: hide favorite column when no items are favorited
Hide the favorite column in chart and dashboard list views when none
of the items on the current page have been marked as favorites. This
improves the UI by removing empty, non-functional columns.

Changes:
- Add memoized hasFavoritesOnPage helper to detect when favorites exist
- Update column hidden condition to include favorite availability check
- Apply consistently to both ChartList and DashboardList components

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 17:06:40 -07:00
4 changed files with 593 additions and 2 deletions

View File

@@ -0,0 +1,259 @@
/**
* 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 } from 'spec/helpers/testing-library';
import {
API_ENDPOINTS,
renderChartList,
setupMocks,
} from './ChartList.testHelpers';
jest.setTimeout(30000);
const mockUser = {
userId: 1,
firstName: 'Test',
lastName: 'User',
roles: {
Admin: [
['can_read', 'Chart'],
['can_write', 'Chart'],
],
},
};
describe('ChartList - Favorite Column Visibility', () => {
beforeEach(() => {
setupMocks();
});
afterEach(() => {
fetchMock.resetHistory();
fetchMock.restore();
});
test('hides favorite column when no charts are favorited', async () => {
// Mock favorite status API to return all false
fetchMock.get(
API_ENDPOINTS.CHART_FAVORITE_STATUS,
{
result: [
{ id: 0, value: false },
{ id: 1, value: false },
{ id: 2, value: false },
{ id: 3, value: false },
],
},
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await waitFor(() => {
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Test Chart 0')).toBeInTheDocument();
});
// Favorite column should be hidden - check that favorite stars are not present
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
// Verify that other columns are still present (check table headers)
expect(
screen.getByRole('columnheader', { name: 'Name' }),
).toBeInTheDocument();
expect(
screen.getByRole('columnheader', { name: 'Type' }),
).toBeInTheDocument();
});
test('shows favorite column when at least one chart is favorited', async () => {
// Mock favorite status API to return mixed favorites
fetchMock.get(
API_ENDPOINTS.CHART_FAVORITE_STATUS,
{
result: [
{ id: 0, value: true }, // This chart is favorited
{ id: 1, value: false },
{ id: 2, value: false },
{ id: 3, value: false },
],
},
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await waitFor(() => {
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Test Chart 0')).toBeInTheDocument();
});
// Favorite column should be visible - wait for stars to appear
await waitFor(
() => {
const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
expect(favoriteStars.length).toBeGreaterThan(0);
},
{ timeout: 10000 },
);
});
test('shows favorite column when all charts are favorited', async () => {
// Mock favorite status API to return all true
fetchMock.get(
API_ENDPOINTS.CHART_FAVORITE_STATUS,
{
result: [
{ id: 0, value: true },
{ id: 1, value: true },
{ id: 2, value: true },
{ id: 3, value: true },
],
},
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await waitFor(() => {
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Test Chart 0')).toBeInTheDocument();
});
// Favorite column should be visible
await waitFor(
() => {
const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
expect(favoriteStars.length).toBeGreaterThan(0);
},
{ timeout: 10000 },
);
});
test('hides favorite column when user is not logged in', async () => {
// Mock favorite status API
fetchMock.get(
API_ENDPOINTS.CHART_FAVORITE_STATUS,
{
result: [
{ id: 0, value: true },
{ id: 1, value: false },
],
},
{ overwriteRoutes: true },
);
// Render without userId (user not logged in)
const noUser = {
userId: null,
firstName: '',
lastName: '',
roles: {},
};
renderChartList(noUser);
await waitFor(() => {
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Test Chart 0')).toBeInTheDocument();
});
// Favorite column should be hidden when user is not logged in
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
});
test('hides favorite column when chart list is empty', async () => {
// Mock empty charts response
fetchMock.get(
API_ENDPOINTS.CHARTS,
{
result: [],
chart_count: 0,
},
{ overwriteRoutes: true },
);
// Mock empty favorite status
fetchMock.get(
API_ENDPOINTS.CHART_FAVORITE_STATUS,
{
result: [],
},
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await waitFor(() => {
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
// No favorite stars should be present when there are no charts
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
});
test('handles partial favorite status loading gracefully', async () => {
// Mock partial favorite status (fewer items than charts)
fetchMock.get(
API_ENDPOINTS.CHART_FAVORITE_STATUS,
{
result: [
{ id: 0, value: false },
{ id: 1, value: false },
// Missing status for charts 2 and 3
],
},
{ overwriteRoutes: true },
);
renderChartList(mockUser);
await waitFor(() => {
expect(screen.getByTestId('chart-list-view')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Test Chart 0')).toBeInTheDocument();
});
// Should hide column when favorite status is incomplete
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
});
});

View File

@@ -315,6 +315,14 @@ function ChartList(props: ChartListProps) {
};
};
const hasFavoritesOnPage = useMemo(
() =>
charts.length > 0 &&
Object.keys(favoriteStatus).length === charts.length &&
Object.values(favoriteStatus).some(status => status === true),
[charts.length, favoriteStatus],
);
const columns = useMemo(
() => [
{
@@ -334,7 +342,7 @@ function ChartList(props: ChartListProps) {
id: 'id',
disableSortBy: true,
size: 'xs',
hidden: !userId,
hidden: !userId || !hasFavoritesOnPage,
},
{
Cell: ({

View File

@@ -0,0 +1,316 @@
/**
* 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 { MemoryRouter } from 'react-router-dom';
import fetchMock from 'fetch-mock';
import { isFeatureEnabled } from '@superset-ui/core';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import { QueryParamProvider } from 'use-query-params';
import DashboardList from 'src/pages/DashboardList';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(),
}));
jest.setTimeout(30000);
const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
const dashboardOwnersEndpoint = 'glob:*/api/v1/dashboard/related/owners*';
const dashboardCreatedByEndpoint =
'glob:*/api/v1/dashboard/related/created_by*';
const dashboardFavoriteStatusEndpoint =
'glob:*/api/v1/dashboard/favorite_status*';
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
const mockDashboards = [...new Array(3)].map((_, i) => ({
id: i,
url: `url-${i}`,
dashboard_title: `Dashboard ${i}`,
changed_by_name: 'user',
changed_by_fk: 1,
published: true,
changed_on_utc: new Date().toISOString(),
changed_on_delta_humanized: '5 minutes ago',
owners: [{ id: 1, first_name: 'admin', last_name: 'admin_user' }],
roles: [{ id: 1, name: 'adminUser' }],
thumbnail_url: '/thumbnail',
}));
const mockUser = {
userId: 1,
firstName: 'Test',
lastName: 'User',
};
const setupBasicMocks = () => {
fetchMock.reset();
fetchMock.get(dashboardsInfoEndpoint, {
permissions: ['can_read', 'can_write'],
});
fetchMock.get(dashboardOwnersEndpoint, {
result: [],
});
fetchMock.get(dashboardCreatedByEndpoint, {
result: [],
});
fetchMock.get(dashboardsEndpoint, {
result: mockDashboards,
dashboard_count: 3,
});
global.URL.createObjectURL = jest.fn();
fetchMock.get('/thumbnail', { body: new Blob(), sendAsJson: false });
};
describe('DashboardList - Favorite Column Visibility', () => {
const renderDashboardList = (props = {}, userProp = mockUser) =>
render(
<MemoryRouter>
<QueryParamProvider>
<DashboardList {...props} user={userProp} />
</QueryParamProvider>
</MemoryRouter>,
{ useRedux: true },
);
beforeEach(() => {
setupBasicMocks();
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation(feature => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW');
});
afterEach(() => {
fetchMock.resetHistory();
fetchMock.restore();
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockReset();
});
test('hides favorite column when no dashboards are favorited', async () => {
// Mock favorite status API to return all false
fetchMock.get(
dashboardFavoriteStatusEndpoint,
{
result: [
{ id: 0, value: false },
{ id: 1, value: false },
{ id: 2, value: false },
],
},
{ overwriteRoutes: true },
);
renderDashboardList();
// Wait for component to load
await waitFor(() => {
expect(screen.getByText('Dashboards')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Dashboard 0')).toBeInTheDocument();
});
// Favorite column should be hidden - check that favorite stars are not present
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
// Verify that other columns are still present
expect(screen.getByText('Title')).toBeInTheDocument();
});
test('shows favorite column when at least one dashboard is favorited', async () => {
// Mock favorite status API to return mixed favorites
fetchMock.get(
dashboardFavoriteStatusEndpoint,
{
result: [
{ id: 0, value: true }, // This dashboard is favorited
{ id: 1, value: false },
{ id: 2, value: false },
],
},
{ overwriteRoutes: true },
);
renderDashboardList();
// Wait for component to load
await waitFor(() => {
expect(screen.getByText('Dashboards')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Dashboard 0')).toBeInTheDocument();
});
// Favorite column should be visible - check that favorite stars are present
await waitFor(
() => {
const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
expect(favoriteStars.length).toBeGreaterThan(0);
},
{ timeout: 10000 },
);
});
test('shows favorite column when all dashboards are favorited', async () => {
// Mock favorite status API to return all true
fetchMock.get(
dashboardFavoriteStatusEndpoint,
{
result: [
{ id: 0, value: true },
{ id: 1, value: true },
{ id: 2, value: true },
],
},
{ overwriteRoutes: true },
);
renderDashboardList();
// Wait for component to load
await waitFor(() => {
expect(screen.getByText('Dashboards')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Dashboard 0')).toBeInTheDocument();
});
// Favorite column should be visible
await waitFor(
() => {
const favoriteStars = screen.getAllByTestId('fave-unfave-icon');
expect(favoriteStars.length).toBeGreaterThan(0);
},
{ timeout: 10000 },
);
});
test('hides favorite column when user is not logged in', async () => {
// Mock favorite status API
fetchMock.get(
dashboardFavoriteStatusEndpoint,
{
result: [
{ id: 0, value: true },
{ id: 1, value: false },
],
},
{ overwriteRoutes: true },
);
// Render without userId (user not logged in)
const noUser = {
userId: 0, // Use 0 instead of null to satisfy type requirements
firstName: '',
lastName: '',
};
renderDashboardList({}, noUser);
// Wait for component to load
await waitFor(() => {
expect(screen.getByText('Dashboards')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Dashboard 0')).toBeInTheDocument();
});
// Favorite column should be hidden when user is not logged in
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
});
test('hides favorite column when dashboard list is empty', async () => {
// Mock empty dashboards response
fetchMock.get(
dashboardsEndpoint,
{
result: [],
dashboard_count: 0,
},
{ overwriteRoutes: true },
);
// Mock empty favorite status
fetchMock.get(
dashboardFavoriteStatusEndpoint,
{
result: [],
},
{ overwriteRoutes: true },
);
renderDashboardList();
// Wait for component to load
await waitFor(() => {
expect(screen.getByText('Dashboards')).toBeInTheDocument();
});
// No favorite stars should be present when there are no dashboards
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
});
test('handles partial favorite status loading gracefully', async () => {
// Mock partial favorite status (fewer items than dashboards)
fetchMock.get(
dashboardFavoriteStatusEndpoint,
{
result: [
{ id: 0, value: false },
// Missing status for dashboards 1 and 2
],
},
{ overwriteRoutes: true },
);
renderDashboardList();
// Wait for component to load
await waitFor(() => {
expect(screen.getByText('Dashboards')).toBeInTheDocument();
});
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Dashboard 0')).toBeInTheDocument();
});
// Should hide column when favorite status is incomplete
const favoriteStars = screen.queryAllByTestId('fave-unfave-icon');
expect(favoriteStars).toHaveLength(0);
});
});

View File

@@ -300,6 +300,14 @@ function DashboardList(props: DashboardListProps) {
);
}
const hasFavoritesOnPage = useMemo(
() =>
dashboards.length > 0 &&
Object.keys(favoriteStatus).length === dashboards.length &&
Object.values(favoriteStatus).some(status => status === true),
[dashboards.length, favoriteStatus],
);
const columns = useMemo(
() => [
{
@@ -319,7 +327,7 @@ function DashboardList(props: DashboardListProps) {
id: 'id',
disableSortBy: true,
size: 'xs',
hidden: !user?.userId,
hidden: !user?.userId || !hasFavoritesOnPage,
},
{
Cell: ({