index.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import {browserHistory} from 'react-router';
  2. import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
  3. import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
  4. import {initializeData} from 'sentry-test/performance/initializePerformanceData';
  5. import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  6. import TeamStore from 'sentry/stores/teamStore';
  7. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  8. import {OrganizationContext} from 'sentry/views/organizationContext';
  9. import {generatePerformanceEventView} from 'sentry/views/performance/data';
  10. import {PerformanceLanding} from 'sentry/views/performance/landing';
  11. import {REACT_NATIVE_COLUMN_TITLES} from 'sentry/views/performance/landing/data';
  12. import {LandingDisplayField} from 'sentry/views/performance/landing/utils';
  13. import {dynamicSamplingMetricsAccuracyMessage} from './dynamicSamplingMetricsAccuracyAlert';
  14. const WrappedComponent = ({data, withStaticFilters = false}) => {
  15. const eventView = generatePerformanceEventView(data.router.location, data.projects, {
  16. withStaticFilters,
  17. });
  18. const client = new QueryClient();
  19. return (
  20. <QueryClientProvider client={client}>
  21. <OrganizationContext.Provider value={data.organization}>
  22. <MetricsCardinalityProvider
  23. location={data.router.location}
  24. organization={data.organization}
  25. >
  26. <PerformanceLanding
  27. router={data.router}
  28. organization={data.organization}
  29. location={data.router.location}
  30. eventView={eventView}
  31. projects={data.projects}
  32. selection={eventView.getPageFilters()}
  33. onboardingProject={undefined}
  34. handleSearch={() => {}}
  35. handleTrendsClick={() => {}}
  36. setError={() => {}}
  37. withStaticFilters={withStaticFilters}
  38. />
  39. </MetricsCardinalityProvider>
  40. </OrganizationContext.Provider>
  41. </QueryClientProvider>
  42. );
  43. };
  44. describe('Performance > Landing > Index', function () {
  45. let eventStatsMock: any;
  46. let eventsV2Mock: any;
  47. let wrapper: any;
  48. act(() => void TeamStore.loadInitialData([], false, null));
  49. beforeEach(function () {
  50. // @ts-ignore no-console
  51. // eslint-disable-next-line no-console
  52. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/sdk-updates/',
  55. body: [],
  56. });
  57. MockApiClient.addMockResponse({
  58. url: '/prompts-activity/',
  59. body: {},
  60. });
  61. MockApiClient.addMockResponse({
  62. url: '/organizations/org-slug/projects/',
  63. body: [],
  64. });
  65. MockApiClient.addMockResponse({
  66. method: 'GET',
  67. url: `/organizations/org-slug/key-transactions-list/`,
  68. body: [],
  69. });
  70. MockApiClient.addMockResponse({
  71. method: 'GET',
  72. url: `/organizations/org-slug/legacy-key-transactions-count/`,
  73. body: [],
  74. });
  75. eventStatsMock = MockApiClient.addMockResponse({
  76. method: 'GET',
  77. url: `/organizations/org-slug/events-stats/`,
  78. body: [],
  79. });
  80. MockApiClient.addMockResponse({
  81. method: 'GET',
  82. url: `/organizations/org-slug/events-trends-stats/`,
  83. body: [],
  84. });
  85. MockApiClient.addMockResponse({
  86. method: 'GET',
  87. url: `/organizations/org-slug/events-histogram/`,
  88. body: [],
  89. });
  90. eventsV2Mock = MockApiClient.addMockResponse({
  91. method: 'GET',
  92. url: `/organizations/org-slug/eventsv2/`,
  93. body: {
  94. meta: {
  95. id: 'string',
  96. },
  97. data: [
  98. {
  99. id: '1234',
  100. },
  101. ],
  102. },
  103. });
  104. MockApiClient.addMockResponse({
  105. method: 'GET',
  106. url: `/organizations/org-slug/events/`,
  107. body: {
  108. data: [{}],
  109. meta: {},
  110. },
  111. });
  112. });
  113. afterEach(function () {
  114. MockApiClient.clearMockResponses();
  115. // @ts-ignore no-console
  116. // eslint-disable-next-line no-console
  117. console.error.mockRestore();
  118. if (wrapper) {
  119. wrapper.unmount();
  120. wrapper = undefined;
  121. }
  122. });
  123. it('renders basic UI elements', function () {
  124. const data = initializeData();
  125. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  126. expect(screen.getByTestId('performance-landing-v3')).toBeInTheDocument();
  127. });
  128. it('renders frontend pageload view', function () {
  129. const data = initializeData({
  130. query: {landingDisplay: LandingDisplayField.FRONTEND_PAGELOAD},
  131. });
  132. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  133. expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument();
  134. expect(screen.getByTestId('performance-table')).toBeInTheDocument();
  135. const titles = screen.getAllByTestId('performance-widget-title');
  136. expect(titles).toHaveLength(5);
  137. expect(titles[0]).toHaveTextContent('p75 LCP');
  138. expect(titles[1]).toHaveTextContent('LCP Distribution');
  139. expect(titles[2]).toHaveTextContent('FCP Distribution');
  140. expect(titles[3]).toHaveTextContent('Worst LCP Web Vitals');
  141. expect(titles[4]).toHaveTextContent('Worst FCP Web Vitals');
  142. });
  143. it('renders frontend other view', function () {
  144. const data = initializeData({
  145. query: {landingDisplay: LandingDisplayField.FRONTEND_OTHER},
  146. });
  147. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  148. expect(screen.getByTestId('performance-table')).toBeInTheDocument();
  149. });
  150. it('renders backend view', function () {
  151. const data = initializeData({
  152. query: {landingDisplay: LandingDisplayField.BACKEND},
  153. });
  154. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  155. expect(screen.getByTestId('performance-table')).toBeInTheDocument();
  156. });
  157. it('renders mobile view', function () {
  158. const data = initializeData({
  159. query: {landingDisplay: LandingDisplayField.MOBILE},
  160. });
  161. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  162. expect(screen.getByTestId('performance-table')).toBeInTheDocument();
  163. });
  164. it('renders react-native table headers in mobile view', async function () {
  165. const project = TestStubs.Project({platform: 'react-native'});
  166. const projects = [project];
  167. const data = initializeData({
  168. query: {landingDisplay: LandingDisplayField.MOBILE},
  169. selectedProject: project.id,
  170. projects,
  171. });
  172. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  173. expect(await screen.findByTestId('performance-table')).toBeInTheDocument();
  174. expect(screen.getByTestId('grid-editable')).toBeInTheDocument();
  175. const columnHeaders = await screen.findAllByTestId('grid-head-cell');
  176. expect(columnHeaders).toHaveLength(REACT_NATIVE_COLUMN_TITLES.length);
  177. for (const [index, title] of columnHeaders.entries()) {
  178. expect(title).toHaveTextContent(REACT_NATIVE_COLUMN_TITLES[index]);
  179. }
  180. });
  181. it('renders all transactions view', async function () {
  182. const data = initializeData({
  183. query: {landingDisplay: LandingDisplayField.ALL},
  184. });
  185. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  186. expect(await screen.findByTestId('performance-table')).toBeInTheDocument();
  187. expect(eventStatsMock).toHaveBeenCalledTimes(1); // Only one request is made since the query batcher is working.
  188. expect(eventStatsMock).toHaveBeenNthCalledWith(
  189. 1,
  190. expect.anything(),
  191. expect.objectContaining({
  192. query: expect.objectContaining({
  193. environment: [],
  194. interval: '15m',
  195. partial: '1',
  196. project: [],
  197. query: 'transaction.duration:<15m event.type:transaction',
  198. referrer: 'api.performance.generic-widget-chart.user-misery-area',
  199. statsPeriod: '48h',
  200. yAxis: ['user_misery()', 'tpm()', 'failure_rate()'],
  201. }),
  202. })
  203. );
  204. expect(eventsV2Mock).toHaveBeenCalledTimes(2);
  205. const titles = await screen.findAllByTestId('performance-widget-title');
  206. expect(titles).toHaveLength(5);
  207. expect(titles.at(0)).toHaveTextContent('User Misery');
  208. expect(titles.at(1)).toHaveTextContent('Transactions Per Minute');
  209. expect(titles.at(2)).toHaveTextContent('Failure Rate');
  210. expect(titles.at(3)).toHaveTextContent('Most Related Issues');
  211. expect(titles.at(4)).toHaveTextContent('Most Improved');
  212. });
  213. it('Can switch between landing displays', function () {
  214. const data = initializeData({
  215. query: {landingDisplay: LandingDisplayField.FRONTEND_PAGELOAD, abc: '123'},
  216. });
  217. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  218. expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument();
  219. userEvent.click(screen.getByRole('tab', {name: 'All Transactions'}));
  220. expect(browserHistory.push).toHaveBeenNthCalledWith(
  221. 1,
  222. expect.objectContaining({
  223. pathname: data.location.pathname,
  224. query: {query: '', abc: '123'},
  225. })
  226. );
  227. });
  228. it('Updating projects switches performance view', function () {
  229. const data = initializeData({
  230. query: {landingDisplay: LandingDisplayField.FRONTEND_PAGELOAD},
  231. });
  232. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  233. expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument();
  234. const updatedData = initializeData({
  235. projects: [TestStubs.Project({id: 123, platform: 'unknown'})],
  236. selectedProject: 123,
  237. });
  238. wrapper.rerender(<WrappedComponent data={updatedData} />, data.routerContext);
  239. expect(screen.getByTestId('all-transactions-view')).toBeInTheDocument();
  240. });
  241. it('View correctly defaults based on project without url param', function () {
  242. const data = initializeData({
  243. projects: [TestStubs.Project({id: 99, platform: 'javascript-react'})],
  244. selectedProject: 99,
  245. });
  246. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  247. expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument();
  248. });
  249. it('renders DynamicSamplingMetricsAccuracyAlert', async function () {
  250. const project = TestStubs.Project({id: 99, platform: 'javascript-react'});
  251. const data = initializeData({
  252. projects: [project],
  253. selectedProject: 99,
  254. features: [
  255. 'server-side-sampling',
  256. 'server-side-sampling-ui',
  257. 'dynamic-sampling-performance-cta',
  258. ],
  259. });
  260. MockApiClient.addMockResponse({
  261. url: `/organizations/${data.organization.slug}/stats_v2/`,
  262. method: 'GET',
  263. body: TestStubs.OutcomesWithLowProcessedEvents(),
  264. });
  265. MockApiClient.addMockResponse({
  266. url: '/organizations/org-slug/projects/',
  267. body: [project],
  268. });
  269. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  270. expect(
  271. await screen.findByText(dynamicSamplingMetricsAccuracyMessage)
  272. ).toBeInTheDocument();
  273. });
  274. it('does not render DynamicSamplingMetricsAccuracyAlert if there are other Dynamic Sampling alerts being rendered', async function () {
  275. const project = TestStubs.Project({id: 99, platform: 'javascript-react'});
  276. const data = initializeData({
  277. projects: [project],
  278. selectedProject: 99,
  279. features: [
  280. 'server-side-sampling',
  281. 'server-side-sampling-ui',
  282. 'dynamic-sampling-performance-cta',
  283. 'performance-transaction-name-only-search',
  284. ],
  285. });
  286. addMetricsDataMock();
  287. MockApiClient.addMockResponse({
  288. method: 'GET',
  289. url: '/organizations/org-slug/metrics/data/',
  290. body: TestStubs.MetricsField(),
  291. });
  292. MockApiClient.addMockResponse({
  293. url: '/organizations/org-slug/projects/',
  294. body: [project],
  295. });
  296. MockApiClient.addMockResponse({
  297. url: `/organizations/${data.organization.slug}/stats_v2/`,
  298. method: 'GET',
  299. body: TestStubs.OutcomesWithLowProcessedEvents(),
  300. });
  301. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  302. expect(
  303. await screen.findByText(dynamicSamplingMetricsAccuracyMessage)
  304. ).toBeInTheDocument();
  305. });
  306. describe('with transaction search feature', function () {
  307. it('renders the search bar', async function () {
  308. addMetricsDataMock();
  309. const data = initializeData({
  310. features: ['performance-transaction-name-only-search'],
  311. query: {
  312. field: 'test',
  313. },
  314. });
  315. wrapper = render(
  316. <WrappedComponent data={data} withStaticFilters />,
  317. data.routerContext
  318. );
  319. expect(await screen.findByTestId('transaction-search-bar')).toBeInTheDocument();
  320. });
  321. });
  322. });