index.spec.tsx 12 KB

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