queryList.spec.tsx 14 KB

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