index.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import {browserHistory} from 'react-router';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {act} from 'sentry-test/reactTestingLibrary';
  5. import ProjectsStore from 'app/stores/projectsStore';
  6. import TeamStore from 'app/stores/teamStore';
  7. import {OrganizationContext} from 'app/views/organizationContext';
  8. import VitalDetail from 'app/views/performance/vitalDetail/';
  9. function initializeData({query} = {query: {}}) {
  10. const features = ['discover-basic', 'performance-view'];
  11. const organization = TestStubs.Organization({
  12. features,
  13. projects: [TestStubs.Project()],
  14. });
  15. const initialData = initializeOrg({
  16. organization,
  17. router: {
  18. location: {
  19. query: {
  20. project: 1,
  21. ...query,
  22. },
  23. },
  24. },
  25. });
  26. act(() => ProjectsStore.loadInitialData(initialData.organization.projects));
  27. return initialData;
  28. }
  29. const WrappedComponent = ({organization, ...rest}) => {
  30. return (
  31. <OrganizationContext.Provider value={organization}>
  32. <VitalDetail {...rest} />
  33. </OrganizationContext.Provider>
  34. );
  35. };
  36. describe('Performance > VitalDetail', function () {
  37. beforeEach(function () {
  38. act(() => void TeamStore.loadInitialData([]));
  39. browserHistory.push = jest.fn();
  40. MockApiClient.addMockResponse({
  41. url: '/organizations/org-slug/projects/',
  42. body: [],
  43. });
  44. MockApiClient.addMockResponse({
  45. url: '/organizations/org-slug/tags/',
  46. body: [],
  47. });
  48. MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/events-stats/',
  50. body: {data: [[123, []]]},
  51. });
  52. MockApiClient.addMockResponse({
  53. url: '/organizations/org-slug/tags/user.email/values/',
  54. body: [],
  55. });
  56. MockApiClient.addMockResponse({
  57. url: '/organizations/org-slug/releases/stats/',
  58. body: [],
  59. });
  60. MockApiClient.addMockResponse({
  61. url: '/organizations/org-slug/users/',
  62. body: [],
  63. });
  64. MockApiClient.addMockResponse({
  65. url: '/organizations/org-slug/recent-searches/',
  66. body: [],
  67. });
  68. MockApiClient.addMockResponse({
  69. url: '/organizations/org-slug/recent-searches/',
  70. method: 'POST',
  71. body: [],
  72. });
  73. MockApiClient.addMockResponse({
  74. url: '/organizations/org-slug/events-vitals/',
  75. body: {
  76. 'measurements.lcp': {
  77. poor: 1,
  78. meh: 2,
  79. good: 3,
  80. total: 6,
  81. p75: 4500,
  82. },
  83. },
  84. });
  85. MockApiClient.addMockResponse(
  86. {
  87. url: '/organizations/org-slug/eventsv2/',
  88. body: {
  89. meta: {
  90. count: 'integer',
  91. p95_measurements_lcp: 'duration',
  92. transaction: 'string',
  93. p50_measurements_lcp: 'duration',
  94. project: 'string',
  95. compare_numeric_aggregate_p75_measurements_lcp_greater_4000: 'number',
  96. 'project.id': 'integer',
  97. count_unique_user: 'integer',
  98. p75_measurements_lcp: 'duration',
  99. },
  100. data: [
  101. {
  102. count: 100000,
  103. p95_measurements_lcp: 5000,
  104. transaction: 'something',
  105. p50_measurements_lcp: 3500,
  106. project: 'javascript',
  107. compare_numeric_aggregate_p75_measurements_lcp_greater_4000: 1,
  108. count_unique_user: 10000,
  109. p75_measurements_lcp: 4500,
  110. },
  111. ],
  112. },
  113. },
  114. {
  115. predicate: (url, options) => {
  116. return (
  117. url.includes('eventsv2') &&
  118. options.query?.field.find(f => f === 'p50(measurements.lcp)')
  119. );
  120. },
  121. }
  122. );
  123. MockApiClient.addMockResponse(
  124. {
  125. url: '/organizations/org-slug/eventsv2/',
  126. body: {
  127. meta: {
  128. compare_numeric_aggregate_p75_measurements_cls_greater_0_1: 'number',
  129. compare_numeric_aggregate_p75_measurements_cls_greater_0_25: 'number',
  130. count: 'integer',
  131. count_unique_user: 'integer',
  132. team_key_transaction: 'boolean',
  133. p50_measurements_cls: 'number',
  134. p75_measurements_cls: 'number',
  135. p95_measurements_cls: 'number',
  136. project: 'string',
  137. transaction: 'string',
  138. },
  139. data: [
  140. {
  141. compare_numeric_aggregate_p75_measurements_cls_greater_0_1: 1,
  142. compare_numeric_aggregate_p75_measurements_cls_greater_0_25: 0,
  143. count: 10000,
  144. count_unique_user: 2740,
  145. team_key_transaction: 1,
  146. p50_measurements_cls: 0.143,
  147. p75_measurements_cls: 0.215,
  148. p95_measurements_cls: 0.302,
  149. project: 'javascript',
  150. transaction: 'something',
  151. },
  152. ],
  153. },
  154. },
  155. {
  156. predicate: (url, options) => {
  157. return (
  158. url.includes('eventsv2') &&
  159. options.query?.field.find(f => f === 'p50(measurements.cls)')
  160. );
  161. },
  162. }
  163. );
  164. MockApiClient.addMockResponse({
  165. method: 'GET',
  166. url: `/organizations/org-slug/key-transactions-list/`,
  167. body: [],
  168. });
  169. });
  170. afterEach(function () {
  171. MockApiClient.clearMockResponses();
  172. act(() => ProjectsStore.reset());
  173. });
  174. it('renders basic UI elements', async function () {
  175. const initialData = initializeData();
  176. const wrapper = mountWithTheme(
  177. <WrappedComponent
  178. organization={initialData.organization}
  179. location={initialData.router.location}
  180. />,
  181. initialData.routerContext
  182. );
  183. await tick();
  184. wrapper.update();
  185. // It shows a search bar
  186. expect(wrapper.find('StyledSearchBar')).toHaveLength(1);
  187. // It shows the vital card
  188. expect(wrapper.find('vitalInfo')).toHaveLength(1);
  189. // It shows a chart
  190. expect(wrapper.find('VitalChart')).toHaveLength(1);
  191. // It shows a table
  192. expect(wrapper.find('Table')).toHaveLength(1);
  193. });
  194. it('triggers a navigation on search', async function () {
  195. const initialData = initializeData();
  196. const wrapper = mountWithTheme(
  197. <WrappedComponent
  198. organization={initialData.organization}
  199. location={initialData.router.location}
  200. />,
  201. initialData.routerContext
  202. );
  203. await tick();
  204. wrapper.update();
  205. // Fill out the search box, and submit it.
  206. const searchBar = wrapper.find('SearchBar textarea');
  207. searchBar
  208. .simulate('change', {target: {value: 'user.email:uhoh*'}})
  209. .simulate('submit', {preventDefault() {}});
  210. // Check the navigation.
  211. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  212. expect(browserHistory.push).toHaveBeenCalledWith({
  213. pathname: undefined,
  214. query: {
  215. project: 1,
  216. statsPeriod: '14d',
  217. query: 'user.email:uhoh*',
  218. },
  219. });
  220. });
  221. it('Applies conditions when linking to transaction summary', async function () {
  222. const initialData = initializeData({
  223. query: {
  224. query: 'sometag:value',
  225. },
  226. });
  227. const wrapper = mountWithTheme(
  228. <WrappedComponent
  229. organization={initialData.organization}
  230. location={initialData.router.location}
  231. />,
  232. initialData.routerContext
  233. );
  234. await tick();
  235. wrapper.update();
  236. const firstTransactionFromList = wrapper.find('Table GridBody GridRow Link').at(1);
  237. expect(firstTransactionFromList.prop('to')).toEqual(
  238. expect.objectContaining({
  239. pathname: '/organizations/org-slug/performance/summary/',
  240. query: expect.objectContaining({
  241. display: 'vitals',
  242. query: 'sometag:value has:measurements.lcp',
  243. showTransactions: 'recent',
  244. statsPeriod: '24h',
  245. transaction: 'something',
  246. }),
  247. })
  248. );
  249. });
  250. it('Check CLS', async function () {
  251. const initialData = initializeData({
  252. query: {
  253. query: 'anothertag:value',
  254. vitalName: 'measurements.cls',
  255. },
  256. });
  257. const wrapper = mountWithTheme(
  258. <WrappedComponent
  259. organization={initialData.organization}
  260. location={initialData.router.location}
  261. />,
  262. initialData.routerContext
  263. );
  264. await tick();
  265. wrapper.update();
  266. expect(wrapper.find('Title').text()).toEqual('Cumulative Layout Shift');
  267. const firstTransactionFromList = wrapper.find('Table GridBody GridRow Link').at(1);
  268. expect(firstTransactionFromList.prop('to')).toEqual(
  269. expect.objectContaining({
  270. pathname: '/organizations/org-slug/performance/summary/',
  271. query: expect.objectContaining({
  272. display: 'vitals',
  273. query: 'anothertag:value has:measurements.cls',
  274. showTransactions: 'recent',
  275. statsPeriod: '24h',
  276. transaction: 'something',
  277. }),
  278. })
  279. );
  280. // Check cells are not in ms
  281. const firstRow = wrapper.find('GridBody GridRow').first();
  282. expect(firstRow.find('GridBodyCell').at(6).text()).toEqual('0.215');
  283. });
  284. it('Pagination links exist to switch between vitals', async function () {
  285. const initialData = initializeData({query: {query: 'tag:value'}});
  286. const wrapper = mountWithTheme(
  287. <WrappedComponent
  288. organization={initialData.organization}
  289. location={initialData.router.location}
  290. />,
  291. initialData.routerContext
  292. );
  293. await tick();
  294. wrapper.update();
  295. const backButton = wrapper.find('HeaderActions ButtonGrid ButtonBar Button').first();
  296. backButton.simulate('click');
  297. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  298. expect(browserHistory.push).toHaveBeenCalledWith({
  299. pathname: undefined,
  300. query: {
  301. project: 1,
  302. query: 'tag:value',
  303. vitalName: 'measurements.fcp',
  304. },
  305. });
  306. });
  307. it('Check LCP vital renders correctly', async function () {
  308. const initialData = initializeData({query: {query: 'tag:value'}});
  309. const wrapper = mountWithTheme(
  310. <WrappedComponent
  311. organization={initialData.organization}
  312. location={initialData.router.location}
  313. />,
  314. initialData.routerContext
  315. );
  316. await tick();
  317. wrapper.update();
  318. expect(wrapper.find('Title').text()).toEqual('Largest Contentful Paint');
  319. expect(wrapper.find('[data-test-id="vital-bar-p75"]').text()).toEqual(
  320. 'The p75 for all transactions is 4500ms'
  321. );
  322. const firstRow = wrapper.find('GridBody GridRow').first();
  323. expect(firstRow.find('GridBodyCell').at(6).text()).toEqual('4.50s');
  324. });
  325. });