results.spec.tsx 39 KB

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