trends.spec.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import React from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {mountWithTheme} from 'sentry-test/enzyme';
  5. import PerformanceLanding from 'app/views/performance/landing';
  6. import ProjectsStore from 'app/stores/projectsStore';
  7. import {
  8. TRENDS_FUNCTIONS,
  9. getTrendAliasedFieldPercentage,
  10. getTrendAliasedQueryPercentage,
  11. getTrendAliasedMinus,
  12. } from 'app/views/performance/trends/utils';
  13. import {TrendFunctionField} from 'app/views/performance/trends/types';
  14. const trendsViewQuery = {
  15. view: 'TRENDS',
  16. };
  17. function selectTrendFunction(wrapper, field) {
  18. const menu = wrapper.find('TrendsDropdown DropdownMenu');
  19. expect(menu).toHaveLength(1);
  20. menu.find('DropdownButton').simulate('click');
  21. const option = menu.find(`DropdownItem[data-test-id="${field}"] span`);
  22. expect(option).toHaveLength(1);
  23. option.simulate('click');
  24. wrapper.update();
  25. }
  26. function initializeData(projects, query) {
  27. const features = ['transaction-event', 'performance-view', 'trends'];
  28. const organization = TestStubs.Organization({
  29. features,
  30. projects,
  31. });
  32. const initialData = initializeOrg({
  33. organization,
  34. router: {
  35. location: {
  36. query: {...trendsViewQuery, ...query},
  37. },
  38. },
  39. });
  40. ProjectsStore.loadInitialData(initialData.organization.projects);
  41. return initialData;
  42. }
  43. describe('Performance > Trends', function() {
  44. let trendsMock;
  45. let baselineMock;
  46. beforeEach(function() {
  47. MockApiClient.addMockResponse({
  48. url: '/organizations/org-slug/projects/',
  49. body: [],
  50. });
  51. MockApiClient.addMockResponse({
  52. url: '/organizations/org-slug/tags/',
  53. body: [],
  54. });
  55. MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/users/',
  57. body: [],
  58. });
  59. MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/recent-searches/',
  61. body: [],
  62. });
  63. MockApiClient.addMockResponse({
  64. url: '/organizations/org-slug/recent-searches/',
  65. method: 'POST',
  66. body: [],
  67. });
  68. MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/releases/',
  70. body: [],
  71. });
  72. trendsMock = MockApiClient.addMockResponse({
  73. url: '/organizations/org-slug/events-trends/',
  74. body: {
  75. stats: {
  76. 'internal,/organizations/:orgId/performance/': {
  77. data: [[123, []]],
  78. },
  79. order: 0,
  80. },
  81. events: {
  82. meta: {
  83. count_range_1: 'integer',
  84. count_range_2: 'integer',
  85. percentage_count_range_2_count_range_1: 'percentage',
  86. percentage_percentile_range_2_percentile_range_1: 'percentage',
  87. minus_percentile_range_2_percentile_range_1: 'number',
  88. percentile_range_1: 'duration',
  89. percentile_range_2: 'duration',
  90. transaction: 'string',
  91. },
  92. data: [
  93. {
  94. count: 8,
  95. project: 'internal',
  96. count_range_1: 2,
  97. count_range_2: 6,
  98. percentage_count_range_2_count_range_1: 3,
  99. percentage_percentile_range_2_percentile_range_1: 1.9235225955967554,
  100. minus_percentile_range_2_percentile_range_1: 797,
  101. percentile_range_1: 863,
  102. percentile_range_2: 1660,
  103. transaction: '/organizations/:orgId/performance/',
  104. },
  105. {
  106. count: 60,
  107. project: 'internal',
  108. count_range_1: 20,
  109. count_range_2: 40,
  110. percentage_count_range_2_count_range_1: 2,
  111. percentage_percentile_range_2_percentile_range_1: 1.204968944099379,
  112. minus_percentile_range_2_percentile_range_1: 66,
  113. percentile_range_1: 322,
  114. percentile_range_2: 388,
  115. transaction: '/api/0/internal/health/',
  116. },
  117. ],
  118. },
  119. },
  120. });
  121. baselineMock = MockApiClient.addMockResponse({
  122. url: '/organizations/org-slug/event-baseline/',
  123. body: {
  124. project: 'sentry',
  125. id: '66877921c6ff440b8b891d3734f074e7',
  126. },
  127. });
  128. });
  129. afterEach(function() {
  130. MockApiClient.clearMockResponses();
  131. ProjectsStore.reset();
  132. });
  133. it('renders basic UI elements', async function() {
  134. const projects = [TestStubs.Project()];
  135. const data = initializeData(projects, {});
  136. const wrapper = mountWithTheme(
  137. <PerformanceLanding
  138. organization={data.organization}
  139. location={data.router.location}
  140. />,
  141. data.routerContext
  142. );
  143. await tick();
  144. wrapper.update();
  145. // Trends dropdown and transaction widgets should render.
  146. expect(wrapper.find('TrendsDropdown')).toHaveLength(1);
  147. expect(wrapper.find('ChangedTransactions')).toHaveLength(2);
  148. });
  149. it('transaction list items are rendered', async function() {
  150. const projects = [TestStubs.Project()];
  151. const data = initializeData(projects, {project: ['-1']});
  152. const wrapper = mountWithTheme(
  153. <PerformanceLanding
  154. organization={data.organization}
  155. location={data.router.location}
  156. />,
  157. data.routerContext
  158. );
  159. await tick();
  160. wrapper.update();
  161. expect(wrapper.find('TrendsListItem')).toHaveLength(4);
  162. });
  163. it('view summary menu action links to the correct view', async function() {
  164. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  165. const data = initializeData(projects, {project: ['1']});
  166. const wrapper = mountWithTheme(
  167. <PerformanceLanding
  168. organization={data.organization}
  169. location={data.router.location}
  170. />,
  171. data.routerContext
  172. );
  173. await tick();
  174. wrapper.update();
  175. wrapper
  176. .find('TransactionMenuButton')
  177. .first()
  178. .simulate('click');
  179. const firstTransaction = wrapper.find('TrendsListItem').first();
  180. const summaryLink = firstTransaction.find('StyledSummaryLink');
  181. expect(summaryLink).toHaveLength(1);
  182. expect(summaryLink.text()).toEqual('View Summary');
  183. expect(summaryLink.props().to.pathname).toEqual(
  184. '/organizations/org-slug/performance/summary/'
  185. );
  186. expect(summaryLink.props().to.query.project).toEqual(1);
  187. });
  188. it('transaction link calls comparison view', async function() {
  189. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  190. const data = initializeData(projects, {project: ['1']});
  191. const wrapper = mountWithTheme(
  192. <PerformanceLanding
  193. organization={data.organization}
  194. location={data.router.location}
  195. />,
  196. data.routerContext
  197. );
  198. await tick();
  199. wrapper.update();
  200. const firstTransaction = wrapper.find('TrendsListItem').first();
  201. const transactionLink = firstTransaction.find('StyledLink').first();
  202. transactionLink.simulate('click');
  203. await tick();
  204. wrapper.update();
  205. expect(baselineMock).toHaveBeenCalledTimes(2);
  206. expect(browserHistory.push).toHaveBeenCalledWith({
  207. pathname:
  208. '/organizations/org-slug/performance/compare/sentry:66877921c6ff440b8b891d3734f074e7/sentry:66877921c6ff440b8b891d3734f074e7/',
  209. query: expect.anything(),
  210. });
  211. });
  212. it('choosing a trend function changes location', async function() {
  213. const projects = [TestStubs.Project()];
  214. const data = initializeData(projects, {project: ['-1']});
  215. const wrapper = mountWithTheme(
  216. <PerformanceLanding
  217. organization={data.organization}
  218. location={data.router.location}
  219. />,
  220. data.routerContext
  221. );
  222. for (const trendFunction of TRENDS_FUNCTIONS) {
  223. selectTrendFunction(wrapper, trendFunction.field);
  224. await tick();
  225. expect(browserHistory.push).toHaveBeenCalledWith({
  226. query: expect.objectContaining({
  227. trendFunction: trendFunction.field,
  228. }),
  229. });
  230. }
  231. });
  232. it('clicking project trend view transactions changes location', async function() {
  233. const projectId = 42;
  234. const projects = [TestStubs.Project({id: projectId, slug: 'internal'})];
  235. const data = initializeData(projects, {project: ['-1']});
  236. const wrapper = mountWithTheme(
  237. <PerformanceLanding
  238. organization={data.organization}
  239. location={data.router.location}
  240. />,
  241. data.routerContext
  242. );
  243. await tick();
  244. wrapper.update();
  245. const mostImprovedProject = wrapper.find('ChangedProjectsContainer').first();
  246. const viewTransactions = mostImprovedProject.find('Button').first();
  247. viewTransactions.simulate('click');
  248. expect(browserHistory.push).toHaveBeenCalledWith({
  249. query: expect.objectContaining({
  250. project: [projectId],
  251. }),
  252. });
  253. });
  254. it('trend functions in location make api calls', async function() {
  255. const projects = [TestStubs.Project(), TestStubs.Project()];
  256. const data = initializeData(projects, {project: ['-1']});
  257. const wrapper = mountWithTheme(
  258. <PerformanceLanding
  259. organization={data.organization}
  260. location={data.router.location}
  261. />,
  262. data.routerContext
  263. );
  264. await tick();
  265. wrapper.update();
  266. for (const trendFunction of TRENDS_FUNCTIONS) {
  267. trendsMock.mockReset();
  268. wrapper.setProps({
  269. location: {query: {...trendsViewQuery, trendFunction: trendFunction.field}},
  270. });
  271. wrapper.update();
  272. await tick();
  273. expect(trendsMock).toHaveBeenCalledTimes(4);
  274. const aliasedFieldDivide = getTrendAliasedFieldPercentage(trendFunction.alias);
  275. const aliasedQueryDivide = getTrendAliasedQueryPercentage(trendFunction.alias);
  276. const sort =
  277. trendFunction.field === TrendFunctionField.USER_MISERY
  278. ? getTrendAliasedMinus(trendFunction.alias)
  279. : aliasedFieldDivide;
  280. const defaultTrendsFields = ['project', 'count()'];
  281. const trendFunctionFields = TRENDS_FUNCTIONS.map(({field}) => field);
  282. const transactionFields = [
  283. ...trendFunctionFields,
  284. 'transaction',
  285. ...defaultTrendsFields,
  286. ];
  287. const projectFields = [...trendFunctionFields, ...defaultTrendsFields];
  288. expect(transactionFields).toHaveLength(8);
  289. expect(projectFields).toHaveLength(transactionFields.length - 1);
  290. // Improved projects call
  291. expect(trendsMock).toHaveBeenNthCalledWith(
  292. 1,
  293. expect.anything(),
  294. expect.objectContaining({
  295. query: expect.objectContaining({
  296. trendFunction: trendFunction.field,
  297. sort,
  298. query: expect.stringContaining(aliasedQueryDivide + ':<1'),
  299. interval: '12h',
  300. field: projectFields,
  301. statsPeriod: '14d',
  302. }),
  303. })
  304. );
  305. // Improved transactions call
  306. expect(trendsMock).toHaveBeenNthCalledWith(
  307. 2,
  308. expect.anything(),
  309. expect.objectContaining({
  310. query: expect.objectContaining({
  311. trendFunction: trendFunction.field,
  312. sort,
  313. query: expect.stringContaining(aliasedQueryDivide + ':<1'),
  314. interval: '12h',
  315. field: transactionFields,
  316. statsPeriod: '14d',
  317. }),
  318. })
  319. );
  320. // Regression projects call
  321. expect(trendsMock).toHaveBeenNthCalledWith(
  322. 3,
  323. expect.anything(),
  324. expect.objectContaining({
  325. query: expect.objectContaining({
  326. trendFunction: trendFunction.field,
  327. sort: '-' + sort,
  328. query: expect.stringContaining(aliasedQueryDivide + ':>1'),
  329. interval: '12h',
  330. field: projectFields,
  331. statsPeriod: '14d',
  332. }),
  333. })
  334. );
  335. // Regression transactions call
  336. expect(trendsMock).toHaveBeenNthCalledWith(
  337. 4,
  338. expect.anything(),
  339. expect.objectContaining({
  340. query: expect.objectContaining({
  341. trendFunction: trendFunction.field,
  342. sort: '-' + sort,
  343. query: expect.stringContaining(aliasedQueryDivide + ':>1'),
  344. interval: '12h',
  345. field: transactionFields,
  346. statsPeriod: '14d',
  347. }),
  348. })
  349. );
  350. }
  351. });
  352. });