results.spec.tsx 39 KB

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