monitorForm.spec.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. const member = MemberFixture({user: UserFixture({name: 'John Smith'})});
  20. const team = TeamFixture({slug: 'test-team'});
  21. const {project, router} = 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. {router, 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. {router, 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 ownerSelect = screen.getByRole('textbox', {name: 'Owner'});
  100. await selectEvent.select(ownerSelect, 'John Smith');
  101. const notifySelect = screen.getByRole('textbox', {name: 'Notify'});
  102. await selectEvent.openMenu(notifySelect);
  103. expect(
  104. screen.getByRole('menuitemcheckbox', {name: 'John Smith'})
  105. ).toBeInTheDocument();
  106. expect(
  107. screen.getByRole('menuitemcheckbox', {name: '#test-team'})
  108. ).toBeInTheDocument();
  109. await selectEvent.select(notifySelect, 'John Smith');
  110. const submitMock = MockApiClient.addMockResponse({
  111. url: apiEndpont,
  112. method: 'POST',
  113. });
  114. await userEvent.click(screen.getByRole('button', {name: 'Add Monitor'}));
  115. const config = {
  116. checkinMargin: '5',
  117. maxRuntime: '20',
  118. failureIssueThreshold: '4',
  119. recoveryThreshold: '2',
  120. schedule: '5 * * * *',
  121. scheduleType: 'crontab',
  122. timezone: 'America/Los_Angeles',
  123. };
  124. const alertRule = {
  125. environment: undefined,
  126. targets: [{targetIdentifier: 1, targetType: 'Member'}],
  127. };
  128. expect(submitMock).toHaveBeenCalledWith(
  129. expect.anything(),
  130. expect.objectContaining({
  131. data: {
  132. name: 'My Monitor',
  133. project: 'project-slug',
  134. owner: `user:${member.user?.id}`,
  135. type: 'cron_job',
  136. config,
  137. alertRule,
  138. },
  139. })
  140. );
  141. expect(mockHandleSubmitSuccess).toHaveBeenCalled();
  142. });
  143. it('prefills with an existing monitor', async function () {
  144. const monitor = MonitorFixture({project});
  145. const apiEndpont = `/projects/${organization.slug}/${monitor.project.slug}/monitors/${monitor.slug}/`;
  146. if (monitor.config.schedule_type !== ScheduleType.CRONTAB) {
  147. throw new Error('Fixture is not crontab');
  148. }
  149. render(
  150. <MonitorForm
  151. monitor={monitor}
  152. apiMethod="POST"
  153. apiEndpoint={apiEndpont}
  154. onSubmitSuccess={jest.fn()}
  155. submitLabel="Edit Monitor"
  156. />,
  157. {router, organization}
  158. );
  159. // Name and slug
  160. expect(screen.getByRole('textbox', {name: 'Name'})).toHaveValue(monitor.name);
  161. expect(screen.getByRole('textbox', {name: 'Slug'})).toHaveValue(monitor.slug);
  162. // Project
  163. expect(screen.getByRole('textbox', {name: 'Project'})).toBeDisabled();
  164. expect(screen.getByText(project.slug)).toBeInTheDocument();
  165. // Schedule type
  166. await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Schedule Type'}));
  167. const crontabOption = screen.getByRole('menuitemradio', {name: 'Crontab'});
  168. expect(crontabOption).toBeChecked();
  169. await userEvent.click(crontabOption);
  170. // Schedule value
  171. expect(screen.getByRole('textbox', {name: 'Crontab Schedule'})).toHaveValue(
  172. monitor.config.schedule
  173. );
  174. // Schedule timezone
  175. await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Timezone'}));
  176. const losAngelesOption = screen.getByRole('menuitemradio', {name: 'Los Angeles'});
  177. expect(losAngelesOption).toBeChecked();
  178. await userEvent.click(losAngelesOption);
  179. // Margins
  180. expect(screen.getByRole('spinbutton', {name: 'Grace Period'})).toHaveValue(5);
  181. expect(screen.getByRole('spinbutton', {name: 'Max Runtime'})).toHaveValue(10);
  182. // Tolerances
  183. expect(screen.getByRole('spinbutton', {name: 'Failure Tolerance'})).toHaveValue(2);
  184. expect(screen.getByRole('spinbutton', {name: 'Recovery Tolerance'})).toHaveValue(2);
  185. // Ownership
  186. await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Owner'}));
  187. const ownerOption = screen.getByRole('menuitemradio', {name: member.user?.name});
  188. expect(ownerOption).toBeChecked();
  189. await userEvent.keyboard('{Escape}');
  190. // Alert rule configuration
  191. await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Notify'}));
  192. const memberOption = screen.getByRole('menuitemcheckbox', {name: member.user?.name});
  193. expect(memberOption).toBeChecked();
  194. await userEvent.keyboard('{Escape}');
  195. const submitMock = MockApiClient.addMockResponse({
  196. url: apiEndpont,
  197. method: 'POST',
  198. });
  199. // Monitor form is not submitable until something is changed
  200. const submitButton = screen.getByRole('button', {name: 'Edit Monitor'});
  201. expect(submitButton).toBeDisabled();
  202. // Change Failure Tolerance
  203. await userEvent.clear(screen.getByRole('spinbutton', {name: 'Failure Tolerance'}));
  204. await userEvent.type(
  205. screen.getByRole('spinbutton', {name: 'Failure Tolerance'}),
  206. '10'
  207. );
  208. await userEvent.click(submitButton);
  209. // XXX(epurkhiser): When the values are loaded directly from the
  210. // monitor they come in as numbers, when changed via the toggles they
  211. // are translated to strings :(
  212. const config = {
  213. maxRuntime: monitor.config.max_runtime,
  214. checkinMargin: monitor.config.checkin_margin,
  215. recoveryThreshold: monitor.config.recovery_threshold,
  216. schedule: monitor.config.schedule,
  217. scheduleType: monitor.config.schedule_type,
  218. timezone: monitor.config.timezone,
  219. failureIssueThreshold: '10',
  220. };
  221. const alertRule = {
  222. environment: undefined,
  223. targets: [{targetIdentifier: 1, targetType: 'Member'}],
  224. };
  225. expect(submitMock).toHaveBeenCalledWith(
  226. expect.anything(),
  227. expect.objectContaining({
  228. data: {
  229. name: monitor.name,
  230. slug: monitor.slug,
  231. project: monitor.project.slug,
  232. owner: `user:${member.user?.id}`,
  233. type: 'cron_job',
  234. config,
  235. alertRule,
  236. },
  237. })
  238. );
  239. });
  240. });