fieldRenderers.spec.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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}/traces/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}/traces/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(
  176. <TransactionRenderer projectSlug={projects[0]!.slug} transaction="foobar" />
  177. );
  178. const link = screen.getByText('foobar');
  179. expect(link).toBeInTheDocument();
  180. expect(link).toHaveAttribute(
  181. 'href',
  182. `/organizations/${organization.slug}/performance/summary/?project=${projects[0]!.id}&referrer=performance-transaction-summary&transaction=foobar&unselectedSeries=p100%28%29&unselectedSeries=avg%28%29`
  183. );
  184. });
  185. });
  186. describe('TraceIssuesRenderer', function () {
  187. it('renders 0 issues', function () {
  188. render(
  189. <TraceIssuesRenderer
  190. trace={{
  191. breakdowns: [],
  192. duration: 123456,
  193. start: 1720015976544,
  194. end: 1720016100000,
  195. matchingSpans: 1,
  196. name: 'trace root',
  197. numErrors: 0,
  198. numOccurrences: 0,
  199. numSpans: 2,
  200. project: projects[0]!.slug,
  201. slices: 40,
  202. trace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  203. }}
  204. />,
  205. context
  206. );
  207. const link = screen.getByRole('button');
  208. expect(link).toBeInTheDocument();
  209. expect(link).toHaveTextContent('\u2014');
  210. expect(link).toBeDisabled();
  211. });
  212. it('renders 99+ issues', function () {
  213. render(
  214. <TraceIssuesRenderer
  215. trace={{
  216. breakdowns: [],
  217. duration: 123456,
  218. start: 1720015976544,
  219. end: 1720016100000,
  220. matchingSpans: 1,
  221. name: 'trace root',
  222. numErrors: 50,
  223. numOccurrences: 50,
  224. numSpans: 2,
  225. project: projects[0]!.slug,
  226. slices: 40,
  227. trace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  228. }}
  229. />,
  230. context
  231. );
  232. const link = screen.getByRole('button');
  233. expect(link).toBeInTheDocument();
  234. expect(link).toHaveTextContent('99+');
  235. expect(link).toBeEnabled();
  236. });
  237. it('renders N issues', function () {
  238. render(
  239. <TraceIssuesRenderer
  240. trace={{
  241. breakdowns: [],
  242. duration: 123456,
  243. start: 1720015976544,
  244. end: 1720016100000,
  245. matchingSpans: 1,
  246. name: 'trace root',
  247. numErrors: 5,
  248. numOccurrences: 5,
  249. numSpans: 2,
  250. project: projects[0]!.slug,
  251. slices: 40,
  252. trace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  253. }}
  254. />,
  255. context
  256. );
  257. const link = screen.getByRole('button');
  258. expect(link).toBeInTheDocument();
  259. expect(link).toHaveTextContent('10');
  260. expect(link).toBeEnabled();
  261. });
  262. });
  263. });