index.spec.tsx 15 KB

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