queryList.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import {act} from 'react-dom/test-utils';
  2. import {browserHistory} from 'react-router';
  3. import {selectDropdownMenuItem} from 'sentry-test/dropdownMenu';
  4. import {mountWithTheme} from 'sentry-test/enzyme';
  5. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  6. import {triggerPress} from 'sentry-test/utils';
  7. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  8. import {DisplayModes} from 'sentry/utils/discover/types';
  9. import {DashboardWidgetSource, DisplayType} from 'sentry/views/dashboardsV2/types';
  10. import QueryList from 'sentry/views/eventsV2/queryList';
  11. jest.mock('sentry/actionCreators/modal');
  12. jest.mock('sentry/components/charts/eventsRequest');
  13. describe('EventsV2 > QueryList', function () {
  14. let location,
  15. savedQueries,
  16. organization,
  17. deleteMock,
  18. duplicateMock,
  19. queryChangeMock,
  20. updateHomepageMock,
  21. wrapper;
  22. beforeAll(async function () {
  23. await import('sentry/components/modals/widgetBuilder/addToDashboardModal');
  24. });
  25. beforeEach(function () {
  26. organization = TestStubs.Organization({
  27. features: [
  28. 'discover-basic',
  29. 'discover-query',
  30. 'discover-query-builder-as-landing-page',
  31. ],
  32. });
  33. savedQueries = [
  34. TestStubs.DiscoverSavedQuery(),
  35. TestStubs.DiscoverSavedQuery({name: 'saved query 2', id: '2'}),
  36. ];
  37. MockApiClient.addMockResponse({
  38. url: '/organizations/org-slug/events-stats/',
  39. method: 'GET',
  40. statusCode: 200,
  41. body: {data: []},
  42. });
  43. deleteMock = MockApiClient.addMockResponse({
  44. url: '/organizations/org-slug/discover/saved/2/',
  45. method: 'DELETE',
  46. statusCode: 200,
  47. body: {},
  48. });
  49. duplicateMock = MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/discover/saved/',
  51. method: 'POST',
  52. body: {
  53. id: '3',
  54. name: 'Saved query copy',
  55. },
  56. });
  57. updateHomepageMock = MockApiClient.addMockResponse({
  58. url: '/organizations/org-slug/discover/homepage/',
  59. method: 'PUT',
  60. statusCode: 204,
  61. });
  62. location = {
  63. pathname: '/organizations/org-slug/discover/queries/',
  64. query: {cursor: '0:1:1', statsPeriod: '14d'},
  65. };
  66. queryChangeMock = jest.fn();
  67. });
  68. afterEach(() => {
  69. jest.clearAllMocks();
  70. wrapper && wrapper.unmount();
  71. wrapper = null;
  72. });
  73. it('renders an empty list', function () {
  74. wrapper = mountWithTheme(
  75. <QueryList
  76. organization={organization}
  77. savedQueries={[]}
  78. savedQuerySearchQuery="no matches"
  79. pageLinks=""
  80. onQueryChange={queryChangeMock}
  81. location={location}
  82. />
  83. );
  84. const content = wrapper.find('QueryCard');
  85. // No queries
  86. expect(content).toHaveLength(0);
  87. expect(wrapper.find('EmptyStateWarning')).toHaveLength(1);
  88. });
  89. it('renders pre-built queries and saved ones', function () {
  90. wrapper = mountWithTheme(
  91. <QueryList
  92. organization={organization}
  93. savedQueries={savedQueries}
  94. renderPrebuilt
  95. pageLinks=""
  96. onQueryChange={queryChangeMock}
  97. location={location}
  98. />
  99. );
  100. const content = wrapper.find('QueryCard');
  101. // pre built + saved queries
  102. expect(content).toHaveLength(5);
  103. });
  104. it('can duplicate and trigger change callback', async function () {
  105. wrapper = mountWithTheme(
  106. <QueryList
  107. organization={organization}
  108. savedQueries={savedQueries}
  109. pageLinks=""
  110. onQueryChange={queryChangeMock}
  111. location={location}
  112. />
  113. );
  114. const card = wrapper.find('QueryCard').last();
  115. expect(card.find('QueryCardContent').text()).toEqual(savedQueries[1].name);
  116. await selectDropdownMenuItem({
  117. wrapper,
  118. specifiers: {prefix: 'QueryCard', last: true},
  119. itemKey: 'duplicate',
  120. });
  121. expect(duplicateMock).toHaveBeenCalled();
  122. expect(queryChangeMock).toHaveBeenCalled();
  123. });
  124. it('can delete and trigger change callback', async function () {
  125. wrapper = mountWithTheme(
  126. <QueryList
  127. organization={organization}
  128. savedQueries={savedQueries}
  129. pageLinks=""
  130. onQueryChange={queryChangeMock}
  131. location={location}
  132. />
  133. );
  134. const card = wrapper.find('QueryCard').last();
  135. expect(card.find('QueryCardContent').text()).toEqual(savedQueries[1].name);
  136. await selectDropdownMenuItem({
  137. wrapper,
  138. specifiers: {prefix: 'QueryCard', last: true},
  139. itemKey: 'delete',
  140. });
  141. expect(deleteMock).toHaveBeenCalled();
  142. expect(queryChangeMock).toHaveBeenCalled();
  143. });
  144. it('returns short url location for saved query', function () {
  145. wrapper = mountWithTheme(
  146. <QueryList
  147. organization={organization}
  148. savedQueries={savedQueries}
  149. pageLinks=""
  150. onQueryChange={queryChangeMock}
  151. location={location}
  152. />
  153. );
  154. const card = wrapper.find('QueryCard').last();
  155. const link = card.find('Link').last().prop('to');
  156. expect(link.pathname).toEqual('/organizations/org-slug/discover/results/');
  157. expect(link.query).toEqual({
  158. id: '2',
  159. statsPeriod: '14d',
  160. });
  161. });
  162. it('can redirect on last query deletion', async function () {
  163. wrapper = mountWithTheme(
  164. <QueryList
  165. organization={organization}
  166. savedQueries={savedQueries.slice(1)}
  167. pageLinks=""
  168. onQueryChange={queryChangeMock}
  169. location={location}
  170. />
  171. );
  172. const card = wrapper.find('QueryCard').last();
  173. expect(card.find('QueryCardContent').text()).toEqual(savedQueries[1].name);
  174. await selectDropdownMenuItem({
  175. wrapper,
  176. specifiers: {prefix: 'QueryCard', last: true},
  177. itemKey: 'delete',
  178. });
  179. expect(deleteMock).toHaveBeenCalled();
  180. expect(queryChangeMock).not.toHaveBeenCalled();
  181. expect(browserHistory.push).toHaveBeenCalledWith({
  182. pathname: location.pathname,
  183. query: {cursor: undefined, statsPeriod: '14d'},
  184. });
  185. });
  186. it('renders Add to Dashboard in context menu with feature flag', async function () {
  187. const featuredOrganization = TestStubs.Organization({
  188. features: ['dashboards-edit'],
  189. });
  190. wrapper = mountWithTheme(
  191. <QueryList
  192. organization={featuredOrganization}
  193. savedQueries={savedQueries.slice(1)}
  194. pageLinks=""
  195. onQueryChange={queryChangeMock}
  196. location={location}
  197. />
  198. );
  199. let card = wrapper.find('QueryCard').last();
  200. await act(async () => {
  201. triggerPress(card.find('DropdownTrigger'));
  202. await tick();
  203. wrapper.update();
  204. });
  205. card = wrapper.find('QueryCard').last();
  206. const menuItems = card.find('MenuItemWrap');
  207. expect(menuItems.length).toEqual(3);
  208. expect(menuItems.at(0).text()).toEqual('Add to Dashboard');
  209. expect(menuItems.at(1).text()).toEqual('Duplicate Query');
  210. expect(menuItems.at(2).text()).toEqual('Delete Query');
  211. });
  212. it('only renders Delete Query and Duplicate Query in context menu', async function () {
  213. wrapper = mountWithTheme(
  214. <QueryList
  215. organization={organization}
  216. savedQueries={savedQueries.slice(1)}
  217. pageLinks=""
  218. onQueryChange={queryChangeMock}
  219. location={location}
  220. />
  221. );
  222. let card = wrapper.find('QueryCard').last();
  223. await act(async () => {
  224. triggerPress(card.find('DropdownTrigger'));
  225. await tick();
  226. wrapper.update();
  227. });
  228. card = wrapper.find('QueryCard').last();
  229. const menuItems = card.find('MenuItemWrap');
  230. expect(menuItems.length).toEqual(3);
  231. expect(menuItems.at(0).text()).toEqual('Use as Discover Home');
  232. expect(menuItems.at(1).text()).toEqual('Duplicate Query');
  233. expect(menuItems.at(2).text()).toEqual('Delete Query');
  234. });
  235. it('passes yAxis from the savedQuery to MiniGraph', function () {
  236. const featuredOrganization = TestStubs.Organization({
  237. features: ['dashboards-edit'],
  238. });
  239. const yAxis = ['count()', 'failure_count()'];
  240. const savedQueryWithMultiYAxis = {
  241. ...savedQueries.slice(1)[0],
  242. yAxis,
  243. };
  244. wrapper = mountWithTheme(
  245. <QueryList
  246. organization={featuredOrganization}
  247. savedQueries={[savedQueryWithMultiYAxis]}
  248. pageLinks=""
  249. onQueryChange={queryChangeMock}
  250. location={location}
  251. />
  252. );
  253. const miniGraph = wrapper.find('MiniGraph');
  254. expect(miniGraph.props().yAxis).toEqual(['count()', 'failure_count()']);
  255. });
  256. it('Use as Discover Home updates the homepage query', function () {
  257. render(
  258. <QueryList
  259. organization={organization}
  260. savedQueries={savedQueries.slice(1)}
  261. pageLinks=""
  262. onQueryChange={queryChangeMock}
  263. location={location}
  264. />
  265. );
  266. userEvent.click(screen.getByTestId('menu-trigger'));
  267. userEvent.click(screen.getByText('Use as Discover Home'));
  268. expect(updateHomepageMock).toHaveBeenCalledWith(
  269. '/organizations/org-slug/discover/homepage/',
  270. expect.objectContaining({
  271. data: expect.objectContaining({fields: ['test'], range: '14d'}),
  272. })
  273. );
  274. });
  275. describe('Add to Dashboard modal', () => {
  276. it('opens a modal with the correct params for Top 5 chart', async function () {
  277. const featuredOrganization = TestStubs.Organization({
  278. features: ['dashboards-edit'],
  279. });
  280. wrapper = mountWithTheme(
  281. <QueryList
  282. organization={featuredOrganization}
  283. savedQueries={[
  284. TestStubs.DiscoverSavedQuery({
  285. display: DisplayModes.TOP5,
  286. orderby: 'test',
  287. fields: ['test', 'count()'],
  288. yAxis: ['count()'],
  289. }),
  290. ]}
  291. pageLinks=""
  292. onQueryChange={queryChangeMock}
  293. location={location}
  294. />
  295. );
  296. let card = wrapper.find('QueryCard').last();
  297. await act(async () => {
  298. triggerPress(card.find('DropdownTrigger'));
  299. await tick();
  300. wrapper.update();
  301. });
  302. card = wrapper.find('QueryCard').last();
  303. const menuItems = card.find('MenuItemWrap');
  304. expect(menuItems.length).toEqual(3);
  305. expect(menuItems.at(0).text()).toEqual('Add to Dashboard');
  306. await act(async () => {
  307. triggerPress(menuItems.at(0));
  308. await tick();
  309. wrapper.update();
  310. });
  311. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  312. expect.objectContaining({
  313. widget: {
  314. title: 'Saved query #1',
  315. displayType: DisplayType.AREA,
  316. limit: 5,
  317. queries: [
  318. {
  319. aggregates: ['count()'],
  320. columns: ['test'],
  321. conditions: '',
  322. fields: ['test', 'count()', 'count()'],
  323. name: '',
  324. orderby: 'test',
  325. },
  326. ],
  327. },
  328. widgetAsQueryParams: expect.objectContaining({
  329. defaultTableColumns: ['test', 'count()'],
  330. defaultTitle: 'Saved query #1',
  331. defaultWidgetQuery:
  332. 'name=&aggregates=count()&columns=test&fields=test%2Ccount()%2Ccount()&conditions=&orderby=test',
  333. displayType: DisplayType.AREA,
  334. source: DashboardWidgetSource.DISCOVERV2,
  335. }),
  336. })
  337. );
  338. });
  339. it('opens a modal with the correct params for other chart', async function () {
  340. const featuredOrganization = TestStubs.Organization({
  341. features: ['dashboards-edit'],
  342. });
  343. wrapper = mountWithTheme(
  344. <QueryList
  345. organization={featuredOrganization}
  346. savedQueries={[
  347. TestStubs.DiscoverSavedQuery({
  348. display: DisplayModes.DEFAULT,
  349. orderby: 'count()',
  350. fields: ['test', 'count()'],
  351. yAxis: ['count()'],
  352. }),
  353. ]}
  354. pageLinks=""
  355. onQueryChange={queryChangeMock}
  356. location={location}
  357. />
  358. );
  359. let card = wrapper.find('QueryCard').last();
  360. await act(async () => {
  361. triggerPress(card.find('DropdownTrigger'));
  362. await tick();
  363. wrapper.update();
  364. });
  365. card = wrapper.find('QueryCard').last();
  366. const menuItems = card.find('MenuItemWrap');
  367. expect(menuItems.length).toEqual(3);
  368. expect(menuItems.at(0).text()).toEqual('Add to Dashboard');
  369. await act(async () => {
  370. triggerPress(menuItems.at(0));
  371. await tick();
  372. wrapper.update();
  373. });
  374. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  375. expect.objectContaining({
  376. widget: {
  377. title: 'Saved query #1',
  378. displayType: DisplayType.LINE,
  379. queries: [
  380. {
  381. aggregates: ['count()'],
  382. columns: [],
  383. conditions: '',
  384. fields: ['count()'],
  385. name: '',
  386. // Orderby gets dropped because ordering only applies to
  387. // Top-N and tables
  388. orderby: '',
  389. },
  390. ],
  391. },
  392. widgetAsQueryParams: expect.objectContaining({
  393. defaultTableColumns: ['test', 'count()'],
  394. defaultTitle: 'Saved query #1',
  395. defaultWidgetQuery:
  396. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  397. displayType: DisplayType.LINE,
  398. source: DashboardWidgetSource.DISCOVERV2,
  399. }),
  400. })
  401. );
  402. });
  403. });
  404. });