fieldRenderers.spec.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {act, fireEvent, render, screen} from 'sentry-test/reactTestingLibrary';
  5. import ProjectsStore from 'sentry/stores/projectsStore';
  6. import type {Project} from 'sentry/types/project';
  7. import type {Field} from 'sentry/views/traces/data';
  8. import {
  9. ProjectRenderer,
  10. ProjectsRenderer,
  11. SpanDescriptionRenderer,
  12. SpanIdRenderer,
  13. TraceIdRenderer,
  14. TraceIssuesRenderer,
  15. TransactionRenderer,
  16. } from 'sentry/views/traces/fieldRenderers';
  17. import type {SpanResult} from 'sentry/views/traces/hooks/useTraceSpans';
  18. describe('Renderers', function () {
  19. let context: ReturnType<typeof initializeOrg>;
  20. const organization = OrganizationFixture({
  21. features: ['trace-view-v1'], // only testing against new trace view
  22. });
  23. const projects = [
  24. ProjectFixture({
  25. id: '1',
  26. slug: 'project-1',
  27. name: 'Project 1',
  28. }),
  29. ProjectFixture({
  30. id: '2',
  31. slug: 'project-2',
  32. name: 'Project 2',
  33. }),
  34. ProjectFixture({
  35. id: '3',
  36. slug: 'project-3',
  37. name: 'Project 3',
  38. }),
  39. ];
  40. function makeSpan(
  41. project: Project,
  42. span?: Partial<SpanResult<Field>>
  43. ): SpanResult<Field> {
  44. return {
  45. project: project.slug,
  46. 'transaction.id': '00000000000000000000000000000000',
  47. id: '11111111111111111111111111111111',
  48. timestamp: '2024-07-03T10:15:00',
  49. 'sdk.name': 'sentry.python',
  50. 'span.op': 'op',
  51. 'span.description': 'description',
  52. 'span.duration': 123.456,
  53. 'span.status': 'internal_error',
  54. 'span.self_time': 12.34,
  55. 'precise.start_ts': 1720015976.544,
  56. 'precise.finish_ts': 1720016100.0,
  57. is_transaction: 1,
  58. ...span,
  59. };
  60. }
  61. beforeEach(function () {
  62. context = initializeOrg({organization, projects});
  63. act(() => ProjectsStore.loadInitialData(projects));
  64. MockApiClient.addMockResponse({
  65. url: `/organizations/${organization.slug}/projects/`,
  66. body: projects,
  67. });
  68. });
  69. describe('SpanDescriptionRenderer', function () {
  70. it('renders op then description', function () {
  71. const span = makeSpan(projects[0]);
  72. render(<SpanDescriptionRenderer span={span} />);
  73. const description = screen.getByTestId('span-description');
  74. expect(description).toBeInTheDocument();
  75. expect(description).toHaveTextContent('op\u2014descriptioninternal_error');
  76. });
  77. it.each(['unknown', 'foobar'])(
  78. 'does not render span status %s',
  79. function (spanStatus) {
  80. const span = makeSpan(projects[0], {'span.status': spanStatus});
  81. render(<SpanDescriptionRenderer span={span} />);
  82. const description = screen.getByTestId('span-description');
  83. expect(description).toBeInTheDocument();
  84. expect(description).toHaveTextContent('op\u2014description');
  85. }
  86. );
  87. it.each(['ok', 'internal_error'])('renders span status %s', function (spanStatus) {
  88. const span = makeSpan(projects[0], {'span.status': spanStatus});
  89. render(<SpanDescriptionRenderer span={span} />);
  90. const description = screen.getByTestId('span-description');
  91. expect(description).toBeInTheDocument();
  92. expect(description).toHaveTextContent(`op\u2014description${spanStatus}`);
  93. });
  94. });
  95. describe('ProjectsRenderer', function () {
  96. it('renders one project', function () {
  97. render(<ProjectsRenderer projectSlugs={[projects[0].slug]} />, context);
  98. expect(screen.getAllByRole('img')).toHaveLength(1);
  99. });
  100. it('renders two projects', function () {
  101. render(
  102. <ProjectsRenderer projectSlugs={[projects[0].slug, projects[1].slug]} />,
  103. context
  104. );
  105. expect(screen.getAllByRole('img')).toHaveLength(2);
  106. });
  107. it('renders three projects', function () {
  108. render(<ProjectsRenderer projectSlugs={projects.map(p => p.slug)} />, context);
  109. expect(screen.getAllByRole('img')).toHaveLength(1);
  110. const collapsed = screen.getByTestId('collapsed-projects-badge');
  111. expect(collapsed).toBeInTheDocument();
  112. expect(collapsed).toHaveTextContent('+2');
  113. });
  114. });
  115. describe('ProjectRenderer', function () {
  116. it('renders project badge with name', function () {
  117. render(<ProjectRenderer projectSlug={projects[0].slug} />);
  118. expect(screen.getByRole('img')).toBeInTheDocument();
  119. expect(screen.getByText(projects[0].slug)).toBeInTheDocument();
  120. });
  121. });
  122. describe('SpanIdRenderer', function () {
  123. it('renders span id with link', function () {
  124. const onClickHandler = jest.fn();
  125. const traceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
  126. const span = makeSpan(projects[0]);
  127. render(
  128. <SpanIdRenderer
  129. projectSlug={span.project}
  130. spanId={span.id}
  131. timestamp={span.timestamp}
  132. traceId={traceId}
  133. transactionId={span['transaction.id']}
  134. onClick={onClickHandler}
  135. />,
  136. context
  137. );
  138. const link = screen.getByText('11111111');
  139. expect(link).toBeInTheDocument();
  140. expect(link).toHaveAttribute(
  141. 'href',
  142. `/organizations/${organization.slug}/performance/trace/${traceId}/?eventId=${span['transaction.id']}&node=span-${span.id}&node=txn-${span['transaction.id']}&source=traces&statsPeriod=14d&timestamp=1720016100`
  143. );
  144. expect(onClickHandler).toHaveBeenCalledTimes(0);
  145. fireEvent.click(link);
  146. expect(onClickHandler).toHaveBeenCalledTimes(1);
  147. });
  148. });
  149. describe('TraceIdRenderer', function () {
  150. it('renders trace id with link', function () {
  151. const onClickHandler = jest.fn();
  152. const traceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
  153. render(
  154. <TraceIdRenderer
  155. location={context.router.location}
  156. timestamp={1720016100000}
  157. traceId={traceId}
  158. onClick={onClickHandler}
  159. />,
  160. context
  161. );
  162. const link = screen.getByText('aaaaaaaa');
  163. expect(link).toBeInTheDocument();
  164. expect(link).toHaveAttribute(
  165. 'href',
  166. `/organizations/${organization.slug}/performance/trace/${traceId}/?pageEnd&pageStart&source=traces&statsPeriod=14d&timestamp=1720016100`
  167. );
  168. expect(onClickHandler).toHaveBeenCalledTimes(0);
  169. fireEvent.click(link);
  170. expect(onClickHandler).toHaveBeenCalledTimes(1);
  171. });
  172. });
  173. describe('TransactionRenderer', function () {
  174. it('renders transaction with link', function () {
  175. render(<TransactionRenderer projectSlug={projects[0].slug} transaction="foobar" />);
  176. const link = screen.getByText('foobar');
  177. expect(link).toBeInTheDocument();
  178. expect(link).toHaveAttribute(
  179. 'href',
  180. `/organizations/${organization.slug}/performance/summary/?project=${projects[0].id}&referrer=performance-transaction-summary&transaction=foobar&unselectedSeries=p100%28%29&unselectedSeries=avg%28%29`
  181. );
  182. });
  183. });
  184. describe('TraceIssuesRenderer', function () {
  185. it('renders 0 issues', function () {
  186. render(
  187. <TraceIssuesRenderer
  188. trace={{
  189. breakdowns: [],
  190. duration: 123456,
  191. start: 1720015976544,
  192. end: 1720016100000,
  193. matchingSpans: 1,
  194. name: 'trace root',
  195. numErrors: 0,
  196. numOccurrences: 0,
  197. numSpans: 2,
  198. project: projects[0].slug,
  199. slices: 40,
  200. trace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  201. }}
  202. />,
  203. context
  204. );
  205. const link = screen.getByRole('button');
  206. expect(link).toBeInTheDocument();
  207. expect(link).toHaveTextContent('\u2014');
  208. expect(link).toBeDisabled();
  209. });
  210. it('renders 99+ issues', function () {
  211. render(
  212. <TraceIssuesRenderer
  213. trace={{
  214. breakdowns: [],
  215. duration: 123456,
  216. start: 1720015976544,
  217. end: 1720016100000,
  218. matchingSpans: 1,
  219. name: 'trace root',
  220. numErrors: 50,
  221. numOccurrences: 50,
  222. numSpans: 2,
  223. project: projects[0].slug,
  224. slices: 40,
  225. trace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  226. }}
  227. />,
  228. context
  229. );
  230. const link = screen.getByRole('button');
  231. expect(link).toBeInTheDocument();
  232. expect(link).toHaveTextContent('99+');
  233. expect(link).toBeEnabled();
  234. });
  235. it('renders N issues', function () {
  236. render(
  237. <TraceIssuesRenderer
  238. trace={{
  239. breakdowns: [],
  240. duration: 123456,
  241. start: 1720015976544,
  242. end: 1720016100000,
  243. matchingSpans: 1,
  244. name: 'trace root',
  245. numErrors: 5,
  246. numOccurrences: 5,
  247. numSpans: 2,
  248. project: projects[0].slug,
  249. slices: 40,
  250. trace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  251. }}
  252. />,
  253. context
  254. );
  255. const link = screen.getByRole('button');
  256. expect(link).toBeInTheDocument();
  257. expect(link).toHaveTextContent('10');
  258. expect(link).toBeEnabled();
  259. });
  260. });
  261. });