index.spec.tsx 12 KB

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