import {MemberFixture} from 'sentry-fixture/member';
import {MonitorFixture} from 'sentry-fixture/monitor';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {TeamFixture} from 'sentry-fixture/team';
import {UserFixture} from 'sentry-fixture/user';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import selectEvent from 'sentry-test/selectEvent';
import {useMembers} from 'sentry/utils/useMembers';
import useProjects from 'sentry/utils/useProjects';
import {useTeams} from 'sentry/utils/useTeams';
import MonitorForm from 'sentry/views/monitors/components/monitorForm';
import {ScheduleType} from 'sentry/views/monitors/types';
jest.mock('sentry/utils/useProjects');
jest.mock('sentry/utils/useTeams');
jest.mock('sentry/utils/useMembers');
describe('MonitorForm', function () {
const organization = OrganizationFixture({
features: ['issue-platform'],
});
const member = MemberFixture({user: UserFixture({name: 'John Smith'})});
const team = TeamFixture({slug: 'test-team'});
const {project, routerContext} = initializeOrg({organization});
beforeEach(() => {
jest.mocked(useProjects).mockReturnValue({
fetchError: null,
fetching: false,
hasMore: false,
initiallyLoaded: false,
onSearch: jest.fn(),
placeholders: [],
projects: [project],
});
jest.mocked(useTeams).mockReturnValue({
fetchError: null,
fetching: false,
hasMore: false,
initiallyLoaded: false,
loadMore: jest.fn(),
onSearch: jest.fn(),
teams: [team],
});
jest.mocked(useMembers).mockReturnValue({
fetchError: null,
fetching: false,
hasMore: false,
initiallyLoaded: false,
loadMore: jest.fn(),
onSearch: jest.fn(),
members: [member.user!],
});
});
it('displays human readable schedule', async function () {
render(
,
{context: routerContext, organization}
);
const schedule = screen.getByRole('textbox', {name: 'Crontab Schedule'});
await userEvent.clear(schedule);
await userEvent.type(schedule, '5 * * * *');
expect(screen.getByText('"At 5 minutes past the hour"')).toBeInTheDocument();
});
it('submits a new monitor', async function () {
const mockHandleSubmitSuccess = jest.fn();
const apiEndpont = `/organizations/${organization.slug}/monitors/`;
render(
,
{context: routerContext, organization}
);
await userEvent.type(screen.getByRole('textbox', {name: 'Name'}), 'My Monitor');
await selectEvent.select(
screen.getByRole('textbox', {name: 'Project'}),
project.slug
);
const schedule = screen.getByRole('textbox', {name: 'Crontab Schedule'});
await userEvent.clear(schedule);
await userEvent.type(schedule, '5 * * * *');
await selectEvent.select(
screen.getByRole('textbox', {name: 'Timezone'}),
'Los Angeles'
);
await userEvent.type(screen.getByRole('spinbutton', {name: 'Grace Period'}), '5');
await userEvent.type(screen.getByRole('spinbutton', {name: 'Max Runtime'}), '20');
await userEvent.type(
screen.getByRole('spinbutton', {name: 'Failure Tolerance'}),
'4'
);
await userEvent.type(
screen.getByRole('spinbutton', {name: 'Recovery Tolerance'}),
'2'
);
const ownerSelect = screen.getByRole('textbox', {name: 'Owner'});
await selectEvent.select(ownerSelect, 'John Smith');
const notifySelect = screen.getByRole('textbox', {name: 'Notify'});
await selectEvent.openMenu(notifySelect);
expect(
screen.getByRole('menuitemcheckbox', {name: 'John Smith'})
).toBeInTheDocument();
expect(
screen.getByRole('menuitemcheckbox', {name: '#test-team'})
).toBeInTheDocument();
await selectEvent.select(notifySelect, 'John Smith');
const submitMock = MockApiClient.addMockResponse({
url: apiEndpont,
method: 'POST',
});
await userEvent.click(screen.getByRole('button', {name: 'Add Monitor'}));
const config = {
checkinMargin: '5',
maxRuntime: '20',
failureIssueThreshold: '4',
recoveryThreshold: '2',
schedule: '5 * * * *',
scheduleType: 'crontab',
timezone: 'America/Los_Angeles',
};
const alertRule = {
environment: undefined,
targets: [{targetIdentifier: 1, targetType: 'Member'}],
};
expect(submitMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: {
name: 'My Monitor',
project: 'project-slug',
owner: `user:${member.user?.id}`,
type: 'cron_job',
config,
alertRule,
},
})
);
expect(mockHandleSubmitSuccess).toHaveBeenCalled();
});
it('prefills with an existing monitor', async function () {
const monitor = MonitorFixture({project});
const apiEndpont = `/projects/${organization.slug}/${monitor.project.slug}/monitors/${monitor.slug}/`;
if (monitor.config.schedule_type !== ScheduleType.CRONTAB) {
throw new Error('Fixture is not crontab');
}
render(
,
{context: routerContext, organization}
);
// Name and slug
expect(screen.getByRole('textbox', {name: 'Name'})).toHaveValue(monitor.name);
expect(screen.getByRole('textbox', {name: 'Slug'})).toHaveValue(monitor.slug);
// Project
expect(screen.getByRole('textbox', {name: 'Project'})).toBeDisabled();
expect(screen.getByText(project.slug)).toBeInTheDocument();
// Schedule type
await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Schedule Type'}));
const crontabOption = screen.getByRole('menuitemradio', {name: 'Crontab'});
expect(crontabOption).toBeChecked();
await userEvent.click(crontabOption);
// Schedule value
expect(screen.getByRole('textbox', {name: 'Crontab Schedule'})).toHaveValue(
monitor.config.schedule
);
// Schedule timezone
await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Timezone'}));
const losAngelesOption = screen.getByRole('menuitemradio', {name: 'Los Angeles'});
expect(losAngelesOption).toBeChecked();
await userEvent.click(losAngelesOption);
// Margins
expect(screen.getByRole('spinbutton', {name: 'Grace Period'})).toHaveValue(5);
expect(screen.getByRole('spinbutton', {name: 'Max Runtime'})).toHaveValue(10);
// Tolerances
expect(screen.getByRole('spinbutton', {name: 'Failure Tolerance'})).toHaveValue(2);
expect(screen.getByRole('spinbutton', {name: 'Recovery Tolerance'})).toHaveValue(2);
// Ownership
await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Owner'}));
const ownerOption = screen.getByRole('menuitemradio', {name: member.user?.name});
expect(ownerOption).toBeChecked();
await userEvent.keyboard('{Escape}');
// Alert rule configuration
await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Notify'}));
const memberOption = screen.getByRole('menuitemcheckbox', {name: member.user?.name});
expect(memberOption).toBeChecked();
await userEvent.keyboard('{Escape}');
const submitMock = MockApiClient.addMockResponse({
url: apiEndpont,
method: 'POST',
});
// Monitor form is not submitable until something is changed
const submitButton = screen.getByRole('button', {name: 'Edit Monitor'});
expect(submitButton).toBeDisabled();
// Change Failure Tolerance
await userEvent.clear(screen.getByRole('spinbutton', {name: 'Failure Tolerance'}));
await userEvent.type(
screen.getByRole('spinbutton', {name: 'Failure Tolerance'}),
'10'
);
await userEvent.click(submitButton);
// XXX(epurkhiser): When the values are loaded directly from the
// monitor they come in as numbers, when changed via the toggles they
// are translated to strings :(
const config = {
maxRuntime: monitor.config.max_runtime,
checkinMargin: monitor.config.checkin_margin,
recoveryThreshold: monitor.config.recovery_threshold,
schedule: monitor.config.schedule,
scheduleType: monitor.config.schedule_type,
timezone: monitor.config.timezone,
failureIssueThreshold: '10',
};
const alertRule = {
environment: undefined,
targets: [{targetIdentifier: 1, targetType: 'Member'}],
};
expect(submitMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: {
name: monitor.name,
slug: monitor.slug,
project: monitor.project.slug,
owner: `user:${member.user?.id}`,
type: 'cron_job',
config,
alertRule,
},
})
);
});
});