index.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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(location, organization, router, errorsViewSaved, savedQuery, yAxis);
  173. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  174. expect(
  175. screen.queryByRole('button', {name: /save changes/i})
  176. ).not.toBeInTheDocument();
  177. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  178. expect(
  179. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  180. ).toBeInTheDocument();
  181. });
  182. it('treats undefined yAxis the same as count() when checking for changes', async () => {
  183. mount(
  184. location,
  185. organization,
  186. router,
  187. errorsViewSaved,
  188. {...savedQuery, yAxis: undefined},
  189. ['count()']
  190. );
  191. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  192. expect(
  193. screen.queryByRole('button', {name: /save changes/i})
  194. ).not.toBeInTheDocument();
  195. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  196. expect(
  197. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  198. ).toBeInTheDocument();
  199. });
  200. it('converts string yAxis values to array when checking for changes', async () => {
  201. mount(
  202. location,
  203. organization,
  204. router,
  205. errorsViewSaved,
  206. {...savedQuery, yAxis: 'count()'},
  207. ['count()']
  208. );
  209. expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument();
  210. expect(
  211. screen.queryByRole('button', {name: /save changes/i})
  212. ).not.toBeInTheDocument();
  213. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  214. expect(
  215. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  216. ).toBeInTheDocument();
  217. });
  218. it('deletes the saved query', async () => {
  219. mount(location, organization, router, errorsViewSaved, savedQuery, yAxis);
  220. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  221. await userEvent.click(
  222. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  223. );
  224. expect(mockUtils).toHaveBeenCalledWith(
  225. expect.anything(), // api
  226. organization,
  227. expect.objectContaining({id: '1'})
  228. );
  229. });
  230. });
  231. describe('modifying a saved query', () => {
  232. let mockUtils;
  233. it('renders the correct set of buttons', async () => {
  234. mount(
  235. location,
  236. organization,
  237. router,
  238. errorsViewModified,
  239. errorsViewSaved.toNewQuery(),
  240. yAxis
  241. );
  242. expect(screen.queryByRole('button', {name: /save as/i})).toBeInTheDocument();
  243. expect(screen.getByRole('button', {name: /save changes/i})).toBeInTheDocument();
  244. await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
  245. expect(
  246. screen.getByRole('menuitemradio', {name: /delete saved query/i})
  247. ).toBeInTheDocument();
  248. });
  249. describe('updates the saved query', () => {
  250. beforeEach(() => {
  251. mockUtils = jest
  252. .spyOn(utils, 'handleUpdateQuery')
  253. .mockImplementation(() => Promise.resolve(savedQuery));
  254. });
  255. afterEach(() => {
  256. mockUtils.mockClear();
  257. });
  258. it('accepts a well-formed query', async () => {
  259. const mockSetSavedQuery = jest.fn();
  260. mount(
  261. location,
  262. organization,
  263. router,
  264. errorsViewModified,
  265. savedQuery,
  266. yAxis,
  267. false,
  268. mockSetSavedQuery
  269. );
  270. // Click on Save in the Dropdown
  271. await userEvent.click(screen.getByRole('button', {name: /save changes/i}));
  272. await waitFor(() => {
  273. expect(mockUtils).toHaveBeenCalledWith(
  274. expect.anything(), // api
  275. organization,
  276. expect.objectContaining({
  277. ...errorsViewModified,
  278. }),
  279. yAxis
  280. );
  281. expect(mockSetSavedQuery).toHaveBeenCalled();
  282. });
  283. });
  284. });
  285. describe('creates a separate query', () => {
  286. beforeEach(() => {
  287. mockUtils = jest
  288. .spyOn(utils, 'handleCreateQuery')
  289. .mockImplementation(() => Promise.resolve(savedQuery));
  290. });
  291. afterEach(() => {
  292. mockUtils.mockClear();
  293. });
  294. it('checks that it is forked from a saved query', async () => {
  295. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  296. // Click on ButtonSaveAs to open dropdown
  297. await userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  298. // Fill in the Input
  299. await userEvent.type(screen.getByPlaceholderText('Display name'), 'Forked Query');
  300. // Click on Save in the Dropdown
  301. await userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  302. expect(mockUtils).toHaveBeenCalledWith(
  303. expect.anything(), // api
  304. organization,
  305. expect.objectContaining({
  306. ...errorsViewModified,
  307. name: 'Forked Query',
  308. }),
  309. yAxis,
  310. false
  311. );
  312. });
  313. });
  314. });
  315. describe('create alert from discover', () => {
  316. it('renders create alert button when metrics alerts is enabled', () => {
  317. const metricAlertOrg = {
  318. ...organization,
  319. features: ['incidents'],
  320. };
  321. mount(location, metricAlertOrg, router, errorsViewModified, savedQuery, yAxis);
  322. expect(screen.getByRole('button', {name: /create alert/i})).toBeInTheDocument();
  323. });
  324. it('does not render create alert button without metric alerts', () => {
  325. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  326. expect(
  327. screen.queryByRole('button', {name: /create alert/i})
  328. ).not.toBeInTheDocument();
  329. });
  330. });
  331. });