index.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import {ProjectFixture} from 'sentry-fixture/project';
  2. import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
  3. import {initializeData} from 'sentry-test/performance/initializePerformanceData';
  4. import {makeTestQueryClient} from 'sentry-test/queryClient';
  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 {browserHistory} from 'sentry/utils/browserHistory';
  15. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  16. import {QueryClientProvider} from 'sentry/utils/queryClient';
  17. import {OrganizationContext} from 'sentry/views/organizationContext';
  18. import {generatePerformanceEventView} from 'sentry/views/performance/data';
  19. import {PerformanceLanding} from 'sentry/views/performance/landing';
  20. import {REACT_NATIVE_COLUMN_TITLES} from 'sentry/views/performance/landing/data';
  21. import {LandingDisplayField} from 'sentry/views/performance/landing/utils';
  22. const searchHandlerMock = jest.fn();
  23. function WrappedComponent({data, withStaticFilters = false}) {
  24. const eventView = generatePerformanceEventView(
  25. data.router.location,
  26. data.projects,
  27. {
  28. withStaticFilters,
  29. },
  30. data.organization
  31. );
  32. return (
  33. <QueryClientProvider client={makeTestQueryClient()}>
  34. <OrganizationContext.Provider value={data.organization}>
  35. <MetricsCardinalityProvider
  36. location={data.router.location}
  37. organization={data.organization}
  38. >
  39. <PerformanceLanding
  40. router={data.router}
  41. organization={data.organization}
  42. location={data.router.location}
  43. eventView={eventView}
  44. projects={data.projects}
  45. selection={eventView.getPageFilters()}
  46. onboardingProject={undefined}
  47. handleSearch={searchHandlerMock}
  48. handleTrendsClick={() => {}}
  49. setError={() => {}}
  50. withStaticFilters={withStaticFilters}
  51. />
  52. </MetricsCardinalityProvider>
  53. </OrganizationContext.Provider>
  54. </QueryClientProvider>
  55. );
  56. }
  57. describe('Performance > Landing > Index', function () {
  58. let eventStatsMock: jest.Mock;
  59. let eventsMock: jest.Mock;
  60. let wrapper: any;
  61. act(() => void TeamStore.loadInitialData([], false, null));
  62. beforeEach(function () {
  63. // eslint-disable-next-line no-console
  64. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  65. MockApiClient.addMockResponse({
  66. url: '/organizations/org-slug/sdk-updates/',
  67. body: [],
  68. });
  69. MockApiClient.addMockResponse({
  70. url: '/organizations/org-slug/prompts-activity/',
  71. body: {},
  72. });
  73. MockApiClient.addMockResponse({
  74. url: '/organizations/org-slug/projects/',
  75. body: [],
  76. });
  77. MockApiClient.addMockResponse({
  78. method: 'GET',
  79. url: `/organizations/org-slug/key-transactions-list/`,
  80. body: [],
  81. });
  82. MockApiClient.addMockResponse({
  83. method: 'GET',
  84. url: `/organizations/org-slug/legacy-key-transactions-count/`,
  85. body: [],
  86. });
  87. eventStatsMock = MockApiClient.addMockResponse({
  88. method: 'GET',
  89. url: `/organizations/org-slug/events-stats/`,
  90. body: [],
  91. });
  92. MockApiClient.addMockResponse({
  93. method: 'GET',
  94. url: `/organizations/org-slug/events-trends-stats/`,
  95. body: [],
  96. });
  97. MockApiClient.addMockResponse({
  98. method: 'GET',
  99. url: `/organizations/org-slug/events-histogram/`,
  100. body: [],
  101. });
  102. eventsMock = MockApiClient.addMockResponse({
  103. method: 'GET',
  104. url: `/organizations/org-slug/events/`,
  105. body: {
  106. meta: {
  107. fields: {
  108. id: 'string',
  109. },
  110. },
  111. data: [
  112. {
  113. id: '1234',
  114. },
  115. ],
  116. },
  117. });
  118. MockApiClient.addMockResponse({
  119. method: 'GET',
  120. url: `/organizations/org-slug/metrics-compatibility/`,
  121. body: [],
  122. });
  123. MockApiClient.addMockResponse({
  124. method: 'GET',
  125. url: `/organizations/org-slug/metrics-compatibility-sums/`,
  126. body: [],
  127. });
  128. });
  129. afterEach(function () {
  130. MockApiClient.clearMockResponses();
  131. jest.clearAllMocks();
  132. jest.restoreAllMocks();
  133. if (wrapper) {
  134. wrapper.unmount();
  135. wrapper = undefined;
  136. }
  137. });
  138. it('renders basic UI elements', function () {
  139. const data = initializeData();
  140. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  141. expect(screen.getByTestId('performance-landing-v3')).toBeInTheDocument();
  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 = ProjectFixture({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. await waitFor(() => 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: '1h',
  195. partial: '1',
  196. project: [],
  197. query: 'event.type:transaction',
  198. referrer: 'api.performance.generic-widget-chart.user-misery-area',
  199. statsPeriod: '14d',
  200. yAxis: ['user_misery()', 'tpm()', 'failure_rate()'],
  201. }),
  202. })
  203. );
  204. expect(eventsMock).toHaveBeenCalledTimes(2);
  205. const titles = await screen.findAllByTestId('performance-widget-title');
  206. expect(titles).toHaveLength(5);
  207. expect(titles.at(0)).toHaveTextContent('Most Regressed');
  208. expect(titles.at(1)).toHaveTextContent('Most Improved');
  209. expect(titles.at(2)).toHaveTextContent('User Misery');
  210. expect(titles.at(3)).toHaveTextContent('Transactions Per Minute');
  211. expect(titles.at(4)).toHaveTextContent('Failure Rate');
  212. });
  213. it('Can switch between landing displays', async function () {
  214. const data = initializeData({
  215. query: {landingDisplay: LandingDisplayField.FRONTEND_OTHER, abc: '123'},
  216. });
  217. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  218. expect(screen.getByTestId('frontend-other-view')).toBeInTheDocument();
  219. await 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_OTHER},
  231. });
  232. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  233. expect(screen.getByTestId('frontend-other-view')).toBeInTheDocument();
  234. const updatedData = initializeData({
  235. projects: [ProjectFixture({id: '123', platform: undefined})],
  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: [ProjectFixture({id: '99', platform: 'javascript-react'})],
  244. selectedProject: 99,
  245. });
  246. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  247. expect(screen.getByTestId('frontend-other-view')).toBeInTheDocument();
  248. });
  249. describe('With transaction search feature', function () {
  250. it('does not search for empty string transaction', async function () {
  251. const data = initializeData();
  252. render(<WrappedComponent data={data} withStaticFilters />, data.routerContext);
  253. await waitForElementToBeRemoved(() => screen.getAllByTestId('loading-indicator'));
  254. await userEvent.type(screen.getByPlaceholderText('Search Transactions'), '{enter}');
  255. expect(searchHandlerMock).toHaveBeenCalledWith('', 'transactionsOnly');
  256. });
  257. it('renders the search bar', async function () {
  258. addMetricsDataMock();
  259. const data = initializeData({
  260. query: {
  261. field: 'test',
  262. },
  263. });
  264. wrapper = render(
  265. <WrappedComponent data={data} withStaticFilters />,
  266. data.routerContext
  267. );
  268. expect(await screen.findByTestId('transaction-search-bar')).toBeInTheDocument();
  269. });
  270. it('extracts free text from the query', async function () {
  271. const data = initializeData();
  272. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  273. await waitForElementToBeRemoved(() => screen.getAllByTestId('loading-indicator'));
  274. expect(await screen.findByPlaceholderText('Search Transactions')).toHaveValue('');
  275. });
  276. });
  277. describe('With span operations widget feature flag', function () {
  278. it('Displays the span operations widget', async function () {
  279. addMetricsDataMock();
  280. const data = initializeData({
  281. features: [
  282. 'performance-transaction-name-only-search',
  283. 'performance-new-widget-designs',
  284. ],
  285. });
  286. wrapper = render(<WrappedComponent data={data} />, data.routerContext);
  287. const titles = await screen.findAllByTestId('performance-widget-title');
  288. expect(titles.at(0)).toHaveTextContent('Most Regressed');
  289. });
  290. });
  291. });