index.spec.tsx 10 KB

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