results.spec.tsx 38 KB

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