breadcrumbs.spec.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import {EventFixture} from 'sentry-fixture/event';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {ProjectFixture} from 'sentry-fixture/project';
  4. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  5. import {textWithMarkupMatcher} from 'sentry-test/utils';
  6. import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs';
  7. import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
  8. import useProjects from 'sentry/utils/useProjects';
  9. jest.mock('sentry/utils/replays/hooks/useReplayOnboarding');
  10. jest.mock('sentry/utils/replays/hooks/useReplayReader');
  11. jest.mock('sentry/utils/useProjects');
  12. jest.mock('screenfull', () => ({
  13. enabled: true,
  14. isFullscreen: false,
  15. request: jest.fn(),
  16. exit: jest.fn(),
  17. on: jest.fn(),
  18. off: jest.fn(),
  19. }));
  20. jest.mock('sentry/utils/useProjects');
  21. describe('Breadcrumbs', () => {
  22. let props: React.ComponentProps<typeof Breadcrumbs>;
  23. beforeEach(() => {
  24. const project = ProjectFixture({platform: 'javascript'});
  25. jest.mocked(useProjects).mockReturnValue({
  26. fetchError: null,
  27. fetching: false,
  28. hasMore: false,
  29. initiallyLoaded: false,
  30. onSearch: () => Promise.resolve(),
  31. reloadProjects: jest.fn(),
  32. placeholders: [],
  33. projects: [project],
  34. });
  35. props = {
  36. organization: OrganizationFixture(),
  37. event: EventFixture({entries: [], projectID: project.id}),
  38. data: {
  39. values: [
  40. {
  41. message: 'sup',
  42. category: 'default',
  43. level: BreadcrumbLevelType.WARNING,
  44. type: BreadcrumbType.INFO,
  45. },
  46. {
  47. message: 'hey',
  48. category: 'error',
  49. level: BreadcrumbLevelType.INFO,
  50. type: BreadcrumbType.INFO,
  51. },
  52. {
  53. message: 'hello',
  54. category: 'default',
  55. level: BreadcrumbLevelType.WARNING,
  56. type: BreadcrumbType.INFO,
  57. },
  58. {
  59. message: 'bye',
  60. category: 'default',
  61. level: BreadcrumbLevelType.WARNING,
  62. type: BreadcrumbType.INFO,
  63. },
  64. {
  65. message: 'ok',
  66. category: 'error',
  67. level: BreadcrumbLevelType.WARNING,
  68. type: BreadcrumbType.INFO,
  69. },
  70. {
  71. message: 'sup',
  72. category: 'default',
  73. level: BreadcrumbLevelType.WARNING,
  74. type: BreadcrumbType.INFO,
  75. },
  76. {
  77. message: 'sup',
  78. category: 'default',
  79. level: BreadcrumbLevelType.INFO,
  80. type: BreadcrumbType.INFO,
  81. },
  82. ],
  83. },
  84. };
  85. MockApiClient.addMockResponse({
  86. url: `/organizations/${props.organization.slug}/events/`,
  87. method: 'GET',
  88. body: {
  89. data: [
  90. {
  91. title: '/settings/',
  92. 'project.name': 'javascript',
  93. id: 'abcdabcdabcdabcdabcdabcdabcdabcd',
  94. },
  95. ],
  96. meta: {},
  97. },
  98. });
  99. });
  100. describe('filterCrumbs', function () {
  101. it('should filter crumbs based on crumb message', async function () {
  102. render(<Breadcrumbs {...props} />);
  103. await userEvent.type(screen.getByPlaceholderText('Search breadcrumbs'), 'hi');
  104. expect(
  105. await screen.findByText('Sorry, no breadcrumbs match your search query')
  106. ).toBeInTheDocument();
  107. await userEvent.click(screen.getByLabelText('Clear'));
  108. await userEvent.type(screen.getByPlaceholderText('Search breadcrumbs'), 'up');
  109. expect(
  110. screen.queryByText('Sorry, no breadcrumbs match your search query')
  111. ).not.toBeInTheDocument();
  112. expect(screen.getAllByText(textWithMarkupMatcher('sup'))).toHaveLength(3);
  113. });
  114. it('should filter crumbs based on crumb level', async function () {
  115. render(<Breadcrumbs {...props} />);
  116. await userEvent.type(screen.getByPlaceholderText('Search breadcrumbs'), 'war');
  117. // breadcrumbs + filter item
  118. // TODO(Priscila): Filter should not render in the dom if not open
  119. expect(screen.getAllByText(textWithMarkupMatcher('Warning'))).toHaveLength(5);
  120. });
  121. it('should filter crumbs based on crumb category', async function () {
  122. render(<Breadcrumbs {...props} />);
  123. await userEvent.type(screen.getByPlaceholderText('Search breadcrumbs'), 'error');
  124. expect(screen.getAllByText(textWithMarkupMatcher('error'))).toHaveLength(2);
  125. });
  126. });
  127. describe('render', function () {
  128. it('should display the correct number of crumbs with no filter', async function () {
  129. props.data.values = props.data.values.slice(0, 4);
  130. render(<Breadcrumbs {...props} />);
  131. // data.values + virtual crumb
  132. expect(await screen.findAllByTestId('crumb')).toHaveLength(4);
  133. expect(screen.getByTestId('last-crumb')).toBeInTheDocument();
  134. });
  135. it('should display the correct number of crumbs with a filter', async function () {
  136. props.data.values = props.data.values.slice(0, 4);
  137. render(<Breadcrumbs {...props} />);
  138. const searchInput = screen.getByPlaceholderText('Search breadcrumbs');
  139. await userEvent.type(searchInput, 'sup');
  140. expect(screen.queryByTestId('crumb')).not.toBeInTheDocument();
  141. expect(screen.getByTestId('last-crumb')).toBeInTheDocument();
  142. });
  143. it('should not crash if data contains a toString attribute', async function () {
  144. // Regression test: A "toString" property in data should not falsely be
  145. // used to coerce breadcrumb data to string. This would cause a TypeError.
  146. const data = {nested: {toString: 'hello'}};
  147. props.data.values = [
  148. {
  149. message: 'sup',
  150. category: 'default',
  151. level: BreadcrumbLevelType.INFO,
  152. type: BreadcrumbType.INFO,
  153. data,
  154. },
  155. ];
  156. render(<Breadcrumbs {...props} />);
  157. // data.values + virtual crumb
  158. expect(await screen.findByTestId('crumb')).toBeInTheDocument();
  159. expect(screen.getByTestId('last-crumb')).toBeInTheDocument();
  160. });
  161. it('should render Sentry Transactions crumb', async function () {
  162. props.organization.features = ['performance-view'];
  163. props.data.values = [
  164. {
  165. message: '12345678123456781234567812345678',
  166. category: 'sentry.transaction',
  167. level: BreadcrumbLevelType.INFO,
  168. type: BreadcrumbType.TRANSACTION,
  169. },
  170. {
  171. message: 'abcdabcdabcdabcdabcdabcdabcdabcd',
  172. category: 'sentry.transaction',
  173. level: BreadcrumbLevelType.INFO,
  174. type: BreadcrumbType.TRANSACTION,
  175. },
  176. ];
  177. render(<Breadcrumbs {...props} />);
  178. // Transaction not in response should show as non-clickable id
  179. expect(
  180. await screen.findByText('12345678123456781234567812345678')
  181. ).toBeInTheDocument();
  182. expect(screen.getByText('12345678123456781234567812345678')).not.toHaveAttribute(
  183. 'href'
  184. );
  185. // Transaction in response should show as clickable title
  186. expect(await screen.findByRole('link', {name: '/settings/'})).toBeInTheDocument();
  187. expect(screen.getByText('/settings/')).toHaveAttribute(
  188. 'href',
  189. '/organizations/org-slug/performance/project-slug:abcdabcdabcdabcdabcdabcdabcdabcd/?referrer=breadcrumbs'
  190. );
  191. });
  192. });
  193. });