results.spec.jsx 38 KB

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