index.spec.tsx 12 KB

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