index.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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 {ALL_VIEWS} 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. const location = {
  41. pathname: '/organization/eventsv2/',
  42. query: {},
  43. };
  44. const router = {
  45. location: {query: {}},
  46. };
  47. const yAxis = ['count()', 'failure_count()'];
  48. const errorsQuery = {
  49. ...(ALL_VIEWS.find(view => view.name === 'Errors by Title') as NewQuery),
  50. yAxis: ['count()'],
  51. display: DisplayModes.DEFAULT,
  52. };
  53. const errorsView = EventView.fromSavedQuery(errorsQuery);
  54. const errorsViewSaved = EventView.fromSavedQuery(errorsQuery);
  55. errorsViewSaved.id = '1';
  56. const errorsViewModified = EventView.fromSavedQuery(errorsQuery);
  57. errorsViewModified.id = '1';
  58. errorsViewModified.name = 'Modified Name';
  59. const savedQuery = {
  60. ...errorsViewSaved.toNewQuery(),
  61. yAxis,
  62. dateCreated: '',
  63. dateUpdated: '',
  64. id: '1',
  65. };
  66. beforeEach(() => {
  67. organization = OrganizationFixture({
  68. features: ['discover-query', 'dashboards-edit'],
  69. });
  70. });
  71. afterEach(() => {
  72. MockApiClient.clearMockResponses();
  73. jest.clearAllMocks();
  74. });
  75. describe('building on a new query', () => {
  76. const mockUtils = jest
  77. .spyOn(utils, 'handleCreateQuery')
  78. .mockImplementation(() => Promise.resolve(savedQuery));
  79. beforeEach(() => {
  80. mockUtils.mockClear();
  81. });
  82. it('renders disabled buttons when disabled prop is used', () => {
  83. mount(location, organization, router, errorsView, undefined, yAxis, true);
  84. expect(screen.getByRole('button', {name: /save as/i})).toBeDisabled();
  85. });
  86. it('renders the correct set of buttons', async () => {
  87. mount(location, organization, router, errorsView, undefined, yAxis);
  88. expect(screen.getByRole('button', {name: /save as/i})).toBeInTheDocument();
  89. expect(
  90. screen.queryByRole('button', {name: /save changes/i})
  91. ).not.toBeInTheDocument();
  92. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  93. expect(
  94. screen.queryByRole('menuitemradio', {name: /delete saved query/i})
  95. ).not.toBeInTheDocument();
  96. });
  97. it('renders the correct set of buttons with the homepage query feature', async () => {
  98. organization = OrganizationFixture({
  99. features: ['discover-query', 'dashboards-edit'],
  100. });
  101. mount(location, organization, router, errorsView, undefined, yAxis);
  102. expect(screen.getByRole('button', {name: /save as/i})).toBeInTheDocument();
  103. expect(screen.getByRole('button', {name: /set as default/i})).toBeInTheDocument();
  104. expect(screen.getByRole('button', {name: /saved queries/i})).toBeInTheDocument();
  105. expect(
  106. screen.getByRole('button', {name: /discover context menu/i})
  107. ).toBeInTheDocument();
  108. expect(
  109. screen.queryByRole('button', {name: /save changes/i})
  110. ).not.toBeInTheDocument();
  111. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  112. expect(
  113. screen.queryByRole('menuitemradio', {name: /add to dashboard/i})
  114. ).toBeInTheDocument();
  115. });
  116. it('opens dashboard modal with the right props', async () => {
  117. organization = OrganizationFixture({
  118. features: [
  119. 'discover-query',
  120. 'dashboards-edit',
  121. 'performance-discover-dataset-selector',
  122. ],
  123. });
  124. mount(
  125. location,
  126. organization,
  127. router,
  128. errorsView,
  129. {...savedQuery, queryDataset: SavedQueryDatasets.ERRORS},
  130. yAxis
  131. );
  132. expect(
  133. screen.getByRole('button', {name: /discover context menu/i})
  134. ).toBeInTheDocument();
  135. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  136. expect(
  137. screen.queryByRole('menuitemradio', {name: /add to dashboard/i})
  138. ).toBeInTheDocument();
  139. await userEvent.click(
  140. screen.getByRole('menuitemradio', {name: /add to dashboard/i})
  141. );
  142. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  143. expect.objectContaining({
  144. widget: {
  145. displayType: 'line',
  146. interval: undefined,
  147. limit: undefined,
  148. queries: [
  149. {
  150. aggregates: ['count()', 'failure_count()'],
  151. columns: [],
  152. conditions: 'event.type:error',
  153. fields: ['count()', 'failure_count()'],
  154. name: '',
  155. orderby: '-count()',
  156. },
  157. ],
  158. title: 'Errors by Title',
  159. widgetType: WidgetType.ERRORS,
  160. },
  161. widgetAsQueryParams: expect.objectContaining({
  162. dataset: WidgetType.ERRORS,
  163. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  164. defaultTitle: 'Errors by Title',
  165. defaultWidgetQuery:
  166. 'name=&aggregates=count()%2Cfailure_count()&columns=&fields=count()%2Cfailure_count()&conditions=event.type%3Aerror&orderby=-count()',
  167. displayType: 'line',
  168. end: undefined,
  169. limit: undefined,
  170. source: 'discoverv2',
  171. start: undefined,
  172. statsPeriod: '24h',
  173. }),
  174. })
  175. );
  176. });
  177. it('hides the banner when save is complete.', async () => {
  178. mount(location, organization, router, errorsView, undefined, yAxis);
  179. // Click on ButtonSaveAs to open dropdown
  180. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  181. // Fill in the Input
  182. await userEvent.type(
  183. screen.getByPlaceholderText('Display name'),
  184. 'My New Query Name'
  185. );
  186. // Click on Save in the Dropdown
  187. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  188. // The banner should not render
  189. expect(screen.queryByText('Discover Trends')).not.toBeInTheDocument();
  190. });
  191. it('saves a well-formed query', async () => {
  192. mount(location, organization, router, errorsView, undefined, yAxis);
  193. // Click on ButtonSaveAs to open dropdown
  194. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  195. // Fill in the Input
  196. await userEvent.type(
  197. screen.getByPlaceholderText('Display name'),
  198. 'My New Query Name'
  199. );
  200. // Click on Save in the Dropdown
  201. await 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', async () => {
  214. mount(location, organization, router, errorsView, undefined, yAxis);
  215. // Click on ButtonSaveAs to open dropdown
  216. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  217. // Do not fill in Input
  218. // Click on Save in the Dropdown
  219. await 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', async () => {
  236. mount(
  237. location,
  238. organization,
  239. router,
  240. EventView.fromSavedQuery({...errorsQuery, yAxis}),
  241. savedQuery,
  242. yAxis
  243. );
  244. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  245. expect(
  246. screen.queryByRole('button', {name: /save changes/i})
  247. ).not.toBeInTheDocument();
  248. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  249. expect(
  250. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  251. ).toBeInTheDocument();
  252. });
  253. it('treats undefined yAxis the same as count() when checking for changes', async () => {
  254. mount(
  255. location,
  256. organization,
  257. router,
  258. errorsViewSaved,
  259. {...savedQuery, yAxis: undefined},
  260. ['count()']
  261. );
  262. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  263. expect(
  264. screen.queryByRole('button', {name: /save changes/i})
  265. ).not.toBeInTheDocument();
  266. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  267. expect(
  268. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  269. ).toBeInTheDocument();
  270. });
  271. it('converts string yAxis values to array when checking for changes', async () => {
  272. mount(
  273. location,
  274. organization,
  275. router,
  276. errorsViewSaved,
  277. {...savedQuery, yAxis: 'count()'},
  278. ['count()']
  279. );
  280. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  281. expect(
  282. screen.queryByRole('button', {name: /save changes/i})
  283. ).not.toBeInTheDocument();
  284. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  285. expect(
  286. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  287. ).toBeInTheDocument();
  288. });
  289. it('deletes the saved query', async () => {
  290. mount(location, organization, router, errorsViewSaved, savedQuery, yAxis);
  291. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  292. await userEvent.click(
  293. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  294. );
  295. expect(mockUtils).toHaveBeenCalledWith(
  296. expect.anything(), // api
  297. organization,
  298. expect.objectContaining({id: '1'})
  299. );
  300. });
  301. });
  302. describe('modifying a saved query', () => {
  303. let mockUtils;
  304. it('renders the correct set of buttons', async () => {
  305. mount(
  306. location,
  307. organization,
  308. router,
  309. errorsViewModified,
  310. errorsViewSaved.toNewQuery(),
  311. yAxis
  312. );
  313. expect(screen.queryByRole('button', {name: /save as/i})).toBeInTheDocument();
  314. expect(screen.getByRole('button', {name: /save changes/i})).toBeInTheDocument();
  315. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  316. expect(
  317. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  318. ).toBeInTheDocument();
  319. });
  320. describe('updates the saved query', () => {
  321. beforeEach(() => {
  322. mockUtils = jest
  323. .spyOn(utils, 'handleUpdateQuery')
  324. .mockImplementation(() => Promise.resolve(savedQuery));
  325. });
  326. afterEach(() => {
  327. mockUtils.mockClear();
  328. });
  329. it('accepts a well-formed query', async () => {
  330. const mockSetSavedQuery = jest.fn();
  331. mount(
  332. location,
  333. organization,
  334. router,
  335. errorsViewModified,
  336. savedQuery,
  337. yAxis,
  338. false,
  339. mockSetSavedQuery
  340. );
  341. // Click on Save in the Dropdown
  342. await userEvent.click(screen.getByRole('button', {name: /save changes/i}));
  343. await waitFor(() => {
  344. expect(mockUtils).toHaveBeenCalledWith(
  345. expect.anything(), // api
  346. organization,
  347. expect.objectContaining({
  348. ...errorsViewModified,
  349. }),
  350. yAxis
  351. );
  352. expect(mockSetSavedQuery).toHaveBeenCalled();
  353. });
  354. });
  355. });
  356. describe('creates a separate query', () => {
  357. beforeEach(() => {
  358. mockUtils = jest
  359. .spyOn(utils, 'handleCreateQuery')
  360. .mockImplementation(() => Promise.resolve(savedQuery));
  361. });
  362. afterEach(() => {
  363. mockUtils.mockClear();
  364. });
  365. it('checks that it is forked from a saved query', async () => {
  366. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  367. // Click on ButtonSaveAs to open dropdown
  368. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  369. // Fill in the Input
  370. await userEvent.type(screen.getByPlaceholderText('Display name'), 'Forked Query');
  371. // Click on Save in the Dropdown
  372. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  373. expect(mockUtils).toHaveBeenCalledWith(
  374. expect.anything(), // api
  375. organization,
  376. expect.objectContaining({
  377. ...errorsViewModified,
  378. name: 'Forked Query',
  379. }),
  380. yAxis,
  381. false
  382. );
  383. });
  384. });
  385. });
  386. describe('create alert from discover', () => {
  387. it('renders create alert button when metrics alerts is enabled', () => {
  388. const metricAlertOrg = {
  389. ...organization,
  390. features: ['incidents'],
  391. };
  392. mount(location, metricAlertOrg, router, errorsViewModified, savedQuery, yAxis);
  393. expect(screen.getByRole('button', {name: /create alert/i})).toBeInTheDocument();
  394. });
  395. it('does not render create alert button without metric alerts', () => {
  396. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  397. expect(
  398. screen.queryByRole('button', {name: /create alert/i})
  399. ).not.toBeInTheDocument();
  400. });
  401. });
  402. });