queryList.spec.jsx 13 KB

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