index.spec.tsx 17 KB

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