fieldRenderers.spec.tsx 13 KB

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