homepage.spec.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. import {LocationFixture} from 'sentry-fixture/locationFixture';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {ProjectFixture} from 'sentry-fixture/project';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {
  6. render,
  7. renderGlobalModal,
  8. screen,
  9. userEvent,
  10. waitFor,
  11. within,
  12. } from 'sentry-test/reactTestingLibrary';
  13. import * as pageFilterUtils from 'sentry/components/organizations/pageFilters/persistence';
  14. import ProjectsStore from 'sentry/stores/projectsStore';
  15. import EventView from 'sentry/utils/discover/eventView';
  16. import {DEFAULT_EVENT_VIEW} from './data';
  17. import Homepage from './homepage';
  18. describe('Discover > Homepage', () => {
  19. const features = ['global-views', 'discover-query'];
  20. let initialData: ReturnType<typeof initializeOrg>;
  21. let organization: ReturnType<typeof OrganizationFixture>;
  22. let mockHomepage: jest.Mock;
  23. let measurementsMetaMock: jest.Mock;
  24. beforeEach(() => {
  25. organization = OrganizationFixture({
  26. features,
  27. });
  28. initialData = initializeOrg({
  29. organization,
  30. router: {
  31. location: LocationFixture(),
  32. },
  33. });
  34. ProjectsStore.loadInitialData(initialData.projects);
  35. MockApiClient.addMockResponse({
  36. url: '/organizations/org-slug/events/',
  37. body: [],
  38. });
  39. MockApiClient.addMockResponse({
  40. url: '/organizations/org-slug/events-meta/',
  41. body: {
  42. count: 2,
  43. },
  44. });
  45. MockApiClient.addMockResponse({
  46. url: '/organizations/org-slug/events-stats/',
  47. body: {data: [[123, []]]},
  48. });
  49. MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/tags/',
  51. body: [],
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/releases/stats/',
  55. body: [],
  56. });
  57. MockApiClient.addMockResponse({
  58. url: '/organizations/org-slug/dynamic-sampling/custom-rules/',
  59. body: '',
  60. });
  61. mockHomepage = MockApiClient.addMockResponse({
  62. url: '/organizations/org-slug/discover/homepage/',
  63. method: 'GET',
  64. statusCode: 200,
  65. body: {
  66. id: '2',
  67. name: 'homepage query',
  68. projects: [],
  69. version: 2,
  70. expired: false,
  71. dateCreated: '2021-04-08T17:53:25.195782Z',
  72. dateUpdated: '2021-04-09T12:13:18.567264Z',
  73. createdBy: {
  74. id: '2',
  75. },
  76. environment: ['alpha'],
  77. fields: ['environment'],
  78. widths: ['-1'],
  79. range: '24h',
  80. orderby: '-environment',
  81. display: 'previous',
  82. query: 'event.type:error',
  83. queryDataset: 'discover',
  84. },
  85. });
  86. measurementsMetaMock = MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/measurements-meta/',
  88. method: 'GET',
  89. body: {},
  90. });
  91. MockApiClient.addMockResponse({
  92. url: '/organizations/org-slug/recent-searches/',
  93. body: [],
  94. });
  95. });
  96. it('renders the Discover banner', async () => {
  97. render(
  98. <Homepage
  99. organization={organization}
  100. location={initialData.router.location}
  101. router={initialData.router}
  102. setSavedQuery={jest.fn()}
  103. loading={false}
  104. />,
  105. {router: initialData.router, organization: initialData.organization}
  106. );
  107. await screen.findByText('Discover Trends');
  108. screen.getByText('Get a Tour');
  109. expect(screen.queryByText('Build a new query')).not.toBeInTheDocument();
  110. });
  111. it('fetches from the homepage URL and renders fields, async page filters, async and chart information', async () => {
  112. render(
  113. <Homepage
  114. organization={organization}
  115. location={initialData.router.location}
  116. router={initialData.router}
  117. setSavedQuery={jest.fn()}
  118. loading={false}
  119. />,
  120. {router: initialData.router, organization: initialData.organization}
  121. );
  122. expect(mockHomepage).toHaveBeenCalled();
  123. await screen.findByText('environment');
  124. // Only the environment field
  125. expect(screen.getAllByTestId('grid-head-cell')).toHaveLength(1);
  126. screen.getByText('Previous Period');
  127. screen.getByRole('row', {name: 'event.type:error'});
  128. expect(screen.queryByText('Dataset')).not.toBeInTheDocument();
  129. });
  130. it('renders event view from URL params over homepage query', async () => {
  131. initialData = initializeOrg({
  132. organization,
  133. router: {
  134. location: {
  135. ...LocationFixture(),
  136. query: {
  137. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  138. field: ['project'],
  139. },
  140. },
  141. },
  142. });
  143. render(
  144. <Homepage
  145. organization={organization}
  146. location={initialData.router.location}
  147. router={initialData.router}
  148. setSavedQuery={jest.fn()}
  149. loading={false}
  150. />,
  151. {router: initialData.router, organization: initialData.organization}
  152. );
  153. expect(mockHomepage).toHaveBeenCalled();
  154. await screen.findByText('project');
  155. // This is the field in the mocked response for the homepage
  156. expect(screen.queryByText('environment')).not.toBeInTheDocument();
  157. });
  158. it('applies URL changes with the homepage pathname', async () => {
  159. render(
  160. <Homepage
  161. organization={organization}
  162. location={initialData.router.location}
  163. router={initialData.router}
  164. setSavedQuery={jest.fn()}
  165. loading={false}
  166. />,
  167. {router: initialData.router, organization: initialData.organization}
  168. );
  169. renderGlobalModal({router: initialData.router});
  170. await userEvent.click(await screen.findByText('Columns'));
  171. const modal = await screen.findByRole('dialog');
  172. await userEvent.click(within(modal).getByTestId('label'));
  173. await userEvent.click(within(modal).getByText('event.type'));
  174. await userEvent.click(within(modal).getByText('Apply'));
  175. expect(initialData.router.push).toHaveBeenCalledWith(
  176. expect.objectContaining({
  177. pathname: '/organizations/org-slug/discover/homepage/',
  178. query: expect.objectContaining({
  179. field: 'event.type',
  180. }),
  181. })
  182. );
  183. });
  184. it('does not show an editable header or author information', async () => {
  185. render(
  186. <Homepage
  187. organization={organization}
  188. location={initialData.router.location}
  189. router={initialData.router}
  190. setSavedQuery={jest.fn()}
  191. loading={false}
  192. />,
  193. {router: initialData.router, organization: initialData.organization}
  194. );
  195. await waitFor(() => {
  196. expect(measurementsMetaMock).toHaveBeenCalled();
  197. });
  198. // 'Discover' is the header for the homepage
  199. expect(screen.getByText('Discover')).toBeInTheDocument();
  200. expect(screen.queryByText(/Created by:/)).not.toBeInTheDocument();
  201. expect(screen.queryByText(/Last edited:/)).not.toBeInTheDocument();
  202. });
  203. it('shows the Remove Default button on initial load', async () => {
  204. MockApiClient.addMockResponse({
  205. url: '/organizations/org-slug/discover/homepage/',
  206. method: 'GET',
  207. statusCode: 200,
  208. body: {
  209. id: '2',
  210. name: 'homepage query',
  211. projects: [],
  212. version: 2,
  213. expired: false,
  214. dateCreated: '2021-04-08T17:53:25.195782Z',
  215. dateUpdated: '2021-04-09T12:13:18.567264Z',
  216. createdBy: {
  217. id: '2',
  218. },
  219. environment: [],
  220. fields: ['environment'],
  221. widths: ['-1'],
  222. range: '14d',
  223. orderby: '-environment',
  224. display: 'previous',
  225. query: 'event.type:error',
  226. topEvents: '5',
  227. },
  228. });
  229. render(
  230. <Homepage
  231. organization={organization}
  232. location={initialData.router.location}
  233. router={initialData.router}
  234. setSavedQuery={jest.fn()}
  235. loading={false}
  236. />,
  237. {router: initialData.router, organization: initialData.organization}
  238. );
  239. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  240. expect(screen.queryByText('Set as Default')).not.toBeInTheDocument();
  241. });
  242. it('Disables the Set as Default button when no saved homepage', async () => {
  243. initialData = initializeOrg({
  244. organization,
  245. router: {
  246. location: {
  247. ...LocationFixture(),
  248. query: {
  249. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  250. },
  251. },
  252. },
  253. });
  254. mockHomepage = MockApiClient.addMockResponse({
  255. url: '/organizations/org-slug/discover/homepage/',
  256. method: 'GET',
  257. statusCode: 200,
  258. });
  259. render(
  260. <Homepage
  261. organization={organization}
  262. location={initialData.router.location}
  263. router={initialData.router}
  264. setSavedQuery={jest.fn()}
  265. loading={false}
  266. />,
  267. {router: initialData.router, organization: initialData.organization}
  268. );
  269. expect(mockHomepage).toHaveBeenCalled();
  270. expect(screen.getByRole('button', {name: /set as default/i})).toBeDisabled();
  271. await waitFor(() => {
  272. expect(measurementsMetaMock).toHaveBeenCalled();
  273. });
  274. });
  275. it('follows absolute date selection', async () => {
  276. initialData = initializeOrg({
  277. organization,
  278. router: {
  279. location: {
  280. ...LocationFixture(),
  281. query: {
  282. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  283. },
  284. },
  285. },
  286. });
  287. MockApiClient.addMockResponse({
  288. url: '/organizations/org-slug/discover/homepage/',
  289. method: 'GET',
  290. statusCode: 200,
  291. });
  292. render(
  293. <Homepage
  294. organization={organization}
  295. location={initialData.router.location}
  296. router={initialData.router}
  297. setSavedQuery={jest.fn()}
  298. loading={false}
  299. />,
  300. {router: initialData.router, organization: initialData.organization}
  301. );
  302. await userEvent.click(await screen.findByText('24H'));
  303. await userEvent.click(await screen.findByText('Absolute date'));
  304. await userEvent.click(screen.getByText('Apply'));
  305. expect(screen.queryByText('14D')).not.toBeInTheDocument();
  306. });
  307. it('renders changes to the discover query when no homepage', async () => {
  308. initialData = initializeOrg({
  309. organization,
  310. router: {
  311. location: {
  312. ...LocationFixture(),
  313. query: {
  314. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  315. field: ['title'],
  316. },
  317. },
  318. },
  319. });
  320. MockApiClient.addMockResponse({
  321. url: '/organizations/org-slug/discover/homepage/',
  322. method: 'GET',
  323. statusCode: 200,
  324. body: '',
  325. });
  326. const {rerender} = render(
  327. <Homepage
  328. organization={organization}
  329. location={initialData.router.location}
  330. router={initialData.router}
  331. setSavedQuery={jest.fn()}
  332. loading={false}
  333. />,
  334. {router: initialData.router, organization: initialData.organization}
  335. );
  336. renderGlobalModal();
  337. // Simulate an update to the columns by changing the URL params
  338. const rerenderData = initializeOrg({
  339. organization,
  340. router: {
  341. location: {
  342. ...LocationFixture(),
  343. query: {
  344. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  345. field: ['event.type'],
  346. },
  347. },
  348. },
  349. });
  350. rerender(
  351. <Homepage
  352. organization={organization}
  353. location={rerenderData.router.location}
  354. router={rerenderData.router}
  355. setSavedQuery={jest.fn()}
  356. loading={false}
  357. />
  358. );
  359. await waitFor(() => {
  360. expect(measurementsMetaMock).toHaveBeenCalled();
  361. });
  362. expect(screen.getByText('event.type')).toBeInTheDocument();
  363. });
  364. it('renders changes to the discover query when loaded with valid event view in url params', async () => {
  365. initialData = initializeOrg({
  366. organization,
  367. router: {
  368. location: {
  369. ...LocationFixture(),
  370. query: {
  371. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  372. field: ['title'],
  373. },
  374. },
  375. },
  376. });
  377. const {rerender} = render(
  378. <Homepage
  379. organization={organization}
  380. location={initialData.router.location}
  381. router={initialData.router}
  382. setSavedQuery={jest.fn()}
  383. loading={false}
  384. />,
  385. {router: initialData.router, organization: initialData.organization}
  386. );
  387. renderGlobalModal();
  388. // Simulate an update to the columns by changing the URL params
  389. const rerenderData = initializeOrg({
  390. organization,
  391. router: {
  392. location: {
  393. ...LocationFixture(),
  394. query: {
  395. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  396. field: ['event.type'],
  397. },
  398. },
  399. },
  400. });
  401. rerender(
  402. <Homepage
  403. organization={organization}
  404. location={rerenderData.router.location}
  405. router={rerenderData.router}
  406. setSavedQuery={jest.fn()}
  407. loading={false}
  408. />
  409. );
  410. await waitFor(() => {
  411. expect(measurementsMetaMock).toHaveBeenCalled();
  412. });
  413. expect(screen.getByText('event.type')).toBeInTheDocument();
  414. });
  415. it('overrides homepage filters with pinned filters if they exist', async () => {
  416. ProjectsStore.loadInitialData([ProjectFixture({id: '1'}), ProjectFixture({id: '2'})]);
  417. jest.spyOn(pageFilterUtils, 'getPageFilterStorage').mockReturnValueOnce({
  418. pinnedFilters: new Set(['projects']),
  419. state: {
  420. project: [2],
  421. environment: [],
  422. start: null,
  423. end: null,
  424. period: '14d',
  425. utc: null,
  426. },
  427. });
  428. render(
  429. <Homepage
  430. organization={organization}
  431. location={initialData.router.location}
  432. router={initialData.router}
  433. setSavedQuery={jest.fn()}
  434. loading={false}
  435. />,
  436. {router: initialData.router, organization: initialData.organization}
  437. );
  438. await waitFor(() => {
  439. expect(measurementsMetaMock).toHaveBeenCalled();
  440. });
  441. expect(screen.getByText('project-slug')).toBeInTheDocument();
  442. });
  443. it('allows users to set the All Events query as default', async () => {
  444. initialData = initializeOrg({
  445. organization,
  446. router: {
  447. location: {
  448. ...LocationFixture(),
  449. query: {
  450. ...EventView.fromSavedQuery(DEFAULT_EVENT_VIEW).generateQueryStringObject(),
  451. },
  452. },
  453. },
  454. });
  455. mockHomepage = MockApiClient.addMockResponse({
  456. url: '/organizations/org-slug/discover/homepage/',
  457. method: 'GET',
  458. statusCode: 200,
  459. });
  460. render(
  461. <Homepage
  462. organization={organization}
  463. location={initialData.router.location}
  464. router={initialData.router}
  465. setSavedQuery={jest.fn()}
  466. loading={false}
  467. />,
  468. {router: initialData.router, organization: initialData.organization}
  469. );
  470. await waitFor(() => expect(screen.getByTestId('set-as-default')).toBeEnabled());
  471. });
  472. it('uses split decision for homepage query', async () => {
  473. organization = OrganizationFixture({
  474. features: [
  475. 'discover-basic',
  476. 'discover-query',
  477. 'performance-discover-dataset-selector',
  478. ],
  479. });
  480. initialData = initializeOrg({
  481. organization,
  482. router: {
  483. location: LocationFixture(),
  484. },
  485. });
  486. MockApiClient.addMockResponse({
  487. url: '/organizations/org-slug/events/',
  488. body: {
  489. meta: {
  490. discoverSplitDecision: 'error-events',
  491. },
  492. data: [],
  493. },
  494. });
  495. MockApiClient.addMockResponse({
  496. url: '/organizations/org-slug/discover/homepage/',
  497. method: 'GET',
  498. statusCode: 200,
  499. body: {
  500. id: '2',
  501. name: 'homepage query',
  502. projects: [],
  503. version: 2,
  504. expired: false,
  505. dateCreated: '2021-04-08T17:53:25.195782Z',
  506. dateUpdated: '2021-04-09T12:13:18.567264Z',
  507. createdBy: {
  508. id: '2',
  509. },
  510. environment: [],
  511. fields: ['environment'],
  512. widths: ['-1'],
  513. range: '14d',
  514. orderby: '-environment',
  515. display: 'previous',
  516. query: 'event.type:error',
  517. topEvents: '5',
  518. queryDataset: 'discover',
  519. },
  520. });
  521. render(
  522. <Homepage
  523. organization={organization}
  524. location={initialData.router.location}
  525. router={initialData.router}
  526. setSavedQuery={jest.fn()}
  527. loading={false}
  528. />,
  529. {router: initialData.router, organization: initialData.organization}
  530. );
  531. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  532. expect(screen.queryByText('Set as Default')).not.toBeInTheDocument();
  533. await screen.findByText('environment');
  534. expect(screen.getAllByTestId('grid-head-cell')).toHaveLength(1);
  535. screen.getByRole('row', {name: 'event.type:error'});
  536. expect(screen.getByRole('tab', {name: 'Errors'})).toHaveAttribute(
  537. 'aria-selected',
  538. 'true'
  539. );
  540. expect(
  541. screen.getByText(
  542. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this query to Errors. Edit as you see fit."
  543. )
  544. ).toBeInTheDocument();
  545. });
  546. it('saves homepage with dataset selection', async () => {
  547. organization = OrganizationFixture({
  548. features: [
  549. 'discover-basic',
  550. 'discover-query',
  551. 'performance-discover-dataset-selector',
  552. ],
  553. });
  554. initialData = initializeOrg({
  555. organization,
  556. router: {
  557. location: LocationFixture(),
  558. },
  559. });
  560. MockApiClient.addMockResponse({
  561. url: '/organizations/org-slug/events/',
  562. body: {
  563. meta: {
  564. discoverSplitDecision: 'error-events',
  565. },
  566. data: [],
  567. },
  568. });
  569. MockApiClient.addMockResponse({
  570. url: '/organizations/org-slug/discover/homepage/',
  571. method: 'GET',
  572. statusCode: 200,
  573. body: {
  574. id: '2',
  575. name: 'homepage query',
  576. projects: [],
  577. version: 2,
  578. expired: false,
  579. dateCreated: '2021-04-08T17:53:25.195782Z',
  580. dateUpdated: '2021-04-09T12:13:18.567264Z',
  581. createdBy: {
  582. id: '2',
  583. },
  584. environment: [],
  585. fields: ['environment'],
  586. widths: ['-1'],
  587. range: '14d',
  588. orderby: '-environment',
  589. display: 'previous',
  590. query: 'event.type:error',
  591. topEvents: '5',
  592. queryDataset: 'discover',
  593. },
  594. });
  595. render(
  596. <Homepage
  597. organization={organization}
  598. location={initialData.router.location}
  599. router={initialData.router}
  600. setSavedQuery={jest.fn()}
  601. loading={false}
  602. />,
  603. {router: initialData.router, organization: initialData.organization}
  604. );
  605. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  606. expect(screen.queryByText('Set as Default')).not.toBeInTheDocument();
  607. await userEvent.click(screen.getByRole('tab', {name: 'Transactions'}));
  608. expect(initialData.router.push).toHaveBeenCalledWith(
  609. expect.objectContaining({
  610. query: expect.objectContaining({
  611. dataset: 'transactions',
  612. name: 'homepage query',
  613. project: undefined,
  614. query: '',
  615. field: 'environment',
  616. queryDataset: 'transaction-like',
  617. }),
  618. })
  619. );
  620. });
  621. });