index.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {ProjectFixture} from 'sentry-fixture/project';
  2. import {RouterFixture} from 'sentry-fixture/routerFixture';
  3. import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
  4. import {initializeData} from 'sentry-test/performance/initializePerformanceData';
  5. import {makeTestQueryClient} from 'sentry-test/queryClient';
  6. import {
  7. act,
  8. render,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. waitForElementToBeRemoved,
  13. } from 'sentry-test/reactTestingLibrary';
  14. import TeamStore from 'sentry/stores/teamStore';
  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}: any) {
  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. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  64. MockApiClient.addMockResponse({
  65. url: '/organizations/org-slug/sdk-updates/',
  66. body: [],
  67. });
  68. MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/prompts-activity/',
  70. body: {},
  71. });
  72. MockApiClient.addMockResponse({
  73. url: '/organizations/org-slug/projects/',
  74. body: [],
  75. });
  76. MockApiClient.addMockResponse({
  77. method: 'GET',
  78. url: `/organizations/org-slug/key-transactions-list/`,
  79. body: [],
  80. });
  81. MockApiClient.addMockResponse({
  82. method: 'GET',
  83. url: `/organizations/org-slug/legacy-key-transactions-count/`,
  84. body: [],
  85. });
  86. eventStatsMock = MockApiClient.addMockResponse({
  87. method: 'GET',
  88. url: `/organizations/org-slug/events-stats/`,
  89. body: [],
  90. });
  91. MockApiClient.addMockResponse({
  92. method: 'GET',
  93. url: `/organizations/org-slug/events-trends-stats/`,
  94. body: [],
  95. });
  96. MockApiClient.addMockResponse({
  97. method: 'GET',
  98. url: `/organizations/org-slug/events-histogram/`,
  99. body: [],
  100. });
  101. eventsMock = MockApiClient.addMockResponse({
  102. method: 'GET',
  103. url: `/organizations/org-slug/events/`,
  104. body: {
  105. meta: {
  106. fields: {
  107. id: 'string',
  108. },
  109. },
  110. data: [
  111. {
  112. id: '1234',
  113. },
  114. ],
  115. },
  116. });
  117. MockApiClient.addMockResponse({
  118. method: 'GET',
  119. url: `/organizations/org-slug/metrics-compatibility/`,
  120. body: [],
  121. });
  122. MockApiClient.addMockResponse({
  123. method: 'GET',
  124. url: `/organizations/org-slug/metrics-compatibility-sums/`,
  125. body: [],
  126. });
  127. });
  128. afterEach(function () {
  129. MockApiClient.clearMockResponses();
  130. jest.clearAllMocks();
  131. jest.restoreAllMocks();
  132. if (wrapper) {
  133. wrapper.unmount();
  134. wrapper = undefined;
  135. }
  136. });
  137. it('renders basic UI elements', function () {
  138. const data = initializeData();
  139. wrapper = render(<WrappedComponent data={data} />);
  140. expect(screen.getByTestId('performance-landing-v3')).toBeInTheDocument();
  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} />);
  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} />);
  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} />);
  161. expect(screen.getByTestId('performance-table')).toBeInTheDocument();
  162. });
  163. it('renders react-native table headers in mobile view', async function () {
  164. const project = ProjectFixture({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} />);
  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]!.title!);
  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} />);
  185. expect(await screen.findByTestId('performance-table')).toBeInTheDocument();
  186. await waitFor(() => 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: '1h',
  194. partial: '1',
  195. project: [],
  196. query: 'event.type:transaction',
  197. referrer: 'api.performance.generic-widget-chart.user-misery-area',
  198. statsPeriod: '14d',
  199. yAxis: ['user_misery()', 'tpm()', 'failure_rate()'],
  200. }),
  201. })
  202. );
  203. expect(eventsMock).toHaveBeenCalledTimes(2);
  204. const titles = await screen.findAllByTestId('performance-widget-title');
  205. expect(titles).toHaveLength(5);
  206. expect(titles.at(0)).toHaveTextContent('Most Regressed');
  207. expect(titles.at(1)).toHaveTextContent('Most Improved');
  208. expect(titles.at(2)).toHaveTextContent('User Misery');
  209. expect(titles.at(3)).toHaveTextContent('Transactions Per Minute');
  210. expect(titles.at(4)).toHaveTextContent('Failure Rate');
  211. });
  212. it('Can switch between landing displays', async function () {
  213. const router = RouterFixture();
  214. const data = initializeData({
  215. query: {landingDisplay: LandingDisplayField.FRONTEND_OTHER, abc: '123'},
  216. });
  217. wrapper = render(<WrappedComponent data={data} />, {router});
  218. expect(screen.getByTestId('frontend-other-view')).toBeInTheDocument();
  219. await userEvent.click(screen.getByRole('tab', {name: 'All Transactions'}));
  220. expect(router.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} />);
  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} />);
  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} />);
  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 />);
  253. await waitForElementToBeRemoved(() => screen.queryAllByTestId('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(<WrappedComponent data={data} withStaticFilters />);
  265. expect(await screen.findByTestId('transaction-search-bar')).toBeInTheDocument();
  266. });
  267. it('extracts free text from the query', async function () {
  268. const data = initializeData();
  269. wrapper = render(<WrappedComponent data={data} />);
  270. await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator'));
  271. expect(await screen.findByPlaceholderText('Search Transactions')).toHaveValue('');
  272. });
  273. });
  274. describe('With span operations widget feature flag', function () {
  275. it('Displays the span operations widget', async function () {
  276. addMetricsDataMock();
  277. const data = initializeData({
  278. features: [
  279. 'performance-transaction-name-only-search',
  280. 'performance-new-widget-designs',
  281. ],
  282. });
  283. wrapper = render(<WrappedComponent data={data} />);
  284. const titles = await screen.findAllByTestId('performance-widget-title');
  285. expect(titles.at(0)).toHaveTextContent('Most Regressed');
  286. });
  287. });
  288. });