import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { noop } from 'lodash';
import React, { PropsWithChildren } from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { selectors } from '@grafana/e2e-selectors';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions, mockDataSource } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { setupDataSources } from '../../testSetup/datasources';
import { DataSourceType } from '../../utils/datasource';
import ContactPoints, { ContactPoint } from './ContactPoints';
import setupGrafanaManagedServer from './__mocks__/grafanaManagedServer';
import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimirFlavoredServer';
import setupVanillaAlertmanagerFlavoredServer, {
VANILLA_ALERTMANAGER_DATASOURCE_UID,
} from './__mocks__/vanillaAlertmanagerServer';
/**
* There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them.
*
* Use MSW to mock API responses, you can copy the JSON results from the network panel and use them in a __mocks__ folder.
*
* 1. Make sure we have "presentation" components we can test without mocking data,
* test these if they have some logic in them (hiding / showing things) and sad paths.
*
* 2. For testing the "container" components, check if data fetching is working as intended (you can use loading state)
* and check if we're not in an error state (although you can test for that too for sad path).
*
* 3. Write tests for the hooks we call in the "container" components
* if those have any logic or data structure transformations in them.
*
* ⚠️ Always set up the MSW server only once – MWS does not support multiple calls to setupServer(); and causes all sorts of weird issues
*/
const server = setupMswServer();
describe('contact points', () => {
describe('Contact points with Grafana managed alertmanager', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
]);
setupGrafanaManagedServer(server);
});
it('should show / hide loading states, have all actions enabled', async () => {
render(
,
{ wrapper: TestProvider }
);
await waitFor(async () => {
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitForElementToBeRemoved(screen.getByText('Loading...'));
expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument();
});
expect(screen.getByText('grafana-default-email')).toBeInTheDocument();
expect(screen.getAllByTestId('contact-point')).toHaveLength(5);
// check for available actions – our mock 4 contact points, 1 of them is provisioned
expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'export all' })).toBeInTheDocument();
// 2 of them are unused by routes in the mock response
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(3);
const viewProvisioned = screen.getByRole('link', { name: 'view-action' });
expect(viewProvisioned).toBeInTheDocument();
expect(viewProvisioned).not.toBeDisabled();
const editButtons = screen.getAllByRole('link', { name: 'edit-action' });
expect(editButtons).toHaveLength(4);
editButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
const moreActionsButtons = screen.getAllByRole('button', { name: 'more-actions' });
expect(moreActionsButtons).toHaveLength(5);
moreActionsButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
});
it('should disable certain actions if the user has no write permissions', async () => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
render(
,
{ wrapper: TestProvider }
);
// wait for loading to be done
await waitFor(async () => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// should disable create contact point
expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true');
// there should be no edit buttons
expect(screen.queryAllByRole('link', { name: 'edit-action' })).toHaveLength(0);
// there should be view buttons though
const viewButtons = screen.getAllByRole('link', { name: 'view-action' });
expect(viewButtons).toHaveLength(5);
// delete should be disabled in the "more" actions
const moreButtons = screen.queryAllByRole('button', { name: 'more-actions' });
expect(moreButtons).toHaveLength(5);
// check if all of the delete buttons are disabled
for await (const button of moreButtons) {
await userEvent.click(button);
const deleteButton = await screen.queryByRole('menuitem', { name: 'delete' });
expect(deleteButton).toBeDisabled();
// click outside the menu to close it otherwise we can't interact with the rest of the page
await userEvent.click(document.body);
}
// check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' });
await userEvent.click(notificationTemplatesTab);
expect(screen.getByRole('link', { name: 'Add notification template' })).toHaveAttribute('aria-disabled', 'true');
});
it('should call delete when clicked and not disabled', async () => {
const onDelete = jest.fn();
render(, {
wrapper,
});
const moreActions = screen.getByRole('button', { name: 'more-actions' });
await userEvent.click(moreActions);
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
await userEvent.click(deleteButton);
expect(onDelete).toHaveBeenCalledWith('my-contact-point');
});
it('should disable edit button', async () => {
render(, {
wrapper,
});
const moreActions = screen.getByRole('button', { name: 'more-actions' });
expect(moreActions).not.toBeDisabled();
const editAction = screen.getByTestId('edit-action');
expect(editAction).toHaveAttribute('aria-disabled', 'true');
});
it('should disable buttons when provisioned', async () => {
render(, {
wrapper,
});
expect(screen.getByText(/provisioned/i)).toBeInTheDocument();
const editAction = screen.queryByTestId('edit-action');
expect(editAction).not.toBeInTheDocument();
const viewAction = screen.getByRole('link', { name: /view/i });
expect(viewAction).toBeInTheDocument();
const moreActions = screen.getByRole('button', { name: 'more-actions' });
expect(moreActions).not.toBeDisabled();
await userEvent.click(moreActions);
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
expect(deleteButton).toBeDisabled();
});
it('should disable delete when contact point is linked to at least one notification policy', async () => {
render(
,
{
wrapper,
}
);
expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument();
const moreActions = screen.getByRole('button', { name: 'more-actions' });
await userEvent.click(moreActions);
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
expect(deleteButton).toBeDisabled();
});
it('should be able to search', async () => {
render(
,
{ wrapper: TestProvider }
);
const searchInput = screen.getByRole('textbox', { name: 'search contact points' });
await userEvent.type(searchInput, 'slack');
expect(searchInput).toHaveValue('slack');
await waitFor(() => {
expect(screen.getByText('Slack with multiple channels')).toBeInTheDocument();
expect(screen.getAllByTestId('contact-point')).toHaveLength(1);
});
// ⚠️ for some reason, the query params are preserved for all tests so don't forget to clear the input
const clearButton = screen.getByRole('button', { name: 'clear' });
await userEvent.click(clearButton);
expect(searchInput).toHaveValue('');
});
});
describe('Contact points with Mimir-flavored alertmanager', () => {
beforeEach(() => {
setupMimirFlavoredServer(server);
});
beforeAll(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
setupDataSources(
mockDataSource({
type: DataSourceType.Alertmanager,
name: MIMIR_DATASOURCE_UID,
uid: MIMIR_DATASOURCE_UID,
})
);
});
it('should show / hide loading states, have the right actions enabled', async () => {
render(
);
await waitFor(async () => {
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitForElementToBeRemoved(screen.getByText('Loading...'));
expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument();
});
expect(screen.getByText('mixed')).toBeInTheDocument();
expect(screen.getByText('some webhook')).toBeInTheDocument();
expect(screen.getAllByTestId('contact-point')).toHaveLength(2);
// check for available actions – export should be disabled
expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'export all' })).not.toBeInTheDocument();
// 1 of them is used by a route in the mock response
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(1);
const editButtons = screen.getAllByRole('link', { name: 'edit-action' });
expect(editButtons).toHaveLength(2);
editButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
const moreActionsButtons = screen.getAllByRole('button', { name: 'more-actions' });
expect(moreActionsButtons).toHaveLength(2);
moreActionsButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
});
});
describe('Vanilla Alertmanager ', () => {
beforeEach(() => {
setupVanillaAlertmanagerFlavoredServer(server);
});
beforeAll(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
const alertManager = mockDataSource({
name: VANILLA_ALERTMANAGER_DATASOURCE_UID,
uid: VANILLA_ALERTMANAGER_DATASOURCE_UID,
type: DataSourceType.Alertmanager,
jsonData: {
implementation: AlertManagerImplementation.prometheus,
handleGrafanaManagedAlerts: true,
},
});
setupDataSources(alertManager);
});
it("should not allow any editing because it's not supported", async () => {
render(
);
await waitFor(async () => {
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitForElementToBeRemoved(screen.getByText('Loading...'));
expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument();
});
expect(screen.queryByRole('link', { name: 'add contact point' })).not.toBeInTheDocument();
const viewProvisioned = screen.getByRole('link', { name: 'view-action' });
expect(viewProvisioned).toBeInTheDocument();
expect(viewProvisioned).not.toBeDisabled();
// check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' });
await userEvent.click(notificationTemplatesTab);
expect(screen.queryByRole('link', { name: 'Add notification template' })).not.toBeInTheDocument();
});
});
});
const wrapper = ({ children }: PropsWithChildren) => (
{children}
);