queryList.spec.tsx 14 KB

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