add tests & code comments
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'test/test-utils';
|
||||
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { GrafanaGroupUpdatedResponse } from '../api/alertRuleModel';
|
||||
import { ContactPoint, RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
|
||||
import { AlertRuleDrawerForm, AlertRuleDrawerFormProps } from './AlertRuleDrawerForm';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
// Mock the hooks
|
||||
const mockExecute = jest.fn();
|
||||
jest.mock('../hooks/ruleGroup/useUpsertRuleFromRuleGroup', () => ({
|
||||
useAddRuleToRuleGroup: () => [{ execute: mockExecute }],
|
||||
}));
|
||||
|
||||
// Mock notification hooks
|
||||
const mockError = jest.fn();
|
||||
const mockSuccess = jest.fn();
|
||||
jest.mock('app/core/copy/appNotification', () => ({
|
||||
useAppNotification: () => ({
|
||||
error: mockError,
|
||||
success: mockSuccess,
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultProps: AlertRuleDrawerFormProps = {
|
||||
isOpen: true,
|
||||
onClose: jest.fn(),
|
||||
};
|
||||
|
||||
const renderDrawer = (props: Partial<AlertRuleDrawerFormProps> = {}) => {
|
||||
return render(<AlertRuleDrawerForm {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
describe('AlertRuleDrawerForm', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
AccessControlAction.AlertingRuleDelete,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should not render when isOpen is false', () => {
|
||||
renderDrawer({ isOpen: false });
|
||||
expect(screen.queryByRole('button', { name: /Create/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Continue in Alerting" button when callback is provided', () => {
|
||||
renderDrawer({ onContinueInAlerting: jest.fn() });
|
||||
expect(screen.getByRole('button', { name: /Continue in Alerting/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cancel button', () => {
|
||||
it('should call onClose when Cancel is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = jest.fn();
|
||||
renderDrawer({ onClose });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Cancel/i }));
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should reset form when Cancel is clicked with prefill', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = jest.fn();
|
||||
const prefill: Partial<RuleFormValues> = {
|
||||
name: 'Prefilled Rule Name',
|
||||
};
|
||||
const { rerender } = renderDrawer({ onClose, prefill });
|
||||
|
||||
// Verify prefilled value is present
|
||||
const nameInput = screen.getByLabelText(/Name/i);
|
||||
expect(nameInput).toHaveValue('Prefilled Rule Name');
|
||||
|
||||
// Modify the field
|
||||
await user.clear(nameInput);
|
||||
await user.type(nameInput, 'Changed Name');
|
||||
expect(nameInput).toHaveValue('Changed Name');
|
||||
|
||||
// Click cancel - this triggers reset to prefill
|
||||
await user.click(screen.getByRole('button', { name: /Cancel/i }));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
|
||||
// Reopen to verify reset happened
|
||||
rerender(<AlertRuleDrawerForm {...defaultProps} onClose={onClose} prefill={prefill} isOpen={true} />);
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('Prefilled Rule Name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Continue in Alerting button', () => {
|
||||
it('should call onContinueInAlerting with current form values', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onContinueInAlerting = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
renderDrawer({ onContinueInAlerting, onClose });
|
||||
|
||||
// Fill in a field
|
||||
const nameInput = screen.getByLabelText(/Name/i);
|
||||
await user.type(nameInput, 'Test Rule');
|
||||
|
||||
// Click Continue in Alerting
|
||||
await user.click(screen.getByRole('button', { name: /Continue in Alerting/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onContinueInAlerting).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Test Rule',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize contact points when calling onContinueInAlerting', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onContinueInAlerting = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
|
||||
// Provide prefill with partial contact point data
|
||||
// We intentionally create an incomplete ContactPoint to test that normalizeContactPoints fills in the missing optional fields with defaults
|
||||
const prefill: Partial<RuleFormValues> = {
|
||||
name: 'Test',
|
||||
contactPoints: {
|
||||
grafana: {
|
||||
selectedContactPoint: 'test-contact',
|
||||
} as ContactPoint,
|
||||
},
|
||||
};
|
||||
|
||||
renderDrawer({ onContinueInAlerting, onClose, prefill });
|
||||
|
||||
// Click Continue in Alerting
|
||||
await user.click(screen.getByRole('button', { name: /Continue in Alerting/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onContinueInAlerting).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contactPoints: expect.objectContaining({
|
||||
grafana: expect.objectContaining({
|
||||
selectedContactPoint: 'test-contact',
|
||||
// Verify normalization added default values
|
||||
overrideGrouping: false,
|
||||
groupBy: [],
|
||||
overrideTimings: false,
|
||||
groupWaitValue: '',
|
||||
groupIntervalValue: '',
|
||||
repeatIntervalValue: '',
|
||||
muteTimeIntervals: [],
|
||||
activeTimeIntervals: [],
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should close drawer after calling onContinueInAlerting', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onContinueInAlerting = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
renderDrawer({ onContinueInAlerting, onClose });
|
||||
|
||||
// Click Continue in Alerting
|
||||
await user.click(screen.getByRole('button', { name: /Continue in Alerting/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onContinueInAlerting).toHaveBeenCalled();
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prefill behavior', () => {
|
||||
it('should initialize form with prefill values', () => {
|
||||
const prefill: Partial<RuleFormValues> = {
|
||||
name: 'Prefilled Rule',
|
||||
type: RuleFormType.grafana,
|
||||
};
|
||||
renderDrawer({ prefill });
|
||||
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('Prefilled Rule');
|
||||
});
|
||||
|
||||
it('should reset form when prefill changes', async () => {
|
||||
const prefill1: Partial<RuleFormValues> = {
|
||||
name: 'First Rule',
|
||||
};
|
||||
const { rerender } = render(<AlertRuleDrawerForm {...defaultProps} prefill={prefill1} />);
|
||||
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('First Rule');
|
||||
|
||||
// Update prefill
|
||||
const prefill2: Partial<RuleFormValues> = {
|
||||
name: 'Second Rule',
|
||||
};
|
||||
rerender(<AlertRuleDrawerForm {...defaultProps} prefill={prefill2} />);
|
||||
|
||||
// Wait for the useEffect to trigger the reset
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('Second Rule');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset to defaults when prefill becomes undefined', async () => {
|
||||
const prefill: Partial<RuleFormValues> = {
|
||||
name: 'Prefilled Rule',
|
||||
};
|
||||
const { rerender } = render(<AlertRuleDrawerForm {...defaultProps} prefill={prefill} />);
|
||||
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('Prefilled Rule');
|
||||
|
||||
// Clear prefill
|
||||
rerender(<AlertRuleDrawerForm {...defaultProps} prefill={undefined} />);
|
||||
|
||||
// Wait for the useEffect to trigger the reset
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/Name/i)).toHaveValue('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create button and submission', () => {
|
||||
it('should close drawer on successful rule creation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = jest.fn();
|
||||
const mockResponse: GrafanaGroupUpdatedResponse = {
|
||||
message: 'Rule created successfully',
|
||||
created: ['rule-uid'],
|
||||
};
|
||||
mockExecute.mockResolvedValue(mockResponse);
|
||||
|
||||
renderDrawer({ onClose });
|
||||
|
||||
// Fill in required field
|
||||
const nameInput = screen.getByLabelText(/Name/i);
|
||||
await user.type(nameInput, 'Test Alert Rule');
|
||||
|
||||
// Click Create
|
||||
await user.click(screen.getByRole('button', { name: /Create/i }));
|
||||
|
||||
// Drawer should close on success
|
||||
await waitFor(() => {
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show validation error when form is invalid', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDrawer();
|
||||
|
||||
// Try to submit without filling required fields (name is required)
|
||||
await user.click(screen.getByRole('button', { name: /Create/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockError).toHaveBeenCalledWith('There are errors in the form. Please correct them and try again!');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { RuleDefinitionSection } from 'app/features/alerting/unified/components/RuleDefinitionSection';
|
||||
|
||||
import { isGrafanaGroupUpdatedResponse } from '../api/alertRuleModel';
|
||||
import { isCloudGroupUpdatedResponse, isGrafanaGroupUpdatedResponse } from '../api/alertRuleModel';
|
||||
import { useAddRuleToRuleGroup } from '../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
|
||||
import { getDefaultFormValues } from '../rule-editor/formDefaults';
|
||||
import { AlertManagerManualRouting, ContactPoint, RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
@@ -19,6 +19,13 @@ import { getRuleGroupLocationFromFormValues } from '../utils/rules';
|
||||
import { RuleConditionSection } from './RuleConditionSection';
|
||||
import { RuleNotificationSection } from './RuleNotificationSection';
|
||||
|
||||
/**
|
||||
* Normalizes contact point fields to ensure all properties have defined values.
|
||||
* This is only needed for the "Continue in Alerting" flow, which passes RuleFormValues
|
||||
* directly to the rule editor page. The submit flow doesn't need this because
|
||||
* getNotificationSettingsForDTO (called by formValuesToRulerGrafanaRuleDTO) already
|
||||
* handles partial/missing fields when building the DTO for the backend.
|
||||
*/
|
||||
function normalizeContactPoints(
|
||||
contactPoints: AlertManagerManualRouting | undefined
|
||||
): AlertManagerManualRouting | undefined {
|
||||
@@ -84,6 +91,9 @@ export function AlertRuleDrawerForm({
|
||||
|
||||
const submit = async (values: RuleFormValues) => {
|
||||
try {
|
||||
// The drawer doesn't expose a group field to keep the UX simple.
|
||||
// We derive the group name from the rule name as a sensible default.
|
||||
// The 'default' fallback should rarely occur since 'name' is a required field.
|
||||
const groupName =
|
||||
values.group && values.group.trim().length > 0 ? values.group : values.name?.trim() || 'default';
|
||||
const effectiveValues: RuleFormValues = { ...values, group: groupName };
|
||||
@@ -97,10 +107,11 @@ export function AlertRuleDrawerForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if result has an error message
|
||||
if (result && typeof result === 'object' && 'error' in result) {
|
||||
notifyApp.error('Failed to create rule', String(result.error));
|
||||
// Handle cloud rules error response
|
||||
if (isCloudGroupUpdatedResponse(result)) {
|
||||
notifyApp.error('Failed to create rule', result.error);
|
||||
} else {
|
||||
// This should not happen with the current discriminated union types
|
||||
notifyApp.error('Failed to create rule', 'Please review the form and try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -147,6 +158,7 @@ export function AlertRuleDrawerForm({
|
||||
...currentValues,
|
||||
contactPoints: normalizeContactPoints(currentValues.contactPoints),
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('alerting.simplified.continue-in-alerting', 'Continue in Alerting')}
|
||||
|
||||
Reference in New Issue
Block a user