queryList.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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: ['discover-basic', 'discover-query'],
  32. });
  33. savedQueries = [
  34. TestStubs.DiscoverSavedQuery(),
  35. TestStubs.DiscoverSavedQuery({name: 'saved query 2', id: '2'}),
  36. ];
  37. eventsStatsMock = 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. render(
  75. <QueryList
  76. organization={organization}
  77. savedQueries={[]}
  78. savedQuerySearchQuery="no matches"
  79. pageLinks=""
  80. onQueryChange={queryChangeMock}
  81. location={location}
  82. />
  83. );
  84. expect(screen.getByText('No saved queries match that filter')).toBeInTheDocument();
  85. });
  86. it('renders pre-built queries and saved ones', function () {
  87. render(
  88. <QueryList
  89. organization={organization}
  90. savedQueries={savedQueries}
  91. renderPrebuilt
  92. pageLinks=""
  93. onQueryChange={queryChangeMock}
  94. location={location}
  95. />
  96. );
  97. expect(screen.getAllByTestId(/card-.*/)).toHaveLength(5);
  98. });
  99. it('can duplicate and trigger change callback', async function () {
  100. render(
  101. <QueryList
  102. organization={organization}
  103. savedQueries={savedQueries}
  104. pageLinks=""
  105. onQueryChange={queryChangeMock}
  106. location={location}
  107. />
  108. );
  109. const card = screen.getAllByTestId(/card-*/).at(0);
  110. const withinCard = within(card);
  111. expect(withinCard.getByText('Saved query #1')).toBeInTheDocument();
  112. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  113. await userEvent.click(withinCard.getByText('Duplicate Query'));
  114. await waitFor(() => {
  115. expect(browserHistory.push).toHaveBeenCalledWith({
  116. pathname: location.pathname,
  117. query: {},
  118. });
  119. });
  120. expect(duplicateMock).toHaveBeenCalled();
  121. expect(queryChangeMock).toHaveBeenCalled();
  122. });
  123. it('can delete and trigger change callback', async function () {
  124. render(
  125. <QueryList
  126. organization={organization}
  127. savedQueries={savedQueries}
  128. pageLinks=""
  129. onQueryChange={queryChangeMock}
  130. location={location}
  131. />
  132. );
  133. const card = screen.getAllByTestId(/card-*/).at(1);
  134. const withinCard = within(card);
  135. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  136. await userEvent.click(withinCard.getByText('Delete Query'));
  137. await waitFor(() => {
  138. expect(queryChangeMock).toHaveBeenCalled();
  139. });
  140. expect(deleteMock).toHaveBeenCalled();
  141. });
  142. it('redirects to Discover on card click', async function () {
  143. render(
  144. <QueryList
  145. organization={organization}
  146. savedQueries={savedQueries}
  147. pageLinks=""
  148. onQueryChange={queryChangeMock}
  149. location={location}
  150. />,
  151. {context: routerContext}
  152. );
  153. await userEvent.click(screen.getAllByTestId(/card-*/).at(0));
  154. expect(router.push).toHaveBeenLastCalledWith({
  155. pathname: '/organizations/org-slug/discover/results/',
  156. query: {id: '1', statsPeriod: '14d'},
  157. });
  158. });
  159. it('can redirect on last query deletion', async function () {
  160. render(
  161. <QueryList
  162. organization={organization}
  163. savedQueries={savedQueries.slice(1)}
  164. pageLinks=""
  165. onQueryChange={queryChangeMock}
  166. location={location}
  167. />,
  168. {context: routerContext}
  169. );
  170. const card = screen.getAllByTestId(/card-*/).at(0);
  171. const withinCard = within(card);
  172. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  173. await userEvent.click(withinCard.getByText('Delete Query'));
  174. expect(deleteMock).toHaveBeenCalled();
  175. expect(queryChangeMock).not.toHaveBeenCalled();
  176. await waitFor(() => {
  177. expect(browserHistory.push).toHaveBeenCalledWith({
  178. pathname: location.pathname,
  179. query: {cursor: undefined, statsPeriod: '14d'},
  180. });
  181. });
  182. });
  183. it('renders Add to Dashboard in context menu', async function () {
  184. const featuredOrganization = TestStubs.Organization({
  185. features: ['dashboards-edit'],
  186. });
  187. render(
  188. <QueryList
  189. organization={featuredOrganization}
  190. savedQueries={savedQueries.slice(1)}
  191. pageLinks=""
  192. onQueryChange={queryChangeMock}
  193. location={location}
  194. />
  195. );
  196. const card = screen.getAllByTestId(/card-*/).at(0);
  197. const withinCard = within(card);
  198. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  199. expect(
  200. screen.getByRole('menuitemradio', {name: 'Add to Dashboard'})
  201. ).toBeInTheDocument();
  202. expect(
  203. screen.getByRole('menuitemradio', {name: 'Set as Default'})
  204. ).toBeInTheDocument();
  205. expect(
  206. screen.getByRole('menuitemradio', {name: 'Duplicate Query'})
  207. ).toBeInTheDocument();
  208. expect(screen.getByRole('menuitemradio', {name: 'Delete Query'})).toBeInTheDocument();
  209. });
  210. it('only renders Delete Query and Duplicate Query in context menu', async function () {
  211. render(
  212. <QueryList
  213. organization={organization}
  214. savedQueries={savedQueries.slice(1)}
  215. pageLinks=""
  216. onQueryChange={queryChangeMock}
  217. location={location}
  218. />
  219. );
  220. const card = screen.getAllByTestId(/card-*/).at(0);
  221. const withinCard = within(card);
  222. await userEvent.click(withinCard.getByTestId('menu-trigger'));
  223. expect(
  224. screen.queryByRole('menuitemradio', {name: 'Add to Dashboard'})
  225. ).not.toBeInTheDocument();
  226. expect(
  227. screen.getByRole('menuitemradio', {name: 'Set as Default'})
  228. ).toBeInTheDocument();
  229. expect(
  230. screen.getByRole('menuitemradio', {name: 'Duplicate Query'})
  231. ).toBeInTheDocument();
  232. expect(screen.getByRole('menuitemradio', {name: 'Delete Query'})).toBeInTheDocument();
  233. });
  234. it('passes yAxis from the savedQuery to MiniGraph', async function () {
  235. const featuredOrganization = TestStubs.Organization({
  236. features: ['dashboards-edit'],
  237. });
  238. const yAxis = ['count()', 'failure_count()'];
  239. const savedQueryWithMultiYAxis = {
  240. ...savedQueries.slice(1)[0],
  241. yAxis,
  242. };
  243. render(
  244. <QueryList
  245. organization={featuredOrganization}
  246. savedQueries={[savedQueryWithMultiYAxis]}
  247. pageLinks=""
  248. onQueryChange={queryChangeMock}
  249. location={location}
  250. />
  251. );
  252. const chart = await screen.findByTestId('area-chart');
  253. expect(chart).toBeInTheDocument();
  254. expect(eventsStatsMock).toHaveBeenCalledWith(
  255. '/organizations/org-slug/events-stats/',
  256. expect.objectContaining({
  257. query: expect.objectContaining({
  258. yAxis: ['count()', 'failure_count()'],
  259. }),
  260. })
  261. );
  262. });
  263. it('Set as Default updates the homepage query', async function () {
  264. render(
  265. <QueryList
  266. organization={organization}
  267. savedQueries={savedQueries.slice(1)}
  268. pageLinks=""
  269. onQueryChange={queryChangeMock}
  270. location={location}
  271. />
  272. );
  273. await userEvent.click(screen.getByTestId('menu-trigger'));
  274. await userEvent.click(screen.getByText('Set as Default'));
  275. expect(updateHomepageMock).toHaveBeenCalledWith(
  276. '/organizations/org-slug/discover/homepage/',
  277. expect.objectContaining({
  278. data: expect.objectContaining({fields: ['test'], range: '14d'}),
  279. })
  280. );
  281. });
  282. describe('Add to Dashboard modal', () => {
  283. it('opens a modal with the correct params for Top 5 chart', async function () {
  284. const featuredOrganization = TestStubs.Organization({
  285. features: ['dashboards-edit'],
  286. });
  287. render(
  288. <QueryList
  289. organization={featuredOrganization}
  290. savedQueries={[
  291. TestStubs.DiscoverSavedQuery({
  292. display: DisplayModes.TOP5,
  293. orderby: 'test',
  294. fields: ['test', 'count()'],
  295. yAxis: ['count()'],
  296. }),
  297. ]}
  298. pageLinks=""
  299. onQueryChange={queryChangeMock}
  300. location={location}
  301. />
  302. );
  303. const contextMenu = await screen.findByTestId('menu-trigger');
  304. expect(contextMenu).toBeInTheDocument();
  305. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  306. await userEvent.click(contextMenu);
  307. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  308. await userEvent.click(addToDashboardMenuItem);
  309. await waitFor(() => {
  310. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  311. expect.objectContaining({
  312. widget: {
  313. title: 'Saved query #1',
  314. displayType: DisplayType.AREA,
  315. limit: 5,
  316. queries: [
  317. {
  318. aggregates: ['count()'],
  319. columns: ['test'],
  320. conditions: '',
  321. fields: ['test', 'count()', 'count()'],
  322. name: '',
  323. orderby: 'test',
  324. },
  325. ],
  326. },
  327. widgetAsQueryParams: expect.objectContaining({
  328. defaultTableColumns: ['test', 'count()'],
  329. defaultTitle: 'Saved query #1',
  330. defaultWidgetQuery:
  331. 'name=&aggregates=count()&columns=test&fields=test%2Ccount()%2Ccount()&conditions=&orderby=test',
  332. displayType: DisplayType.AREA,
  333. source: DashboardWidgetSource.DISCOVERV2,
  334. }),
  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. render(
  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. const contextMenu = await screen.findByTestId('menu-trigger');
  360. expect(contextMenu).toBeInTheDocument();
  361. expect(screen.queryByTestId('add-to-dashboard')).not.toBeInTheDocument();
  362. await userEvent.click(contextMenu);
  363. const addToDashboardMenuItem = await screen.findByTestId('add-to-dashboard');
  364. await userEvent.click(addToDashboardMenuItem);
  365. await waitFor(() => {
  366. expect(openAddToDashboardModal).toHaveBeenCalledWith(
  367. expect.objectContaining({
  368. widget: {
  369. title: 'Saved query #1',
  370. displayType: DisplayType.LINE,
  371. queries: [
  372. {
  373. aggregates: ['count()'],
  374. columns: [],
  375. conditions: '',
  376. fields: ['count()'],
  377. name: '',
  378. // Orderby gets dropped because ordering only applies to
  379. // Top-N and tables
  380. orderby: '',
  381. },
  382. ],
  383. },
  384. widgetAsQueryParams: expect.objectContaining({
  385. defaultTableColumns: ['test', 'count()'],
  386. defaultTitle: 'Saved query #1',
  387. defaultWidgetQuery:
  388. 'name=&aggregates=count()&columns=&fields=count()&conditions=&orderby=',
  389. displayType: DisplayType.LINE,
  390. source: DashboardWidgetSource.DISCOVERV2,
  391. }),
  392. })
  393. );
  394. });
  395. });
  396. });
  397. });