createAlertButton.spec.jsx 8.1 KB

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