groupDetails.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import {ConfigFixture} from 'sentry-fixture/config';
  2. import {EnvironmentsFixture} from 'sentry-fixture/environments';
  3. import {EventFixture} from 'sentry-fixture/event';
  4. import {GroupFixture} from 'sentry-fixture/group';
  5. import {LocationFixture} from 'sentry-fixture/locationFixture';
  6. import {ProjectFixture} from 'sentry-fixture/project';
  7. import {TeamFixture} from 'sentry-fixture/team';
  8. import {UserFixture} from 'sentry-fixture/user';
  9. import {initializeOrg} from 'sentry-test/initializeOrg';
  10. import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  11. import ConfigStore from 'sentry/stores/configStore';
  12. import GroupStore from 'sentry/stores/groupStore';
  13. import OrganizationStore from 'sentry/stores/organizationStore';
  14. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  15. import ProjectsStore from 'sentry/stores/projectsStore';
  16. import type {Group} from 'sentry/types/group';
  17. import {IssueCategory} from 'sentry/types/group';
  18. import type {Environment} from 'sentry/types/project';
  19. import GroupDetails from 'sentry/views/issueDetails/groupDetails';
  20. jest.unmock('sentry/utils/recreateRoute');
  21. jest.mock('screenfull', () => ({
  22. enabled: true,
  23. isFullscreen: false,
  24. request: jest.fn(),
  25. exit: jest.fn(),
  26. on: jest.fn(),
  27. off: jest.fn(),
  28. }));
  29. const SAMPLE_EVENT_ALERT_TEXT =
  30. 'You are viewing a sample error. Configure Sentry to start viewing real errors.';
  31. describe('groupDetails', () => {
  32. const group = GroupFixture({issueCategory: IssueCategory.ERROR});
  33. const event = EventFixture();
  34. const project = ProjectFixture({teams: [TeamFixture()]});
  35. const routes = [
  36. {path: '/', childRoutes: []},
  37. {childRoutes: []},
  38. {
  39. path: '/organizations/:orgId/issues/:groupId/',
  40. childRoutes: [],
  41. },
  42. {},
  43. ];
  44. const initRouter = {
  45. location: {
  46. pathname: `/organizations/org-slug/issues/${group.id}/`,
  47. query: {},
  48. search: '?foo=bar',
  49. hash: '#hash',
  50. },
  51. params: {
  52. groupId: group.id,
  53. },
  54. routes,
  55. };
  56. const defaultInit = initializeOrg<{groupId: string}>({
  57. router: initRouter,
  58. });
  59. const recommendedUser = UserFixture({
  60. options: {
  61. ...UserFixture().options,
  62. defaultIssueEvent: 'recommended',
  63. },
  64. });
  65. const latestUser = UserFixture({
  66. options: {
  67. ...UserFixture().options,
  68. defaultIssueEvent: 'latest',
  69. },
  70. });
  71. const oldestUser = UserFixture({
  72. options: {
  73. ...UserFixture().options,
  74. defaultIssueEvent: 'oldest',
  75. },
  76. });
  77. function MockComponent({
  78. group: groupProp,
  79. environments,
  80. eventError,
  81. }: {
  82. environments?: Environment[];
  83. eventError?: boolean;
  84. group?: Group;
  85. }) {
  86. return (
  87. <div>
  88. Group Details Mock
  89. <div>title: {groupProp?.title}</div>
  90. <div>environment: {environments?.join(' ')}</div>
  91. {eventError && <div>eventError</div>}
  92. </div>
  93. );
  94. }
  95. const createWrapper = (init = defaultInit) => {
  96. return render(
  97. <GroupDetails {...init.routerProps}>
  98. <MockComponent />
  99. </GroupDetails>,
  100. {organization: init.organization, router: init.router}
  101. );
  102. };
  103. beforeEach(() => {
  104. MockApiClient.clearMockResponses();
  105. OrganizationStore.onUpdate(defaultInit.organization);
  106. act(() => ProjectsStore.loadInitialData(defaultInit.projects));
  107. MockApiClient.addMockResponse({
  108. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/`,
  109. body: {...group},
  110. });
  111. MockApiClient.addMockResponse({
  112. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/recommended/`,
  113. statusCode: 200,
  114. body: {
  115. ...event,
  116. },
  117. });
  118. MockApiClient.addMockResponse({
  119. url: `/projects/org-slug/${project.slug}/issues/`,
  120. method: 'PUT',
  121. body: {
  122. hasSeen: false,
  123. },
  124. });
  125. MockApiClient.addMockResponse({
  126. url: '/organizations/org-slug/projects/',
  127. body: [project],
  128. });
  129. MockApiClient.addMockResponse({
  130. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/first-last-release/`,
  131. method: 'GET',
  132. });
  133. MockApiClient.addMockResponse({
  134. url: `/organizations/${defaultInit.organization.slug}/events/`,
  135. statusCode: 200,
  136. body: {
  137. data: [
  138. {
  139. 'count()': 1,
  140. },
  141. ],
  142. },
  143. });
  144. MockApiClient.addMockResponse({
  145. url: `/organizations/${defaultInit.organization.slug}/environments/`,
  146. body: EnvironmentsFixture(),
  147. });
  148. MockApiClient.addMockResponse({
  149. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/tags/`,
  150. body: [],
  151. });
  152. MockApiClient.addMockResponse({
  153. url: '/organizations/org-slug/replay-count/',
  154. body: {},
  155. });
  156. MockApiClient.addMockResponse({
  157. url: `/projects/${defaultInit.organization.slug}/${project.slug}/`,
  158. body: project,
  159. });
  160. });
  161. afterEach(() => {
  162. act(() => ProjectsStore.reset());
  163. GroupStore.reset();
  164. PageFiltersStore.reset();
  165. MockApiClient.clearMockResponses();
  166. });
  167. it('renders', async function () {
  168. act(() => ProjectsStore.reset());
  169. createWrapper();
  170. expect(screen.queryByText(group.title)).not.toBeInTheDocument();
  171. act(() => ProjectsStore.loadInitialData(defaultInit.projects));
  172. expect(await screen.findByText(group.title, {exact: false})).toBeInTheDocument();
  173. // Sample event alert should not show up
  174. expect(screen.queryByText(SAMPLE_EVENT_ALERT_TEXT)).not.toBeInTheDocument();
  175. });
  176. it('renders error when issue is not found', async function () {
  177. MockApiClient.addMockResponse({
  178. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/`,
  179. statusCode: 404,
  180. });
  181. MockApiClient.addMockResponse({
  182. url: `/organization/${defaultInit.organization.slug}/issues/${group.id}/events/recommended/`,
  183. statusCode: 404,
  184. });
  185. createWrapper();
  186. await waitFor(() =>
  187. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument()
  188. );
  189. expect(
  190. await screen.findByText('The issue you were looking for was not found.')
  191. ).toBeInTheDocument();
  192. });
  193. it('renders MissingProjectMembership when trying to access issue in project the user does not belong to', async function () {
  194. MockApiClient.addMockResponse({
  195. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/`,
  196. statusCode: 403,
  197. });
  198. MockApiClient.addMockResponse({
  199. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/recommended/`,
  200. statusCode: 403,
  201. });
  202. createWrapper();
  203. await waitFor(() =>
  204. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument()
  205. );
  206. expect(
  207. await screen.findByText(
  208. 'No teams have access to this project yet. Ask an admin to add your team to this project.'
  209. )
  210. ).toBeInTheDocument();
  211. });
  212. it('fetches issue details for a given environment', async function () {
  213. const init = initializeOrg({
  214. router: {
  215. ...initRouter,
  216. location: LocationFixture({
  217. ...initRouter.location,
  218. query: {environment: 'staging'},
  219. }),
  220. },
  221. });
  222. createWrapper(init);
  223. await waitFor(() =>
  224. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument()
  225. );
  226. expect(await screen.findByText('environment: staging')).toBeInTheDocument();
  227. });
  228. it('renders issue event error', async function () {
  229. MockApiClient.addMockResponse({
  230. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/recommended/`,
  231. statusCode: 404,
  232. });
  233. createWrapper();
  234. expect(await screen.findByText('eventError')).toBeInTheDocument();
  235. });
  236. it('renders substatus badge', async function () {
  237. MockApiClient.addMockResponse({
  238. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/`,
  239. body: {
  240. ...group,
  241. inbox: null,
  242. status: 'unresolved',
  243. substatus: 'ongoing',
  244. },
  245. });
  246. createWrapper({
  247. ...defaultInit,
  248. organization: {...defaultInit.organization},
  249. });
  250. expect(await screen.findByText('Ongoing')).toBeInTheDocument();
  251. });
  252. it('renders alert for sample event', async function () {
  253. MockApiClient.addMockResponse({
  254. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/tags/`,
  255. body: [{key: 'sample_event'}],
  256. });
  257. createWrapper();
  258. expect(await screen.findByText(SAMPLE_EVENT_ALERT_TEXT)).toBeInTheDocument();
  259. });
  260. it('renders error when project does not exist', async function () {
  261. MockApiClient.addMockResponse({
  262. url: `/projects/org-slug/other-project-slug/issues/`,
  263. method: 'PUT',
  264. });
  265. MockApiClient.addMockResponse({
  266. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/`,
  267. body: {...group, project: {slug: 'other-project-slug'}},
  268. });
  269. MockApiClient.addMockResponse({
  270. url: `/projects/${defaultInit.organization.slug}/other-project-slug/`,
  271. body: {},
  272. });
  273. createWrapper();
  274. expect(
  275. await screen.findByText('The project other-project-slug does not exist')
  276. ).toBeInTheDocument();
  277. });
  278. it('uses /recommended endpoint when feature flag is on and no event is provided', async function () {
  279. const recommendedMock = MockApiClient.addMockResponse({
  280. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/recommended/`,
  281. statusCode: 200,
  282. body: event,
  283. });
  284. createWrapper();
  285. await waitFor(() => expect(recommendedMock).toHaveBeenCalledTimes(1));
  286. });
  287. it('uses /latest endpoint when default is set to latest', async function () {
  288. ConfigStore.loadInitialData(ConfigFixture({user: latestUser}));
  289. const latestMock = MockApiClient.addMockResponse({
  290. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/latest/`,
  291. statusCode: 200,
  292. body: event,
  293. });
  294. createWrapper();
  295. await waitFor(() => expect(latestMock).toHaveBeenCalledTimes(1));
  296. });
  297. it('uses /oldest endpoint when default is set to oldest', async function () {
  298. ConfigStore.loadInitialData(ConfigFixture({user: oldestUser}));
  299. const oldestMock = MockApiClient.addMockResponse({
  300. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/oldest/`,
  301. statusCode: 200,
  302. body: event,
  303. });
  304. createWrapper();
  305. await waitFor(() => expect(oldestMock).toHaveBeenCalledTimes(1));
  306. });
  307. it('uses /recommended endpoint when default is set to recommended', async function () {
  308. ConfigStore.loadInitialData(ConfigFixture({user: recommendedUser}));
  309. const recommendedMock = MockApiClient.addMockResponse({
  310. url: `/organizations/${defaultInit.organization.slug}/issues/${group.id}/events/recommended/`,
  311. statusCode: 200,
  312. body: event,
  313. });
  314. createWrapper();
  315. await waitFor(() => expect(recommendedMock).toHaveBeenCalledTimes(1));
  316. });
  317. });