index.spec.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  4. import type {NewQuery} from 'sentry/types/organization';
  5. import EventView from 'sentry/utils/discover/eventView';
  6. import {DisplayModes, SavedQueryDatasets} from 'sentry/utils/discover/types';
  7. import {WidgetType} from 'sentry/views/dashboards/types';
  8. import {getAllViews} from 'sentry/views/discover/data';
  9. import SavedQueryButtonGroup from 'sentry/views/discover/savedQuery';
  10. import * as utils from 'sentry/views/discover/savedQuery/utils';
  11. jest.mock('sentry/actionCreators/modal');
  12. function mount(
  13. location,
  14. organization,
  15. router,
  16. eventView,
  17. savedQuery,
  18. yAxis,
  19. disabled = false,
  20. setSavedQuery = jest.fn()
  21. ) {
  22. return render(
  23. <SavedQueryButtonGroup
  24. location={location}
  25. organization={organization}
  26. eventView={eventView}
  27. savedQuery={savedQuery}
  28. disabled={disabled}
  29. updateCallback={() => {}}
  30. yAxis={yAxis}
  31. router={router}
  32. queryDataLoading={false}
  33. setSavedQuery={setSavedQuery}
  34. setHomepageQuery={jest.fn()}
  35. />
  36. );
  37. }
  38. describe('Discover > SaveQueryButtonGroup', function () {
  39. let organization,
  40. errorsView,
  41. savedQuery,
  42. errorsViewSaved,
  43. errorsViewModified,
  44. errorsQuery;
  45. const location = {
  46. pathname: '/organization/eventsv2/',
  47. query: {},
  48. };
  49. const router = {
  50. location: {query: {}},
  51. };
  52. const yAxis = ['count()', 'failure_count()'];
  53. beforeEach(() => {
  54. organization = OrganizationFixture({
  55. features: ['discover-query', 'dashboards-edit'],
  56. });
  57. errorsQuery = {
  58. ...(getAllViews(organization).find(
  59. view => view.name === 'Errors by Title'
  60. ) as NewQuery),
  61. yAxis: ['count()'],
  62. display: DisplayModes.DEFAULT,
  63. };
  64. errorsView = EventView.fromSavedQuery(errorsQuery);
  65. errorsViewSaved = EventView.fromSavedQuery(errorsQuery);
  66. errorsViewSaved.id = '1';
  67. errorsViewModified = EventView.fromSavedQuery(errorsQuery);
  68. errorsViewModified.id = '1';
  69. errorsViewModified.name = 'Modified Name';
  70. savedQuery = {
  71. ...errorsViewSaved.toNewQuery(),
  72. yAxis,
  73. dateCreated: '',
  74. dateUpdated: '',
  75. id: '1',
  76. };
  77. });
  78. afterEach(() => {
  79. MockApiClient.clearMockResponses();
  80. jest.clearAllMocks();
  81. });
  82. describe('building on a new query', () => {
  83. const mockUtils = jest
  84. .spyOn(utils, 'handleCreateQuery')
  85. .mockImplementation(() => Promise.resolve(savedQuery));
  86. beforeEach(() => {
  87. mockUtils.mockClear();
  88. });
  89. it('renders disabled buttons when disabled prop is used', () => {
  90. mount(location, organization, router, errorsView, undefined, yAxis, true);
  91. expect(screen.getByRole('button', {name: /save as/i})).toBeDisabled();
  92. });
  93. it('renders the correct set of buttons', async () => {
  94. mount(location, organization, router, errorsView, undefined, yAxis);
  95. expect(screen.getByRole('button', {name: /save as/i})).toBeInTheDocument();
  96. expect(
  97. screen.queryByRole('button', {name: /save changes/i})
  98. ).not.toBeInTheDocument();
  99. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  100. expect(
  101. screen.queryByRole('menuitemradio', {name: /delete saved query/i})
  102. ).not.toBeInTheDocument();
  103. });
  104. it('renders the correct set of buttons with the homepage query feature', async () => {
  105. organization = OrganizationFixture({
  106. features: ['discover-query', 'dashboards-edit'],
  107. });
  108. mount(location, organization, router, errorsView, undefined, yAxis);
  109. expect(screen.getByRole('button', {name: /save as/i})).toBeInTheDocument();
  110. expect(screen.getByRole('button', {name: /set as default/i})).toBeInTheDocument();
  111. expect(screen.getByRole('button', {name: /saved queries/i})).toBeInTheDocument();
  112. expect(
  113. screen.getByRole('button', {name: /discover context menu/i})
  114. ).toBeInTheDocument();
  115. expect(
  116. screen.queryByRole('button', {name: /save changes/i})
  117. ).not.toBeInTheDocument();
  118. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  119. expect(
  120. screen.queryByRole('menuitemradio', {name: /add to dashboard/i})
  121. ).toBeInTheDocument();
  122. });
  123. it('opens dashboard modal with the right props', async () => {
  124. organization = OrganizationFixture({
  125. features: [
  126. 'discover-query',
  127. 'dashboards-edit',
  128. 'performance-discover-dataset-selector',
  129. ],
  130. });
  131. mount(
  132. location,
  133. organization,
  134. router,
  135. errorsView,
  136. {...savedQuery, queryDataset: SavedQueryDatasets.ERRORS},
  137. yAxis
  138. );
  139. expect(
  140. screen.getByRole('button', {name: /discover context menu/i})
  141. ).toBeInTheDocument();
  142. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  143. expect(
  144. screen.queryByRole('menuitemradio', {name: /add to dashboard/i})
  145. ).toBeInTheDocument();
  146. await userEvent.click(
  147. screen.getByRole('menuitemradio', {name: /add to dashboard/i})
  148. );
  149. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  150. expect.objectContaining({
  151. widget: {
  152. displayType: 'line',
  153. interval: undefined,
  154. limit: undefined,
  155. queries: [
  156. {
  157. aggregates: ['count()', 'failure_count()'],
  158. columns: [],
  159. conditions: 'event.type:error',
  160. fields: ['count()', 'failure_count()'],
  161. name: '',
  162. orderby: '-count()',
  163. },
  164. ],
  165. title: 'Errors by Title',
  166. widgetType: WidgetType.ERRORS,
  167. },
  168. widgetAsQueryParams: expect.objectContaining({
  169. dataset: WidgetType.ERRORS,
  170. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  171. defaultTitle: 'Errors by Title',
  172. defaultWidgetQuery:
  173. 'name=&aggregates=count()%2Cfailure_count()&columns=&fields=count()%2Cfailure_count()&conditions=event.type%3Aerror&orderby=-count()',
  174. displayType: 'line',
  175. end: undefined,
  176. limit: undefined,
  177. source: 'discoverv2',
  178. start: undefined,
  179. statsPeriod: '24h',
  180. }),
  181. })
  182. );
  183. });
  184. it('hides the banner when save is complete.', async () => {
  185. mount(location, organization, router, errorsView, undefined, yAxis);
  186. // Click on ButtonSaveAs to open dropdown
  187. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  188. // Fill in the Input
  189. await userEvent.type(
  190. screen.getByPlaceholderText('Display name'),
  191. 'My New Query Name'
  192. );
  193. // Click on Save in the Dropdown
  194. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  195. // The banner should not render
  196. expect(screen.queryByText('Discover Trends')).not.toBeInTheDocument();
  197. });
  198. it('saves a well-formed query', async () => {
  199. mount(location, organization, router, errorsView, undefined, yAxis);
  200. // Click on ButtonSaveAs to open dropdown
  201. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  202. // Fill in the Input
  203. await userEvent.type(
  204. screen.getByPlaceholderText('Display name'),
  205. 'My New Query Name'
  206. );
  207. // Click on Save in the Dropdown
  208. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  209. expect(mockUtils).toHaveBeenCalledWith(
  210. expect.anything(), // api
  211. organization,
  212. expect.objectContaining({
  213. ...errorsView,
  214. name: 'My New Query Name',
  215. }),
  216. yAxis,
  217. true
  218. );
  219. });
  220. it('rejects if query.name is empty', async () => {
  221. mount(location, organization, router, errorsView, undefined, yAxis);
  222. // Click on ButtonSaveAs to open dropdown
  223. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  224. // Do not fill in Input
  225. // Click on Save in the Dropdown
  226. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  227. // Check that EventView has a name
  228. expect(errorsView.name).toBe('Errors by Title');
  229. expect(mockUtils).not.toHaveBeenCalled();
  230. });
  231. });
  232. describe('viewing a saved query', () => {
  233. let mockUtils;
  234. beforeEach(() => {
  235. mockUtils = jest
  236. .spyOn(utils, 'handleDeleteQuery')
  237. .mockImplementation(() => Promise.resolve());
  238. });
  239. afterEach(() => {
  240. mockUtils.mockClear();
  241. });
  242. it('renders the correct set of buttons', async () => {
  243. mount(
  244. location,
  245. organization,
  246. router,
  247. EventView.fromSavedQuery({...errorsQuery, yAxis}),
  248. savedQuery,
  249. yAxis
  250. );
  251. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  252. expect(
  253. screen.queryByRole('button', {name: /save changes/i})
  254. ).not.toBeInTheDocument();
  255. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  256. expect(
  257. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  258. ).toBeInTheDocument();
  259. });
  260. it('treats undefined yAxis the same as count() when checking for changes', async () => {
  261. mount(
  262. location,
  263. organization,
  264. router,
  265. errorsViewSaved,
  266. {...savedQuery, yAxis: undefined},
  267. ['count()']
  268. );
  269. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  270. expect(
  271. screen.queryByRole('button', {name: /save changes/i})
  272. ).not.toBeInTheDocument();
  273. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  274. expect(
  275. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  276. ).toBeInTheDocument();
  277. });
  278. it('converts string yAxis values to array when checking for changes', async () => {
  279. mount(
  280. location,
  281. organization,
  282. router,
  283. errorsViewSaved,
  284. {...savedQuery, yAxis: 'count()'},
  285. ['count()']
  286. );
  287. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  288. expect(
  289. screen.queryByRole('button', {name: /save changes/i})
  290. ).not.toBeInTheDocument();
  291. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  292. expect(
  293. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  294. ).toBeInTheDocument();
  295. });
  296. it('deletes the saved query', async () => {
  297. mount(location, organization, router, errorsViewSaved, savedQuery, yAxis);
  298. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  299. await userEvent.click(
  300. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  301. );
  302. expect(mockUtils).toHaveBeenCalledWith(
  303. expect.anything(), // api
  304. organization,
  305. expect.objectContaining({id: '1'})
  306. );
  307. });
  308. });
  309. describe('modifying a saved query', () => {
  310. let mockUtils;
  311. it('renders the correct set of buttons', async () => {
  312. mount(
  313. location,
  314. organization,
  315. router,
  316. errorsViewModified,
  317. errorsViewSaved.toNewQuery(),
  318. yAxis
  319. );
  320. expect(screen.queryByRole('button', {name: /save as/i})).toBeInTheDocument();
  321. expect(screen.getByRole('button', {name: /save changes/i})).toBeInTheDocument();
  322. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  323. expect(
  324. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  325. ).toBeInTheDocument();
  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 = jest.fn();
  338. mount(
  339. location,
  340. organization,
  341. router,
  342. errorsViewModified,
  343. savedQuery,
  344. yAxis,
  345. false,
  346. mockSetSavedQuery
  347. );
  348. // Click on Save in the Dropdown
  349. await userEvent.click(screen.getByRole('button', {name: /save changes/i}));
  350. await waitFor(() => {
  351. expect(mockUtils).toHaveBeenCalledWith(
  352. expect.anything(), // api
  353. organization,
  354. expect.objectContaining({
  355. ...errorsViewModified,
  356. }),
  357. yAxis
  358. );
  359. expect(mockSetSavedQuery).toHaveBeenCalled();
  360. });
  361. });
  362. });
  363. describe('creates a separate query', () => {
  364. beforeEach(() => {
  365. mockUtils = jest
  366. .spyOn(utils, 'handleCreateQuery')
  367. .mockImplementation(() => Promise.resolve(savedQuery));
  368. });
  369. afterEach(() => {
  370. mockUtils.mockClear();
  371. });
  372. it('checks that it is forked from a saved query', async () => {
  373. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  374. // Click on ButtonSaveAs to open dropdown
  375. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  376. // Fill in the Input
  377. await userEvent.type(screen.getByPlaceholderText('Display name'), 'Forked Query');
  378. // Click on Save in the Dropdown
  379. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  380. expect(mockUtils).toHaveBeenCalledWith(
  381. expect.anything(), // api
  382. organization,
  383. expect.objectContaining({
  384. ...errorsViewModified,
  385. name: 'Forked Query',
  386. }),
  387. yAxis,
  388. false
  389. );
  390. });
  391. });
  392. });
  393. describe('create alert from discover', () => {
  394. it('renders create alert button when metrics alerts is enabled', () => {
  395. const metricAlertOrg = {
  396. ...organization,
  397. features: ['incidents'],
  398. };
  399. mount(location, metricAlertOrg, router, errorsViewModified, savedQuery, yAxis);
  400. expect(screen.getByRole('button', {name: /create alert/i})).toBeInTheDocument();
  401. });
  402. it('does not render create alert button without metric alerts', () => {
  403. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  404. expect(
  405. screen.queryByRole('button', {name: /create alert/i})
  406. ).not.toBeInTheDocument();
  407. });
  408. it('uses the throughput alert type for transaction queries', () => {
  409. const metricAlertOrg = {
  410. ...organization,
  411. features: ['incidents', 'performance-discover-dataset-selector'],
  412. };
  413. const transactionSavedQuery = {
  414. ...savedQuery,
  415. queryDataset: SavedQueryDatasets.TRANSACTIONS,
  416. query: 'foo:bar',
  417. };
  418. const transactionView = EventView.fromSavedQuery(transactionSavedQuery);
  419. mount(
  420. location,
  421. metricAlertOrg,
  422. router,
  423. transactionView,
  424. transactionSavedQuery,
  425. yAxis
  426. );
  427. const createAlertButton = screen.getByRole('button', {name: /create alert/i});
  428. const href = createAlertButton.getAttribute('href')!;
  429. const queryParameters = new URLSearchParams(href.split('?')[1]);
  430. expect(queryParameters.get('query')).toEqual(
  431. '(foo:bar) AND (event.type:transaction)'
  432. );
  433. expect(queryParameters.get('dataset')).toEqual('transactions');
  434. expect(queryParameters.get('eventTypes')).toEqual('transaction');
  435. });
  436. it('uses the num errors alert type for error queries', () => {
  437. const metricAlertOrg = {
  438. ...organization,
  439. features: ['incidents', 'performance-discover-dataset-selector'],
  440. };
  441. const errorSavedQuery = {
  442. ...savedQuery,
  443. queryDataset: SavedQueryDatasets.ERRORS,
  444. query: 'foo:bar',
  445. };
  446. const transactionView = EventView.fromSavedQuery(errorSavedQuery);
  447. mount(location, metricAlertOrg, router, transactionView, errorSavedQuery, yAxis);
  448. const createAlertButton = screen.getByRole('button', {name: /create alert/i});
  449. const href = createAlertButton.getAttribute('href')!;
  450. const queryParameters = new URLSearchParams(href.split('?')[1]);
  451. expect(queryParameters.get('query')).toEqual('foo:bar');
  452. expect(queryParameters.get('dataset')).toEqual('events');
  453. expect(queryParameters.get('eventTypes')).toEqual('error');
  454. });
  455. });
  456. });