monitorForm.spec.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import selectEvent from 'react-select-event';
  2. import {Member} from 'sentry-fixture/member';
  3. import {MonitorFixture} from 'sentry-fixture/monitor';
  4. import {Organization} from 'sentry-fixture/organization';
  5. import {Team} from 'sentry-fixture/team';
  6. import {User} from 'sentry-fixture/user';
  7. import {initializeOrg} from 'sentry-test/initializeOrg';
  8. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  9. import {useMembers} from 'sentry/utils/useMembers';
  10. import useProjects from 'sentry/utils/useProjects';
  11. import {useTeams} from 'sentry/utils/useTeams';
  12. import MonitorForm from 'sentry/views/monitors/components/monitorForm';
  13. import {ScheduleType} from 'sentry/views/monitors/types';
  14. jest.mock('sentry/utils/useProjects');
  15. jest.mock('sentry/utils/useTeams');
  16. jest.mock('sentry/utils/useMembers');
  17. describe('MonitorForm', function () {
  18. const organization = Organization({features: ['issue-platform']});
  19. const member = Member({user: User({name: 'John Smith'})});
  20. const team = Team({slug: 'test-team'});
  21. const {project, routerContext} = initializeOrg({organization});
  22. beforeEach(() => {
  23. jest.mocked(useProjects).mockReturnValue({
  24. fetchError: null,
  25. fetching: false,
  26. hasMore: false,
  27. initiallyLoaded: false,
  28. onSearch: jest.fn(),
  29. placeholders: [],
  30. projects: [project],
  31. });
  32. jest.mocked(useTeams).mockReturnValue({
  33. fetchError: null,
  34. fetching: false,
  35. hasMore: false,
  36. initiallyLoaded: false,
  37. loadMore: jest.fn(),
  38. onSearch: jest.fn(),
  39. teams: [team],
  40. });
  41. jest.mocked(useMembers).mockReturnValue({
  42. fetchError: null,
  43. fetching: false,
  44. hasMore: false,
  45. initiallyLoaded: false,
  46. loadMore: jest.fn(),
  47. onSearch: jest.fn(),
  48. members: [member.user!],
  49. });
  50. });
  51. it('displays human readable schedule', async function () {
  52. render(
  53. <MonitorForm
  54. apiMethod="POST"
  55. apiEndpoint={`/organizations/${organization.slug}/monitors/`}
  56. onSubmitSuccess={jest.fn()}
  57. />,
  58. {context: routerContext, organization}
  59. );
  60. const schedule = screen.getByRole('textbox', {name: 'Crontab Schedule'});
  61. await userEvent.clear(schedule);
  62. await userEvent.type(schedule, '5 * * * *');
  63. expect(screen.getByText('"At 5 minutes past the hour"')).toBeInTheDocument();
  64. });
  65. it('submits a new monitor', async function () {
  66. const mockHandleSubmitSuccess = jest.fn();
  67. const apiEndpont = `/organizations/${organization.slug}/monitors/`;
  68. render(
  69. <MonitorForm
  70. apiMethod="POST"
  71. apiEndpoint={apiEndpont}
  72. onSubmitSuccess={mockHandleSubmitSuccess}
  73. submitLabel="Add Monitor"
  74. />,
  75. {context: routerContext, organization}
  76. );
  77. await userEvent.type(screen.getByRole('textbox', {name: 'Name'}), 'My Monitor');
  78. await selectEvent.select(
  79. screen.getByRole('textbox', {name: 'Project'}),
  80. project.slug
  81. );
  82. const schedule = screen.getByRole('textbox', {name: 'Crontab Schedule'});
  83. await userEvent.clear(schedule);
  84. await userEvent.type(schedule, '5 * * * *');
  85. await selectEvent.select(
  86. screen.getByRole('textbox', {name: 'Timezone'}),
  87. 'Los Angeles'
  88. );
  89. await userEvent.type(screen.getByRole('spinbutton', {name: 'Grace Period'}), '5');
  90. await userEvent.type(screen.getByRole('spinbutton', {name: 'Max Runtime'}), '20');
  91. await userEvent.type(
  92. screen.getByRole('spinbutton', {name: 'Failure Tolerance'}),
  93. '4'
  94. );
  95. await userEvent.type(
  96. screen.getByRole('spinbutton', {name: 'Recovery Tolerance'}),
  97. '2'
  98. );
  99. const notifySelect = screen.getByRole('textbox', {name: 'Notify'});
  100. selectEvent.openMenu(notifySelect);
  101. expect(
  102. screen.getByRole('menuitemcheckbox', {name: 'John Smith'})
  103. ).toBeInTheDocument();
  104. expect(
  105. screen.getByRole('menuitemcheckbox', {name: '#test-team'})
  106. ).toBeInTheDocument();
  107. await selectEvent.select(notifySelect, 'John Smith');
  108. const submitMock = MockApiClient.addMockResponse({
  109. url: apiEndpont,
  110. method: 'POST',
  111. });
  112. await userEvent.click(screen.getByRole('button', {name: 'Add Monitor'}));
  113. const config = {
  114. checkin_margin: '5',
  115. max_runtime: '20',
  116. failure_issue_threshold: '4',
  117. recovery_threshold: '2',
  118. schedule: '5 * * * *',
  119. schedule_type: 'crontab',
  120. timezone: 'America/Los_Angeles',
  121. };
  122. const alertRule = {
  123. environment: undefined,
  124. targets: [{targetIdentifier: 1, targetType: 'Member'}],
  125. };
  126. expect(submitMock).toHaveBeenCalledWith(
  127. expect.anything(),
  128. expect.objectContaining({
  129. data: {
  130. name: 'My Monitor',
  131. project: 'project-slug',
  132. type: 'cron_job',
  133. config,
  134. alertRule,
  135. },
  136. })
  137. );
  138. expect(mockHandleSubmitSuccess).toHaveBeenCalled();
  139. });
  140. it('prefills with an existing monitor', async function () {
  141. const monitor = MonitorFixture({project});
  142. const apiEndpont = `/organizations/${organization.slug}/monitors/${monitor.slug}/`;
  143. if (monitor.config.schedule_type !== ScheduleType.CRONTAB) {
  144. throw new Error('Fixture is not crontab');
  145. }
  146. render(
  147. <MonitorForm
  148. monitor={monitor}
  149. apiMethod="POST"
  150. apiEndpoint={apiEndpont}
  151. onSubmitSuccess={jest.fn()}
  152. submitLabel="Edit Monitor"
  153. />,
  154. {context: routerContext, organization}
  155. );
  156. // Name and slug
  157. expect(screen.getByRole('textbox', {name: 'Name'})).toHaveValue(monitor.name);
  158. expect(screen.getByRole('textbox', {name: 'Slug'})).toHaveValue(monitor.slug);
  159. // Project
  160. expect(screen.getByRole('textbox', {name: 'Project'})).toBeDisabled();
  161. expect(screen.getByText(project.slug)).toBeInTheDocument();
  162. // Schedule type
  163. selectEvent.openMenu(screen.getByRole('textbox', {name: 'Schedule Type'}));
  164. const crontabOption = screen.getByRole('menuitemradio', {name: 'Crontab'});
  165. expect(crontabOption).toBeChecked();
  166. await userEvent.click(crontabOption);
  167. // Schedule value
  168. expect(screen.getByRole('textbox', {name: 'Crontab Schedule'})).toHaveValue(
  169. monitor.config.schedule
  170. );
  171. // Schedule timezone
  172. selectEvent.openMenu(screen.getByRole('textbox', {name: 'Timezone'}));
  173. const losAngelesOption = screen.getByRole('menuitemradio', {name: 'Los Angeles'});
  174. expect(losAngelesOption).toBeChecked();
  175. await userEvent.click(losAngelesOption);
  176. // Margins
  177. expect(screen.getByRole('spinbutton', {name: 'Grace Period'})).toHaveValue(5);
  178. expect(screen.getByRole('spinbutton', {name: 'Max Runtime'})).toHaveValue(10);
  179. // Tolerances
  180. expect(screen.getByRole('spinbutton', {name: 'Failure Tolerance'})).toHaveValue(2);
  181. expect(screen.getByRole('spinbutton', {name: 'Recovery Tolerance'})).toHaveValue(2);
  182. // Alert rule configuration
  183. selectEvent.openMenu(screen.getByRole('textbox', {name: 'Notify'}));
  184. const memberOption = screen.getByRole('menuitemcheckbox', {name: member.user?.name});
  185. expect(memberOption).toBeChecked();
  186. await userEvent.keyboard('{Escape}');
  187. const submitMock = MockApiClient.addMockResponse({
  188. url: apiEndpont,
  189. method: 'POST',
  190. });
  191. // Monitor form is not submitable until something is changed
  192. const submitButton = screen.getByRole('button', {name: 'Edit Monitor'});
  193. expect(submitButton).toBeDisabled();
  194. // Change Failure Tolerance
  195. await userEvent.clear(screen.getByRole('spinbutton', {name: 'Failure Tolerance'}));
  196. await userEvent.type(
  197. screen.getByRole('spinbutton', {name: 'Failure Tolerance'}),
  198. '10'
  199. );
  200. await userEvent.click(submitButton);
  201. // XXX(epurkhiser): When the values are loaded directly from the
  202. // monitor they come in as numbers, when changed via the toggles they
  203. // are translated to strings :(
  204. const config = {
  205. max_runtime: monitor.config.max_runtime,
  206. checkin_margin: monitor.config.checkin_margin,
  207. recovery_threshold: monitor.config.recovery_threshold,
  208. schedule: monitor.config.schedule,
  209. schedule_type: monitor.config.schedule_type,
  210. timezone: monitor.config.timezone,
  211. failure_issue_threshold: '10',
  212. };
  213. const alertRule = {
  214. environment: undefined,
  215. targets: [{targetIdentifier: 1, targetType: 'Member'}],
  216. };
  217. expect(submitMock).toHaveBeenCalledWith(
  218. expect.anything(),
  219. expect.objectContaining({
  220. data: {
  221. name: monitor.name,
  222. slug: monitor.slug,
  223. project: monitor.project.slug,
  224. type: 'cron_job',
  225. config,
  226. alertRule,
  227. },
  228. })
  229. );
  230. });
  231. });