results.spec.tsx 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence';
  6. import ProjectsStore from 'sentry/stores/projectsStore';
  7. import EventView from 'sentry/utils/discover/eventView';
  8. import Results from 'sentry/views/discover/results';
  9. import {DEFAULT_EVENT_VIEW, TRANSACTION_VIEWS} from './data';
  10. const FIELDS = [
  11. {
  12. field: 'title',
  13. },
  14. {
  15. field: 'timestamp',
  16. },
  17. {
  18. field: 'user',
  19. },
  20. {
  21. field: 'count()',
  22. },
  23. ];
  24. const generateFields = () => ({
  25. field: FIELDS.map(i => i.field),
  26. });
  27. const eventTitle = 'Oh no something bad';
  28. function renderMockRequests() {
  29. MockApiClient.addMockResponse({
  30. url: '/organizations/org-slug/projects/',
  31. body: [],
  32. });
  33. MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/projects-count/',
  35. body: {myProjects: 10, allProjects: 300},
  36. });
  37. MockApiClient.addMockResponse({
  38. url: '/organizations/org-slug/tags/',
  39. body: [],
  40. });
  41. const eventsStatsMock = MockApiClient.addMockResponse({
  42. url: '/organizations/org-slug/events-stats/',
  43. body: {data: [[123, []]]},
  44. });
  45. MockApiClient.addMockResponse({
  46. url: '/organizations/org-slug/recent-searches/',
  47. body: [],
  48. });
  49. MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/recent-searches/',
  51. method: 'POST',
  52. body: [],
  53. });
  54. MockApiClient.addMockResponse({
  55. url: '/organizations/org-slug/releases/stats/',
  56. body: [],
  57. });
  58. const eventsResultsMock = MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/events/',
  60. body: {
  61. meta: {
  62. fields: {
  63. id: 'string',
  64. title: 'string',
  65. 'project.name': 'string',
  66. timestamp: 'date',
  67. 'user.id': 'string',
  68. },
  69. },
  70. data: [
  71. {
  72. id: 'deadbeef',
  73. 'user.id': 'alberto leal',
  74. title: eventTitle,
  75. 'project.name': 'project-slug',
  76. timestamp: '2019-05-23T22:12:48+00:00',
  77. },
  78. ],
  79. },
  80. });
  81. const eventsMetaMock = MockApiClient.addMockResponse({
  82. url: '/organizations/org-slug/events-meta/',
  83. body: {
  84. count: 2,
  85. },
  86. });
  87. MockApiClient.addMockResponse({
  88. url: '/organizations/org-slug/events/project-slug:deadbeef/',
  89. method: 'GET',
  90. body: {
  91. id: '1234',
  92. size: 1200,
  93. eventID: 'deadbeef',
  94. title: 'Oh no something bad',
  95. message: 'It was not good',
  96. dateCreated: '2019-05-23T22:12:48+00:00',
  97. entries: [
  98. {
  99. type: 'message',
  100. message: 'bad stuff',
  101. data: {},
  102. },
  103. ],
  104. tags: [{key: 'browser', value: 'Firefox'}],
  105. },
  106. });
  107. const eventFacetsMock = MockApiClient.addMockResponse({
  108. url: '/organizations/org-slug/events-facets/',
  109. body: [
  110. {
  111. key: 'release',
  112. topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
  113. },
  114. {
  115. key: 'environment',
  116. topValues: [
  117. {count: 2, value: 'dev', name: 'dev'},
  118. {count: 1, value: 'prod', name: 'prod'},
  119. ],
  120. },
  121. {
  122. key: 'foo',
  123. topValues: [
  124. {count: 2, value: 'bar', name: 'bar'},
  125. {count: 1, value: 'baz', name: 'baz'},
  126. ],
  127. },
  128. ],
  129. });
  130. const mockVisit = MockApiClient.addMockResponse({
  131. url: '/organizations/org-slug/discover/saved/1/visit/',
  132. method: 'POST',
  133. body: [],
  134. statusCode: 200,
  135. });
  136. const mockSaved = MockApiClient.addMockResponse({
  137. url: '/organizations/org-slug/discover/saved/1/',
  138. method: 'GET',
  139. statusCode: 200,
  140. body: {
  141. id: '1',
  142. name: 'new',
  143. projects: [],
  144. version: 2,
  145. expired: false,
  146. dateCreated: '2021-04-08T17:53:25.195782Z',
  147. dateUpdated: '2021-04-09T12:13:18.567264Z',
  148. createdBy: {
  149. id: '2',
  150. },
  151. environment: [],
  152. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  153. widths: ['-1', '-1', '-1', '-1', '-1'],
  154. range: '24h',
  155. orderby: '-user.display',
  156. },
  157. });
  158. MockApiClient.addMockResponse({
  159. url: '/organizations/org-slug/discover/homepage/',
  160. method: 'GET',
  161. statusCode: 200,
  162. body: {
  163. id: '2',
  164. name: '',
  165. projects: [],
  166. version: 2,
  167. expired: false,
  168. dateCreated: '2021-04-08T17:53:25.195782Z',
  169. dateUpdated: '2021-04-09T12:13:18.567264Z',
  170. createdBy: {
  171. id: '2',
  172. },
  173. environment: [],
  174. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  175. widths: ['-1', '-1', '-1', '-1', '-1'],
  176. range: '24h',
  177. orderby: '-user.display',
  178. },
  179. });
  180. return {
  181. eventsStatsMock,
  182. eventsMetaMock,
  183. eventsResultsMock,
  184. mockVisit,
  185. mockSaved,
  186. eventFacetsMock,
  187. };
  188. }
  189. describe('Results', function () {
  190. describe('Events', function () {
  191. const features = ['discover-basic'];
  192. it('loads data when moving from an invalid to valid EventView', function () {
  193. const organization = TestStubs.Organization({
  194. features,
  195. });
  196. // Start off with an invalid view (empty is invalid)
  197. const initialData = initializeOrg({
  198. ...initializeOrg(),
  199. organization,
  200. router: {
  201. location: {query: {query: 'tag:value'}},
  202. },
  203. });
  204. const mockRequests = renderMockRequests();
  205. ProjectsStore.loadInitialData([TestStubs.Project()]);
  206. render(
  207. <Results
  208. location={initialData.router.location}
  209. router={initialData.router}
  210. loading={false}
  211. setSavedQuery={jest.fn()}
  212. />,
  213. {
  214. context: initialData.routerContext,
  215. organization,
  216. }
  217. );
  218. // No request as eventview was invalid.
  219. expect(mockRequests.eventsStatsMock).not.toHaveBeenCalled();
  220. // Should redirect and retain the old query value
  221. expect(browserHistory.replace).toHaveBeenCalledWith(
  222. expect.objectContaining({
  223. pathname: '/organizations/org-slug/discover/results/',
  224. query: expect.objectContaining({
  225. query: 'tag:value',
  226. }),
  227. })
  228. );
  229. });
  230. it('pagination cursor should be cleared when making a search', async function () {
  231. const organization = TestStubs.Organization({
  232. features,
  233. });
  234. const initialData = initializeOrg({
  235. ...initializeOrg(),
  236. organization,
  237. router: {
  238. location: {
  239. query: {
  240. ...generateFields(),
  241. cursor: '0%3A50%3A0',
  242. },
  243. },
  244. },
  245. });
  246. const mockRequests = renderMockRequests();
  247. ProjectsStore.loadInitialData([TestStubs.Project()]);
  248. render(
  249. <Results
  250. organization={organization}
  251. location={initialData.router.location}
  252. router={initialData.router}
  253. loading={false}
  254. setSavedQuery={jest.fn()}
  255. />,
  256. {
  257. context: initialData.routerContext,
  258. organization,
  259. }
  260. );
  261. // ensure cursor query string is initially present in the location
  262. expect(initialData.router.location).toEqual({
  263. query: {
  264. ...generateFields(),
  265. cursor: '0%3A50%3A0',
  266. },
  267. });
  268. await waitFor(() =>
  269. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument()
  270. );
  271. // perform a search
  272. await userEvent.click(
  273. screen.getByPlaceholderText('Search for events, users, tags, and more')
  274. );
  275. await userEvent.paste('geo:canada');
  276. await userEvent.keyboard('{enter}');
  277. // should only be called with saved queries
  278. expect(mockRequests.mockVisit).not.toHaveBeenCalled();
  279. // cursor query string should be omitted from the query string
  280. expect(initialData.router.push).toHaveBeenCalledWith({
  281. pathname: undefined,
  282. query: {
  283. ...generateFields(),
  284. query: 'geo:canada',
  285. statsPeriod: '14d',
  286. },
  287. });
  288. });
  289. it('renders a y-axis selector', async function () {
  290. const organization = TestStubs.Organization({
  291. features,
  292. });
  293. const initialData = initializeOrg({
  294. ...initializeOrg(),
  295. organization,
  296. router: {
  297. location: {query: {...generateFields(), yAxis: 'count()'}},
  298. },
  299. });
  300. renderMockRequests();
  301. ProjectsStore.loadInitialData([TestStubs.Project()]);
  302. render(
  303. <Results
  304. organization={organization}
  305. location={initialData.router.location}
  306. router={initialData.router}
  307. loading={false}
  308. setSavedQuery={jest.fn()}
  309. />,
  310. {
  311. context: initialData.routerContext,
  312. organization,
  313. }
  314. );
  315. // Click the 'default' option.
  316. await selectEvent.select(
  317. await screen.findByRole('button', {name: 'Y-Axis count()'}),
  318. 'count_unique(user)'
  319. );
  320. });
  321. it('renders a display selector', async function () {
  322. const organization = TestStubs.Organization({
  323. features,
  324. });
  325. const initialData = initializeOrg({
  326. ...initializeOrg(),
  327. organization,
  328. router: {
  329. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  330. },
  331. });
  332. renderMockRequests();
  333. ProjectsStore.loadInitialData([TestStubs.Project()]);
  334. render(
  335. <Results
  336. organization={organization}
  337. location={initialData.router.location}
  338. router={initialData.router}
  339. loading={false}
  340. setSavedQuery={jest.fn()}
  341. />,
  342. {
  343. context: initialData.routerContext,
  344. organization,
  345. }
  346. );
  347. // Click the 'default' option.
  348. await selectEvent.select(
  349. await screen.findByRole('button', {name: /Display/}),
  350. 'Total Period'
  351. );
  352. });
  353. it('excludes top5 options when plan does not include discover-query', async function () {
  354. const organization = TestStubs.Organization({
  355. features: ['discover-basic'],
  356. });
  357. const initialData = initializeOrg({
  358. ...initializeOrg(),
  359. organization,
  360. router: {
  361. location: {query: {...generateFields(), display: 'previous'}},
  362. },
  363. });
  364. ProjectsStore.loadInitialData([TestStubs.Project()]);
  365. renderMockRequests();
  366. render(
  367. <Results
  368. organization={organization}
  369. location={initialData.router.location}
  370. router={initialData.router}
  371. loading={false}
  372. setSavedQuery={jest.fn()}
  373. />,
  374. {
  375. context: initialData.routerContext,
  376. organization,
  377. }
  378. );
  379. await userEvent.click(await screen.findByRole('button', {name: /Display/}));
  380. expect(screen.queryByText('Top 5 Daily')).not.toBeInTheDocument();
  381. expect(screen.queryByText('Top 5 Period')).not.toBeInTheDocument();
  382. });
  383. it('needs confirmation on long queries', function () {
  384. const organization = TestStubs.Organization({
  385. features: ['discover-basic'],
  386. });
  387. const initialData = initializeOrg({
  388. ...initializeOrg(),
  389. organization,
  390. router: {
  391. location: {query: {...generateFields(), statsPeriod: '60d', project: '-1'}},
  392. },
  393. });
  394. ProjectsStore.loadInitialData([TestStubs.Project()]);
  395. const mockRequests = renderMockRequests();
  396. render(
  397. <Results
  398. organization={organization}
  399. location={initialData.router.location}
  400. router={initialData.router}
  401. loading={false}
  402. setSavedQuery={jest.fn()}
  403. />,
  404. {
  405. context: initialData.routerContext,
  406. organization,
  407. }
  408. );
  409. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(0);
  410. });
  411. it('needs confirmation on long query with explicit projects', function () {
  412. const organization = TestStubs.Organization({
  413. features: ['discover-basic'],
  414. });
  415. const initialData = initializeOrg({
  416. ...initializeOrg(),
  417. organization,
  418. router: {
  419. location: {
  420. query: {
  421. ...generateFields(),
  422. statsPeriod: '60d',
  423. project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
  424. },
  425. },
  426. },
  427. });
  428. ProjectsStore.loadInitialData([TestStubs.Project()]);
  429. const mockRequests = renderMockRequests();
  430. render(
  431. <Results
  432. organization={organization}
  433. location={initialData.router.location}
  434. router={initialData.router}
  435. loading={false}
  436. setSavedQuery={jest.fn()}
  437. />,
  438. {
  439. context: initialData.routerContext,
  440. organization,
  441. }
  442. );
  443. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(0);
  444. });
  445. it('does not need confirmation on short queries', function () {
  446. const organization = TestStubs.Organization({
  447. features: ['discover-basic'],
  448. });
  449. const initialData = initializeOrg({
  450. ...initializeOrg(),
  451. organization,
  452. router: {
  453. location: {query: {...generateFields(), statsPeriod: '30d', project: '-1'}},
  454. },
  455. });
  456. ProjectsStore.loadInitialData([TestStubs.Project()]);
  457. const mockRequests = renderMockRequests();
  458. render(
  459. <Results
  460. organization={organization}
  461. location={initialData.router.location}
  462. router={initialData.router}
  463. loading={false}
  464. setSavedQuery={jest.fn()}
  465. />,
  466. {
  467. context: initialData.routerContext,
  468. organization,
  469. }
  470. );
  471. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(1);
  472. });
  473. it('does not need confirmation with to few projects', function () {
  474. const organization = TestStubs.Organization({
  475. features: ['discover-basic'],
  476. });
  477. const initialData = initializeOrg({
  478. ...initializeOrg(),
  479. organization,
  480. router: {
  481. location: {
  482. query: {...generateFields(), statsPeriod: '90d', project: [1, 2, 3, 4]},
  483. },
  484. },
  485. });
  486. ProjectsStore.loadInitialData([TestStubs.Project()]);
  487. const mockRequests = renderMockRequests();
  488. render(
  489. <Results
  490. organization={organization}
  491. location={initialData.router.location}
  492. router={initialData.router}
  493. loading={false}
  494. setSavedQuery={jest.fn()}
  495. />,
  496. {
  497. context: initialData.routerContext,
  498. organization,
  499. }
  500. );
  501. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(1);
  502. });
  503. it('creates event view from saved query', async function () {
  504. const organization = TestStubs.Organization({
  505. features,
  506. slug: 'org-slug',
  507. });
  508. const initialData = initializeOrg({
  509. ...initializeOrg(),
  510. organization,
  511. router: {
  512. location: {query: {id: '1', statsPeriod: '24h'}},
  513. },
  514. });
  515. ProjectsStore.loadInitialData([TestStubs.Project()]);
  516. const mockRequests = renderMockRequests();
  517. render(
  518. <Results
  519. organization={organization}
  520. location={initialData.router.location}
  521. router={initialData.router}
  522. loading={false}
  523. setSavedQuery={jest.fn()}
  524. />,
  525. {
  526. context: initialData.routerContext,
  527. organization,
  528. }
  529. );
  530. await waitFor(() => expect(mockRequests.mockVisit).toHaveBeenCalled());
  531. expect(screen.getByRole('link', {name: 'timestamp'})).toHaveAttribute(
  532. 'href',
  533. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-timestamp&statsPeriod=24h&topEvents=5'
  534. );
  535. expect(screen.getByRole('link', {name: 'project'})).toHaveAttribute(
  536. 'href',
  537. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-project&statsPeriod=24h&topEvents=5'
  538. );
  539. expect(screen.getByRole('link', {name: 'deadbeef'})).toHaveAttribute(
  540. 'href',
  541. '/organizations/org-slug/discover/project-slug:deadbeef/?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-user.display&statsPeriod=24h&topEvents=5&yAxis=count%28%29'
  542. );
  543. expect(screen.getByRole('link', {name: 'user.display'})).toHaveAttribute(
  544. 'href',
  545. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=user.display&statsPeriod=24h&topEvents=5'
  546. );
  547. expect(screen.getByRole('link', {name: 'title'})).toHaveAttribute(
  548. 'href',
  549. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-title&statsPeriod=24h&topEvents=5'
  550. );
  551. });
  552. it('overrides saved query params with location query params', async function () {
  553. const organization = TestStubs.Organization({
  554. features,
  555. slug: 'org-slug',
  556. });
  557. const initialData = initializeOrg({
  558. ...initializeOrg(),
  559. organization,
  560. router: {
  561. location: {
  562. query: {
  563. id: '1',
  564. statsPeriod: '7d',
  565. project: [2],
  566. environment: ['production'],
  567. },
  568. },
  569. },
  570. });
  571. ProjectsStore.loadInitialData([TestStubs.Project()]);
  572. const mockRequests = renderMockRequests();
  573. render(
  574. <Results
  575. organization={organization}
  576. location={initialData.router.location}
  577. router={initialData.router}
  578. loading={false}
  579. setSavedQuery={jest.fn()}
  580. />,
  581. {
  582. context: initialData.routerContext,
  583. organization,
  584. }
  585. );
  586. await waitFor(() => expect(mockRequests.mockVisit).toHaveBeenCalled());
  587. expect(screen.getByRole('link', {name: 'timestamp'})).toHaveAttribute(
  588. 'href',
  589. 'undefined?environment=production&field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&project=2&query=&sort=-timestamp&statsPeriod=7d&topEvents=5'
  590. );
  591. });
  592. it('updates chart whenever yAxis parameter changes', function () {
  593. const organization = TestStubs.Organization({
  594. features,
  595. });
  596. const initialData = initializeOrg({
  597. ...initializeOrg(),
  598. organization,
  599. router: {
  600. location: {query: {...generateFields(), yAxis: 'count()'}},
  601. },
  602. });
  603. ProjectsStore.loadInitialData([TestStubs.Project()]);
  604. const {eventsStatsMock} = renderMockRequests();
  605. const {rerender} = render(
  606. <Results
  607. organization={organization}
  608. location={initialData.router.location}
  609. router={initialData.router}
  610. loading={false}
  611. setSavedQuery={jest.fn()}
  612. />,
  613. {
  614. context: initialData.routerContext,
  615. organization,
  616. }
  617. );
  618. // Should load events once
  619. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  620. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  621. 1,
  622. '/organizations/org-slug/events-stats/',
  623. expect.objectContaining({
  624. query: expect.objectContaining({
  625. statsPeriod: '14d',
  626. yAxis: ['count()'],
  627. }),
  628. })
  629. );
  630. // Update location simulating a browser back button action
  631. rerender(
  632. <Results
  633. organization={organization}
  634. location={{
  635. ...initialData.router.location,
  636. query: {...generateFields(), yAxis: 'count_unique(user)'},
  637. }}
  638. router={initialData.router}
  639. loading={false}
  640. setSavedQuery={jest.fn()}
  641. />
  642. );
  643. // Should load events again
  644. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  645. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  646. 2,
  647. '/organizations/org-slug/events-stats/',
  648. expect.objectContaining({
  649. query: expect.objectContaining({
  650. statsPeriod: '14d',
  651. yAxis: ['count_unique(user)'],
  652. }),
  653. })
  654. );
  655. });
  656. it('updates chart whenever display parameter changes', function () {
  657. const organization = TestStubs.Organization({
  658. features,
  659. });
  660. const initialData = initializeOrg({
  661. ...initializeOrg(),
  662. organization,
  663. router: {
  664. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  665. },
  666. });
  667. const {eventsStatsMock} = renderMockRequests();
  668. ProjectsStore.loadInitialData([TestStubs.Project()]);
  669. const {rerender} = render(
  670. <Results
  671. organization={organization}
  672. location={initialData.router.location}
  673. router={initialData.router}
  674. loading={false}
  675. setSavedQuery={jest.fn()}
  676. />,
  677. {
  678. context: initialData.routerContext,
  679. organization,
  680. }
  681. );
  682. // Should load events once
  683. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  684. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  685. 1,
  686. '/organizations/org-slug/events-stats/',
  687. expect.objectContaining({
  688. query: expect.objectContaining({
  689. statsPeriod: '14d',
  690. yAxis: ['count()'],
  691. }),
  692. })
  693. );
  694. // Update location simulating a browser back button action
  695. rerender(
  696. <Results
  697. organization={organization}
  698. location={{
  699. ...initialData.router.location,
  700. query: {...generateFields(), display: 'previous', yAxis: 'count()'},
  701. }}
  702. router={initialData.router}
  703. loading={false}
  704. setSavedQuery={jest.fn()}
  705. />
  706. );
  707. // Should load events again
  708. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  709. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  710. 2,
  711. '/organizations/org-slug/events-stats/',
  712. expect.objectContaining({
  713. query: expect.objectContaining({
  714. statsPeriod: '28d',
  715. yAxis: ['count()'],
  716. }),
  717. })
  718. );
  719. });
  720. it('updates chart whenever display and yAxis parameters change', function () {
  721. const organization = TestStubs.Organization({
  722. features,
  723. });
  724. const initialData = initializeOrg({
  725. ...initializeOrg(),
  726. organization,
  727. router: {
  728. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  729. },
  730. });
  731. const {eventsStatsMock} = renderMockRequests();
  732. ProjectsStore.loadInitialData([TestStubs.Project()]);
  733. const {rerender} = render(
  734. <Results
  735. organization={organization}
  736. location={initialData.router.location}
  737. router={initialData.router}
  738. loading={false}
  739. setSavedQuery={jest.fn()}
  740. />,
  741. {
  742. context: initialData.routerContext,
  743. organization,
  744. }
  745. );
  746. // Should load events once
  747. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  748. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  749. 1,
  750. '/organizations/org-slug/events-stats/',
  751. expect.objectContaining({
  752. query: expect.objectContaining({
  753. statsPeriod: '14d',
  754. yAxis: ['count()'],
  755. }),
  756. })
  757. );
  758. // Update location simulating a browser back button action
  759. rerender(
  760. <Results
  761. organization={organization}
  762. location={{
  763. ...initialData.router.location,
  764. query: {
  765. ...generateFields(),
  766. display: 'previous',
  767. yAxis: 'count_unique(user)',
  768. },
  769. }}
  770. router={initialData.router}
  771. loading={false}
  772. setSavedQuery={jest.fn()}
  773. />
  774. );
  775. // Should load events again
  776. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  777. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  778. 2,
  779. '/organizations/org-slug/events-stats/',
  780. expect.objectContaining({
  781. query: expect.objectContaining({
  782. statsPeriod: '28d',
  783. yAxis: ['count_unique(user)'],
  784. }),
  785. })
  786. );
  787. });
  788. it('appends tag value to existing query when clicked', async function () {
  789. const organization = TestStubs.Organization({
  790. features,
  791. });
  792. const initialData = initializeOrg({
  793. ...initializeOrg(),
  794. organization,
  795. router: {
  796. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  797. },
  798. });
  799. const mockRequests = renderMockRequests();
  800. ProjectsStore.loadInitialData([TestStubs.Project()]);
  801. render(
  802. <Results
  803. organization={organization}
  804. location={initialData.router.location}
  805. router={initialData.router}
  806. loading={false}
  807. setSavedQuery={jest.fn()}
  808. />,
  809. {
  810. context: initialData.routerContext,
  811. organization,
  812. }
  813. );
  814. await userEvent.click(await screen.findByRole('button', {name: 'Show Tags'}));
  815. await waitFor(() => expect(mockRequests.eventFacetsMock).toHaveBeenCalled());
  816. // TODO(edward): update this to be less generic
  817. await userEvent.click(screen.getByText('environment'));
  818. await userEvent.click(screen.getByText('foo'));
  819. // since environment collides with the environment field, it is wrapped with `tags[...]`
  820. expect(
  821. await screen.findByRole('link', {
  822. name: 'environment, dev, 100% of all events. View events with this tag value.',
  823. })
  824. ).toBeInTheDocument();
  825. expect(
  826. screen.getByRole('link', {
  827. name: 'foo, bar, 100% of all events. View events with this tag value.',
  828. })
  829. ).toBeInTheDocument();
  830. });
  831. it('respects pinned filters for prebuilt queries', async function () {
  832. const organization = TestStubs.Organization({
  833. features: [...features, 'global-views'],
  834. });
  835. const initialData = initializeOrg({
  836. ...initializeOrg(),
  837. organization,
  838. router: {
  839. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  840. },
  841. });
  842. renderMockRequests();
  843. jest.spyOn(PageFilterPersistence, 'getPageFilterStorage').mockReturnValue({
  844. state: {
  845. project: [1],
  846. environment: [],
  847. start: null,
  848. end: null,
  849. period: '14d',
  850. utc: null,
  851. },
  852. pinnedFilters: new Set(['projects']),
  853. });
  854. ProjectsStore.loadInitialData([TestStubs.Project({id: 1, slug: 'Pinned Project'})]);
  855. render(
  856. <Results
  857. organization={organization}
  858. location={initialData.router.location}
  859. router={initialData.router}
  860. loading={false}
  861. setSavedQuery={jest.fn()}
  862. />,
  863. {context: initialData.routerContext, organization}
  864. );
  865. const projectPageFilter = await screen.findByTestId('page-filter-project-selector');
  866. expect(projectPageFilter).toHaveTextContent('Pinned Project');
  867. });
  868. it('displays tip when events response contains a tip', async function () {
  869. renderMockRequests();
  870. const eventsResultsMock = MockApiClient.addMockResponse({
  871. url: '/organizations/org-slug/events/',
  872. body: {
  873. meta: {
  874. fields: {},
  875. tips: {query: 'this is a tip'},
  876. },
  877. data: [],
  878. },
  879. });
  880. const organization = TestStubs.Organization({
  881. features,
  882. });
  883. const initialData = initializeOrg({
  884. ...initializeOrg(),
  885. organization,
  886. router: {
  887. location: {query: {...generateFields(), yAxis: 'count()'}},
  888. },
  889. });
  890. ProjectsStore.loadInitialData([TestStubs.Project()]);
  891. render(
  892. <Results
  893. organization={organization}
  894. location={initialData.router.location}
  895. router={initialData.router}
  896. loading={false}
  897. setSavedQuery={jest.fn()}
  898. />,
  899. {context: initialData.routerContext, organization}
  900. );
  901. await waitFor(() => {
  902. expect(eventsResultsMock).toHaveBeenCalled();
  903. });
  904. expect(screen.getByText('this is a tip')).toBeInTheDocument();
  905. });
  906. it('renders metric fallback alert', async function () {
  907. const organization = TestStubs.Organization({
  908. features: ['discover-basic'],
  909. });
  910. const initialData = initializeOrg({
  911. ...initializeOrg(),
  912. organization,
  913. router: {
  914. location: {query: {fromMetric: true, id: '1'}},
  915. },
  916. });
  917. ProjectsStore.loadInitialData([TestStubs.Project()]);
  918. renderMockRequests();
  919. render(
  920. <Results
  921. organization={organization}
  922. location={initialData.router.location}
  923. router={initialData.router}
  924. loading={false}
  925. setSavedQuery={jest.fn()}
  926. />,
  927. {
  928. context: initialData.routerContext,
  929. organization,
  930. }
  931. );
  932. expect(
  933. await screen.findByText(
  934. /You've navigated to this page from a performance metric widget generated from processed events/
  935. )
  936. ).toBeInTheDocument();
  937. });
  938. it('renders unparameterized data banner', async function () {
  939. const organization = TestStubs.Organization({
  940. features: ['discover-basic'],
  941. });
  942. const initialData = initializeOrg({
  943. ...initializeOrg(),
  944. organization,
  945. router: {
  946. location: {query: {showUnparameterizedBanner: true, id: '1'}},
  947. },
  948. });
  949. ProjectsStore.loadInitialData([TestStubs.Project()]);
  950. renderMockRequests();
  951. render(
  952. <Results
  953. organization={organization}
  954. location={initialData.router.location}
  955. router={initialData.router}
  956. loading={false}
  957. setSavedQuery={jest.fn()}
  958. />,
  959. {
  960. context: initialData.routerContext,
  961. organization,
  962. }
  963. );
  964. expect(
  965. await screen.findByText(/These are unparameterized transactions/)
  966. ).toBeInTheDocument();
  967. });
  968. it('updates the homepage query with up to date eventView when Set as Default is clicked', async () => {
  969. const mockHomepageUpdate = MockApiClient.addMockResponse({
  970. url: '/organizations/org-slug/discover/homepage/',
  971. method: 'PUT',
  972. statusCode: 200,
  973. });
  974. const organization = TestStubs.Organization({
  975. features: ['discover-basic', 'discover-query'],
  976. });
  977. const initialData = initializeOrg({
  978. ...initializeOrg(),
  979. organization,
  980. router: {
  981. // These fields take priority and should be sent in the request
  982. location: {query: {field: ['title', 'user'], id: '1'}},
  983. },
  984. });
  985. ProjectsStore.loadInitialData([TestStubs.Project()]);
  986. render(
  987. <Results
  988. organization={organization}
  989. location={initialData.router.location}
  990. router={initialData.router}
  991. loading={false}
  992. setSavedQuery={jest.fn()}
  993. />,
  994. {context: initialData.routerContext, organization}
  995. );
  996. await waitFor(() =>
  997. expect(screen.getByRole('button', {name: /set as default/i})).toBeEnabled()
  998. );
  999. await userEvent.click(screen.getByText('Set as Default'));
  1000. expect(mockHomepageUpdate).toHaveBeenCalledWith(
  1001. '/organizations/org-slug/discover/homepage/',
  1002. expect.objectContaining({
  1003. data: expect.objectContaining({
  1004. fields: ['title', 'user'],
  1005. }),
  1006. })
  1007. );
  1008. });
  1009. it('Changes the Use as Discover button to a reset button for saved query', async () => {
  1010. renderMockRequests();
  1011. MockApiClient.addMockResponse({
  1012. url: '/organizations/org-slug/discover/homepage/',
  1013. method: 'PUT',
  1014. statusCode: 200,
  1015. body: {
  1016. id: '2',
  1017. name: '',
  1018. projects: [],
  1019. version: 2,
  1020. expired: false,
  1021. dateCreated: '2021-04-08T17:53:25.195782Z',
  1022. dateUpdated: '2021-04-09T12:13:18.567264Z',
  1023. createdBy: {
  1024. id: '2',
  1025. },
  1026. environment: [],
  1027. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  1028. widths: ['-1', '-1', '-1', '-1', '-1'],
  1029. range: '24h',
  1030. orderby: '-user.display',
  1031. },
  1032. });
  1033. const organization = TestStubs.Organization({
  1034. features: ['discover-basic', 'discover-query'],
  1035. });
  1036. const initialData = initializeOrg({
  1037. ...initializeOrg(),
  1038. organization,
  1039. router: {
  1040. location: {query: {id: '1'}},
  1041. },
  1042. });
  1043. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1044. const {rerender} = render(
  1045. <Results
  1046. loading={false}
  1047. setSavedQuery={jest.fn()}
  1048. organization={organization}
  1049. location={initialData.router.location}
  1050. router={initialData.router}
  1051. />,
  1052. {context: initialData.routerContext, organization}
  1053. );
  1054. await waitFor(() =>
  1055. expect(screen.getByRole('button', {name: /set as default/i})).toBeEnabled()
  1056. );
  1057. await userEvent.click(screen.getByText('Set as Default'));
  1058. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  1059. await userEvent.click(screen.getByText('Total Period'));
  1060. await userEvent.click(screen.getByText('Previous Period'));
  1061. const rerenderData = initializeOrg({
  1062. ...initializeOrg(),
  1063. organization,
  1064. router: {
  1065. location: {query: {...initialData.router.location.query, display: 'previous'}},
  1066. },
  1067. });
  1068. rerender(
  1069. <Results
  1070. loading={false}
  1071. setSavedQuery={jest.fn()}
  1072. organization={organization}
  1073. location={rerenderData.router.location}
  1074. router={rerenderData.router}
  1075. />
  1076. );
  1077. screen.getByText('Previous Period');
  1078. expect(await screen.findByText('Set as Default')).toBeInTheDocument();
  1079. });
  1080. it('Changes the Use as Discover button to a reset button for prebuilt query', async () => {
  1081. MockApiClient.addMockResponse({
  1082. url: '/organizations/org-slug/discover/homepage/',
  1083. method: 'PUT',
  1084. statusCode: 200,
  1085. body: {...TRANSACTION_VIEWS[0], name: ''},
  1086. });
  1087. const organization = TestStubs.Organization({
  1088. features: ['discover-basic', 'discover-query'],
  1089. });
  1090. const initialData = initializeOrg({
  1091. ...initializeOrg(),
  1092. organization,
  1093. router: {
  1094. location: {
  1095. ...TestStubs.location(),
  1096. query: {
  1097. ...EventView.fromNewQueryWithLocation(
  1098. TRANSACTION_VIEWS[0],
  1099. TestStubs.location()
  1100. ).generateQueryStringObject(),
  1101. },
  1102. },
  1103. },
  1104. });
  1105. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1106. const {rerender} = render(
  1107. <Results
  1108. organization={organization}
  1109. location={initialData.router.location}
  1110. router={initialData.router}
  1111. loading={false}
  1112. setSavedQuery={jest.fn()}
  1113. />,
  1114. {context: initialData.routerContext, organization}
  1115. );
  1116. await screen.findAllByText(TRANSACTION_VIEWS[0].name);
  1117. await userEvent.click(screen.getByText('Set as Default'));
  1118. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  1119. await userEvent.click(screen.getByText('Total Period'));
  1120. await userEvent.click(screen.getByText('Previous Period'));
  1121. const rerenderData = initializeOrg({
  1122. ...initializeOrg(),
  1123. organization,
  1124. router: {
  1125. location: {query: {...initialData.router.location.query, display: 'previous'}},
  1126. },
  1127. });
  1128. rerender(
  1129. <Results
  1130. organization={organization}
  1131. location={rerenderData.router.location}
  1132. router={rerenderData.router}
  1133. loading={false}
  1134. setSavedQuery={jest.fn()}
  1135. />
  1136. );
  1137. screen.getByText('Previous Period');
  1138. expect(await screen.findByText('Set as Default')).toBeInTheDocument();
  1139. });
  1140. it('links back to the homepage through the Discover breadcrumb', () => {
  1141. const organization = TestStubs.Organization({
  1142. features: ['discover-basic', 'discover-query'],
  1143. });
  1144. const initialData = initializeOrg({
  1145. ...initializeOrg(),
  1146. organization,
  1147. router: {
  1148. location: {query: {id: '1'}},
  1149. },
  1150. });
  1151. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1152. render(
  1153. <Results
  1154. organization={organization}
  1155. location={initialData.router.location}
  1156. router={initialData.router}
  1157. loading={false}
  1158. setSavedQuery={jest.fn()}
  1159. />,
  1160. {context: initialData.routerContext, organization}
  1161. );
  1162. expect(screen.getByText('Discover')).toHaveAttribute(
  1163. 'href',
  1164. expect.stringMatching(new RegExp('^/organizations/org-slug/discover/homepage/'))
  1165. );
  1166. });
  1167. it('links back to the Saved Queries through the Saved Queries breadcrumb', () => {
  1168. const organization = TestStubs.Organization({
  1169. features: ['discover-basic', 'discover-query'],
  1170. });
  1171. const initialData = initializeOrg({
  1172. ...initializeOrg(),
  1173. organization,
  1174. router: {
  1175. location: {query: {id: '1'}},
  1176. },
  1177. });
  1178. render(
  1179. <Results
  1180. organization={organization}
  1181. location={initialData.router.location}
  1182. router={initialData.router}
  1183. loading={false}
  1184. setSavedQuery={jest.fn()}
  1185. />,
  1186. {context: initialData.routerContext, organization}
  1187. );
  1188. expect(screen.getByRole('link', {name: 'Saved Queries'})).toHaveAttribute(
  1189. 'href',
  1190. expect.stringMatching(new RegExp('^/organizations/org-slug/discover/queries/'))
  1191. );
  1192. });
  1193. it('allows users to Set As Default on the All Events query', () => {
  1194. const organization = TestStubs.Organization({
  1195. features: ['discover-basic', 'discover-query'],
  1196. });
  1197. const initialData = initializeOrg({
  1198. ...initializeOrg(),
  1199. organization,
  1200. router: {
  1201. location: {
  1202. ...TestStubs.location(),
  1203. query: {
  1204. ...EventView.fromNewQueryWithLocation(
  1205. DEFAULT_EVENT_VIEW,
  1206. TestStubs.location()
  1207. ).generateQueryStringObject(),
  1208. },
  1209. },
  1210. },
  1211. });
  1212. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1213. render(
  1214. <Results
  1215. organization={organization}
  1216. location={initialData.router.location}
  1217. router={initialData.router}
  1218. loading={false}
  1219. setSavedQuery={jest.fn()}
  1220. />,
  1221. {context: initialData.routerContext, organization}
  1222. );
  1223. expect(screen.getByTestId('set-as-default')).toBeEnabled();
  1224. });
  1225. });
  1226. });