queryList.spec.tsx 14 KB

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