mirror of https://github.com/grafana/grafana.git
504 lines
18 KiB
TypeScript
504 lines
18 KiB
TypeScript
import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
|
import { PluginExtensionTypes, IconName } from '@grafana/data';
|
|
import { setPluginLinksHook, config, getDataSourceSrv } from '@grafana/runtime';
|
|
import { contextSrv } from 'app/core/services/context_srv';
|
|
|
|
import { getMockDataSource } from '../mocks/dataSourcesMocks';
|
|
|
|
import { EditDataSourceActions } from './EditDataSourceActions';
|
|
|
|
// Mock dependencies
|
|
jest.mock('app/core/services/context_srv');
|
|
jest.mock('../utils', () => ({
|
|
constructDataSourceExploreUrl: jest.fn(
|
|
() => '/explore?left=%7B%22datasource%22:%22Test%20Prometheus%22,%22context%22:%22explore%22%7D'
|
|
),
|
|
}));
|
|
|
|
// Mock @grafana/runtime
|
|
jest.mock('@grafana/runtime', () => ({
|
|
...jest.requireActual('@grafana/runtime'),
|
|
config: {
|
|
featureToggles: {
|
|
favoriteDatasources: false,
|
|
},
|
|
},
|
|
getDataSourceSrv: jest.fn(),
|
|
useFavoriteDatasources: jest.fn(),
|
|
}));
|
|
|
|
// Set default plugin links hook
|
|
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
|
|
|
// Mock contextSrv
|
|
const mockContextSrv = jest.mocked(contextSrv);
|
|
|
|
// Mock getDataSourceSrv and favorite hooks
|
|
const mockGetDataSourceSrv = jest.mocked(getDataSourceSrv);
|
|
const mockUseFavoriteDatasources = jest.mocked(require('@grafana/runtime').useFavoriteDatasources);
|
|
|
|
// Create mock datasource instance
|
|
const mockDataSourceInstance = {
|
|
uid: 'test-uid',
|
|
name: 'Test Prometheus',
|
|
type: 'prometheus',
|
|
meta: {
|
|
name: 'Prometheus',
|
|
builtIn: false,
|
|
},
|
|
};
|
|
|
|
// Mock favorite datasources hook return value
|
|
const mockFavoriteHook = {
|
|
enabled: true,
|
|
favoriteDatasources: [],
|
|
initialFavoriteDataSources: [],
|
|
isFavoriteDatasource: jest.fn(),
|
|
addFavoriteDatasource: jest.fn(),
|
|
removeFavoriteDatasource: jest.fn(),
|
|
};
|
|
|
|
// Helper function to create mock plugin link extensions with all required properties
|
|
const createMockPluginLink = (
|
|
overrides: Partial<{
|
|
id: string;
|
|
path: string;
|
|
onClick: jest.Mock;
|
|
title: string;
|
|
description: string;
|
|
pluginId: string;
|
|
icon?: IconName;
|
|
}> = {}
|
|
) => ({
|
|
id: 'test-link',
|
|
type: PluginExtensionTypes.link as const,
|
|
title: 'Test Action',
|
|
description: 'Test action description',
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
path: '/test-action',
|
|
onClick: jest.fn(),
|
|
...overrides,
|
|
});
|
|
|
|
const mockDataSource = getMockDataSource({
|
|
uid: 'test-uid',
|
|
type: 'prometheus',
|
|
name: 'Test Prometheus',
|
|
typeName: 'Prometheus',
|
|
});
|
|
|
|
// Mock useDataSource hook
|
|
jest.mock('../state/hooks', () => ({
|
|
useDataSource: (uid: string) => (uid === 'not-found' ? {} : mockDataSource),
|
|
}));
|
|
|
|
describe('EditDataSourceActions', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
// Reset plugin links hook to default
|
|
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
|
// Default contextSrv mock - user has explore rights
|
|
mockContextSrv.hasAccessToExplore.mockReturnValue(true);
|
|
|
|
// Setup default mocks for favorite functionality
|
|
mockGetDataSourceSrv.mockReturnValue({
|
|
getInstanceSettings: jest.fn().mockReturnValue(mockDataSourceInstance),
|
|
get: jest.fn(),
|
|
getList: jest.fn(),
|
|
reload: jest.fn(),
|
|
registerRuntimeDataSource: jest.fn(),
|
|
});
|
|
|
|
// Reset favorite hook mocks
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
|
mockFavoriteHook.addFavoriteDatasource.mockClear();
|
|
mockFavoriteHook.removeFavoriteDatasource.mockClear();
|
|
|
|
// Default: feature toggle disabled, so no favorite hook
|
|
mockUseFavoriteDatasources.mockReturnValue({ ...mockFavoriteHook, enabled: false });
|
|
config.featureToggles.favoriteDatasources = false;
|
|
});
|
|
|
|
describe('Core Actions', () => {
|
|
it('should render core Grafana actions when user has explore rights', async () => {
|
|
mockContextSrv.hasAccessToExplore.mockReturnValue(true);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Core actions should be rendered as separate buttons
|
|
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
|
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render explore action when user lacks explore rights', () => {
|
|
mockContextSrv.hasAccessToExplore.mockReturnValue(false);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Should render just the "Build a dashboard" button
|
|
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
|
// Should not render explore action
|
|
expect(screen.queryByText('Explore data')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should have correct href for explore action when no extensions', () => {
|
|
// No extensions, so explore should be a direct link
|
|
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
const exploreLink = screen.getByText('Explore data').closest('a');
|
|
// The explore URL uses the datasource name, not uid, and includes context
|
|
expect(exploreLink).toHaveAttribute(
|
|
'href',
|
|
'/explore?left=%7B%22datasource%22:%22Test%20Prometheus%22,%22context%22:%22explore%22%7D'
|
|
);
|
|
});
|
|
|
|
it('should have correct href for explore action when extensions are present', () => {
|
|
// Extensions present, so explore should be a dropdown with "Open in Explore View"
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
id: 'test-extension',
|
|
title: 'Test Extension',
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Click to open dropdown
|
|
const exploreButton = screen.getByText('Explore data');
|
|
fireEvent.click(exploreButton);
|
|
|
|
const exploreViewLink = screen.getByText('Open in Explore View').closest('a');
|
|
// The explore URL uses the datasource name, not uid, and includes context
|
|
expect(exploreViewLink).toHaveAttribute(
|
|
'href',
|
|
'/explore?left=%7B%22datasource%22:%22Test%20Prometheus%22,%22context%22:%22explore%22%7D'
|
|
);
|
|
});
|
|
|
|
it('should have correct href for build dashboard action', () => {
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
const dashboardLink = screen.getByText('Build a dashboard').closest('a');
|
|
expect(dashboardLink).toHaveAttribute('href', 'dashboard/new-with-ds/test-uid');
|
|
});
|
|
});
|
|
|
|
describe('Plugin Extension Actions', () => {
|
|
it('should render plugin extension links from allowed plugins', () => {
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
id: 'loki-explore',
|
|
title: 'Explore Logs',
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
path: '/a/grafana-lokiexplore-app',
|
|
}),
|
|
createMockPluginLink({
|
|
id: 'traces-explore',
|
|
title: 'Explore Traces',
|
|
pluginId: 'grafana-exploretraces-app',
|
|
path: '/a/grafana-exploretraces-app',
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Click the Explore data dropdown to open the menu
|
|
const exploreButton = screen.getByText('Explore data');
|
|
fireEvent.click(exploreButton);
|
|
|
|
// Should have "Open in Explore View" as first item
|
|
expect(screen.getByText('Open in Explore View')).toBeInTheDocument();
|
|
// Should have extension links
|
|
expect(screen.getByText('Explore Logs')).toBeInTheDocument();
|
|
expect(screen.getByText('Explore Traces')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should filter out links from non-allowed plugins', () => {
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
id: 'allowed-plugin',
|
|
title: 'Allowed Action',
|
|
pluginId: 'grafana-lokiexplore-app', // Allowed
|
|
}),
|
|
createMockPluginLink({
|
|
id: 'disallowed-plugin',
|
|
title: 'Disallowed Action',
|
|
pluginId: 'some-random-plugin', // Not allowed
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Click the Explore data dropdown to open the menu
|
|
const exploreButton = screen.getByText('Explore data');
|
|
fireEvent.click(exploreButton);
|
|
|
|
expect(screen.getByText('Allowed Action')).toBeInTheDocument();
|
|
expect(screen.queryByText('Disallowed Action')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should call usePluginLinks with correct parameters', () => {
|
|
// This test verifies the component calls usePluginLinks correctly
|
|
// We can't easily test the exact parameters without more complex mocking
|
|
// but we can verify the component renders without errors when links are provided
|
|
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
|
|
|
expect(() => {
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should handle plugin link onClick events', () => {
|
|
const mockOnClick = jest.fn();
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
id: 'clickable-action',
|
|
title: 'Clickable Action',
|
|
onClick: mockOnClick,
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Click the Explore data dropdown to open the menu
|
|
const exploreButton = screen.getByText('Explore data');
|
|
fireEvent.click(exploreButton);
|
|
|
|
const actionButton = screen.getByText('Clickable Action');
|
|
fireEvent.click(actionButton);
|
|
|
|
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should render extension links with correct attributes', () => {
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
id: 'test-action',
|
|
title: 'Test Action',
|
|
description: 'Test description',
|
|
path: '/test-path',
|
|
icon: 'external-link-alt',
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Click the Explore data dropdown to open the menu
|
|
const exploreButton = screen.getByText('Explore data');
|
|
fireEvent.click(exploreButton);
|
|
|
|
const actionButton = screen.getByText('Test Action');
|
|
const linkElement = actionButton.closest('a');
|
|
|
|
expect(linkElement).toHaveAttribute('href', '/test-path');
|
|
// The description is passed as tooltip, which may not appear as a title attribute
|
|
// This is handled by the LinkButton component internally
|
|
});
|
|
|
|
it('should not render extensions when isLoading is true', () => {
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
title: 'Should Not Appear',
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: true }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// When isLoading is true, Explore data should be a regular link, not a dropdown
|
|
const exploreElement = screen.getByText('Explore data');
|
|
const exploreLink = exploreElement.closest('a');
|
|
expect(exploreLink).toBeInTheDocument();
|
|
|
|
expect(screen.queryByText('Should Not Appear')).not.toBeInTheDocument();
|
|
// Core actions should still be there
|
|
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
|
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should handle empty extension links gracefully', () => {
|
|
setPluginLinksHook(() => ({ links: [], isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// When there are no extension links, Explore data should be a regular link
|
|
const exploreElement = screen.getByText('Explore data');
|
|
const exploreLink = exploreElement.closest('a');
|
|
expect(exploreLink).toBeInTheDocument();
|
|
|
|
// Should render core actions without errors
|
|
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
|
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render Explore dropdown when there are plugin links', () => {
|
|
const mockLinks = [
|
|
createMockPluginLink({
|
|
id: 'test-extension',
|
|
title: 'Test Extension',
|
|
pluginId: 'grafana-lokiexplore-app',
|
|
}),
|
|
];
|
|
|
|
setPluginLinksHook(() => ({ links: mockLinks, isLoading: false }));
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// When there are extension links, Explore data should be a dropdown button
|
|
const exploreElement = screen.getByText('Explore data');
|
|
const exploreButton = exploreElement.closest('button');
|
|
expect(exploreButton).toBeInTheDocument();
|
|
|
|
// Core actions should still be there
|
|
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
|
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('DataSource Not Found', () => {
|
|
it('should not render actions when data source is not found', () => {
|
|
render(<EditDataSourceActions uid="not-found" />);
|
|
expect(screen.queryByText('Explore data')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Build a dashboard')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Favorite Actions', () => {
|
|
it('should not render favorite button when feature toggle is disabled', () => {
|
|
config.featureToggles.favoriteDatasources = false;
|
|
mockUseFavoriteDatasources.mockReturnValue({ ...mockFavoriteHook, enabled: false });
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Should not find any favorite button
|
|
expect(screen.queryByTestId('favorite-button')).not.toBeInTheDocument();
|
|
// Core actions should still be rendered
|
|
expect(screen.getByText('Explore data')).toBeInTheDocument();
|
|
expect(screen.getByText('Build a dashboard')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render favorite button for built-in datasources', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
|
|
|
// Mock built-in datasource
|
|
const builtInDataSource = { ...mockDataSourceInstance, meta: { ...mockDataSourceInstance.meta, builtIn: true } };
|
|
mockGetDataSourceSrv.mockReturnValue({
|
|
getInstanceSettings: jest.fn().mockReturnValue(builtInDataSource),
|
|
get: jest.fn(),
|
|
getList: jest.fn(),
|
|
reload: jest.fn(),
|
|
registerRuntimeDataSource: jest.fn(),
|
|
});
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Should not find any favorite button for built-in datasources
|
|
expect(screen.queryByTestId('favorite-button')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render favorite button when feature toggle is enabled and datasource is not built-in', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Should find star icon for non-favorite datasource
|
|
const favoriteButton = screen.getByTestId('favorite-button');
|
|
expect(favoriteButton).toBeInTheDocument();
|
|
|
|
// Should have correct aria-label for non-favorite datasource
|
|
expect(favoriteButton).toHaveAttribute('aria-label', 'Add to favorites');
|
|
});
|
|
|
|
it('should show favorite icon when datasource is favorited', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(true);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
// Should find favorite button for favorited datasource
|
|
const favoriteButton = screen.getByTestId('favorite-button');
|
|
expect(favoriteButton).toBeInTheDocument();
|
|
|
|
// Should have correct aria-label for favorited datasource
|
|
expect(favoriteButton).toHaveAttribute('aria-label', 'Remove from favorites');
|
|
});
|
|
|
|
it('should add datasource to favorites when star button is clicked', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
const favoriteButton = screen.getByTestId('favorite-button');
|
|
fireEvent.click(favoriteButton);
|
|
|
|
expect(mockFavoriteHook.addFavoriteDatasource).toHaveBeenCalledTimes(1);
|
|
expect(mockFavoriteHook.addFavoriteDatasource).toHaveBeenCalledWith(mockDataSourceInstance);
|
|
expect(mockFavoriteHook.removeFavoriteDatasource).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should remove datasource from favorites when favorite button is clicked', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(true);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
const favoriteButton = screen.getByTestId('favorite-button');
|
|
fireEvent.click(favoriteButton);
|
|
|
|
expect(mockFavoriteHook.removeFavoriteDatasource).toHaveBeenCalledTimes(1);
|
|
expect(mockFavoriteHook.removeFavoriteDatasource).toHaveBeenCalledWith(mockDataSourceInstance);
|
|
expect(mockFavoriteHook.addFavoriteDatasource).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call isFavoriteDatasource with correct uid', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue(mockFavoriteHook);
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
expect(mockFavoriteHook.isFavoriteDatasource).toHaveBeenCalledWith('test-uid');
|
|
});
|
|
|
|
it('should disable favorite button when isLoading is true', () => {
|
|
config.featureToggles.favoriteDatasources = true;
|
|
mockUseFavoriteDatasources.mockReturnValue({
|
|
...mockFavoriteHook,
|
|
isLoading: true,
|
|
});
|
|
mockFavoriteHook.isFavoriteDatasource.mockReturnValue(false);
|
|
|
|
render(<EditDataSourceActions uid="test-uid" />);
|
|
|
|
const favoriteButton = screen.getByTestId('favorite-button');
|
|
expect(favoriteButton).toBeDisabled();
|
|
});
|
|
});
|
|
});
|