queryList.spec.tsx 14 KB

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