mirror of https://github.com/grafana/grafana.git
303 lines
8.7 KiB
TypeScript
303 lines
8.7 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
|
|
import { AppEvents } from '@grafana/data';
|
|
import { getAppEvents } from '@grafana/runtime';
|
|
import { useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
|
|
|
|
import { useProvisionedDashboardData, ProvisionedDashboardData } from '../saving/provisioned/hooks';
|
|
import { DashboardScene } from '../scene/DashboardScene';
|
|
|
|
import { DeleteProvisionedDashboardDrawer, Props } from './DeleteProvisionedDashboardDrawer';
|
|
|
|
// Mock the hooks and dependencies
|
|
jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
|
|
useDeleteRepositoryFilesWithPathMutation: jest.fn(),
|
|
provisioningAPIv0alpha1: {
|
|
endpoints: {
|
|
listRepository: {
|
|
select: jest.fn(() => () => ({ data: { items: [] } })),
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
jest.mock('../saving/provisioned/hooks');
|
|
jest.mock('@grafana/runtime', () => ({
|
|
...jest.requireActual('@grafana/runtime'),
|
|
getAppEvents: jest.fn(),
|
|
}));
|
|
jest.mock('react-router-dom-v5-compat', () => ({
|
|
...jest.requireActual('react-router-dom-v5-compat'),
|
|
useNavigate: () => mockNavigate,
|
|
}));
|
|
// Add this variable declaration near your other mock variables
|
|
const mockNavigate = jest.fn();
|
|
|
|
// Mock shared form components
|
|
jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({
|
|
ResourceEditFormSharedFields: ({ disabled }: { disabled: boolean }) => (
|
|
<textarea data-testid="shared-fields" disabled={disabled} />
|
|
),
|
|
}));
|
|
|
|
const mockDeleteRepoFile = jest.fn();
|
|
const mockPublish = jest.fn();
|
|
const mockUseDeleteRepositoryFiles = useDeleteRepositoryFilesWithPathMutation as jest.MockedFunction<
|
|
typeof useDeleteRepositoryFilesWithPathMutation
|
|
>;
|
|
const mockUseProvisionedDashboardData = useProvisionedDashboardData as jest.MockedFunction<
|
|
typeof useProvisionedDashboardData
|
|
>;
|
|
|
|
// Mock request state helper
|
|
type MockRequestState = {
|
|
isLoading: boolean;
|
|
isSuccess: boolean;
|
|
isError: boolean;
|
|
error?: Error;
|
|
reset: () => void;
|
|
};
|
|
|
|
const createMockRequestState = (overrides: Partial<MockRequestState> = {}): MockRequestState => ({
|
|
isLoading: false,
|
|
isSuccess: false,
|
|
isError: false,
|
|
reset: jest.fn(),
|
|
...overrides,
|
|
});
|
|
|
|
interface SetupOptions extends Partial<Props> {
|
|
provisionedData?: Partial<ProvisionedDashboardData>;
|
|
requestState?: Partial<MockRequestState>;
|
|
}
|
|
|
|
function setup(options: SetupOptions = {}) {
|
|
const { provisionedData = {}, requestState = {}, ...props } = options;
|
|
const user = userEvent.setup();
|
|
|
|
const defaultDashboard = new DashboardScene({
|
|
title: 'Test Dashboard',
|
|
uid: 'test-uid',
|
|
meta: { slug: 'test-slug' },
|
|
});
|
|
|
|
const defaultProvisionedData: ProvisionedDashboardData = {
|
|
isReady: true,
|
|
isLoading: false,
|
|
setIsLoading: jest.fn(),
|
|
defaultValues: {
|
|
repo: 'test-repo',
|
|
ref: 'main',
|
|
workflow: 'branch' as const,
|
|
path: 'dashboards/test.json',
|
|
comment: '',
|
|
title: 'Test Dashboard',
|
|
description: 'Test Description',
|
|
folder: {
|
|
uid: 'test-folder',
|
|
title: 'Test Folder',
|
|
},
|
|
},
|
|
repository: {
|
|
name: 'test-repo',
|
|
target: 'folder' as const,
|
|
title: 'Test Repository',
|
|
type: 'github' as const,
|
|
workflows: ['branch', 'write'] as Array<'branch' | 'write'>,
|
|
},
|
|
loadedFromRef: 'main',
|
|
readOnly: false,
|
|
isGitHub: true,
|
|
workflowOptions: [
|
|
{ label: 'Branch', value: 'branch' },
|
|
{ label: 'Write', value: 'write' },
|
|
],
|
|
isNew: false,
|
|
...provisionedData,
|
|
};
|
|
|
|
const defaultProps: Props = {
|
|
dashboard: defaultDashboard,
|
|
onDismiss: jest.fn(),
|
|
...props,
|
|
};
|
|
|
|
// Set up mocks with the merged data
|
|
mockUseProvisionedDashboardData.mockReturnValue(defaultProvisionedData);
|
|
mockUseDeleteRepositoryFiles.mockReturnValue([
|
|
mockDeleteRepoFile,
|
|
createMockRequestState(requestState) as ReturnType<typeof useDeleteRepositoryFilesWithPathMutation>[1],
|
|
]);
|
|
|
|
return {
|
|
user,
|
|
props: defaultProps,
|
|
defaultProvisionedData,
|
|
...render(<DeleteProvisionedDashboardDrawer {...defaultProps} />),
|
|
};
|
|
}
|
|
|
|
describe('DeleteProvisionedDashboardDrawer', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Mock getAppEvents
|
|
(getAppEvents as jest.Mock).mockReturnValue({
|
|
publish: mockPublish,
|
|
});
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render the drawer with correct title and subtitle', () => {
|
|
setup();
|
|
|
|
expect(screen.getByRole('heading', { name: 'Delete Provisioned Dashboard' })).toBeInTheDocument();
|
|
expect(screen.getByText('Test Dashboard')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should return null when defaultValues are not provided', () => {
|
|
setup({
|
|
provisionedData: {
|
|
defaultValues: null,
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render shared form fields correctly', () => {
|
|
setup();
|
|
|
|
expect(screen.getByTestId('shared-fields')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render delete and cancel buttons', () => {
|
|
setup();
|
|
|
|
expect(screen.getByRole('button', { name: /delete dashboard/i })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Form Submission', () => {
|
|
it('should successfully delete dashboard with branch workflow', async () => {
|
|
const { user } = setup();
|
|
|
|
const deleteButton = screen.getByRole('button', { name: /delete dashboard/i });
|
|
await user.click(deleteButton);
|
|
|
|
await waitFor(() => {
|
|
expect(mockDeleteRepoFile).toHaveBeenCalledWith({
|
|
name: 'test-repo',
|
|
path: 'dashboards/test.json',
|
|
ref: 'main',
|
|
message: 'Delete dashboard: Test Dashboard',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle missing repository name', async () => {
|
|
const { user } = setup({
|
|
provisionedData: {
|
|
defaultValues: {
|
|
repo: '',
|
|
ref: 'main',
|
|
workflow: 'branch' as const,
|
|
path: 'dashboards/test.json',
|
|
comment: '',
|
|
title: 'Test Dashboard',
|
|
description: 'Test Description',
|
|
folder: { uid: 'test-folder', title: 'Test Folder' },
|
|
},
|
|
repository: undefined,
|
|
},
|
|
});
|
|
|
|
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
|
|
const deleteButton = screen.getByRole('button', { name: /delete dashboard/i });
|
|
await user.click(deleteButton);
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith('Missing required fields for deletion:', {
|
|
repo: '',
|
|
path: 'dashboards/test.json',
|
|
});
|
|
expect(mockDeleteRepoFile).not.toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should handle missing path', async () => {
|
|
const { user } = setup({
|
|
provisionedData: {
|
|
defaultValues: {
|
|
repo: 'test-repo',
|
|
ref: 'main',
|
|
workflow: 'branch' as const,
|
|
path: '',
|
|
comment: '',
|
|
title: 'Test Dashboard',
|
|
description: 'Test Description',
|
|
folder: { uid: 'test-folder', title: 'Test Folder' },
|
|
},
|
|
},
|
|
});
|
|
|
|
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
|
|
const deleteButton = screen.getByRole('button', { name: /delete dashboard/i });
|
|
await user.click(deleteButton);
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith('Missing required fields for deletion:', {
|
|
repo: 'test-repo',
|
|
path: '',
|
|
});
|
|
expect(mockDeleteRepoFile).not.toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle error state', async () => {
|
|
const error = new Error('API Error');
|
|
setup({
|
|
requestState: {
|
|
isError: true,
|
|
error,
|
|
},
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockPublish).toHaveBeenCalledWith({
|
|
type: AppEvents.alertError.name,
|
|
payload: ['Failed to delete dashboard', error],
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Loading State', () => {
|
|
it('should show loading state when deletion is in progress', () => {
|
|
setup({
|
|
requestState: {
|
|
isLoading: true,
|
|
},
|
|
});
|
|
|
|
const deleteButton = screen.getByRole('button', { name: /deleting/i });
|
|
expect(deleteButton).toBeDisabled();
|
|
expect(deleteButton).toHaveTextContent('Deleting...');
|
|
});
|
|
});
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onDismiss when cancel button is clicked', async () => {
|
|
const { user, props } = setup();
|
|
|
|
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
await user.click(cancelButton);
|
|
|
|
expect(props.onDismiss).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|