transactionEvents.spec.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {act} from 'sentry-test/reactTestingLibrary';
  4. import {t} from 'sentry/locale';
  5. import ProjectsStore from 'sentry/stores/projectsStore';
  6. import {Organization} from 'sentry/types';
  7. import {WebVital} from 'sentry/utils/fields';
  8. import {OrganizationContext} from 'sentry/views/organizationContext';
  9. import TransactionEvents from 'sentry/views/performance/transactionSummary/transactionEvents';
  10. type Data = {
  11. features?: string[];
  12. query?: {
  13. webVital?: WebVital;
  14. };
  15. };
  16. function initializeData({features: additionalFeatures = [], query = {}}: Data = {}) {
  17. const features = ['discover-basic', 'performance-view', ...additionalFeatures];
  18. const organization = TestStubs.Organization({
  19. features,
  20. projects: [TestStubs.Project()],
  21. apdexThreshold: 400,
  22. });
  23. const initialData = initializeOrg({
  24. organization,
  25. router: {
  26. location: {
  27. query: {
  28. transaction: '/performance',
  29. project: '1',
  30. transactionCursor: '1:0:0',
  31. ...query,
  32. },
  33. },
  34. },
  35. project: 1,
  36. projects: [],
  37. });
  38. act(() => ProjectsStore.loadInitialData(initialData.organization.projects));
  39. return initialData;
  40. }
  41. const WrappedComponent = ({
  42. organization,
  43. ...props
  44. }: Omit<React.ComponentProps<typeof TransactionEvents>, 'organization'> & {
  45. organization: Organization;
  46. }) => {
  47. return (
  48. <OrganizationContext.Provider value={organization}>
  49. <TransactionEvents organization={organization} {...props} />
  50. </OrganizationContext.Provider>
  51. );
  52. };
  53. describe('Performance > TransactionSummary', function () {
  54. enforceActOnUseLegacyStoreHook();
  55. beforeEach(function () {
  56. // @ts-ignore no-console
  57. // eslint-disable-next-line no-console
  58. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  59. MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/projects/',
  61. body: [],
  62. });
  63. MockApiClient.addMockResponse({
  64. url: '/prompts-activity/',
  65. body: {},
  66. });
  67. MockApiClient.addMockResponse({
  68. url: '/organizations/org-slug/sdk-updates/',
  69. body: [],
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/events/',
  73. body: {
  74. data: [
  75. {
  76. 'p100()': 9502,
  77. 'p99()': 9285.7,
  78. 'p95()': 7273.6,
  79. 'p75()': 3639.5,
  80. 'p50()': 755.5,
  81. },
  82. ],
  83. meta: {
  84. fields: {
  85. 'p100()': 'duration',
  86. 'p99()': 'duration',
  87. 'p95()': 'duration',
  88. 'p75()': 'duration',
  89. 'p50()': 'duration',
  90. },
  91. },
  92. },
  93. match: [
  94. (_, options) => {
  95. return options.query?.field?.includes('p95()');
  96. },
  97. ],
  98. });
  99. // Transaction list response
  100. MockApiClient.addMockResponse({
  101. url: '/organizations/org-slug/events/',
  102. headers: {
  103. Link:
  104. '<http://localhost/api/0/organizations/org-slug/events/?cursor=2:0:0>; rel="next"; results="true"; cursor="2:0:0",' +
  105. '<http://localhost/api/0/organizations/org-slug/events/?cursor=1:0:0>; rel="previous"; results="false"; cursor="1:0:0"',
  106. },
  107. body: {
  108. meta: {
  109. fields: {
  110. id: 'string',
  111. 'user.display': 'string',
  112. 'transaction.duration': 'duration',
  113. 'project.id': 'integer',
  114. timestamp: 'date',
  115. },
  116. },
  117. data: [
  118. {
  119. id: 'deadbeef',
  120. 'user.display': 'uhoh@example.com',
  121. 'transaction.duration': 400,
  122. 'project.id': 1,
  123. timestamp: '2020-05-21T15:31:18+00:00',
  124. trace: '1234',
  125. 'measurements.lcp': 200,
  126. },
  127. {
  128. id: 'moredeadbeef',
  129. 'user.display': 'moreuhoh@example.com',
  130. 'transaction.duration': 600,
  131. 'project.id': 1,
  132. timestamp: '2020-05-22T15:31:18+00:00',
  133. trace: '4321',
  134. 'measurements.lcp': 300,
  135. },
  136. ],
  137. },
  138. match: [
  139. (_url, options) => {
  140. return options.query?.field?.includes('user.display');
  141. },
  142. ],
  143. });
  144. MockApiClient.addMockResponse({
  145. url: '/organizations/org-slug/events/',
  146. body: {
  147. data: [{'count()': 5161}],
  148. },
  149. match: [
  150. (_url, options) => {
  151. return options.query?.field?.includes('count()');
  152. },
  153. ],
  154. });
  155. MockApiClient.addMockResponse({
  156. url: '/organizations/org-slug/events-has-measurements/',
  157. body: {measurements: false},
  158. });
  159. });
  160. afterEach(function () {
  161. MockApiClient.clearMockResponses();
  162. // @ts-ignore no-console
  163. // eslint-disable-next-line no-console
  164. console.error.mockRestore();
  165. act(() => ProjectsStore.reset());
  166. jest.clearAllMocks();
  167. });
  168. it('renders basic UI elements', async function () {
  169. const initialData = initializeData();
  170. const wrapper = mountWithTheme(
  171. <WrappedComponent
  172. organization={initialData.organization}
  173. location={initialData.router.location}
  174. />,
  175. initialData.routerContext
  176. );
  177. await tick();
  178. wrapper.update();
  179. expect(
  180. wrapper.find('NavTabs').find({children: 'All Events'}).find('Link')
  181. ).toHaveLength(1);
  182. expect(wrapper.find('SentryDocumentTitle')).toHaveLength(1);
  183. expect(wrapper.find('SearchBar')).toHaveLength(1);
  184. expect(wrapper.find('GridEditable')).toHaveLength(1);
  185. expect(wrapper.find('Pagination')).toHaveLength(1);
  186. expect(wrapper.find('EventsContent')).toHaveLength(1);
  187. expect(wrapper.find('TransactionHeader')).toHaveLength(1);
  188. });
  189. it('renders relative span breakdown header when no filter selected', async function () {
  190. const initialData = initializeData();
  191. const wrapper = mountWithTheme(
  192. <WrappedComponent
  193. organization={initialData.organization}
  194. location={initialData.router.location}
  195. />,
  196. initialData.routerContext
  197. );
  198. await tick();
  199. wrapper.update();
  200. expect(wrapper.find('GridHeadCell')).toHaveLength(6);
  201. expect(
  202. wrapper.find('OperationTitle').children().children().children().at(0).html()
  203. ).toEqual(t('operation duration'));
  204. });
  205. it('renders event column results correctly', async function () {
  206. const initialData = initializeData();
  207. const wrapper = mountWithTheme(
  208. <WrappedComponent
  209. organization={initialData.organization}
  210. location={initialData.router.location}
  211. />,
  212. initialData.routerContext
  213. );
  214. await tick();
  215. wrapper.update();
  216. function keyAt(index) {
  217. return wrapper.find('CellAction').at(index).props().column.key;
  218. }
  219. function valueAt(index, element = 'div') {
  220. return wrapper.find('CellAction').at(index).find(element).last().children().html();
  221. }
  222. expect(wrapper.find('CellAction')).toHaveLength(12);
  223. expect(keyAt(0)).toEqual('id');
  224. expect(valueAt(0)).toEqual('deadbeef');
  225. expect(keyAt(1)).toEqual('user.display');
  226. expect(valueAt(1, 'span')).toEqual('uhoh@example.com');
  227. expect(keyAt(2)).toEqual('span_ops_breakdown.relative');
  228. expect(valueAt(2, 'span')).toEqual('(no value)');
  229. expect(keyAt(3)).toEqual('transaction.duration');
  230. expect(valueAt(3, 'span')).toEqual('400.00ms');
  231. expect(keyAt(4)).toEqual('trace');
  232. expect(valueAt(4)).toEqual('1234');
  233. expect(keyAt(5)).toEqual('timestamp');
  234. expect(valueAt(5, 'time')).toEqual('May 21, 2020 3:31:18 PM UTC');
  235. });
  236. it('renders additional Web Vital column', async function () {
  237. const initialData = initializeData({
  238. query: {webVital: WebVital.LCP},
  239. });
  240. const wrapper = mountWithTheme(
  241. <WrappedComponent
  242. organization={initialData.organization}
  243. location={initialData.router.location}
  244. />,
  245. initialData.routerContext
  246. );
  247. await tick();
  248. wrapper.update();
  249. function keyAt(index) {
  250. return wrapper.find('CellAction').at(index).props().column.key;
  251. }
  252. function valueAt(index, element = 'div') {
  253. return wrapper.find('CellAction').at(index).find(element).last().children().html();
  254. }
  255. expect(wrapper.find('CellAction')).toHaveLength(14);
  256. expect(keyAt(0)).toEqual('id');
  257. expect(valueAt(0)).toEqual('deadbeef');
  258. expect(keyAt(1)).toEqual('user.display');
  259. expect(valueAt(1, 'span')).toEqual('uhoh@example.com');
  260. expect(keyAt(2)).toEqual('span_ops_breakdown.relative');
  261. expect(valueAt(2, 'span')).toEqual('(no value)');
  262. expect(keyAt(3)).toEqual('measurements.lcp');
  263. expect(valueAt(3)).toEqual('200');
  264. expect(keyAt(4)).toEqual('transaction.duration');
  265. expect(valueAt(4, 'span')).toEqual('400.00ms');
  266. expect(keyAt(5)).toEqual('trace');
  267. expect(valueAt(5)).toEqual('1234');
  268. expect(keyAt(6)).toEqual('timestamp');
  269. expect(valueAt(6, 'time')).toEqual('May 21, 2020 3:31:18 PM UTC');
  270. });
  271. });