index.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  3. import {NewQuery} from 'sentry/types';
  4. import EventView from 'sentry/utils/discover/eventView';
  5. import {DisplayModes} from 'sentry/utils/discover/types';
  6. import DiscoverBanner from 'sentry/views/eventsV2/banner';
  7. import {ALL_VIEWS} from 'sentry/views/eventsV2/data';
  8. import SavedQueryButtonGroup from 'sentry/views/eventsV2/savedQuery';
  9. import * as utils from 'sentry/views/eventsV2/savedQuery/utils';
  10. const SELECTOR_BUTTON_SAVE_AS = 'button[aria-label="Save as"]';
  11. const SELECTOR_BUTTON_SAVED = '[data-test-id="discover2-savedquery-button-saved"]';
  12. const SELECTOR_BUTTON_UPDATE = '[data-test-id="discover2-savedquery-button-update"]';
  13. const SELECTOR_BUTTON_DELETE = '[data-test-id="discover2-savedquery-button-delete"]';
  14. const SELECTOR_BUTTON_CREATE_ALERT = '[data-test-id="discover2-create-from-discover"]';
  15. jest.mock('sentry/actionCreators/modal');
  16. function mount(
  17. location,
  18. organization,
  19. router,
  20. eventView,
  21. savedQuery,
  22. yAxis,
  23. disabled = false
  24. ) {
  25. return render(
  26. <SavedQueryButtonGroup
  27. location={location}
  28. organization={organization}
  29. eventView={eventView}
  30. savedQuery={savedQuery}
  31. disabled={disabled}
  32. updateCallback={() => {}}
  33. yAxis={yAxis}
  34. router={router}
  35. queryDataLoading={false}
  36. setSavedQuery={jest.fn()}
  37. setHomepageQuery={jest.fn()}
  38. />
  39. );
  40. }
  41. function generateWrappedComponent(
  42. location,
  43. organization,
  44. router,
  45. eventView,
  46. savedQuery,
  47. yAxis,
  48. disabled = false
  49. ) {
  50. const mockSetSavedQuery = jest.fn();
  51. return {
  52. mockSetSavedQuery,
  53. wrapper: mountWithTheme(
  54. <SavedQueryButtonGroup
  55. location={location}
  56. organization={organization}
  57. eventView={eventView}
  58. savedQuery={savedQuery}
  59. disabled={disabled}
  60. updateCallback={() => {}}
  61. yAxis={yAxis}
  62. router={router}
  63. queryDataLoading={false}
  64. setSavedQuery={mockSetSavedQuery}
  65. setHomepageQuery={jest.fn()}
  66. />
  67. ),
  68. };
  69. }
  70. describe('EventsV2 > SaveQueryButtonGroup', function () {
  71. const organization = TestStubs.Organization({
  72. features: ['discover-query', 'dashboards-edit'],
  73. });
  74. const location = {
  75. pathname: '/organization/eventsv2/',
  76. query: {},
  77. };
  78. const router = {
  79. location: {query: {}},
  80. };
  81. const yAxis = ['count()', 'failure_count()'];
  82. const errorsQuery = {
  83. ...(ALL_VIEWS.find(view => view.name === 'Errors by Title') as NewQuery),
  84. yAxis: ['count()'],
  85. display: DisplayModes.DEFAULT,
  86. };
  87. const errorsView = EventView.fromSavedQuery(errorsQuery);
  88. const errorsViewSaved = EventView.fromSavedQuery(errorsQuery);
  89. errorsViewSaved.id = '1';
  90. const errorsViewModified = EventView.fromSavedQuery(errorsQuery);
  91. errorsViewModified.id = '1';
  92. errorsViewModified.name = 'Modified Name';
  93. const savedQuery = {
  94. ...errorsViewSaved.toNewQuery(),
  95. yAxis,
  96. dateCreated: '',
  97. dateUpdated: '',
  98. id: '1',
  99. };
  100. afterEach(() => {
  101. MockApiClient.clearMockResponses();
  102. jest.clearAllMocks();
  103. });
  104. describe('building on a new query', () => {
  105. const mockUtils = jest
  106. .spyOn(utils, 'handleCreateQuery')
  107. .mockImplementation(() => Promise.resolve(savedQuery));
  108. beforeEach(() => {
  109. mockUtils.mockClear();
  110. });
  111. it('renders disabled buttons when disabled prop is used', () => {
  112. const {wrapper} = generateWrappedComponent(
  113. location,
  114. organization,
  115. router,
  116. errorsView,
  117. undefined,
  118. yAxis,
  119. true
  120. );
  121. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  122. expect(buttonSaveAs.props()['aria-disabled']).toBe(true);
  123. });
  124. it('renders the correct set of buttons', () => {
  125. const {wrapper} = generateWrappedComponent(
  126. location,
  127. organization,
  128. router,
  129. errorsView,
  130. undefined,
  131. yAxis
  132. );
  133. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  134. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  135. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  136. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  137. expect(buttonSaveAs.exists()).toBe(true);
  138. expect(buttonSaved.exists()).toBe(false);
  139. expect(buttonUpdate.exists()).toBe(false);
  140. expect(buttonDelete.exists()).toBe(false);
  141. });
  142. it('hides the banner when save is complete.', () => {
  143. mount(location, organization, router, errorsView, undefined, yAxis);
  144. // Click on ButtonSaveAs to open dropdown
  145. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  146. // Fill in the Input
  147. userEvent.type(screen.getByPlaceholderText('Display name'), 'My New Query Name');
  148. // Click on Save in the Dropdown
  149. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  150. // The banner should not render
  151. mountWithTheme(<DiscoverBanner organization={organization} resultsUrl="" />);
  152. expect(screen.queryByText('Discover Trends')).not.toBeInTheDocument();
  153. });
  154. it('saves a well-formed query', () => {
  155. mount(location, organization, router, errorsView, undefined, yAxis);
  156. // Click on ButtonSaveAs to open dropdown
  157. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  158. // Fill in the Input
  159. userEvent.type(screen.getByPlaceholderText('Display name'), 'My New Query Name');
  160. // Click on Save in the Dropdown
  161. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  162. expect(mockUtils).toHaveBeenCalledWith(
  163. expect.anything(), // api
  164. organization,
  165. expect.objectContaining({
  166. ...errorsView,
  167. name: 'My New Query Name',
  168. }),
  169. yAxis,
  170. true
  171. );
  172. });
  173. it('rejects if query.name is empty', () => {
  174. mount(location, organization, router, errorsView, undefined, yAxis);
  175. // Click on ButtonSaveAs to open dropdown
  176. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  177. // Do not fill in Input
  178. // Click on Save in the Dropdown
  179. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  180. // Check that EventView has a name
  181. expect(errorsView.name).toBe('Errors by Title');
  182. expect(mockUtils).not.toHaveBeenCalled();
  183. });
  184. });
  185. describe('viewing a saved query', () => {
  186. let mockUtils;
  187. beforeEach(() => {
  188. mockUtils = jest
  189. .spyOn(utils, 'handleDeleteQuery')
  190. .mockImplementation(() => Promise.resolve());
  191. });
  192. afterEach(() => {
  193. mockUtils.mockClear();
  194. });
  195. it('renders the correct set of buttons', () => {
  196. const {wrapper} = generateWrappedComponent(
  197. location,
  198. organization,
  199. router,
  200. errorsViewSaved,
  201. savedQuery,
  202. yAxis
  203. );
  204. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  205. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  206. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  207. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  208. expect(buttonSaveAs.exists()).toBe(false);
  209. expect(buttonSaved.exists()).toBe(true);
  210. expect(buttonUpdate.exists()).toBe(false);
  211. expect(buttonDelete.exists()).toBe(true);
  212. });
  213. it('treats undefined yAxis the same as count() when checking for changes', () => {
  214. const {wrapper} = generateWrappedComponent(
  215. location,
  216. organization,
  217. router,
  218. errorsViewSaved,
  219. {...savedQuery, yAxis: undefined},
  220. ['count()']
  221. );
  222. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  223. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  224. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  225. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  226. expect(buttonSaveAs.exists()).toBe(false);
  227. expect(buttonSaved.exists()).toBe(true);
  228. expect(buttonUpdate.exists()).toBe(false);
  229. expect(buttonDelete.exists()).toBe(true);
  230. });
  231. it('converts string yAxis values to array when checking for changes', () => {
  232. const {wrapper} = generateWrappedComponent(
  233. location,
  234. organization,
  235. router,
  236. errorsViewSaved,
  237. {...savedQuery, yAxis: 'count()'},
  238. ['count()']
  239. );
  240. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  241. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  242. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  243. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  244. expect(buttonSaveAs.exists()).toBe(false);
  245. expect(buttonSaved.exists()).toBe(true);
  246. expect(buttonUpdate.exists()).toBe(false);
  247. expect(buttonDelete.exists()).toBe(true);
  248. });
  249. it('deletes the saved query', async () => {
  250. const {wrapper} = generateWrappedComponent(
  251. location,
  252. organization,
  253. router,
  254. errorsViewSaved,
  255. savedQuery,
  256. yAxis
  257. );
  258. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE).first();
  259. await buttonDelete.simulate('click');
  260. expect(mockUtils).toHaveBeenCalledWith(
  261. expect.anything(), // api
  262. organization,
  263. expect.objectContaining({id: '1'})
  264. );
  265. });
  266. });
  267. describe('modifying a saved query', () => {
  268. let mockUtils;
  269. it('renders the correct set of buttons', () => {
  270. const {wrapper} = generateWrappedComponent(
  271. location,
  272. organization,
  273. router,
  274. errorsViewModified,
  275. errorsViewSaved.toNewQuery(),
  276. yAxis
  277. );
  278. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  279. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  280. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  281. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  282. expect(buttonSaveAs.exists()).toBe(true);
  283. expect(buttonSaved.exists()).toBe(false);
  284. expect(buttonUpdate.exists()).toBe(true);
  285. expect(buttonDelete.exists()).toBe(true);
  286. });
  287. describe('updates the saved query', () => {
  288. beforeEach(() => {
  289. mockUtils = jest
  290. .spyOn(utils, 'handleUpdateQuery')
  291. .mockImplementation(() => Promise.resolve(savedQuery));
  292. });
  293. afterEach(() => {
  294. mockUtils.mockClear();
  295. });
  296. it('accepts a well-formed query', async () => {
  297. const {mockSetSavedQuery, wrapper} = generateWrappedComponent(
  298. location,
  299. organization,
  300. router,
  301. errorsViewModified,
  302. savedQuery,
  303. yAxis
  304. );
  305. // Click on Save in the Dropdown
  306. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE).first();
  307. await buttonUpdate.simulate('click');
  308. expect(mockUtils).toHaveBeenCalledWith(
  309. expect.anything(), // api
  310. organization,
  311. expect.objectContaining({
  312. ...errorsViewModified,
  313. }),
  314. yAxis
  315. );
  316. expect(mockSetSavedQuery).toHaveBeenCalled();
  317. });
  318. });
  319. describe('creates a separate query', () => {
  320. beforeEach(() => {
  321. mockUtils = jest
  322. .spyOn(utils, 'handleCreateQuery')
  323. .mockImplementation(() => Promise.resolve(savedQuery));
  324. });
  325. afterEach(() => {
  326. mockUtils.mockClear();
  327. });
  328. it('checks that it is forked from a saved query', () => {
  329. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  330. // Click on ButtonSaveAs to open dropdown
  331. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  332. // Fill in the Input
  333. userEvent.type(screen.getByPlaceholderText('Display name'), 'Forked Query');
  334. // Click on Save in the Dropdown
  335. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  336. expect(mockUtils).toHaveBeenCalledWith(
  337. expect.anything(), // api
  338. organization,
  339. expect.objectContaining({
  340. ...errorsViewModified,
  341. name: 'Forked Query',
  342. }),
  343. yAxis,
  344. false
  345. );
  346. });
  347. });
  348. });
  349. describe('create alert from discover', () => {
  350. it('renders create alert button when metrics alerts is enabled', () => {
  351. const metricAlertOrg = {
  352. ...organization,
  353. features: ['incidents'],
  354. };
  355. const {wrapper} = generateWrappedComponent(
  356. location,
  357. metricAlertOrg,
  358. router,
  359. errorsViewModified,
  360. savedQuery,
  361. yAxis
  362. );
  363. const buttonCreateAlert = wrapper.find(SELECTOR_BUTTON_CREATE_ALERT);
  364. expect(buttonCreateAlert.exists()).toBe(true);
  365. });
  366. it('does not render create alert button without metric alerts', () => {
  367. const {wrapper} = generateWrappedComponent(
  368. location,
  369. organization,
  370. router,
  371. errorsViewModified,
  372. savedQuery,
  373. yAxis
  374. );
  375. const buttonCreateAlert = wrapper.find(SELECTOR_BUTTON_CREATE_ALERT);
  376. expect(buttonCreateAlert.exists()).toBe(false);
  377. });
  378. });
  379. });