index.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import type {InjectedRouter} from 'react-router';
  2. import {MetricsFieldFixture} from 'sentry-fixture/metrics';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {ProjectFixture} from 'sentry-fixture/project';
  5. import {RouterContextFixture} from 'sentry-fixture/routerContextFixture';
  6. import {initializeOrg} from 'sentry-test/initializeOrg';
  7. import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  8. import {textWithMarkupMatcher} from 'sentry-test/utils';
  9. import ProjectsStore from 'sentry/stores/projectsStore';
  10. import TeamStore from 'sentry/stores/teamStore';
  11. import {browserHistory} from 'sentry/utils/browserHistory';
  12. import {WebVital} from 'sentry/utils/fields';
  13. import {Browser} from 'sentry/utils/performance/vitals/constants';
  14. import {DEFAULT_STATS_PERIOD} from 'sentry/views/performance/data';
  15. import VitalDetail from 'sentry/views/performance/vitalDetail';
  16. import {vitalSupportedBrowsers} from 'sentry/views/performance/vitalDetail/utils';
  17. const api = new MockApiClient();
  18. const organization = OrganizationFixture({
  19. features: ['discover-basic', 'performance-view'],
  20. projects: [ProjectFixture()],
  21. });
  22. const {
  23. routerContext,
  24. organization: org,
  25. router,
  26. project,
  27. } = initializeOrg({
  28. organization,
  29. router: {
  30. location: {
  31. query: {
  32. project: '1',
  33. },
  34. },
  35. },
  36. });
  37. function TestComponent(props: {router?: InjectedRouter} = {}) {
  38. return (
  39. <VitalDetail
  40. api={api}
  41. location={props.router?.location ?? router.location}
  42. router={props.router ?? router}
  43. params={{}}
  44. route={{}}
  45. routes={[]}
  46. routeParams={{}}
  47. />
  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/${organization.slug}/projects/`,
  69. body: [],
  70. });
  71. MockApiClient.addMockResponse({
  72. url: `/organizations/${organization.slug}/tags/`,
  73. body: [],
  74. });
  75. MockApiClient.addMockResponse({
  76. url: `/organizations/${organization.slug}/events-stats/`,
  77. body: {data: [[123, []]]},
  78. });
  79. MockApiClient.addMockResponse({
  80. url: `/organizations/${organization.slug}/tags/user.email/values/`,
  81. body: [],
  82. });
  83. MockApiClient.addMockResponse({
  84. url: `/organizations/${organization.slug}/releases/stats/`,
  85. body: [],
  86. });
  87. MockApiClient.addMockResponse({
  88. url: `/organizations/${organization.slug}/users/`,
  89. body: [],
  90. });
  91. MockApiClient.addMockResponse({
  92. url: `/organizations/${organization.slug}/recent-searches/`,
  93. body: [],
  94. });
  95. MockApiClient.addMockResponse({
  96. url: `/organizations/${organization.slug}/recent-searches/`,
  97. method: 'POST',
  98. body: [],
  99. });
  100. MockApiClient.addMockResponse({
  101. url: `/organizations/${organization.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/${organization.slug}/events/`,
  114. body: {
  115. meta: {
  116. fields: {
  117. 'count()': 'integer',
  118. 'p95(measurements.lcp)': 'duration',
  119. transaction: 'string',
  120. 'p50(measurements.lcp)': 'duration',
  121. project: 'string',
  122. 'compare_numeric_aggregate(p75_measurements_lcp,greater,4000)': 'number',
  123. 'project.id': 'integer',
  124. 'count_unique_user()': 'integer',
  125. 'p75(measurements.lcp)': 'duration',
  126. },
  127. },
  128. data: [
  129. {
  130. 'count()': 100000,
  131. 'p95(measurements.lcp)': 5000,
  132. transaction: 'something',
  133. 'p50(measurements.lcp)': 3500,
  134. project: 'javascript',
  135. 'compare_numeric_aggregate(p75_measurements_lcp,greater,4000)': 1,
  136. 'count_unique_user()': 10000,
  137. 'p75(measurements.lcp)': 4500,
  138. },
  139. ],
  140. },
  141. match: [
  142. (_url, options) => {
  143. return options.query?.field?.find(f => f === 'p50(measurements.lcp)');
  144. },
  145. ],
  146. });
  147. MockApiClient.addMockResponse({
  148. url: `/organizations/${organization.slug}/events/`,
  149. body: {
  150. meta: {
  151. fields: {
  152. 'compare_numeric_aggregate(p75_measurements_cls,greater,0.1)': 'number',
  153. 'compare_numeric_aggregate(p75_measurements_cls,greater,0.25)': 'number',
  154. 'count()': 'integer',
  155. 'count_unique_user()': 'integer',
  156. team_key_transaction: 'boolean',
  157. 'p50(measurements.cls)': 'number',
  158. 'p75(measurements.cls)': 'number',
  159. 'p95(measurements.cls)': 'number',
  160. project: 'string',
  161. transaction: 'string',
  162. },
  163. },
  164. data: [
  165. {
  166. 'compare_numeric_aggregate(p75_measurements_cls,greater,0.1)': 1,
  167. 'compare_numeric_aggregate(p75_measurements_cls,greater,0.25)': 0,
  168. 'count()': 10000,
  169. 'count_unique_user()': 2740,
  170. team_key_transaction: 1,
  171. 'p50(measurements.cls)': 0.143,
  172. 'p75(measurements.cls)': 0.215,
  173. 'p95(measurements.cls)': 0.302,
  174. project: 'javascript',
  175. transaction: 'something',
  176. },
  177. ],
  178. },
  179. match: [
  180. (_url, options) => {
  181. return options.query?.field?.find(f => f === 'p50(measurements.cls)');
  182. },
  183. ],
  184. });
  185. MockApiClient.addMockResponse({
  186. method: 'GET',
  187. url: `/organizations/${organization.slug}/key-transactions-list/`,
  188. body: [],
  189. });
  190. // Metrics Requests
  191. MockApiClient.addMockResponse({
  192. method: 'GET',
  193. url: `/organizations/${organization.slug}/metrics/tags/`,
  194. body: [],
  195. });
  196. MockApiClient.addMockResponse({
  197. method: 'GET',
  198. url: `/organizations/${organization.slug}/metrics/data/`,
  199. body: MetricsFieldFixture('p75(sentry.transactions.measurements.lcp)'),
  200. match: [
  201. MockApiClient.matchQuery({
  202. field: ['p75(sentry.transactions.measurements.lcp)'],
  203. }),
  204. ],
  205. });
  206. });
  207. afterEach(function () {
  208. MockApiClient.clearMockResponses();
  209. ProjectsStore.reset();
  210. });
  211. it('renders basic UI elements', async function () {
  212. render(<TestComponent />, {
  213. context: routerContext,
  214. organization: org,
  215. });
  216. // It shows a search bar
  217. expect(await screen.findByLabelText('Search events')).toBeInTheDocument();
  218. // It shows the vital card
  219. expect(
  220. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 4500ms'))
  221. ).toBeInTheDocument();
  222. expect(screen.getByText('Good 50%', {exact: false})).toBeInTheDocument();
  223. expect(screen.getByText('Meh 33%', {exact: false})).toBeInTheDocument();
  224. expect(screen.getByText('Poor 17%', {exact: false})).toBeInTheDocument();
  225. // It shows a chart
  226. expect(screen.getByText('Duration p75')).toBeInTheDocument();
  227. // It shows a table
  228. expect(screen.getByText('something').closest('td')).toBeInTheDocument();
  229. });
  230. it('triggers a navigation on search', async function () {
  231. render(<TestComponent />, {
  232. context: routerContext,
  233. organization: org,
  234. });
  235. // Fill out the search box, and submit it.
  236. await userEvent.click(await screen.findByLabelText('Search events'));
  237. await userEvent.paste('user.email:uhoh*');
  238. await userEvent.keyboard('{enter}');
  239. // Check the navigation.
  240. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  241. expect(browserHistory.push).toHaveBeenCalledWith({
  242. pathname: undefined,
  243. query: {
  244. project: '1',
  245. statsPeriod: '14d',
  246. query: 'user.email:uhoh*',
  247. },
  248. });
  249. });
  250. it('applies conditions when linking to transaction summary', async function () {
  251. const newRouter = {
  252. ...router,
  253. location: {
  254. ...router.location,
  255. query: {
  256. query: 'sometag:value',
  257. },
  258. },
  259. };
  260. const context = RouterContextFixture([
  261. {
  262. project,
  263. router: newRouter,
  264. location: newRouter.location,
  265. },
  266. ]);
  267. render(<TestComponent router={newRouter} />, {
  268. context,
  269. organization: org,
  270. });
  271. expect(
  272. await screen.findByRole('heading', {name: 'Largest Contentful Paint'})
  273. ).toBeInTheDocument();
  274. await userEvent.click(
  275. screen.getByLabelText('See transaction summary of the transaction something')
  276. );
  277. expect(newRouter.push).toHaveBeenCalledWith({
  278. pathname: `/organizations/${organization.slug}/performance/summary/`,
  279. query: {
  280. transaction: 'something',
  281. project: undefined,
  282. environment: [],
  283. statsPeriod: DEFAULT_STATS_PERIOD,
  284. start: undefined,
  285. end: undefined,
  286. query: 'sometag:value has:measurements.lcp',
  287. referrer: 'performance-transaction-summary',
  288. unselectedSeries: ['p100()', 'avg()'],
  289. showTransactions: 'recent',
  290. display: 'vitals',
  291. trendFunction: undefined,
  292. trendColumn: undefined,
  293. },
  294. });
  295. });
  296. it('check CLS', async function () {
  297. const newRouter = {
  298. ...router,
  299. location: {
  300. ...router.location,
  301. query: {
  302. query: 'anothertag:value',
  303. vitalName: 'measurements.cls',
  304. },
  305. },
  306. };
  307. const context = RouterContextFixture([
  308. {
  309. project,
  310. router: newRouter,
  311. location: newRouter.location,
  312. },
  313. ]);
  314. render(<TestComponent router={newRouter} />, {
  315. context,
  316. organization: org,
  317. });
  318. expect(await screen.findByText('Cumulative Layout Shift')).toBeInTheDocument();
  319. await userEvent.click(
  320. screen.getByLabelText('See transaction summary of the transaction something')
  321. );
  322. expect(newRouter.push).toHaveBeenCalledWith({
  323. pathname: `/organizations/${organization.slug}/performance/summary/`,
  324. query: {
  325. transaction: 'something',
  326. project: undefined,
  327. environment: [],
  328. statsPeriod: DEFAULT_STATS_PERIOD,
  329. start: undefined,
  330. end: undefined,
  331. query: 'anothertag:value has:measurements.cls',
  332. referrer: 'performance-transaction-summary',
  333. unselectedSeries: ['p100()', 'avg()'],
  334. showTransactions: 'recent',
  335. display: 'vitals',
  336. trendFunction: undefined,
  337. trendColumn: undefined,
  338. },
  339. });
  340. // Check cells are not in ms
  341. expect(screen.getByText('0.215').closest('td')).toBeInTheDocument();
  342. });
  343. it('can switch vitals with dropdown menu', async function () {
  344. const newRouter = {
  345. ...router,
  346. location: {
  347. ...router.location,
  348. query: {
  349. project: 1,
  350. query: 'tag:value',
  351. },
  352. },
  353. };
  354. const context = RouterContextFixture([
  355. {
  356. project,
  357. router: newRouter,
  358. location: newRouter.location,
  359. },
  360. ]);
  361. render(<TestComponent router={newRouter} />, {
  362. context,
  363. organization: org,
  364. });
  365. const button = screen.getByRole('button', {name: /web vitals: lcp/i});
  366. expect(button).toBeInTheDocument();
  367. await userEvent.click(button);
  368. const menuItem = screen.getByRole('menuitemradio', {name: /fcp/i});
  369. expect(menuItem).toBeInTheDocument();
  370. await userEvent.click(menuItem);
  371. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  372. expect(browserHistory.push).toHaveBeenCalledWith({
  373. pathname: undefined,
  374. query: {
  375. project: 1,
  376. query: 'tag:value',
  377. vitalName: 'measurements.fcp',
  378. },
  379. });
  380. });
  381. it('renders LCP vital correctly', async function () {
  382. render(<TestComponent />, {
  383. context: routerContext,
  384. organization: org,
  385. });
  386. expect(await screen.findByText('Largest Contentful Paint')).toBeInTheDocument();
  387. expect(
  388. screen.getByText(textWithMarkupMatcher('The p75 for all transactions is 4500ms'))
  389. ).toBeInTheDocument();
  390. expect(screen.getByText('4.50s').closest('td')).toBeInTheDocument();
  391. });
  392. it('correctly renders which browsers support LCP', async function () {
  393. render(<TestComponent />, {
  394. context: routerContext,
  395. organization: org,
  396. });
  397. expect(await screen.findAllByText(/Largest Contentful Paint/)).toHaveLength(2);
  398. testSupportedBrowserRendering(WebVital.LCP);
  399. });
  400. it('correctly renders which browsers support CLS', async function () {
  401. const newRouter = {
  402. ...router,
  403. location: {
  404. ...router.location,
  405. query: {
  406. vitalName: 'measurements.cls',
  407. },
  408. },
  409. };
  410. render(<TestComponent router={newRouter} />, {
  411. context: routerContext,
  412. organization: org,
  413. });
  414. expect(await screen.findAllByText(/Cumulative Layout Shift/)).toHaveLength(2);
  415. testSupportedBrowserRendering(WebVital.CLS);
  416. });
  417. it('correctly renders which browsers support FCP', async function () {
  418. const newRouter = {
  419. ...router,
  420. location: {
  421. ...router.location,
  422. query: {
  423. vitalName: 'measurements.fcp',
  424. },
  425. },
  426. };
  427. MockApiClient.addMockResponse({
  428. url: `/organizations/${organization.slug}/events/`,
  429. body: [],
  430. });
  431. render(<TestComponent router={newRouter} />, {
  432. context: routerContext,
  433. organization: org,
  434. });
  435. expect(await screen.findAllByText(/First Contentful Paint/)).toHaveLength(2);
  436. testSupportedBrowserRendering(WebVital.FCP);
  437. });
  438. it('correctly renders which browsers support FID', async function () {
  439. const newRouter = {
  440. ...router,
  441. location: {
  442. ...router.location,
  443. query: {
  444. vitalName: 'measurements.fid',
  445. },
  446. },
  447. };
  448. MockApiClient.addMockResponse({
  449. url: `/organizations/${organization.slug}/events/`,
  450. body: [],
  451. });
  452. render(<TestComponent router={newRouter} />, {
  453. context: routerContext,
  454. organization: org,
  455. });
  456. expect(await screen.findAllByText(/First Input Delay/)).toHaveLength(2);
  457. testSupportedBrowserRendering(WebVital.FID);
  458. });
  459. });