index.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. import {browserHistory, InjectedRouter} from 'react-router';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {mountWithTheme, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  4. import {textWithMarkupMatcher} from 'sentry-test/utils';
  5. import ProjectsStore from 'sentry/stores/projectsStore';
  6. import TeamStore from 'sentry/stores/teamStore';
  7. import {WebVital} from 'sentry/utils/discover/fields';
  8. import {Browser} from 'sentry/utils/performance/vitals/constants';
  9. import {MetricsSwitchContext} from 'sentry/views/performance/metricsSwitch';
  10. import VitalDetail from 'sentry/views/performance/vitalDetail';
  11. import {vitalSupportedBrowsers} from 'sentry/views/performance/vitalDetail/utils';
  12. const api = new MockApiClient();
  13. const organization = TestStubs.Organization({
  14. features: ['discover-basic', 'performance-view'],
  15. projects: [TestStubs.Project()],
  16. });
  17. const {
  18. routerContext,
  19. organization: org,
  20. router,
  21. project,
  22. } = initializeOrg({
  23. ...initializeOrg(),
  24. organization,
  25. router: {
  26. location: {
  27. query: {
  28. project: 1,
  29. },
  30. },
  31. },
  32. });
  33. function TestComponent(props: {isMetricsData?: boolean; router?: InjectedRouter} = {}) {
  34. return (
  35. <MetricsSwitchContext.Provider
  36. value={{isMetricsData: props.isMetricsData ?? false, setIsMetricsData: jest.fn()}}
  37. >
  38. <VitalDetail
  39. api={api}
  40. location={props.router?.location ?? router.location}
  41. router={props.router ?? router}
  42. params={{}}
  43. route={{}}
  44. routes={[]}
  45. routeParams={{}}
  46. />
  47. </MetricsSwitchContext.Provider>
  48. );
  49. }
  50. const testSupportedBrowserRendering = (webVital: WebVital) => {
  51. Object.values(Browser).forEach(browser => {
  52. const browserElement = screen.getByText(browser);
  53. expect(browserElement).toBeInTheDocument();
  54. const isSupported = vitalSupportedBrowsers[webVital]?.includes(browser);
  55. if (isSupported) {
  56. expect(within(browserElement).getByTestId('icon-check-mark')).toBeInTheDocument();
  57. } else {
  58. expect(within(browserElement).getByTestId('icon-close')).toBeInTheDocument();
  59. }
  60. });
  61. };
  62. describe('Performance > VitalDetail', function () {
  63. beforeEach(function () {
  64. TeamStore.loadInitialData([], false, null);
  65. ProjectsStore.loadInitialData(org.projects);
  66. browserHistory.push = jest.fn();
  67. MockApiClient.addMockResponse({
  68. url: '/organizations/org-slug/projects/',
  69. body: [],
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/tags/',
  73. body: [],
  74. });
  75. MockApiClient.addMockResponse({
  76. url: '/organizations/org-slug/events-stats/',
  77. body: {data: [[123, []]]},
  78. });
  79. MockApiClient.addMockResponse({
  80. url: '/organizations/org-slug/tags/user.email/values/',
  81. body: [],
  82. });
  83. MockApiClient.addMockResponse({
  84. url: '/organizations/org-slug/releases/stats/',
  85. body: [],
  86. });
  87. MockApiClient.addMockResponse({
  88. url: '/organizations/org-slug/users/',
  89. body: [],
  90. });
  91. MockApiClient.addMockResponse({
  92. url: '/organizations/org-slug/recent-searches/',
  93. body: [],
  94. });
  95. MockApiClient.addMockResponse({
  96. url: '/organizations/org-slug/recent-searches/',
  97. method: 'POST',
  98. body: [],
  99. });
  100. MockApiClient.addMockResponse({
  101. url: '/organizations/org-slug/events-vitals/',
  102. body: {
  103. 'measurements.lcp': {
  104. poor: 1,
  105. meh: 2,
  106. good: 3,
  107. total: 6,
  108. p75: 4500,
  109. },
  110. },
  111. });
  112. MockApiClient.addMockResponse({
  113. url: '/organizations/org-slug/eventsv2/',
  114. body: {
  115. meta: {
  116. count: 'integer',
  117. p95_measurements_lcp: 'duration',
  118. transaction: 'string',
  119. p50_measurements_lcp: 'duration',
  120. project: 'string',
  121. compare_numeric_aggregate_p75_measurements_lcp_greater_4000: 'number',
  122. 'project.id': 'integer',
  123. count_unique_user: 'integer',
  124. p75_measurements_lcp: 'duration',
  125. },
  126. data: [
  127. {
  128. count: 100000,
  129. p95_measurements_lcp: 5000,
  130. transaction: 'something',
  131. p50_measurements_lcp: 3500,
  132. project: 'javascript',
  133. compare_numeric_aggregate_p75_measurements_lcp_greater_4000: 1,
  134. count_unique_user: 10000,
  135. p75_measurements_lcp: 4500,
  136. },
  137. ],
  138. },
  139. match: [
  140. (_url, options) => {
  141. return options.query?.field?.find(f => f === 'p50(measurements.lcp)');
  142. },
  143. ],
  144. });
  145. MockApiClient.addMockResponse({
  146. url: '/organizations/org-slug/eventsv2/',
  147. body: {
  148. meta: {
  149. compare_numeric_aggregate_p75_measurements_cls_greater_0_1: 'number',
  150. compare_numeric_aggregate_p75_measurements_cls_greater_0_25: 'number',
  151. count: 'integer',
  152. count_unique_user: 'integer',
  153. team_key_transaction: 'boolean',
  154. p50_measurements_cls: 'number',
  155. p75_measurements_cls: 'number',
  156. p95_measurements_cls: 'number',
  157. project: 'string',
  158. transaction: 'string',
  159. },
  160. data: [
  161. {
  162. compare_numeric_aggregate_p75_measurements_cls_greater_0_1: 1,
  163. compare_numeric_aggregate_p75_measurements_cls_greater_0_25: 0,
  164. count: 10000,
  165. count_unique_user: 2740,
  166. team_key_transaction: 1,
  167. p50_measurements_cls: 0.143,
  168. p75_measurements_cls: 0.215,
  169. p95_measurements_cls: 0.302,
  170. project: 'javascript',
  171. transaction: 'something',
  172. },
  173. ],
  174. },
  175. match: [
  176. (_url, options) => {
  177. return options.query?.field?.find(f => f === 'p50(measurements.cls)');
  178. },
  179. ],
  180. });
  181. MockApiClient.addMockResponse({
  182. method: 'GET',
  183. url: `/organizations/org-slug/key-transactions-list/`,
  184. body: [],
  185. });
  186. // Metrics Requests
  187. MockApiClient.addMockResponse({
  188. method: 'GET',
  189. url: `/organizations/org-slug/metrics/tags/`,
  190. body: [],
  191. });
  192. MockApiClient.addMockResponse({
  193. method: 'GET',
  194. url: `/organizations/org-slug/metrics/data/`,
  195. body: TestStubs.MetricsField({
  196. field: 'p75(sentry.transactions.measurements.lcp)',
  197. }),
  198. match: [
  199. MockApiClient.matchQuery({
  200. field: ['p75(sentry.transactions.measurements.lcp)'],
  201. }),
  202. ],
  203. });
  204. MockApiClient.addMockResponse({
  205. method: 'GET',
  206. url: `/organizations/org-slug/metrics/data/`,
  207. body: TestStubs.MetricsFieldByMeasurementRating({
  208. field: 'count(sentry.transactions.measurements.lcp)',
  209. }),
  210. match: [
  211. MockApiClient.matchQuery({
  212. groupBy: ['measurement_rating'],
  213. field: ['count(sentry.transactions.measurements.lcp)'],
  214. }),
  215. ],
  216. });
  217. MockApiClient.addMockResponse({
  218. method: 'GET',
  219. url: `/organizations/org-slug/metrics/data/`,
  220. body: TestStubs.MetricsField({
  221. field: 'p75(sentry.transactions.measurements.cls)',
  222. }),
  223. match: [
  224. MockApiClient.matchQuery({
  225. field: ['p75(sentry.transactions.measurements.cls)'],
  226. }),
  227. ],
  228. });
  229. MockApiClient.addMockResponse({
  230. method: 'GET',
  231. url: `/organizations/org-slug/metrics/data/`,
  232. body: TestStubs.MetricsFieldByMeasurementRating({
  233. field: 'count(sentry.transactions.measurements.cls)',
  234. }),
  235. match: [
  236. MockApiClient.matchQuery({
  237. groupBy: ['measurement_rating'],
  238. field: ['count(sentry.transactions.measurements.cls)'],
  239. }),
  240. ],
  241. });
  242. });
  243. afterEach(function () {
  244. MockApiClient.clearMockResponses();
  245. ProjectsStore.reset();
  246. });
  247. it('MetricsSwitch is visible if feature flag enabled', async () => {
  248. mountWithTheme(<TestComponent isMetricsData />, {
  249. context: routerContext,
  250. organization: {...org, features: [...org.features, 'metrics-performance-ui']},
  251. });
  252. expect(await screen.findByText('Metrics Data')).toBeInTheDocument();
  253. });
  254. it('renders basic UI elements', async function () {
  255. mountWithTheme(<TestComponent />, {
  256. context: routerContext,
  257. organization: org,
  258. });
  259. // It shows a search bar
  260. expect(await screen.findByLabelText('Search events')).toBeInTheDocument();
  261. // It shows the vital card
  262. expect(
  263. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 4500ms'))
  264. ).toBeInTheDocument();
  265. expect(screen.getByText('Good 50%')).toBeInTheDocument();
  266. expect(screen.getByText('Meh 33%')).toBeInTheDocument();
  267. expect(screen.getByText('Poor 17%')).toBeInTheDocument();
  268. // It shows a chart
  269. expect(screen.getByText('Duration p75')).toBeInTheDocument();
  270. // It shows a table
  271. expect(screen.getByText('something').closest('td')).toBeInTheDocument();
  272. });
  273. it('renders basic UI elements - metrics based', async function () {
  274. mountWithTheme(<TestComponent isMetricsData />, {
  275. context: routerContext,
  276. organization: {...org, features: [...org.features, 'metrics-performance-ui']},
  277. });
  278. // It shows a search bar
  279. expect(await screen.findByLabelText('Search events')).toBeInTheDocument();
  280. // It shows the vital card
  281. expect(
  282. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 534ms'))
  283. ).toBeInTheDocument();
  284. expect(screen.getByText('Good 28%')).toBeInTheDocument();
  285. expect(screen.getByText('Meh 40%')).toBeInTheDocument();
  286. expect(screen.getByText('Poor 32%')).toBeInTheDocument();
  287. // It shows a chart
  288. expect(screen.getByText('Duration p75')).toBeInTheDocument();
  289. // The table is still a TODO
  290. expect(screen.getByText('TODO')).toBeInTheDocument();
  291. });
  292. it('triggers a navigation on search', async function () {
  293. mountWithTheme(<TestComponent />, {
  294. context: routerContext,
  295. organization: org,
  296. });
  297. // Fill out the search box, and submit it.
  298. userEvent.type(
  299. await screen.findByLabelText('Search events'),
  300. 'user.email:uhoh*{enter}'
  301. );
  302. // Check the navigation.
  303. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  304. expect(browserHistory.push).toHaveBeenCalledWith({
  305. pathname: undefined,
  306. query: {
  307. project: 1,
  308. statsPeriod: '14d',
  309. query: 'user.email:uhoh*',
  310. },
  311. });
  312. });
  313. it('triggers a navigation on search - metrics based', async function () {
  314. mountWithTheme(<TestComponent isMetricsData />, {
  315. context: routerContext,
  316. organization: {...org, features: [...org.features, 'metrics-performance-ui']},
  317. });
  318. // Fill out the search box, and submit it.
  319. userEvent.type(
  320. await screen.findByLabelText('Search events'),
  321. 'user.email:uhoh*{enter}'
  322. );
  323. // Check the navigation.
  324. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  325. expect(browserHistory.push).toHaveBeenCalledWith({
  326. pathname: undefined,
  327. query: {
  328. project: 1,
  329. statsPeriod: '14d',
  330. query: 'user.email:uhoh*',
  331. },
  332. });
  333. });
  334. it('Applies conditions when linking to transaction summary', async function () {
  335. const newRouter = {
  336. ...router,
  337. location: {
  338. ...router.location,
  339. query: {
  340. query: 'sometag:value',
  341. },
  342. },
  343. };
  344. const context = TestStubs.routerContext([
  345. {
  346. organization,
  347. project,
  348. router: newRouter,
  349. location: newRouter.location,
  350. },
  351. ]);
  352. mountWithTheme(<TestComponent router={newRouter} />, {
  353. context,
  354. organization: org,
  355. });
  356. expect(
  357. await screen.findByRole('heading', {name: 'Largest Contentful Paint'})
  358. ).toBeInTheDocument();
  359. userEvent.click(
  360. screen.getByLabelText('See transaction summary of the transaction something')
  361. );
  362. expect(newRouter.push).toHaveBeenCalledWith({
  363. pathname: '/organizations/org-slug/performance/summary/',
  364. query: {
  365. transaction: 'something',
  366. project: undefined,
  367. environment: [],
  368. statsPeriod: '24h',
  369. start: undefined,
  370. end: undefined,
  371. query: 'sometag:value has:measurements.lcp',
  372. unselectedSeries: 'p100()',
  373. showTransactions: 'recent',
  374. display: 'vitals',
  375. trendFunction: undefined,
  376. trendColumn: undefined,
  377. },
  378. });
  379. });
  380. it('Check CLS', async function () {
  381. const newRouter = {
  382. ...router,
  383. location: {
  384. ...router.location,
  385. query: {
  386. query: 'anothertag:value',
  387. vitalName: 'measurements.cls',
  388. },
  389. },
  390. };
  391. const context = TestStubs.routerContext([
  392. {
  393. organization,
  394. project,
  395. router: newRouter,
  396. location: newRouter.location,
  397. },
  398. ]);
  399. mountWithTheme(<TestComponent router={newRouter} />, {
  400. context,
  401. organization: org,
  402. });
  403. expect(await screen.findByText('Cumulative Layout Shift')).toBeInTheDocument();
  404. userEvent.click(
  405. screen.getByLabelText('See transaction summary of the transaction something')
  406. );
  407. expect(newRouter.push).toHaveBeenCalledWith({
  408. pathname: '/organizations/org-slug/performance/summary/',
  409. query: {
  410. transaction: 'something',
  411. project: undefined,
  412. environment: [],
  413. statsPeriod: '24h',
  414. start: undefined,
  415. end: undefined,
  416. query: 'anothertag:value has:measurements.cls',
  417. unselectedSeries: 'p100()',
  418. showTransactions: 'recent',
  419. display: 'vitals',
  420. trendFunction: undefined,
  421. trendColumn: undefined,
  422. },
  423. });
  424. // Check cells are not in ms
  425. expect(screen.getByText('0.215').closest('td')).toBeInTheDocument();
  426. });
  427. it('Check CLS - metrics based', async function () {
  428. const newRouter = {
  429. ...router,
  430. location: {
  431. ...router.location,
  432. query: {
  433. project: 1,
  434. query: 'anothertag:value',
  435. vitalName: 'measurements.cls',
  436. },
  437. },
  438. };
  439. const context = TestStubs.routerContext([
  440. {
  441. organization,
  442. project,
  443. router: newRouter,
  444. location: newRouter.location,
  445. },
  446. ]);
  447. mountWithTheme(<TestComponent router={newRouter} isMetricsData />, {
  448. context,
  449. organization: {...org, features: [...org.features, 'metrics-performance-ui']},
  450. });
  451. expect(await screen.findByText('Cumulative Layout Shift')).toBeInTheDocument();
  452. expect(
  453. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 534.30'))
  454. ).toBeInTheDocument();
  455. // The table is still a TODO
  456. expect(screen.getByText('TODO')).toBeInTheDocument();
  457. });
  458. it('Pagination links exist to switch between vitals', async function () {
  459. const newRouter = {
  460. ...router,
  461. location: {
  462. ...router.location,
  463. query: {
  464. project: 1,
  465. query: 'tag:value',
  466. },
  467. },
  468. };
  469. const context = TestStubs.routerContext([
  470. {
  471. organization,
  472. project,
  473. router: newRouter,
  474. location: newRouter.location,
  475. },
  476. ]);
  477. mountWithTheme(<TestComponent router={newRouter} />, {
  478. context,
  479. organization: org,
  480. });
  481. expect(await screen.findByLabelText('Previous')).toBeInTheDocument();
  482. userEvent.click(screen.getByLabelText('Previous'));
  483. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  484. expect(browserHistory.push).toHaveBeenCalledWith({
  485. pathname: undefined,
  486. query: {
  487. project: 1,
  488. query: 'tag:value',
  489. vitalName: 'measurements.fcp',
  490. },
  491. });
  492. });
  493. it('Check LCP vital renders correctly', async function () {
  494. mountWithTheme(<TestComponent />, {
  495. context: routerContext,
  496. organization: org,
  497. });
  498. expect(await screen.findByText('Largest Contentful Paint')).toBeInTheDocument();
  499. expect(
  500. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 4500ms'))
  501. ).toBeInTheDocument();
  502. expect(screen.getByText('4.50s').closest('td')).toBeInTheDocument();
  503. });
  504. it('Check LCP vital renders correctly - Metrics based', async function () {
  505. const newRouter = {
  506. ...router,
  507. location: {
  508. ...router.location,
  509. query: {
  510. project: 1,
  511. query: 'tag:value',
  512. },
  513. },
  514. };
  515. const context = TestStubs.routerContext([
  516. {
  517. organization,
  518. project,
  519. router: newRouter,
  520. location: newRouter.location,
  521. },
  522. ]);
  523. mountWithTheme(<TestComponent router={newRouter} isMetricsData />, {
  524. context,
  525. organization: {...org, features: [...org.features, 'metrics-performance-ui']},
  526. });
  527. expect(await screen.findByText('Largest Contentful Paint')).toBeInTheDocument();
  528. expect(
  529. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 534ms'))
  530. ).toBeInTheDocument();
  531. // The table is still a TODO
  532. expect(screen.getByText('TODO')).toBeInTheDocument();
  533. });
  534. it('correctly renders which browsers support LCP', async function () {
  535. mountWithTheme(<TestComponent />, {
  536. context: routerContext,
  537. organization: org,
  538. });
  539. testSupportedBrowserRendering(WebVital.LCP);
  540. });
  541. it('correctly renders which browsers support CLS', async function () {
  542. const newRouter = {
  543. ...router,
  544. location: {
  545. ...router.location,
  546. query: {
  547. vitalName: 'measurements.cls',
  548. },
  549. },
  550. };
  551. mountWithTheme(<TestComponent router={newRouter} />, {
  552. context: routerContext,
  553. organization: org,
  554. });
  555. testSupportedBrowserRendering(WebVital.CLS);
  556. });
  557. it('correctly renders which browsers support FCP', async function () {
  558. const newRouter = {
  559. ...router,
  560. location: {
  561. ...router.location,
  562. query: {
  563. vitalName: 'measurements.fcp',
  564. },
  565. },
  566. };
  567. mountWithTheme(<TestComponent router={newRouter} />, {
  568. context: routerContext,
  569. organization: org,
  570. });
  571. testSupportedBrowserRendering(WebVital.FCP);
  572. });
  573. it('correctly renders which browsers support FID', async function () {
  574. const newRouter = {
  575. ...router,
  576. location: {
  577. ...router.location,
  578. query: {
  579. vitalName: 'measurements.fid',
  580. },
  581. },
  582. };
  583. mountWithTheme(<TestComponent router={newRouter} />, {
  584. context: routerContext,
  585. organization: org,
  586. });
  587. testSupportedBrowserRendering(WebVital.FID);
  588. });
  589. });