createAlertButton.spec.jsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import * as navigation from 'sentry/actionCreators/navigation';
  3. import CreateAlertButton, {
  4. CreateAlertFromViewButton,
  5. } from 'sentry/components/createAlertButton';
  6. import EventView from 'sentry/utils/discover/eventView';
  7. import {ALL_VIEWS, DEFAULT_EVENT_VIEW} from 'sentry/views/eventsV2/data';
  8. const onIncompatibleQueryMock = jest.fn();
  9. const onCloseMock = jest.fn();
  10. const onSuccessMock = jest.fn();
  11. function generateWrappedComponent(organization, eventView) {
  12. return mountWithTheme(
  13. <CreateAlertFromViewButton
  14. location={location}
  15. organization={organization}
  16. eventView={eventView}
  17. projects={[TestStubs.Project()]}
  18. onIncompatibleQuery={onIncompatibleQueryMock}
  19. onSuccess={onSuccessMock}
  20. />
  21. );
  22. }
  23. function generateWrappedComponentButton(organization, extraProps) {
  24. return mountWithTheme(
  25. <CreateAlertButton organization={organization} {...extraProps} />
  26. );
  27. }
  28. describe('CreateAlertFromViewButton', () => {
  29. const organization = TestStubs.Organization();
  30. afterEach(() => {
  31. jest.resetAllMocks();
  32. });
  33. it('renders', () => {
  34. const eventView = EventView.fromSavedQuery(DEFAULT_EVENT_VIEW);
  35. const wrapper = generateWrappedComponent(organization, eventView);
  36. expect(wrapper.text()).toBe('Create Alert');
  37. });
  38. it('should warn when project is not selected', () => {
  39. const eventView = EventView.fromSavedQuery({
  40. ...DEFAULT_EVENT_VIEW,
  41. query: 'event.type:error',
  42. });
  43. const wrapper = generateWrappedComponent(organization, eventView);
  44. wrapper.simulate('click');
  45. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(1);
  46. const errorsAlert = mountWithTheme(
  47. onIncompatibleQueryMock.mock.calls[0][0](onCloseMock)
  48. );
  49. expect(errorsAlert.text()).toBe(
  50. 'An alert can use data from only one Project. Select one and try again.'
  51. );
  52. });
  53. it('should warn when all projects are selected (-1)', () => {
  54. const eventView = EventView.fromSavedQuery({
  55. ...DEFAULT_EVENT_VIEW,
  56. query: 'event.type:error',
  57. projects: [-1],
  58. });
  59. const wrapper = generateWrappedComponent(organization, eventView);
  60. wrapper.simulate('click');
  61. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(1);
  62. const errorsAlert = mountWithTheme(
  63. onIncompatibleQueryMock.mock.calls[0][0](onCloseMock)
  64. );
  65. expect(errorsAlert.text()).toBe(
  66. 'An alert can use data from only one Project. Select one and try again.'
  67. );
  68. });
  69. it('should warn when event.type is not specified', () => {
  70. const eventView = EventView.fromSavedQuery({
  71. ...DEFAULT_EVENT_VIEW,
  72. query: '',
  73. projects: [2],
  74. });
  75. const wrapper = generateWrappedComponent(organization, eventView);
  76. wrapper.simulate('click');
  77. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(1);
  78. const errorsAlert = mountWithTheme(
  79. onIncompatibleQueryMock.mock.calls[0][0](onCloseMock)
  80. );
  81. expect(errorsAlert.text()).toContain('An alert needs a filter of event.type:error');
  82. });
  83. it('should warn when yAxis is not allowed', () => {
  84. const eventView = EventView.fromSavedQuery({
  85. ...ALL_VIEWS.find(view => view.name === 'Errors by URL'),
  86. query: 'event.type:error',
  87. yAxis: 'count_unique(issue)',
  88. projects: [2],
  89. });
  90. expect(eventView.getYAxis()).toBe('count_unique(issue)');
  91. const wrapper = generateWrappedComponent(organization, eventView);
  92. wrapper.simulate('click');
  93. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(1);
  94. const errorsAlert = mountWithTheme(
  95. onIncompatibleQueryMock.mock.calls[0][0](onCloseMock)
  96. );
  97. expect(errorsAlert.text()).toBe(
  98. 'An alert can’t use the metric count_unique(issue) just yet. Select another metric and try again.'
  99. );
  100. });
  101. it('should allow yAxis with a number as the parameter', () => {
  102. const eventView = EventView.fromSavedQuery({
  103. ...DEFAULT_EVENT_VIEW,
  104. query: 'event.type:transaction',
  105. yAxis: 'apdex(300)',
  106. fields: [...DEFAULT_EVENT_VIEW.fields, 'apdex(300)'],
  107. projects: [2],
  108. });
  109. expect(eventView.getYAxis()).toBe('apdex(300)');
  110. const wrapper = generateWrappedComponent(organization, eventView);
  111. wrapper.simulate('click');
  112. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(0);
  113. });
  114. it('should allow yAxis with a measurement as the parameter', () => {
  115. const eventView = EventView.fromSavedQuery({
  116. ...DEFAULT_EVENT_VIEW,
  117. query: 'event.type:transaction',
  118. yAxis: 'p75(measurements.fcp)',
  119. fields: [...DEFAULT_EVENT_VIEW.fields, 'p75(measurements.fcp)'],
  120. projects: [2],
  121. });
  122. expect(eventView.getYAxis()).toBe('p75(measurements.fcp)');
  123. const wrapper = generateWrappedComponent(organization, eventView);
  124. wrapper.simulate('click');
  125. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(0);
  126. });
  127. it('should warn with multiple errors, missing event.type and project', () => {
  128. const eventView = EventView.fromSavedQuery({
  129. ...ALL_VIEWS.find(view => view.name === 'Errors by URL'),
  130. query: '',
  131. yAxis: 'count_unique(issue.id)',
  132. projects: [],
  133. });
  134. const wrapper = generateWrappedComponent(organization, eventView);
  135. wrapper.simulate('click');
  136. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(1);
  137. const errorsAlert = mountWithTheme(
  138. onIncompatibleQueryMock.mock.calls[0][0](onCloseMock)
  139. );
  140. expect(errorsAlert.text()).toContain('Yikes! That button didn’t work.');
  141. });
  142. it('should trigger success callback', () => {
  143. const eventView = EventView.fromSavedQuery({
  144. ...DEFAULT_EVENT_VIEW,
  145. query: 'event.type:error',
  146. projects: [2],
  147. });
  148. const wrapper = generateWrappedComponent(organization, eventView);
  149. wrapper.simulate('click');
  150. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(0);
  151. expect(onSuccessMock).toHaveBeenCalledTimes(1);
  152. });
  153. it('should allow alert to close', () => {
  154. const eventView = EventView.fromSavedQuery({
  155. ...DEFAULT_EVENT_VIEW,
  156. });
  157. const wrapper = generateWrappedComponent(organization, eventView);
  158. wrapper.simulate('click');
  159. expect(onIncompatibleQueryMock).toHaveBeenCalledTimes(1);
  160. const errorsAlert = mountWithTheme(
  161. onIncompatibleQueryMock.mock.calls[0][0](onCloseMock)
  162. );
  163. errorsAlert.find('[aria-label="Close"]').at(0).simulate('click');
  164. expect(onCloseMock).toHaveBeenCalledTimes(1);
  165. });
  166. it('disables the create alert button for members', () => {
  167. const eventView = EventView.fromSavedQuery({
  168. ...DEFAULT_EVENT_VIEW,
  169. });
  170. const noAccessOrg = {
  171. ...organization,
  172. access: [],
  173. };
  174. const wrapper = generateWrappedComponent(noAccessOrg, eventView);
  175. const button = wrapper.find('button[aria-label="Create Alert"]');
  176. expect(button.props()['aria-disabled']).toBe(true);
  177. });
  178. it('shows a guide for members', () => {
  179. const noAccessOrg = {
  180. ...organization,
  181. access: [],
  182. };
  183. const wrapper = generateWrappedComponentButton(noAccessOrg, {
  184. showPermissionGuide: true,
  185. });
  186. const guide = wrapper.find('GuideAnchor');
  187. expect(guide.props().target).toBe('alerts_write_member');
  188. });
  189. it('shows a guide for owners/admins', () => {
  190. const adminAccessOrg = {
  191. ...organization,
  192. access: ['org:write'],
  193. };
  194. const wrapper = generateWrappedComponentButton(adminAccessOrg, {
  195. showPermissionGuide: true,
  196. });
  197. const guide = wrapper.find('GuideAnchor');
  198. expect(guide.props().target).toBe('alerts_write_owner');
  199. expect(guide.props().onFinish).toBeDefined();
  200. });
  201. it('redirects to alert wizard with no project', () => {
  202. jest.spyOn(navigation, 'navigateTo');
  203. const wrapper = generateWrappedComponentButton(organization);
  204. wrapper.simulate('click');
  205. expect(navigation.navigateTo).toHaveBeenCalledWith(
  206. `/organizations/org-slug/alerts/:projectId/wizard/`,
  207. undefined
  208. );
  209. });
  210. it('redirects to alert wizard with a project', () => {
  211. const wrapper = generateWrappedComponentButton(organization, {
  212. projectSlug: 'proj-slug',
  213. });
  214. expect(wrapper.find('Button').props().to).toBe(
  215. `/organizations/org-slug/alerts/proj-slug/wizard/`
  216. );
  217. });
  218. it('removes a duplicate project filter', () => {
  219. const eventView = EventView.fromSavedQuery({
  220. ...DEFAULT_EVENT_VIEW,
  221. query: 'event.type:error project:project-slug',
  222. projects: [2],
  223. });
  224. const wrapper = generateWrappedComponent(organization, eventView);
  225. expect(wrapper.find('Button').props().to).toEqual(
  226. expect.objectContaining({
  227. pathname: `/organizations/org-slug/alerts/project-slug/new/`,
  228. query: expect.objectContaining({
  229. query: 'event.type:error ',
  230. }),
  231. })
  232. );
  233. });
  234. });