results.spec.jsx 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918
  1. import {browserHistory} from 'react-router';
  2. import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import {triggerPress} from 'sentry-test/utils';
  6. import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence';
  7. import ProjectsStore from 'sentry/stores/projectsStore';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import Results from 'sentry/views/eventsV2/results';
  10. import {OrganizationContext} from 'sentry/views/organizationContext';
  11. import {TRANSACTION_VIEWS} from './data';
  12. const FIELDS = [
  13. {
  14. field: 'title',
  15. },
  16. {
  17. field: 'timestamp',
  18. },
  19. {
  20. field: 'user',
  21. },
  22. {
  23. field: 'count()',
  24. },
  25. ];
  26. const generateFields = () => ({
  27. field: FIELDS.map(i => i.field),
  28. });
  29. describe('Results', function () {
  30. enforceActOnUseLegacyStoreHook();
  31. const eventTitle = 'Oh no something bad';
  32. let eventsResultsMock, eventsv2ResultsMock, mockSaved, eventsStatsMock, mockVisit;
  33. const mountWithThemeAndOrg = (component, opts, organization) =>
  34. mountWithTheme(component, {
  35. ...opts,
  36. wrappingComponent: ({children}) => (
  37. <OrganizationContext.Provider value={organization}>
  38. {children}
  39. </OrganizationContext.Provider>
  40. ),
  41. });
  42. beforeEach(function () {
  43. MockApiClient.addMockResponse({
  44. url: '/organizations/org-slug/projects/',
  45. body: [],
  46. });
  47. MockApiClient.addMockResponse({
  48. url: '/organizations/org-slug/projects-count/',
  49. body: {myProjects: 10, allProjects: 300},
  50. });
  51. MockApiClient.addMockResponse({
  52. url: '/organizations/org-slug/tags/',
  53. body: [],
  54. });
  55. eventsStatsMock = MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/events-stats/',
  57. body: {data: [[123, []]]},
  58. });
  59. MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/recent-searches/',
  61. body: [],
  62. });
  63. MockApiClient.addMockResponse({
  64. url: '/organizations/org-slug/recent-searches/',
  65. method: 'POST',
  66. body: [],
  67. });
  68. MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/releases/stats/',
  70. body: [],
  71. });
  72. const eventsV2ResultsMockBody = {
  73. meta: {
  74. id: 'string',
  75. title: 'string',
  76. 'project.name': 'string',
  77. timestamp: 'date',
  78. 'user.id': 'string',
  79. },
  80. data: [
  81. {
  82. id: 'deadbeef',
  83. 'user.id': 'alberto leal',
  84. title: eventTitle,
  85. 'project.name': 'project-slug',
  86. timestamp: '2019-05-23T22:12:48+00:00',
  87. },
  88. ],
  89. };
  90. const eventsResultsMockBody = {
  91. meta: {
  92. fields: {
  93. id: 'string',
  94. title: 'string',
  95. 'project.name': 'string',
  96. timestamp: 'date',
  97. 'user.id': 'string',
  98. },
  99. },
  100. data: [
  101. {
  102. id: 'deadbeef',
  103. 'user.id': 'alberto leal',
  104. title: eventTitle,
  105. 'project.name': 'project-slug',
  106. timestamp: '2019-05-23T22:12:48+00:00',
  107. },
  108. ],
  109. };
  110. eventsv2ResultsMock = MockApiClient.addMockResponse({
  111. url: '/organizations/org-slug/eventsv2/',
  112. body: eventsV2ResultsMockBody,
  113. });
  114. eventsResultsMock = MockApiClient.addMockResponse({
  115. url: '/organizations/org-slug/events/',
  116. body: eventsResultsMockBody,
  117. });
  118. MockApiClient.addMockResponse({
  119. url: '/organizations/org-slug/events-meta/',
  120. body: {
  121. count: 2,
  122. },
  123. });
  124. MockApiClient.addMockResponse({
  125. url: '/organizations/org-slug/events/project-slug:deadbeef/',
  126. method: 'GET',
  127. body: {
  128. id: '1234',
  129. size: 1200,
  130. eventID: 'deadbeef',
  131. title: 'Oh no something bad',
  132. message: 'It was not good',
  133. dateCreated: '2019-05-23T22:12:48+00:00',
  134. entries: [
  135. {
  136. type: 'message',
  137. message: 'bad stuff',
  138. data: {},
  139. },
  140. ],
  141. tags: [{key: 'browser', value: 'Firefox'}],
  142. },
  143. });
  144. MockApiClient.addMockResponse({
  145. url: '/organizations/org-slug/events-facets/',
  146. body: [
  147. {
  148. key: 'release',
  149. topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
  150. },
  151. {
  152. key: 'environment',
  153. topValues: [{count: 2, value: 'dev', name: 'dev'}],
  154. },
  155. {
  156. key: 'foo',
  157. topValues: [{count: 1, value: 'bar', name: 'bar'}],
  158. },
  159. ],
  160. });
  161. mockVisit = MockApiClient.addMockResponse({
  162. url: '/organizations/org-slug/discover/saved/1/visit/',
  163. method: 'POST',
  164. body: [],
  165. statusCode: 200,
  166. });
  167. mockSaved = MockApiClient.addMockResponse({
  168. url: '/organizations/org-slug/discover/saved/1/',
  169. method: 'GET',
  170. statusCode: 200,
  171. body: {
  172. id: '1',
  173. name: 'new',
  174. projects: [],
  175. version: 2,
  176. expired: false,
  177. dateCreated: '2021-04-08T17:53:25.195782Z',
  178. dateUpdated: '2021-04-09T12:13:18.567264Z',
  179. createdBy: {
  180. id: '2',
  181. },
  182. environment: [],
  183. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  184. widths: ['-1', '-1', '-1', '-1', '-1'],
  185. range: '24h',
  186. orderby: '-user.display',
  187. },
  188. });
  189. MockApiClient.addMockResponse({
  190. url: '/organizations/org-slug/discover/homepage/',
  191. method: 'GET',
  192. statusCode: 200,
  193. body: {
  194. id: '2',
  195. name: '',
  196. projects: [],
  197. version: 2,
  198. expired: false,
  199. dateCreated: '2021-04-08T17:53:25.195782Z',
  200. dateUpdated: '2021-04-09T12:13:18.567264Z',
  201. createdBy: {
  202. id: '2',
  203. },
  204. environment: [],
  205. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  206. widths: ['-1', '-1', '-1', '-1', '-1'],
  207. range: '24h',
  208. orderby: '-user.display',
  209. },
  210. });
  211. });
  212. afterEach(function () {
  213. jest.clearAllMocks();
  214. MockApiClient.clearMockResponses();
  215. act(() => ProjectsStore.reset());
  216. });
  217. describe('EventsV2', function () {
  218. const features = ['discover-basic'];
  219. it('loads data when moving from an invalid to valid EventView', async function () {
  220. const organization = TestStubs.Organization({
  221. features,
  222. });
  223. // Start off with an invalid view (empty is invalid)
  224. const initialData = initializeOrg({
  225. organization,
  226. router: {
  227. location: {query: {query: 'tag:value'}},
  228. },
  229. });
  230. ProjectsStore.loadInitialData([TestStubs.Project()]);
  231. const wrapper = mountWithThemeAndOrg(
  232. <Results
  233. organization={organization}
  234. location={initialData.router.location}
  235. router={initialData.router}
  236. />,
  237. initialData.routerContext,
  238. organization
  239. );
  240. await tick();
  241. wrapper.update();
  242. // No request as eventview was invalid.
  243. expect(eventsv2ResultsMock).not.toHaveBeenCalled();
  244. // Should redirect and retain the old query value..
  245. expect(browserHistory.replace).toHaveBeenCalledWith(
  246. expect.objectContaining({
  247. pathname: '/organizations/org-slug/discover/results/',
  248. query: expect.objectContaining({
  249. query: 'tag:value',
  250. }),
  251. })
  252. );
  253. // Update location simulating a redirect.
  254. wrapper.setProps({location: {query: {...generateFields()}}});
  255. wrapper.update();
  256. // Should load events once
  257. expect(eventsv2ResultsMock).toHaveBeenCalled();
  258. });
  259. it('pagination cursor should be cleared when making a search', async function () {
  260. const organization = TestStubs.Organization({
  261. features,
  262. });
  263. const initialData = initializeOrg({
  264. organization,
  265. router: {
  266. location: {
  267. query: {
  268. ...generateFields(),
  269. cursor: '0%3A50%3A0',
  270. },
  271. },
  272. },
  273. });
  274. ProjectsStore.loadInitialData([TestStubs.Project()]);
  275. const wrapper = mountWithThemeAndOrg(
  276. <Results
  277. organization={organization}
  278. location={initialData.router.location}
  279. router={initialData.router}
  280. />,
  281. initialData.routerContext,
  282. organization
  283. );
  284. await tick();
  285. wrapper.update();
  286. // ensure cursor query string is initially present in the location
  287. expect(initialData.router.location).toEqual({
  288. query: {
  289. ...generateFields(),
  290. cursor: '0%3A50%3A0',
  291. },
  292. });
  293. // perform a search
  294. const search = wrapper.find('#smart-search-input').first();
  295. search.simulate('change', {target: {value: 'geo:canada'}}).simulate('submit', {
  296. preventDefault() {},
  297. });
  298. await tick();
  299. // should only be called with saved queries
  300. expect(mockVisit).not.toHaveBeenCalled();
  301. // cursor query string should be omitted from the query string
  302. expect(initialData.router.push).toHaveBeenCalledWith({
  303. pathname: undefined,
  304. query: {
  305. ...generateFields(),
  306. query: 'geo:canada',
  307. statsPeriod: '14d',
  308. },
  309. });
  310. wrapper.unmount();
  311. });
  312. it('renders a y-axis selector', async function () {
  313. const organization = TestStubs.Organization({
  314. features,
  315. });
  316. const initialData = initializeOrg({
  317. organization,
  318. router: {
  319. location: {query: {...generateFields(), yAxis: 'count()'}},
  320. },
  321. });
  322. ProjectsStore.loadInitialData([TestStubs.Project()]);
  323. const wrapper = mountWithThemeAndOrg(
  324. <Results
  325. organization={organization}
  326. location={initialData.router.location}
  327. router={initialData.router}
  328. />,
  329. initialData.routerContext,
  330. organization
  331. );
  332. // y-axis selector is last.
  333. const selector = wrapper.find('OptionSelector').last();
  334. // Open the selector
  335. act(() => {
  336. triggerPress(selector.find('button[aria-haspopup="listbox"]'));
  337. });
  338. await tick();
  339. wrapper.update();
  340. // Click one of the options.
  341. wrapper.find('Option').first().simulate('click');
  342. await tick();
  343. wrapper.update();
  344. const eventsRequest = wrapper.find('EventsChart');
  345. expect(eventsRequest.props().yAxis).toEqual(['count()']);
  346. wrapper.unmount();
  347. });
  348. it('renders a display selector', async function () {
  349. const organization = TestStubs.Organization({
  350. features,
  351. });
  352. const initialData = initializeOrg({
  353. organization,
  354. router: {
  355. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  356. },
  357. });
  358. const wrapper = mountWithThemeAndOrg(
  359. <Results
  360. organization={organization}
  361. location={initialData.router.location}
  362. router={initialData.router}
  363. />,
  364. initialData.routerContext,
  365. organization
  366. );
  367. act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
  368. await tick();
  369. wrapper.update();
  370. // display selector is first.
  371. const selector = wrapper.find('OptionSelector').first();
  372. // Open the selector
  373. act(() => {
  374. triggerPress(selector.find('button[aria-haspopup="listbox"]'));
  375. });
  376. await tick();
  377. wrapper.update();
  378. // Click the 'default' option.
  379. wrapper.find('Option').first().simulate('click');
  380. await tick();
  381. wrapper.update();
  382. const eventsRequest = wrapper.find('EventsChart').props();
  383. expect(eventsRequest.disableReleases).toEqual(false);
  384. expect(eventsRequest.disablePrevious).toEqual(true);
  385. wrapper.unmount();
  386. });
  387. it('excludes top5 options when plan does not include discover-query', async function () {
  388. const organization = TestStubs.Organization({
  389. features: ['discover-basic'],
  390. });
  391. const initialData = initializeOrg({
  392. organization,
  393. router: {
  394. location: {query: {...generateFields(), display: 'previous'}},
  395. },
  396. });
  397. ProjectsStore.loadInitialData([TestStubs.Project()]);
  398. const wrapper = mountWithThemeAndOrg(
  399. <Results
  400. organization={organization}
  401. location={initialData.router.location}
  402. router={initialData.router}
  403. />,
  404. initialData.routerContext,
  405. organization
  406. );
  407. // display selector is first.
  408. const selector = wrapper.find('OptionSelector').first();
  409. // Open the selector
  410. act(() => {
  411. triggerPress(selector.find('button[aria-haspopup="listbox"]'));
  412. });
  413. await tick();
  414. wrapper.update();
  415. // Make sure the top5 option isn't present
  416. const options = wrapper
  417. .find('Option [data-test-id]')
  418. .map(item => item.prop('data-test-id'));
  419. expect(options).not.toContain('top5');
  420. expect(options).not.toContain('dailytop5');
  421. expect(options).toContain('default');
  422. wrapper.unmount();
  423. });
  424. it('needs confirmation on long queries', async function () {
  425. const organization = TestStubs.Organization({
  426. features: ['discover-basic'],
  427. });
  428. const initialData = initializeOrg({
  429. organization,
  430. router: {
  431. location: {query: {...generateFields(), statsPeriod: '60d', project: '-1'}},
  432. },
  433. });
  434. const wrapper = mountWithThemeAndOrg(
  435. <Results
  436. organization={organization}
  437. location={initialData.router.location}
  438. router={initialData.router}
  439. />,
  440. initialData.routerContext,
  441. organization
  442. );
  443. await tick();
  444. const results = wrapper.find('Results');
  445. expect(results.state('needConfirmation')).toEqual(true);
  446. wrapper.unmount();
  447. });
  448. it('needs confirmation on long query with explicit projects', async function () {
  449. const organization = TestStubs.Organization({
  450. features: ['discover-basic'],
  451. });
  452. const initialData = initializeOrg({
  453. organization,
  454. router: {
  455. location: {
  456. query: {
  457. ...generateFields(),
  458. statsPeriod: '60d',
  459. project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
  460. },
  461. },
  462. },
  463. });
  464. const wrapper = mountWithThemeAndOrg(
  465. <Results
  466. organization={organization}
  467. location={initialData.router.location}
  468. router={initialData.router}
  469. />,
  470. initialData.routerContext,
  471. organization
  472. );
  473. await tick();
  474. const results = wrapper.find('Results');
  475. expect(results.state('needConfirmation')).toEqual(true);
  476. wrapper.unmount();
  477. });
  478. it('does not need confirmation on short queries', async function () {
  479. const organization = TestStubs.Organization({
  480. features: ['discover-basic'],
  481. });
  482. const initialData = initializeOrg({
  483. organization,
  484. router: {
  485. location: {query: {...generateFields(), statsPeriod: '30d', project: '-1'}},
  486. },
  487. });
  488. const wrapper = mountWithThemeAndOrg(
  489. <Results
  490. organization={organization}
  491. location={initialData.router.location}
  492. router={initialData.router}
  493. />,
  494. initialData.routerContext,
  495. organization
  496. );
  497. await tick();
  498. const results = wrapper.find('Results');
  499. expect(results.state('needConfirmation')).toEqual(false);
  500. wrapper.unmount();
  501. });
  502. it('does not need confirmation with to few projects', async function () {
  503. const organization = TestStubs.Organization({
  504. features: ['discover-basic'],
  505. });
  506. const initialData = initializeOrg({
  507. organization,
  508. router: {
  509. location: {
  510. query: {...generateFields(), statsPeriod: '90d', project: [1, 2, 3, 4]},
  511. },
  512. },
  513. });
  514. const wrapper = mountWithThemeAndOrg(
  515. <Results
  516. organization={organization}
  517. location={initialData.router.location}
  518. router={initialData.router}
  519. />,
  520. initialData.routerContext,
  521. organization
  522. );
  523. await tick();
  524. const results = wrapper.find('Results');
  525. expect(results.state('needConfirmation')).toEqual(false);
  526. wrapper.unmount();
  527. });
  528. it('retrieves saved query', async function () {
  529. const organization = TestStubs.Organization({
  530. features,
  531. slug: 'org-slug',
  532. });
  533. const initialData = initializeOrg({
  534. organization,
  535. router: {
  536. location: {query: {id: '1', statsPeriod: '24h'}},
  537. },
  538. });
  539. const wrapper = mountWithThemeAndOrg(
  540. <Results
  541. organization={organization}
  542. location={initialData.router.location}
  543. router={initialData.router}
  544. />,
  545. initialData.routerContext,
  546. organization
  547. );
  548. await tick();
  549. const savedQuery = wrapper.find('SavedQueryAPI').state('savedQuery');
  550. expect(savedQuery.name).toEqual('new');
  551. expect(savedQuery.id).toEqual('1');
  552. expect(savedQuery.fields).toEqual([
  553. 'title',
  554. 'event.type',
  555. 'project',
  556. 'user.display',
  557. 'timestamp',
  558. ]);
  559. expect(savedQuery.projects).toEqual([]);
  560. expect(savedQuery.range).toEqual('24h');
  561. expect(mockSaved).toHaveBeenCalled();
  562. expect(mockVisit).toHaveBeenCalledTimes(1);
  563. wrapper.unmount();
  564. });
  565. it('creates event view from saved query', async function () {
  566. const organization = TestStubs.Organization({
  567. features,
  568. slug: 'org-slug',
  569. });
  570. const initialData = initializeOrg({
  571. organization,
  572. router: {
  573. location: {query: {id: '1', statsPeriod: '24h'}},
  574. },
  575. });
  576. const wrapper = mountWithThemeAndOrg(
  577. <Results
  578. organization={organization}
  579. location={initialData.router.location}
  580. router={initialData.router}
  581. />,
  582. initialData.routerContext,
  583. organization
  584. );
  585. await tick();
  586. const eventView = wrapper.find('Results').state('eventView');
  587. expect(eventView.name).toEqual('new');
  588. expect(eventView.id).toEqual('1');
  589. expect(eventView.fields.length).toEqual(5);
  590. expect(eventView.project).toEqual([]);
  591. expect(eventView.statsPeriod).toEqual('24h');
  592. expect(eventView.sorts).toEqual([{field: 'user.display', kind: 'desc'}]);
  593. wrapper.unmount();
  594. });
  595. it('overrides saved query params with location query params', async function () {
  596. const organization = TestStubs.Organization({
  597. features,
  598. slug: 'org-slug',
  599. });
  600. const initialData = initializeOrg({
  601. organization,
  602. router: {
  603. location: {
  604. query: {
  605. id: '1',
  606. statsPeriod: '7d',
  607. project: [2],
  608. environment: ['production'],
  609. },
  610. },
  611. },
  612. });
  613. const wrapper = mountWithThemeAndOrg(
  614. <Results
  615. organization={organization}
  616. location={initialData.router.location}
  617. router={initialData.router}
  618. />,
  619. initialData.routerContext,
  620. organization
  621. );
  622. await tick();
  623. const eventView = wrapper.find('Results').state('eventView');
  624. expect(eventView.name).toEqual('new');
  625. expect(eventView.id).toEqual('1');
  626. expect(eventView.fields.length).toEqual(5);
  627. expect(eventView.project).toEqual([2]);
  628. expect(eventView.statsPeriod).toEqual('7d');
  629. expect(eventView.environment).toEqual(['production']);
  630. expect(mockVisit).toHaveBeenCalledTimes(1);
  631. wrapper.unmount();
  632. });
  633. it('updates chart whenever yAxis parameter changes', async function () {
  634. const organization = TestStubs.Organization({
  635. features,
  636. });
  637. const initialData = initializeOrg({
  638. organization,
  639. router: {
  640. location: {query: {...generateFields(), yAxis: 'count()'}},
  641. },
  642. });
  643. ProjectsStore.loadInitialData([TestStubs.Project()]);
  644. const wrapper = mountWithThemeAndOrg(
  645. <Results
  646. organization={organization}
  647. location={initialData.router.location}
  648. router={initialData.router}
  649. />,
  650. initialData.routerContext,
  651. organization
  652. );
  653. // Should load events once
  654. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  655. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  656. 1,
  657. '/organizations/org-slug/events-stats/',
  658. expect.objectContaining({
  659. query: expect.objectContaining({
  660. statsPeriod: '14d',
  661. yAxis: ['count()'],
  662. }),
  663. })
  664. );
  665. // Update location simulating a browser back button action
  666. wrapper.setProps({
  667. location: {
  668. query: {...generateFields(), yAxis: 'count_unique(user)'},
  669. },
  670. });
  671. await tick();
  672. wrapper.update();
  673. // Should load events again
  674. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  675. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  676. 2,
  677. '/organizations/org-slug/events-stats/',
  678. expect.objectContaining({
  679. query: expect.objectContaining({
  680. statsPeriod: '14d',
  681. yAxis: ['count_unique(user)'],
  682. }),
  683. })
  684. );
  685. wrapper.unmount();
  686. });
  687. it('updates chart whenever display parameter changes', async function () {
  688. const organization = TestStubs.Organization({
  689. features,
  690. });
  691. const initialData = initializeOrg({
  692. organization,
  693. router: {
  694. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  695. },
  696. });
  697. ProjectsStore.loadInitialData([TestStubs.Project()]);
  698. const wrapper = mountWithThemeAndOrg(
  699. <Results
  700. organization={organization}
  701. location={initialData.router.location}
  702. router={initialData.router}
  703. />,
  704. initialData.routerContext,
  705. organization
  706. );
  707. // Should load events once
  708. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  709. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  710. 1,
  711. '/organizations/org-slug/events-stats/',
  712. expect.objectContaining({
  713. query: expect.objectContaining({
  714. statsPeriod: '14d',
  715. yAxis: ['count()'],
  716. }),
  717. })
  718. );
  719. // Update location simulating a browser back button action
  720. wrapper.setProps({
  721. location: {
  722. query: {...generateFields(), display: 'previous', yAxis: 'count()'},
  723. },
  724. });
  725. await tick();
  726. wrapper.update();
  727. // Should load events again
  728. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  729. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  730. 2,
  731. '/organizations/org-slug/events-stats/',
  732. expect.objectContaining({
  733. query: expect.objectContaining({
  734. statsPeriod: '28d',
  735. yAxis: ['count()'],
  736. }),
  737. })
  738. );
  739. wrapper.unmount();
  740. });
  741. it('updates chart whenever display and yAxis parameters change', async function () {
  742. const organization = TestStubs.Organization({
  743. features,
  744. });
  745. const initialData = initializeOrg({
  746. organization,
  747. router: {
  748. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  749. },
  750. });
  751. ProjectsStore.loadInitialData([TestStubs.Project()]);
  752. const wrapper = mountWithThemeAndOrg(
  753. <Results
  754. organization={organization}
  755. location={initialData.router.location}
  756. router={initialData.router}
  757. />,
  758. initialData.routerContext,
  759. organization
  760. );
  761. // Should load events once
  762. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  763. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  764. 1,
  765. '/organizations/org-slug/events-stats/',
  766. expect.objectContaining({
  767. query: expect.objectContaining({
  768. statsPeriod: '14d',
  769. yAxis: ['count()'],
  770. }),
  771. })
  772. );
  773. // Update location simulating a browser back button action
  774. wrapper.setProps({
  775. location: {
  776. query: {...generateFields(), display: 'previous', yAxis: 'count_unique(user)'},
  777. },
  778. });
  779. await tick();
  780. wrapper.update();
  781. // Should load events again
  782. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  783. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  784. 2,
  785. '/organizations/org-slug/events-stats/',
  786. expect.objectContaining({
  787. query: expect.objectContaining({
  788. statsPeriod: '28d',
  789. yAxis: ['count_unique(user)'],
  790. }),
  791. })
  792. );
  793. wrapper.unmount();
  794. });
  795. });
  796. describe('Events', function () {
  797. const features = ['discover-basic', 'discover-frontend-use-events-endpoint'];
  798. it('loads data when moving from an invalid to valid EventView', async function () {
  799. const organization = TestStubs.Organization({
  800. features,
  801. });
  802. // Start off with an invalid view (empty is invalid)
  803. const initialData = initializeOrg({
  804. organization,
  805. router: {
  806. location: {query: {query: 'tag:value'}},
  807. },
  808. });
  809. ProjectsStore.loadInitialData([TestStubs.Project()]);
  810. const wrapper = mountWithThemeAndOrg(
  811. <Results
  812. organization={organization}
  813. location={initialData.router.location}
  814. router={initialData.router}
  815. />,
  816. initialData.routerContext,
  817. organization
  818. );
  819. await tick();
  820. wrapper.update();
  821. // No request as eventview was invalid.
  822. expect(eventsResultsMock).not.toHaveBeenCalled();
  823. // Should redirect and retain the old query value..
  824. expect(browserHistory.replace).toHaveBeenCalledWith(
  825. expect.objectContaining({
  826. pathname: '/organizations/org-slug/discover/results/',
  827. query: expect.objectContaining({
  828. query: 'tag:value',
  829. }),
  830. })
  831. );
  832. // Update location simulating a redirect.
  833. wrapper.setProps({location: {query: {...generateFields()}}});
  834. wrapper.update();
  835. // Should load events once
  836. expect(eventsResultsMock).toHaveBeenCalled();
  837. });
  838. it('pagination cursor should be cleared when making a search', async function () {
  839. const organization = TestStubs.Organization({
  840. features,
  841. });
  842. const initialData = initializeOrg({
  843. organization,
  844. router: {
  845. location: {
  846. query: {
  847. ...generateFields(),
  848. cursor: '0%3A50%3A0',
  849. },
  850. },
  851. },
  852. });
  853. ProjectsStore.loadInitialData([TestStubs.Project()]);
  854. const wrapper = mountWithThemeAndOrg(
  855. <Results
  856. organization={organization}
  857. location={initialData.router.location}
  858. router={initialData.router}
  859. />,
  860. initialData.routerContext,
  861. organization
  862. );
  863. await tick();
  864. wrapper.update();
  865. // ensure cursor query string is initially present in the location
  866. expect(initialData.router.location).toEqual({
  867. query: {
  868. ...generateFields(),
  869. cursor: '0%3A50%3A0',
  870. },
  871. });
  872. // perform a search
  873. const search = wrapper.find('#smart-search-input').first();
  874. search.simulate('change', {target: {value: 'geo:canada'}}).simulate('submit', {
  875. preventDefault() {},
  876. });
  877. await tick();
  878. // should only be called with saved queries
  879. expect(mockVisit).not.toHaveBeenCalled();
  880. // cursor query string should be omitted from the query string
  881. expect(initialData.router.push).toHaveBeenCalledWith({
  882. pathname: undefined,
  883. query: {
  884. ...generateFields(),
  885. query: 'geo:canada',
  886. statsPeriod: '14d',
  887. },
  888. });
  889. wrapper.unmount();
  890. });
  891. it('renders a y-axis selector', async function () {
  892. const organization = TestStubs.Organization({
  893. features,
  894. });
  895. const initialData = initializeOrg({
  896. organization,
  897. router: {
  898. location: {query: {...generateFields(), yAxis: 'count()'}},
  899. },
  900. });
  901. ProjectsStore.loadInitialData([TestStubs.Project()]);
  902. const wrapper = mountWithThemeAndOrg(
  903. <Results
  904. organization={organization}
  905. location={initialData.router.location}
  906. router={initialData.router}
  907. />,
  908. initialData.routerContext,
  909. organization
  910. );
  911. // y-axis selector is last.
  912. const selector = wrapper.find('OptionSelector').last();
  913. // Open the selector
  914. act(() => {
  915. triggerPress(selector.find('button[aria-haspopup="listbox"]'));
  916. });
  917. await tick();
  918. wrapper.update();
  919. // Click one of the options.
  920. wrapper.find('Option').first().simulate('click');
  921. await tick();
  922. wrapper.update();
  923. const eventsRequest = wrapper.find('EventsChart');
  924. expect(eventsRequest.props().yAxis).toEqual(['count()']);
  925. wrapper.unmount();
  926. });
  927. it('renders a display selector', async function () {
  928. const organization = TestStubs.Organization({
  929. features,
  930. });
  931. const initialData = initializeOrg({
  932. organization,
  933. router: {
  934. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  935. },
  936. });
  937. const wrapper = mountWithThemeAndOrg(
  938. <Results
  939. organization={organization}
  940. location={initialData.router.location}
  941. router={initialData.router}
  942. />,
  943. initialData.routerContext,
  944. organization
  945. );
  946. act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
  947. await tick();
  948. wrapper.update();
  949. // display selector is first.
  950. const selector = wrapper.find('OptionSelector').first();
  951. // Open the selector
  952. act(() => {
  953. triggerPress(selector.find('button[aria-haspopup="listbox"]'));
  954. });
  955. await tick();
  956. wrapper.update();
  957. // Click the 'default' option.
  958. wrapper.find('Option').first().simulate('click');
  959. await tick();
  960. wrapper.update();
  961. const eventsRequest = wrapper.find('EventsChart').props();
  962. expect(eventsRequest.disableReleases).toEqual(false);
  963. expect(eventsRequest.disablePrevious).toEqual(true);
  964. wrapper.unmount();
  965. });
  966. it('excludes top5 options when plan does not include discover-query', async function () {
  967. const organization = TestStubs.Organization({
  968. features: ['discover-basic'],
  969. });
  970. const initialData = initializeOrg({
  971. organization,
  972. router: {
  973. location: {query: {...generateFields(), display: 'previous'}},
  974. },
  975. });
  976. ProjectsStore.loadInitialData([TestStubs.Project()]);
  977. const wrapper = mountWithThemeAndOrg(
  978. <Results
  979. organization={organization}
  980. location={initialData.router.location}
  981. router={initialData.router}
  982. />,
  983. initialData.routerContext,
  984. organization
  985. );
  986. // display selector is first.
  987. const selector = wrapper.find('OptionSelector').first();
  988. // Open the selector
  989. act(() => {
  990. triggerPress(selector.find('button[aria-haspopup="listbox"]'));
  991. });
  992. await tick();
  993. wrapper.update();
  994. // Make sure the top5 option isn't present
  995. const options = wrapper
  996. .find('Option [data-test-id]')
  997. .map(item => item.prop('data-test-id'));
  998. expect(options).not.toContain('top5');
  999. expect(options).not.toContain('dailytop5');
  1000. expect(options).toContain('default');
  1001. wrapper.unmount();
  1002. });
  1003. it('needs confirmation on long queries', async function () {
  1004. const organization = TestStubs.Organization({
  1005. features: ['discover-basic'],
  1006. });
  1007. const initialData = initializeOrg({
  1008. organization,
  1009. router: {
  1010. location: {query: {...generateFields(), statsPeriod: '60d', project: '-1'}},
  1011. },
  1012. });
  1013. const wrapper = mountWithThemeAndOrg(
  1014. <Results
  1015. organization={organization}
  1016. location={initialData.router.location}
  1017. router={initialData.router}
  1018. />,
  1019. initialData.routerContext,
  1020. organization
  1021. );
  1022. await tick();
  1023. const results = wrapper.find('Results');
  1024. expect(results.state('needConfirmation')).toEqual(true);
  1025. wrapper.unmount();
  1026. });
  1027. it('needs confirmation on long query with explicit projects', async function () {
  1028. const organization = TestStubs.Organization({
  1029. features: ['discover-basic'],
  1030. });
  1031. const initialData = initializeOrg({
  1032. organization,
  1033. router: {
  1034. location: {
  1035. query: {
  1036. ...generateFields(),
  1037. statsPeriod: '60d',
  1038. project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
  1039. },
  1040. },
  1041. },
  1042. });
  1043. const wrapper = mountWithThemeAndOrg(
  1044. <Results
  1045. organization={organization}
  1046. location={initialData.router.location}
  1047. router={initialData.router}
  1048. />,
  1049. initialData.routerContext,
  1050. organization
  1051. );
  1052. await tick();
  1053. const results = wrapper.find('Results');
  1054. expect(results.state('needConfirmation')).toEqual(true);
  1055. wrapper.unmount();
  1056. });
  1057. it('does not need confirmation on short queries', async function () {
  1058. const organization = TestStubs.Organization({
  1059. features: ['discover-basic'],
  1060. });
  1061. const initialData = initializeOrg({
  1062. organization,
  1063. router: {
  1064. location: {query: {...generateFields(), statsPeriod: '30d', project: '-1'}},
  1065. },
  1066. });
  1067. const wrapper = mountWithThemeAndOrg(
  1068. <Results
  1069. organization={organization}
  1070. location={initialData.router.location}
  1071. router={initialData.router}
  1072. />,
  1073. initialData.routerContext,
  1074. organization
  1075. );
  1076. await tick();
  1077. const results = wrapper.find('Results');
  1078. expect(results.state('needConfirmation')).toEqual(false);
  1079. wrapper.unmount();
  1080. });
  1081. it('does not need confirmation with to few projects', async function () {
  1082. const organization = TestStubs.Organization({
  1083. features: ['discover-basic'],
  1084. });
  1085. const initialData = initializeOrg({
  1086. organization,
  1087. router: {
  1088. location: {
  1089. query: {...generateFields(), statsPeriod: '90d', project: [1, 2, 3, 4]},
  1090. },
  1091. },
  1092. });
  1093. const wrapper = mountWithThemeAndOrg(
  1094. <Results
  1095. organization={organization}
  1096. location={initialData.router.location}
  1097. router={initialData.router}
  1098. />,
  1099. initialData.routerContext,
  1100. organization
  1101. );
  1102. await tick();
  1103. const results = wrapper.find('Results');
  1104. expect(results.state('needConfirmation')).toEqual(false);
  1105. wrapper.unmount();
  1106. });
  1107. it('retrieves saved query', async function () {
  1108. const organization = TestStubs.Organization({
  1109. features,
  1110. slug: 'org-slug',
  1111. });
  1112. const initialData = initializeOrg({
  1113. organization,
  1114. router: {
  1115. location: {query: {id: '1', statsPeriod: '24h'}},
  1116. },
  1117. });
  1118. const wrapper = mountWithThemeAndOrg(
  1119. <Results
  1120. organization={organization}
  1121. location={initialData.router.location}
  1122. router={initialData.router}
  1123. />,
  1124. initialData.routerContext,
  1125. organization
  1126. );
  1127. await tick();
  1128. const savedQuery = wrapper.find('SavedQueryAPI').state('savedQuery');
  1129. expect(savedQuery.name).toEqual('new');
  1130. expect(savedQuery.id).toEqual('1');
  1131. expect(savedQuery.fields).toEqual([
  1132. 'title',
  1133. 'event.type',
  1134. 'project',
  1135. 'user.display',
  1136. 'timestamp',
  1137. ]);
  1138. expect(savedQuery.projects).toEqual([]);
  1139. expect(savedQuery.range).toEqual('24h');
  1140. expect(mockSaved).toHaveBeenCalled();
  1141. expect(mockVisit).toHaveBeenCalledTimes(1);
  1142. wrapper.unmount();
  1143. });
  1144. it('creates event view from saved query', async function () {
  1145. const organization = TestStubs.Organization({
  1146. features,
  1147. slug: 'org-slug',
  1148. });
  1149. const initialData = initializeOrg({
  1150. organization,
  1151. router: {
  1152. location: {query: {id: '1', statsPeriod: '24h'}},
  1153. },
  1154. });
  1155. const wrapper = mountWithThemeAndOrg(
  1156. <Results
  1157. organization={organization}
  1158. location={initialData.router.location}
  1159. router={initialData.router}
  1160. />,
  1161. initialData.routerContext,
  1162. organization
  1163. );
  1164. await tick();
  1165. const eventView = wrapper.find('Results').state('eventView');
  1166. expect(eventView.name).toEqual('new');
  1167. expect(eventView.id).toEqual('1');
  1168. expect(eventView.fields.length).toEqual(5);
  1169. expect(eventView.project).toEqual([]);
  1170. expect(eventView.statsPeriod).toEqual('24h');
  1171. expect(eventView.sorts).toEqual([{field: 'user.display', kind: 'desc'}]);
  1172. wrapper.unmount();
  1173. });
  1174. it('overrides saved query params with location query params', async function () {
  1175. const organization = TestStubs.Organization({
  1176. features,
  1177. slug: 'org-slug',
  1178. });
  1179. const initialData = initializeOrg({
  1180. organization,
  1181. router: {
  1182. location: {
  1183. query: {
  1184. id: '1',
  1185. statsPeriod: '7d',
  1186. project: [2],
  1187. environment: ['production'],
  1188. },
  1189. },
  1190. },
  1191. });
  1192. const wrapper = mountWithThemeAndOrg(
  1193. <Results
  1194. organization={organization}
  1195. location={initialData.router.location}
  1196. router={initialData.router}
  1197. />,
  1198. initialData.routerContext,
  1199. organization
  1200. );
  1201. await tick();
  1202. const eventView = wrapper.find('Results').state('eventView');
  1203. expect(eventView.name).toEqual('new');
  1204. expect(eventView.id).toEqual('1');
  1205. expect(eventView.fields.length).toEqual(5);
  1206. expect(eventView.project).toEqual([2]);
  1207. expect(eventView.statsPeriod).toEqual('7d');
  1208. expect(eventView.environment).toEqual(['production']);
  1209. expect(mockVisit).toHaveBeenCalledTimes(1);
  1210. wrapper.unmount();
  1211. });
  1212. it('updates chart whenever yAxis parameter changes', async function () {
  1213. const organization = TestStubs.Organization({
  1214. features,
  1215. });
  1216. const initialData = initializeOrg({
  1217. organization,
  1218. router: {
  1219. location: {query: {...generateFields(), yAxis: 'count()'}},
  1220. },
  1221. });
  1222. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1223. const wrapper = mountWithThemeAndOrg(
  1224. <Results
  1225. organization={organization}
  1226. location={initialData.router.location}
  1227. router={initialData.router}
  1228. />,
  1229. initialData.routerContext,
  1230. organization
  1231. );
  1232. // Should load events once
  1233. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1234. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1235. 1,
  1236. '/organizations/org-slug/events-stats/',
  1237. expect.objectContaining({
  1238. query: expect.objectContaining({
  1239. statsPeriod: '14d',
  1240. yAxis: ['count()'],
  1241. }),
  1242. })
  1243. );
  1244. // Update location simulating a browser back button action
  1245. wrapper.setProps({
  1246. location: {
  1247. query: {...generateFields(), yAxis: 'count_unique(user)'},
  1248. },
  1249. });
  1250. await tick();
  1251. wrapper.update();
  1252. // Should load events again
  1253. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  1254. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1255. 2,
  1256. '/organizations/org-slug/events-stats/',
  1257. expect.objectContaining({
  1258. query: expect.objectContaining({
  1259. statsPeriod: '14d',
  1260. yAxis: ['count_unique(user)'],
  1261. }),
  1262. })
  1263. );
  1264. wrapper.unmount();
  1265. });
  1266. it('updates chart whenever display parameter changes', async function () {
  1267. const organization = TestStubs.Organization({
  1268. features,
  1269. });
  1270. const initialData = initializeOrg({
  1271. organization,
  1272. router: {
  1273. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  1274. },
  1275. });
  1276. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1277. const wrapper = mountWithThemeAndOrg(
  1278. <Results
  1279. organization={organization}
  1280. location={initialData.router.location}
  1281. router={initialData.router}
  1282. />,
  1283. initialData.routerContext,
  1284. organization
  1285. );
  1286. // Should load events once
  1287. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1288. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1289. 1,
  1290. '/organizations/org-slug/events-stats/',
  1291. expect.objectContaining({
  1292. query: expect.objectContaining({
  1293. statsPeriod: '14d',
  1294. yAxis: ['count()'],
  1295. }),
  1296. })
  1297. );
  1298. // Update location simulating a browser back button action
  1299. wrapper.setProps({
  1300. location: {
  1301. query: {...generateFields(), display: 'previous', yAxis: 'count()'},
  1302. },
  1303. });
  1304. await tick();
  1305. wrapper.update();
  1306. // Should load events again
  1307. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  1308. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1309. 2,
  1310. '/organizations/org-slug/events-stats/',
  1311. expect.objectContaining({
  1312. query: expect.objectContaining({
  1313. statsPeriod: '28d',
  1314. yAxis: ['count()'],
  1315. }),
  1316. })
  1317. );
  1318. wrapper.unmount();
  1319. });
  1320. it('updates chart whenever display and yAxis parameters change', async function () {
  1321. const organization = TestStubs.Organization({
  1322. features,
  1323. });
  1324. const initialData = initializeOrg({
  1325. organization,
  1326. router: {
  1327. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  1328. },
  1329. });
  1330. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1331. const wrapper = mountWithThemeAndOrg(
  1332. <Results
  1333. organization={organization}
  1334. location={initialData.router.location}
  1335. router={initialData.router}
  1336. />,
  1337. initialData.routerContext,
  1338. organization
  1339. );
  1340. // Should load events once
  1341. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1342. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1343. 1,
  1344. '/organizations/org-slug/events-stats/',
  1345. expect.objectContaining({
  1346. query: expect.objectContaining({
  1347. statsPeriod: '14d',
  1348. yAxis: ['count()'],
  1349. }),
  1350. })
  1351. );
  1352. // Update location simulating a browser back button action
  1353. wrapper.setProps({
  1354. location: {
  1355. query: {...generateFields(), display: 'previous', yAxis: 'count_unique(user)'},
  1356. },
  1357. });
  1358. await tick();
  1359. wrapper.update();
  1360. // Should load events again
  1361. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  1362. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1363. 2,
  1364. '/organizations/org-slug/events-stats/',
  1365. expect.objectContaining({
  1366. query: expect.objectContaining({
  1367. statsPeriod: '28d',
  1368. yAxis: ['count_unique(user)'],
  1369. }),
  1370. })
  1371. );
  1372. wrapper.unmount();
  1373. });
  1374. it('appends tag value to existing query when clicked', async function () {
  1375. const organization = TestStubs.Organization({
  1376. features,
  1377. });
  1378. const initialData = initializeOrg({
  1379. organization,
  1380. router: {
  1381. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  1382. },
  1383. });
  1384. const wrapper = mountWithThemeAndOrg(
  1385. <Results
  1386. organization={organization}
  1387. location={initialData.router.location}
  1388. router={initialData.router}
  1389. />,
  1390. initialData.routerContext,
  1391. organization
  1392. );
  1393. act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
  1394. await tick();
  1395. wrapper.update();
  1396. wrapper.find('[data-test-id="toggle-show-tags"]').first().simulate('click');
  1397. await tick();
  1398. wrapper.update();
  1399. // since environment collides with the environment field, it is wrapped with `tags[...]`
  1400. const envSegment = wrapper.find(
  1401. '[data-test-id="tag-environment-segment-dev"] Segment'
  1402. );
  1403. const envTarget = envSegment.props().to;
  1404. expect(envTarget.query.query).toEqual('tags[environment]:dev');
  1405. const fooSegment = wrapper.find('[data-test-id="tag-foo-segment-bar"] Segment');
  1406. const fooTarget = fooSegment.props().to;
  1407. expect(fooTarget.query.query).toEqual('foo:bar');
  1408. });
  1409. it('respects pinned filters for prebuilt queries', async function () {
  1410. const organization = TestStubs.Organization({
  1411. features: [...features, 'global-views'],
  1412. });
  1413. const initialData = initializeOrg({
  1414. organization,
  1415. router: {
  1416. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  1417. },
  1418. });
  1419. jest.spyOn(PageFilterPersistence, 'getPageFilterStorage').mockReturnValue({
  1420. state: {
  1421. project: [1],
  1422. environment: [],
  1423. start: null,
  1424. end: null,
  1425. period: '14d',
  1426. utc: null,
  1427. },
  1428. pinnedFilters: new Set(['projects']),
  1429. });
  1430. const wrapper = mountWithThemeAndOrg(
  1431. <Results
  1432. organization={organization}
  1433. location={initialData.router.location}
  1434. router={initialData.router}
  1435. />,
  1436. initialData.routerContext,
  1437. organization
  1438. );
  1439. act(() =>
  1440. ProjectsStore.loadInitialData([
  1441. TestStubs.Project({id: 1, slug: 'Pinned Project'}),
  1442. ])
  1443. );
  1444. await tick();
  1445. wrapper.update();
  1446. const projectPageFilter = wrapper
  1447. .find('[data-test-id="page-filter-project-selector"]')
  1448. .first();
  1449. expect(projectPageFilter.text()).toEqual('Pinned Project');
  1450. });
  1451. });
  1452. it('renders metric fallback alert', async function () {
  1453. const organization = TestStubs.Organization({
  1454. features: ['discover-basic'],
  1455. });
  1456. const initialData = initializeOrg({
  1457. organization,
  1458. router: {
  1459. location: {query: {fromMetric: true}},
  1460. },
  1461. });
  1462. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1463. const wrapper = mountWithThemeAndOrg(
  1464. <Results
  1465. organization={organization}
  1466. location={initialData.router.location}
  1467. router={initialData.router}
  1468. />,
  1469. initialData.routerContext,
  1470. organization
  1471. );
  1472. await tick();
  1473. wrapper.update();
  1474. expect(wrapper.find('Alert').find('Message').text()).toEqual(
  1475. "You've navigated to this page from a performance metric widget generated from processed events. The results here only show indexed events."
  1476. );
  1477. });
  1478. it('renders unparameterized data banner', async function () {
  1479. const organization = TestStubs.Organization({
  1480. features: ['discover-basic'],
  1481. });
  1482. const initialData = initializeOrg({
  1483. organization,
  1484. router: {
  1485. location: {query: {showUnparameterizedBanner: true}},
  1486. },
  1487. });
  1488. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1489. const wrapper = mountWithThemeAndOrg(
  1490. <Results
  1491. organization={organization}
  1492. location={initialData.router.location}
  1493. router={initialData.router}
  1494. />,
  1495. initialData.routerContext,
  1496. organization
  1497. );
  1498. await tick();
  1499. wrapper.update();
  1500. expect(wrapper.find('Alert').find('Message').text()).toEqual(
  1501. 'These are unparameterized transactions. To better organize your transactions, set transaction names manually.'
  1502. );
  1503. });
  1504. it('updates the homepage query with up to date eventView when Use as Discover Home is clicked', async () => {
  1505. const mockHomepageUpdate = MockApiClient.addMockResponse({
  1506. url: '/organizations/org-slug/discover/homepage/',
  1507. method: 'PUT',
  1508. statusCode: 200,
  1509. });
  1510. const organization = TestStubs.Organization({
  1511. features: [
  1512. 'discover-basic',
  1513. 'discover-query',
  1514. 'discover-query-builder-as-landing-page',
  1515. ],
  1516. });
  1517. const initialData = initializeOrg({
  1518. organization,
  1519. router: {
  1520. // These fields take priority and should be sent in the request
  1521. location: {query: {field: ['title', 'user'], id: '1'}},
  1522. },
  1523. });
  1524. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1525. render(
  1526. <Results
  1527. organization={organization}
  1528. location={initialData.router.location}
  1529. router={initialData.router}
  1530. />,
  1531. {context: initialData.routerContext, organization}
  1532. );
  1533. await waitFor(() =>
  1534. expect(screen.getByRole('button', {name: /use as discover home/i})).toBeEnabled()
  1535. );
  1536. userEvent.click(screen.getByText('Use as Discover Home'));
  1537. expect(mockHomepageUpdate).toHaveBeenCalledWith(
  1538. '/organizations/org-slug/discover/homepage/',
  1539. expect.objectContaining({
  1540. data: expect.objectContaining({
  1541. fields: ['title', 'user'],
  1542. }),
  1543. })
  1544. );
  1545. });
  1546. it('Changes the Use as Discover button to a reset button for saved query', async () => {
  1547. MockApiClient.addMockResponse({
  1548. url: '/organizations/org-slug/discover/homepage/',
  1549. method: 'PUT',
  1550. statusCode: 200,
  1551. body: {
  1552. id: '2',
  1553. name: '',
  1554. projects: [],
  1555. version: 2,
  1556. expired: false,
  1557. dateCreated: '2021-04-08T17:53:25.195782Z',
  1558. dateUpdated: '2021-04-09T12:13:18.567264Z',
  1559. createdBy: {
  1560. id: '2',
  1561. },
  1562. environment: [],
  1563. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  1564. widths: ['-1', '-1', '-1', '-1', '-1'],
  1565. range: '14d',
  1566. orderby: '-user.display',
  1567. },
  1568. });
  1569. const organization = TestStubs.Organization({
  1570. features: [
  1571. 'discover-basic',
  1572. 'discover-query',
  1573. 'discover-query-builder-as-landing-page',
  1574. ],
  1575. });
  1576. const initialData = initializeOrg({
  1577. organization,
  1578. router: {
  1579. location: {query: {id: '1'}},
  1580. },
  1581. });
  1582. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1583. const {rerender} = render(
  1584. <Results
  1585. organization={organization}
  1586. location={initialData.router.location}
  1587. router={initialData.router}
  1588. />,
  1589. {context: initialData.routerContext, organization}
  1590. );
  1591. await waitFor(() =>
  1592. expect(screen.getByRole('button', {name: /use as discover home/i})).toBeEnabled()
  1593. );
  1594. userEvent.click(screen.getByText('Use as Discover Home'));
  1595. expect(await screen.findByText('Reset Discover Home')).toBeInTheDocument();
  1596. userEvent.click(screen.getByText('Total Period'));
  1597. userEvent.click(screen.getByText('Previous Period'));
  1598. const rerenderData = initializeOrg({
  1599. organization,
  1600. router: {
  1601. location: {query: {...initialData.router.location.query, display: 'previous'}},
  1602. },
  1603. });
  1604. rerender(
  1605. <Results
  1606. organization={organization}
  1607. location={rerenderData.router.location}
  1608. router={rerenderData.router}
  1609. />
  1610. );
  1611. screen.getByText('Previous Period');
  1612. expect(await screen.findByText('Use as Discover Home')).toBeInTheDocument();
  1613. });
  1614. it('Changes the Use as Discover button to a reset button for prebuilt query', async () => {
  1615. MockApiClient.addMockResponse({
  1616. url: '/organizations/org-slug/discover/homepage/',
  1617. method: 'PUT',
  1618. statusCode: 200,
  1619. body: {...TRANSACTION_VIEWS[0], name: ''},
  1620. });
  1621. const organization = TestStubs.Organization({
  1622. features: [
  1623. 'discover-basic',
  1624. 'discover-query',
  1625. 'discover-query-builder-as-landing-page',
  1626. ],
  1627. });
  1628. const initialData = initializeOrg({
  1629. organization,
  1630. router: {
  1631. location: {
  1632. ...TestStubs.location(),
  1633. query: {
  1634. ...EventView.fromNewQueryWithLocation(
  1635. TRANSACTION_VIEWS[0],
  1636. TestStubs.location()
  1637. ).generateQueryStringObject(),
  1638. },
  1639. },
  1640. },
  1641. });
  1642. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1643. const {rerender} = render(
  1644. <Results
  1645. organization={organization}
  1646. location={initialData.router.location}
  1647. router={initialData.router}
  1648. />,
  1649. {context: initialData.routerContext, organization}
  1650. );
  1651. await screen.findAllByText(TRANSACTION_VIEWS[0].name);
  1652. userEvent.click(screen.getByText('Use as Discover Home'));
  1653. expect(await screen.findByText('Reset Discover Home')).toBeInTheDocument();
  1654. userEvent.click(screen.getByText('Total Period'));
  1655. userEvent.click(screen.getByText('Previous Period'));
  1656. const rerenderData = initializeOrg({
  1657. organization,
  1658. router: {
  1659. location: {query: {...initialData.router.location.query, display: 'previous'}},
  1660. },
  1661. });
  1662. rerender(
  1663. <Results
  1664. organization={organization}
  1665. location={rerenderData.router.location}
  1666. router={rerenderData.router}
  1667. />
  1668. );
  1669. screen.getByText('Previous Period');
  1670. expect(await screen.findByText('Use as Discover Home')).toBeInTheDocument();
  1671. });
  1672. });