index.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import {
  5. openAddDashboardWidgetModal,
  6. openAddToDashboardModal,
  7. } from 'sentry/actionCreators/modal';
  8. import {NewQuery} from 'sentry/types';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import {DisplayModes} from 'sentry/utils/discover/types';
  11. import {DashboardWidgetSource, DisplayType} from 'sentry/views/dashboardsV2/types';
  12. import DiscoverBanner from 'sentry/views/eventsV2/banner';
  13. import {ALL_VIEWS} from 'sentry/views/eventsV2/data';
  14. import SavedQueryButtonGroup from 'sentry/views/eventsV2/savedQuery';
  15. import * as utils from 'sentry/views/eventsV2/savedQuery/utils';
  16. const SELECTOR_BUTTON_SAVE_AS = 'button[aria-label="Save as"]';
  17. const SELECTOR_BUTTON_SAVED = '[data-test-id="discover2-savedquery-button-saved"]';
  18. const SELECTOR_BUTTON_UPDATE = '[data-test-id="discover2-savedquery-button-update"]';
  19. const SELECTOR_BUTTON_DELETE = '[data-test-id="discover2-savedquery-button-delete"]';
  20. const SELECTOR_BUTTON_CREATE_ALERT = '[data-test-id="discover2-create-from-discover"]';
  21. jest.mock('sentry/actionCreators/modal');
  22. function mount(
  23. location,
  24. organization,
  25. router,
  26. eventView,
  27. savedQuery,
  28. yAxis,
  29. disabled = false
  30. ) {
  31. return render(
  32. <SavedQueryButtonGroup
  33. location={location}
  34. organization={organization}
  35. eventView={eventView}
  36. savedQuery={savedQuery}
  37. disabled={disabled}
  38. updateCallback={() => {}}
  39. yAxis={yAxis}
  40. router={router}
  41. savedQueryLoading={false}
  42. />
  43. );
  44. }
  45. function generateWrappedComponent(
  46. location,
  47. organization,
  48. router,
  49. eventView,
  50. savedQuery,
  51. yAxis,
  52. disabled = false
  53. ) {
  54. return mountWithTheme(
  55. <SavedQueryButtonGroup
  56. location={location}
  57. organization={organization}
  58. eventView={eventView}
  59. savedQuery={savedQuery}
  60. disabled={disabled}
  61. updateCallback={() => {}}
  62. yAxis={yAxis}
  63. router={router}
  64. savedQueryLoading={false}
  65. />
  66. );
  67. }
  68. describe('EventsV2 > SaveQueryButtonGroup', function () {
  69. const organization = TestStubs.Organization({
  70. features: ['discover-query', 'dashboards-edit'],
  71. });
  72. const location = {
  73. pathname: '/organization/eventsv2/',
  74. query: {},
  75. };
  76. const router = {
  77. location: {query: {}},
  78. };
  79. const yAxis = ['count()', 'failure_count()'];
  80. const errorsQuery = {
  81. ...(ALL_VIEWS.find(view => view.name === 'Errors by Title') as NewQuery),
  82. yAxis: ['count()'],
  83. display: DisplayModes.DEFAULT,
  84. };
  85. const errorsView = EventView.fromSavedQuery(errorsQuery);
  86. const errorsViewSaved = EventView.fromSavedQuery(errorsQuery);
  87. errorsViewSaved.id = '1';
  88. const errorsViewModified = EventView.fromSavedQuery(errorsQuery);
  89. errorsViewModified.id = '1';
  90. errorsViewModified.name = 'Modified Name';
  91. const savedQuery = {
  92. ...errorsViewSaved.toNewQuery(),
  93. yAxis,
  94. dateCreated: '',
  95. dateUpdated: '',
  96. id: '1',
  97. };
  98. afterEach(() => {
  99. MockApiClient.clearMockResponses();
  100. jest.clearAllMocks();
  101. });
  102. describe('building on a new query', () => {
  103. const mockUtils = jest
  104. .spyOn(utils, 'handleCreateQuery')
  105. .mockImplementation(() => Promise.resolve(savedQuery));
  106. beforeEach(() => {
  107. mockUtils.mockClear();
  108. });
  109. it('renders disabled buttons when disabled prop is used', () => {
  110. const wrapper = generateWrappedComponent(
  111. location,
  112. organization,
  113. router,
  114. errorsView,
  115. undefined,
  116. yAxis,
  117. true
  118. );
  119. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  120. expect(buttonSaveAs.props()['aria-disabled']).toBe(true);
  121. });
  122. it('renders the correct set of buttons', () => {
  123. const wrapper = generateWrappedComponent(
  124. location,
  125. organization,
  126. router,
  127. errorsView,
  128. undefined,
  129. yAxis
  130. );
  131. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  132. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  133. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  134. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  135. expect(buttonSaveAs.exists()).toBe(true);
  136. expect(buttonSaved.exists()).toBe(false);
  137. expect(buttonUpdate.exists()).toBe(false);
  138. expect(buttonDelete.exists()).toBe(false);
  139. });
  140. it('hides the banner when save is complete.', () => {
  141. mount(location, organization, router, errorsView, undefined, yAxis);
  142. // Click on ButtonSaveAs to open dropdown
  143. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  144. // Fill in the Input
  145. userEvent.type(screen.getByPlaceholderText('Display name'), 'My New Query Name');
  146. // Click on Save in the Dropdown
  147. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  148. // The banner should not render
  149. mountWithTheme(<DiscoverBanner organization={organization} resultsUrl="" />);
  150. expect(screen.queryByText('Discover Trends')).not.toBeInTheDocument();
  151. });
  152. it('saves a well-formed query', () => {
  153. mount(location, organization, router, errorsView, undefined, yAxis);
  154. // Click on ButtonSaveAs to open dropdown
  155. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  156. // Fill in the Input
  157. userEvent.type(screen.getByPlaceholderText('Display name'), 'My New Query Name');
  158. // Click on Save in the Dropdown
  159. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  160. expect(mockUtils).toHaveBeenCalledWith(
  161. expect.anything(), // api
  162. organization,
  163. expect.objectContaining({
  164. ...errorsView,
  165. name: 'My New Query Name',
  166. }),
  167. yAxis,
  168. true
  169. );
  170. });
  171. it('rejects if query.name is empty', () => {
  172. mount(location, organization, router, errorsView, undefined, yAxis);
  173. // Click on ButtonSaveAs to open dropdown
  174. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  175. // Do not fill in Input
  176. // Click on Save in the Dropdown
  177. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  178. // Check that EventView has a name
  179. expect(errorsView.name).toBe('Errors by Title');
  180. expect(mockUtils).not.toHaveBeenCalled();
  181. });
  182. });
  183. describe('viewing a saved query', () => {
  184. let mockUtils;
  185. beforeEach(() => {
  186. mockUtils = jest
  187. .spyOn(utils, 'handleDeleteQuery')
  188. .mockImplementation(() => Promise.resolve());
  189. });
  190. afterEach(() => {
  191. mockUtils.mockClear();
  192. });
  193. it('renders the correct set of buttons', () => {
  194. const wrapper = generateWrappedComponent(
  195. location,
  196. organization,
  197. router,
  198. errorsViewSaved,
  199. savedQuery,
  200. yAxis
  201. );
  202. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  203. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  204. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  205. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  206. expect(buttonSaveAs.exists()).toBe(false);
  207. expect(buttonSaved.exists()).toBe(true);
  208. expect(buttonUpdate.exists()).toBe(false);
  209. expect(buttonDelete.exists()).toBe(true);
  210. });
  211. it('treats undefined yAxis the same as count() when checking for changes', () => {
  212. const wrapper = generateWrappedComponent(
  213. location,
  214. organization,
  215. router,
  216. errorsViewSaved,
  217. {...savedQuery, yAxis: undefined},
  218. ['count()']
  219. );
  220. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  221. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  222. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  223. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  224. expect(buttonSaveAs.exists()).toBe(false);
  225. expect(buttonSaved.exists()).toBe(true);
  226. expect(buttonUpdate.exists()).toBe(false);
  227. expect(buttonDelete.exists()).toBe(true);
  228. });
  229. it('converts string yAxis values to array when checking for changes', () => {
  230. const wrapper = generateWrappedComponent(
  231. location,
  232. organization,
  233. router,
  234. errorsViewSaved,
  235. {...savedQuery, yAxis: 'count()'},
  236. ['count()']
  237. );
  238. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  239. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  240. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  241. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  242. expect(buttonSaveAs.exists()).toBe(false);
  243. expect(buttonSaved.exists()).toBe(true);
  244. expect(buttonUpdate.exists()).toBe(false);
  245. expect(buttonDelete.exists()).toBe(true);
  246. });
  247. it('deletes the saved query', async () => {
  248. const wrapper = generateWrappedComponent(
  249. location,
  250. organization,
  251. router,
  252. errorsViewSaved,
  253. savedQuery,
  254. yAxis
  255. );
  256. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE).first();
  257. await buttonDelete.simulate('click');
  258. expect(mockUtils).toHaveBeenCalledWith(
  259. expect.anything(), // api
  260. organization,
  261. expect.objectContaining({id: '1'})
  262. );
  263. });
  264. it('opens a modal with the correct params for top 5 display mode', async function () {
  265. const featuredOrganization = TestStubs.Organization({
  266. features: ['dashboards-edit', 'new-widget-builder-experience-design'],
  267. });
  268. const testData = initializeOrg({
  269. ...initializeOrg(),
  270. organization: featuredOrganization,
  271. });
  272. const savedTopNQuery = TestStubs.DiscoverSavedQuery({
  273. display: DisplayModes.TOP5,
  274. orderby: 'test',
  275. fields: ['test', 'count()'],
  276. topEvents: '2',
  277. });
  278. mount(
  279. testData.router.location,
  280. testData.organization,
  281. testData.router,
  282. EventView.fromSavedQuery(savedTopNQuery),
  283. savedTopNQuery,
  284. ['count()']
  285. );
  286. userEvent.click(screen.getByText('Add to Dashboard'));
  287. expect(openAddDashboardWidgetModal).not.toHaveBeenCalled();
  288. await waitFor(() => {
  289. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  290. expect.objectContaining({
  291. widget: {
  292. title: 'Saved query #1',
  293. displayType: DisplayType.AREA,
  294. limit: 2,
  295. queries: [
  296. {
  297. aggregates: ['count()'],
  298. columns: ['test'],
  299. conditions: '',
  300. fields: ['test', 'count()', 'count()'],
  301. name: '',
  302. orderby: 'test',
  303. },
  304. ],
  305. },
  306. widgetAsQueryParams: expect.objectContaining({
  307. defaultTableColumns: ['test', 'count()'],
  308. defaultTitle: 'Saved query #1',
  309. defaultWidgetQuery:
  310. 'name=&aggregates=count()&columns=test&fields=test%2Ccount()%2Ccount()&conditions=&orderby=test',
  311. displayType: DisplayType.AREA,
  312. source: DashboardWidgetSource.DISCOVERV2,
  313. }),
  314. })
  315. );
  316. });
  317. });
  318. it('opens a modal with the correct params for default display mode', async function () {
  319. const featuredOrganization = TestStubs.Organization({
  320. features: ['dashboards-edit', 'new-widget-builder-experience-design'],
  321. });
  322. const testData = initializeOrg({
  323. ...initializeOrg(),
  324. organization: featuredOrganization,
  325. });
  326. const savedDefaultQuery = TestStubs.DiscoverSavedQuery({
  327. display: DisplayModes.DEFAULT,
  328. orderby: 'count()',
  329. fields: ['test', 'count()'],
  330. yAxis: ['count()'],
  331. });
  332. mount(
  333. testData.router.location,
  334. testData.organization,
  335. testData.router,
  336. EventView.fromSavedQuery(savedDefaultQuery),
  337. savedDefaultQuery,
  338. ['count()']
  339. );
  340. userEvent.click(screen.getByText('Add to Dashboard'));
  341. expect(openAddDashboardWidgetModal).not.toHaveBeenCalled();
  342. await waitFor(() => {
  343. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  344. expect.objectContaining({
  345. widget: {
  346. title: 'Saved query #1',
  347. displayType: DisplayType.LINE,
  348. queries: [
  349. {
  350. aggregates: ['count()'],
  351. columns: [],
  352. conditions: '',
  353. fields: ['count()'],
  354. name: '',
  355. // Orderby gets dropped because ordering only applies to
  356. // Top-N and tables
  357. orderby: '',
  358. },
  359. ],
  360. },
  361. widgetAsQueryParams: expect.objectContaining({
  362. defaultTableColumns: ['test', 'count()'],
  363. defaultTitle: 'Saved query #1',
  364. defaultWidgetQuery:
  365. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  366. displayType: DisplayType.LINE,
  367. source: DashboardWidgetSource.DISCOVERV2,
  368. }),
  369. })
  370. );
  371. });
  372. });
  373. });
  374. describe('modifying a saved query', () => {
  375. let mockUtils;
  376. it('renders the correct set of buttons', () => {
  377. const wrapper = generateWrappedComponent(
  378. location,
  379. organization,
  380. router,
  381. errorsViewModified,
  382. errorsViewSaved.toNewQuery(),
  383. yAxis
  384. );
  385. const buttonSaveAs = wrapper.find(SELECTOR_BUTTON_SAVE_AS);
  386. const buttonSaved = wrapper.find(SELECTOR_BUTTON_SAVED);
  387. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE);
  388. const buttonDelete = wrapper.find(SELECTOR_BUTTON_DELETE);
  389. expect(buttonSaveAs.exists()).toBe(true);
  390. expect(buttonSaved.exists()).toBe(false);
  391. expect(buttonUpdate.exists()).toBe(true);
  392. expect(buttonDelete.exists()).toBe(true);
  393. });
  394. describe('updates the saved query', () => {
  395. beforeEach(() => {
  396. mockUtils = jest
  397. .spyOn(utils, 'handleUpdateQuery')
  398. .mockImplementation(() => Promise.resolve(savedQuery));
  399. });
  400. afterEach(() => {
  401. mockUtils.mockClear();
  402. });
  403. it('accepts a well-formed query', async () => {
  404. const wrapper = generateWrappedComponent(
  405. location,
  406. organization,
  407. router,
  408. errorsViewModified,
  409. savedQuery,
  410. yAxis
  411. );
  412. // Click on Save in the Dropdown
  413. const buttonUpdate = wrapper.find(SELECTOR_BUTTON_UPDATE).first();
  414. await buttonUpdate.simulate('click');
  415. expect(mockUtils).toHaveBeenCalledWith(
  416. expect.anything(), // api
  417. organization,
  418. expect.objectContaining({
  419. ...errorsViewModified,
  420. }),
  421. yAxis
  422. );
  423. });
  424. });
  425. describe('creates a separate query', () => {
  426. beforeEach(() => {
  427. mockUtils = jest
  428. .spyOn(utils, 'handleCreateQuery')
  429. .mockImplementation(() => Promise.resolve(savedQuery));
  430. });
  431. afterEach(() => {
  432. mockUtils.mockClear();
  433. });
  434. it('checks that it is forked from a saved query', () => {
  435. mount(location, organization, router, errorsViewModified, savedQuery, yAxis);
  436. // Click on ButtonSaveAs to open dropdown
  437. userEvent.click(screen.getByRole('button', {name: 'Save as'}));
  438. // Fill in the Input
  439. userEvent.type(screen.getByPlaceholderText('Display name'), 'Forked Query');
  440. // Click on Save in the Dropdown
  441. userEvent.click(screen.getByRole('button', {name: 'Save for Org'}));
  442. expect(mockUtils).toHaveBeenCalledWith(
  443. expect.anything(), // api
  444. organization,
  445. expect.objectContaining({
  446. ...errorsViewModified,
  447. name: 'Forked Query',
  448. }),
  449. yAxis,
  450. false
  451. );
  452. });
  453. });
  454. });
  455. describe('create alert from discover', () => {
  456. it('renders create alert button when metrics alerts is enabled', () => {
  457. const metricAlertOrg = {
  458. ...organization,
  459. features: ['incidents'],
  460. };
  461. const wrapper = generateWrappedComponent(
  462. location,
  463. metricAlertOrg,
  464. router,
  465. errorsViewModified,
  466. savedQuery,
  467. yAxis
  468. );
  469. const buttonCreateAlert = wrapper.find(SELECTOR_BUTTON_CREATE_ALERT);
  470. expect(buttonCreateAlert.exists()).toBe(true);
  471. });
  472. it('does not render create alert button without metric alerts', () => {
  473. const wrapper = generateWrappedComponent(
  474. location,
  475. organization,
  476. router,
  477. errorsViewModified,
  478. savedQuery,
  479. yAxis
  480. );
  481. const buttonCreateAlert = wrapper.find(SELECTOR_BUTTON_CREATE_ALERT);
  482. expect(buttonCreateAlert.exists()).toBe(false);
  483. });
  484. });
  485. describe('add dashboard widget', () => {
  486. let initialData;
  487. beforeEach(() => {
  488. initialData = initializeOrg({
  489. organization: {
  490. features: ['discover-query', 'widget-viewer-modal', 'dashboards-edit'],
  491. apdexThreshold: 400,
  492. },
  493. router: {
  494. location: {query: {}},
  495. },
  496. project: 1,
  497. projects: [],
  498. });
  499. MockApiClient.addMockResponse({
  500. url: '/organizations/org-slug/events-stats/',
  501. body: [],
  502. });
  503. MockApiClient.addMockResponse({
  504. url: '/organizations/org-slug/dashboards/',
  505. body: [],
  506. });
  507. });
  508. afterEach(() => {
  509. MockApiClient.clearMockResponses();
  510. });
  511. it('opens widget modal when add to dashboard is clicked', () => {
  512. mount(
  513. initialData.router.location,
  514. initialData.organization,
  515. initialData.router,
  516. errorsViewModified,
  517. savedQuery,
  518. ['count()']
  519. );
  520. userEvent.click(screen.getByText('Add to Dashboard'));
  521. expect(openAddDashboardWidgetModal).toHaveBeenCalledWith(
  522. expect.objectContaining({
  523. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  524. defaultTitle: 'Errors by Title',
  525. defaultWidgetQuery: {
  526. conditions: 'event.type:error',
  527. fields: ['count()'],
  528. aggregates: ['count()'],
  529. columns: [],
  530. name: '',
  531. orderby: '-count()',
  532. },
  533. displayType: 'line',
  534. })
  535. );
  536. });
  537. it('populates dashboard widget modal with saved query data if created from discover', () => {
  538. mount(
  539. initialData.router.location,
  540. initialData.organization,
  541. initialData.router,
  542. errorsViewModified,
  543. savedQuery,
  544. yAxis
  545. );
  546. userEvent.click(screen.getByText('Add to Dashboard'));
  547. expect(openAddDashboardWidgetModal).toHaveBeenCalledWith(
  548. expect.objectContaining({
  549. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  550. defaultTitle: 'Errors by Title',
  551. defaultWidgetQuery: {
  552. conditions: 'event.type:error',
  553. fields: ['count()', 'failure_count()'],
  554. aggregates: ['count()', 'failure_count()'],
  555. columns: [],
  556. name: '',
  557. orderby: '-count()',
  558. },
  559. displayType: 'line',
  560. })
  561. );
  562. });
  563. it('adds equation to query fields if yAxis includes comprising functions', () => {
  564. mount(
  565. initialData.router.location,
  566. initialData.organization,
  567. initialData.router,
  568. errorsViewModified,
  569. savedQuery,
  570. [...yAxis, 'equation|count() + failure_count()']
  571. );
  572. userEvent.click(screen.getByText('Add to Dashboard'));
  573. expect(openAddDashboardWidgetModal).toHaveBeenCalledWith(
  574. expect.objectContaining({
  575. defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
  576. defaultTitle: 'Errors by Title',
  577. defaultWidgetQuery: {
  578. conditions: 'event.type:error',
  579. fields: ['count()', 'failure_count()', 'equation|count() + failure_count()'],
  580. aggregates: [
  581. 'count()',
  582. 'failure_count()',
  583. 'equation|count() + failure_count()',
  584. ],
  585. columns: [],
  586. name: '',
  587. orderby: '-count()',
  588. },
  589. displayType: 'line',
  590. })
  591. );
  592. });
  593. });
  594. });