monitorForm.spec.tsx 9.1 KB

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