fieldRenderers.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {act, render, screen} from 'sentry-test/reactTestingLibrary';
  3. import ConfigStore from 'sentry/stores/configStore';
  4. import ProjectsStore from 'sentry/stores/projectsStore';
  5. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  6. import {SPAN_OP_RELATIVE_BREAKDOWN_FIELD} from 'sentry/utils/discover/fields';
  7. describe('getFieldRenderer', function () {
  8. let location, context, project, organization, data, user;
  9. beforeEach(function () {
  10. context = initializeOrg({
  11. project: TestStubs.Project(),
  12. });
  13. organization = context.organization;
  14. project = context.project;
  15. act(() => ProjectsStore.loadInitialData([project]));
  16. user = 'email:text@example.com';
  17. location = {
  18. pathname: '/events',
  19. query: {},
  20. };
  21. data = {
  22. id: '1',
  23. team_key_transaction: 1,
  24. title: 'ValueError: something bad',
  25. transaction: 'api.do_things',
  26. boolValue: 1,
  27. numeric: 1.23,
  28. createdAt: new Date(2019, 9, 3, 12, 13, 14),
  29. url: '/example',
  30. project: project.slug,
  31. release: 'F2520C43515BD1F0E8A6BD46233324641A370BF6',
  32. issue: 'SENTRY-T6P',
  33. user,
  34. 'span_ops_breakdown.relative': '',
  35. 'spans.browser': 10,
  36. 'spans.db': 30,
  37. 'spans.http': 15,
  38. 'spans.resource': 20,
  39. 'spans.total.time': 75,
  40. 'transaction.duration': 75,
  41. 'timestamp.to_day': '2021-09-05T00:00:00+00:00',
  42. 'issue.id': '123214',
  43. lifetimeCount: 10000,
  44. filteredCount: 3000,
  45. count: 6000,
  46. selectionDateString: 'last 7 days',
  47. };
  48. MockApiClient.addMockResponse({
  49. url: `/organizations/${organization.slug}/projects/${project.slug}/`,
  50. body: project,
  51. });
  52. MockApiClient.addMockResponse({
  53. url: `/organizations/${organization.slug}/key-transactions/`,
  54. method: 'POST',
  55. });
  56. MockApiClient.addMockResponse({
  57. url: `/organizations/${organization.slug}/key-transactions/`,
  58. method: 'DELETE',
  59. });
  60. MockApiClient.addMockResponse({
  61. url: `/organizations/${organization.slug}/projects/`,
  62. body: [project],
  63. });
  64. });
  65. it('can render string fields', function () {
  66. const renderer = getFieldRenderer('url', {url: 'string'});
  67. render(renderer(data, {location, organization}));
  68. expect(screen.getByText(data.url)).toBeInTheDocument();
  69. });
  70. it('can render empty string fields', function () {
  71. const renderer = getFieldRenderer('url', {url: 'string'});
  72. data.url = '';
  73. render(renderer(data, {location, organization}));
  74. expect(screen.getByText('(empty string)')).toBeInTheDocument();
  75. });
  76. it('can render boolean fields', function () {
  77. const renderer = getFieldRenderer('boolValue', {boolValue: 'boolean'});
  78. render(renderer(data, {location, organization}));
  79. expect(screen.getByText('true')).toBeInTheDocument();
  80. });
  81. it('can render integer fields', function () {
  82. const renderer = getFieldRenderer('numeric', {numeric: 'integer'});
  83. render(renderer(data, {location, organization}));
  84. expect(screen.getByText(data.numeric)).toBeInTheDocument();
  85. });
  86. describe('date', function () {
  87. beforeEach(function () {
  88. ConfigStore.loadInitialData({
  89. user: {
  90. options: {
  91. timezone: 'America/Los_Angeles',
  92. },
  93. },
  94. });
  95. });
  96. it('can render date fields', function () {
  97. const renderer = getFieldRenderer('createdAt', {createdAt: 'date'});
  98. render(renderer(data, {location, organization}));
  99. expect(screen.getByText('Oct 3, 2019 9:13:14 AM PDT')).toBeInTheDocument();
  100. });
  101. it('can render date fields using utc when query string has utc set to true', function () {
  102. const renderer = getFieldRenderer('createdAt', {createdAt: 'date'});
  103. render(
  104. renderer(data, {location: {...location, query: {utc: 'true'}}, organization})
  105. );
  106. expect(screen.getByText('Oct 3, 2019 4:13:14 PM UTC')).toBeInTheDocument();
  107. });
  108. });
  109. it('can render null date fields', function () {
  110. const renderer = getFieldRenderer('nope', {nope: 'date'});
  111. render(renderer(data, {location, organization}));
  112. expect(screen.getByText('(no value)')).toBeInTheDocument();
  113. });
  114. it('can render timestamp.to_day', function () {
  115. // Set timezone
  116. ConfigStore.loadInitialData({
  117. user: {
  118. options: {
  119. timezone: 'America/Los_Angeles',
  120. },
  121. },
  122. });
  123. const renderer = getFieldRenderer('timestamp.to_day', {'timestamp.to_day': 'date'});
  124. render(renderer(data, {location, organization}));
  125. expect(screen.getByText('Sep 5, 2021')).toBeInTheDocument();
  126. });
  127. it('can render error.handled values', function () {
  128. const renderer = getFieldRenderer('error.handled', {'error.handled': 'boolean'});
  129. function validate(value, expectText) {
  130. const {unmount} = render(
  131. renderer({'error.handled': value}, {location, organization})
  132. );
  133. expect(screen.getByText(expectText)).toBeInTheDocument();
  134. unmount();
  135. }
  136. // Should render the same as the filter.
  137. // ie. all 1 or null
  138. validate([0, 1], 'false');
  139. validate([1, 0], 'false');
  140. validate([null, 0], 'false');
  141. validate([0, null], 'false');
  142. validate([null, 1], 'true');
  143. validate([1, null], 'true');
  144. // null = true for error.handled data.
  145. validate([null], 'true');
  146. // Default events won't have error.handled and will return an empty list.
  147. validate([], '(no value)');
  148. // Transactions will have null for error.handled as the 'tag' won't be set.
  149. validate(null, '(no value)');
  150. });
  151. it('can render user fields with aliased user', function () {
  152. const renderer = getFieldRenderer('user', {user: 'string'});
  153. render(renderer(data, {location, organization}));
  154. expect(screen.getByTestId('letter_avatar-avatar')).toBeInTheDocument();
  155. expect(screen.getByText('text@example.com')).toBeInTheDocument();
  156. });
  157. it('can render null user fields', function () {
  158. const renderer = getFieldRenderer('user', {user: 'string'});
  159. delete data.user;
  160. render(renderer(data, {location, organization}));
  161. expect(screen.queryByTestId('letter_avatar-avatar')).not.toBeInTheDocument();
  162. expect(screen.getByText('(no value)')).toBeInTheDocument();
  163. });
  164. it('can render null release fields', function () {
  165. const renderer = getFieldRenderer('release', {release: 'string'});
  166. delete data.release;
  167. render(renderer(data, {location, organization}));
  168. expect(screen.getByText('(no value)')).toBeInTheDocument();
  169. });
  170. it('renders release version with hyperlink', function () {
  171. const renderer = getFieldRenderer('release', {release: 'string'});
  172. render(renderer(data, {location, organization}), {
  173. context: context.routerContext,
  174. });
  175. expect(screen.queryByRole('link')).toHaveAttribute(
  176. 'href',
  177. `/organizations/org-slug/releases/F2520C43515BD1F0E8A6BD46233324641A370BF6/`
  178. );
  179. expect(screen.getByText('F2520C43515B')).toBeInTheDocument();
  180. });
  181. it('renders issue hyperlink', function () {
  182. const renderer = getFieldRenderer('issue', {issue: 'string'});
  183. render(renderer(data, {location, organization}), {
  184. context: context.routerContext,
  185. });
  186. expect(screen.queryByRole('link')).toHaveAttribute(
  187. 'href',
  188. `/organizations/org-slug/issues/123214/`
  189. );
  190. expect(screen.getByText('SENTRY-T6P')).toBeInTheDocument();
  191. });
  192. it('can render project as an avatar', function () {
  193. const renderer = getFieldRenderer('project', {project: 'string'});
  194. render(renderer(data, {location, organization}), {
  195. context: context.routerContext,
  196. });
  197. expect(screen.queryByTestId('letter_avatar-avatar')).not.toBeInTheDocument();
  198. expect(screen.getByText(project.slug)).toBeInTheDocument();
  199. });
  200. it('can render project id as an avatar', function () {
  201. const renderer = getFieldRenderer('project', {project: 'number'});
  202. data = {...data, project: parseInt(project.id, 10)};
  203. render(renderer(data, {location, organization}), {
  204. context: context.routerContext,
  205. });
  206. expect(screen.queryByTestId('letter_avatar-avatar')).not.toBeInTheDocument();
  207. expect(screen.getByText(project.slug)).toBeInTheDocument();
  208. });
  209. it('can render team key transaction as a star with the dropdown', function () {
  210. const renderer = getFieldRenderer('team_key_transaction', {
  211. team_key_transaction: 'boolean',
  212. });
  213. render(renderer(data, {location, organization}), {
  214. context: context.routerContext,
  215. });
  216. const star = screen.getByRole('button', {name: 'Toggle star for team'});
  217. // Enabled, can't open the menu in the test without setting up the
  218. // TeamKeyTransactionManager
  219. expect(star).toBeEnabled();
  220. });
  221. it('can render team key transaction as a star without the dropdown', function () {
  222. const renderer = getFieldRenderer('team_key_transaction', {
  223. team_key_transaction: 'boolean',
  224. });
  225. delete data.project;
  226. render(renderer(data, {location, organization}), {
  227. context: context.routerContext,
  228. });
  229. const star = screen.getByRole('button', {name: 'Toggle star for team'});
  230. // Not enabled without a project
  231. expect(star).toBeDisabled();
  232. });
  233. describe('ops breakdown', () => {
  234. const getWidths = () =>
  235. [...screen.getByTestId('relative-ops-breakdown').children].map(
  236. node => node.style.width
  237. );
  238. it('can render operation breakdowns', function () {
  239. const renderer = getFieldRenderer(SPAN_OP_RELATIVE_BREAKDOWN_FIELD, {
  240. [SPAN_OP_RELATIVE_BREAKDOWN_FIELD]: 'string',
  241. });
  242. render(renderer(data, {location, organization}), {
  243. context: context.routerContext,
  244. });
  245. expect(getWidths()).toEqual(['13.333%', '40.000%', '20.000%', '26.667%', '0.000%']);
  246. });
  247. it('renders operation breakdowns in sorted order when a sort field is provided', function () {
  248. const renderer = getFieldRenderer(SPAN_OP_RELATIVE_BREAKDOWN_FIELD, {
  249. [SPAN_OP_RELATIVE_BREAKDOWN_FIELD]: 'string',
  250. });
  251. render(
  252. renderer(data, {
  253. location,
  254. organization,
  255. eventView: {sorts: [{field: 'spans.db'}]},
  256. }),
  257. {context: context.routerContext}
  258. );
  259. expect(getWidths()).toEqual(['40.000%', '13.333%', '20.000%', '26.667%', '0.000%']);
  260. });
  261. });
  262. });