queryList.spec.tsx 14 KB

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