groupReplays.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import ProjectsStore from 'sentry/stores/projectsStore';
  4. import GroupReplays from 'sentry/views/issueDetails/groupReplays';
  5. jest.mock('sentry/utils/useMedia', () => ({
  6. __esModule: true,
  7. default: jest.fn(() => true),
  8. }));
  9. const mockEventsUrl = '/organizations/org-slug/events/';
  10. const mockReplayUrl = '/organizations/org-slug/replays/';
  11. type InitializeOrgProps = {
  12. organizationProps?: {
  13. features?: string[];
  14. };
  15. };
  16. function init({
  17. organizationProps = {features: ['session-replay-ui']},
  18. }: InitializeOrgProps) {
  19. const mockProject = TestStubs.Project();
  20. const {router, organization, routerContext} = initializeOrg({
  21. organization: {
  22. ...organizationProps,
  23. },
  24. project: mockProject,
  25. projects: [mockProject],
  26. router: {
  27. routes: [
  28. {path: '/'},
  29. {path: '/organizations/:orgId/issues/:groupId/'},
  30. {path: 'replays/'},
  31. ],
  32. location: {
  33. pathname: '/organizations/org-slug/replays/',
  34. query: {},
  35. },
  36. },
  37. });
  38. ProjectsStore.init();
  39. ProjectsStore.loadInitialData(organization.projects);
  40. return {router, organization, routerContext};
  41. }
  42. describe('GroupReplays', () => {
  43. beforeEach(() => {
  44. MockApiClient.clearMockResponses();
  45. });
  46. describe('Replay Feature Disabled', () => {
  47. const mockGroup = TestStubs.Group();
  48. const {router, organization, routerContext} = init({
  49. organizationProps: {features: []},
  50. });
  51. it("should show a message when the organization doesn't have access to the replay feature", () => {
  52. render(<GroupReplays group={mockGroup} />, {
  53. context: routerContext,
  54. organization,
  55. router,
  56. });
  57. expect(
  58. screen.getByText("You don't have access to this feature")
  59. ).toBeInTheDocument();
  60. });
  61. });
  62. describe('Replay Feature Enabled', () => {
  63. const {router, organization, routerContext} = init({});
  64. it('should query the events endpoint with the fetched replayIds', async () => {
  65. const mockGroup = TestStubs.Group();
  66. const mockEventsApi = MockApiClient.addMockResponse({
  67. url: mockEventsUrl,
  68. body: {
  69. data: [
  70. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  71. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  72. ],
  73. },
  74. });
  75. const mockReplayApi = MockApiClient.addMockResponse({
  76. url: mockReplayUrl,
  77. body: {
  78. data: [],
  79. },
  80. });
  81. render(<GroupReplays group={mockGroup} />, {
  82. context: routerContext,
  83. organization,
  84. router,
  85. });
  86. await waitFor(() => {
  87. expect(mockEventsApi).toHaveBeenCalledWith(
  88. mockEventsUrl,
  89. expect.objectContaining({
  90. query: {
  91. environment: [],
  92. field: ['replayId', 'count()'],
  93. per_page: 50,
  94. project: ['2'],
  95. query: `issue.id:${mockGroup.id} !replayId:""`,
  96. statsPeriod: '14d',
  97. },
  98. })
  99. );
  100. // Expect api path to have the correct query params
  101. expect(mockReplayApi).toHaveBeenCalledWith(
  102. mockReplayUrl,
  103. expect.objectContaining({
  104. query: expect.objectContaining({
  105. environment: [],
  106. field: [
  107. 'activity',
  108. 'count_errors',
  109. 'duration',
  110. 'finished_at',
  111. 'id',
  112. 'project_id',
  113. 'started_at',
  114. 'urls',
  115. 'user',
  116. ],
  117. per_page: 50,
  118. project: ['2'],
  119. query:
  120. 'id:[346789a703f6454384f1de473b8b9fcc,b05dae9b6be54d21a4d5ad9f8f02b780]',
  121. sort: '-started_at',
  122. statsPeriod: '14d',
  123. }),
  124. })
  125. );
  126. });
  127. });
  128. it('should show empty message when no replays are found', async () => {
  129. const mockGroup = TestStubs.Group();
  130. const mockEventsApi = MockApiClient.addMockResponse({
  131. url: mockEventsUrl,
  132. body: {
  133. data: [
  134. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  135. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  136. ],
  137. },
  138. });
  139. const mockReplayApi = MockApiClient.addMockResponse({
  140. url: mockReplayUrl,
  141. body: {
  142. data: [],
  143. },
  144. });
  145. const {container} = render(<GroupReplays group={mockGroup} />, {
  146. context: routerContext,
  147. organization,
  148. router,
  149. });
  150. expect(
  151. await screen.findByText('There are no items to display')
  152. ).toBeInTheDocument();
  153. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  154. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  155. expect(container).toSnapshot();
  156. });
  157. it('should display error message when api call fails', async () => {
  158. const mockGroup = TestStubs.Group();
  159. const mockEventsApi = MockApiClient.addMockResponse({
  160. url: mockEventsUrl,
  161. body: {
  162. data: [
  163. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  164. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  165. ],
  166. },
  167. });
  168. const mockReplayApi = MockApiClient.addMockResponse({
  169. url: mockReplayUrl,
  170. statusCode: 500,
  171. body: {
  172. detail: 'Invalid number: asdf. Expected number.',
  173. },
  174. });
  175. render(<GroupReplays group={mockGroup} />, {
  176. context: routerContext,
  177. organization,
  178. router,
  179. });
  180. await waitFor(() => {
  181. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  182. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  183. expect(
  184. screen.getByText('Invalid number: asdf. Expected number.')
  185. ).toBeInTheDocument();
  186. });
  187. });
  188. it('should display default error message when api call fails without a body', async () => {
  189. const mockGroup = TestStubs.Group();
  190. const mockEventsApi = MockApiClient.addMockResponse({
  191. url: mockEventsUrl,
  192. body: {
  193. data: [
  194. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  195. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  196. ],
  197. },
  198. });
  199. const mockReplayApi = MockApiClient.addMockResponse({
  200. url: mockReplayUrl,
  201. statusCode: 500,
  202. body: {},
  203. });
  204. render(<GroupReplays group={mockGroup} />, {
  205. context: routerContext,
  206. organization,
  207. router,
  208. });
  209. await waitFor(() => {
  210. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  211. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  212. expect(
  213. screen.getByText(
  214. 'Sorry, the list of replays could not be loaded. This could be due to invalid search parameters or an internal systems error.'
  215. )
  216. ).toBeInTheDocument();
  217. });
  218. });
  219. it('should show loading indicator when loading replays', async () => {
  220. const mockGroup = TestStubs.Group();
  221. const mockEventsApi = MockApiClient.addMockResponse({
  222. url: mockEventsUrl,
  223. body: {
  224. data: [
  225. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  226. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  227. ],
  228. },
  229. });
  230. const mockReplayApi = MockApiClient.addMockResponse({
  231. url: mockReplayUrl,
  232. statusCode: 200,
  233. body: {
  234. data: [],
  235. },
  236. });
  237. render(<GroupReplays group={mockGroup} />, {
  238. context: routerContext,
  239. organization,
  240. router,
  241. });
  242. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  243. await waitFor(() => {
  244. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  245. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  246. });
  247. });
  248. it('should show a list of replays and have the correct values', async () => {
  249. const mockGroup = TestStubs.Group();
  250. const mockEventsApi = MockApiClient.addMockResponse({
  251. url: mockEventsUrl,
  252. body: {
  253. data: [
  254. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  255. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  256. ],
  257. },
  258. });
  259. const mockReplayApi = MockApiClient.addMockResponse({
  260. url: mockReplayUrl,
  261. statusCode: 200,
  262. body: {
  263. data: [
  264. {
  265. count_errors: 1,
  266. duration: 52346,
  267. finished_at: '2022-09-15T06:54:00+00:00',
  268. id: '346789a703f6454384f1de473b8b9fcc',
  269. project_id: '2',
  270. started_at: '2022-09-15T06:50:03+00:00',
  271. urls: [
  272. 'https://dev.getsentry.net:7999/organizations/sentry-emerging-tech/replays/',
  273. '/organizations/sentry-emerging-tech/replays/?project=2',
  274. ],
  275. user: {
  276. id: '147086',
  277. name: '',
  278. email: '',
  279. ip: '127.0.0.1',
  280. display_name: 'testDisplayName',
  281. },
  282. },
  283. {
  284. count_errors: 4,
  285. duration: 400,
  286. finished_at: '2022-09-21T21:40:38+00:00',
  287. id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
  288. project_id: '2',
  289. started_at: '2022-09-21T21:30:44+00:00',
  290. urls: [
  291. 'https://dev.getsentry.net:7999/organizations/sentry-emerging-tech/replays/?project=2&statsPeriod=24h',
  292. '/organizations/sentry-emerging-tech/issues/',
  293. '/organizations/sentry-emerging-tech/issues/?project=2',
  294. ],
  295. user: {
  296. id: '147086',
  297. name: '',
  298. email: '',
  299. ip: '127.0.0.1',
  300. display_name: 'testDisplayName',
  301. },
  302. },
  303. ],
  304. },
  305. });
  306. // Mock the system date to be 2022-09-28
  307. jest.useFakeTimers().setSystemTime(new Date('Sep 28, 2022 11:29:13 PM UTC'));
  308. render(<GroupReplays group={mockGroup} />, {
  309. context: routerContext,
  310. organization,
  311. router,
  312. });
  313. await waitFor(() => {
  314. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  315. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  316. });
  317. // Expect the table to have 2 rows
  318. expect(screen.getAllByText('testDisplayName')).toHaveLength(2);
  319. const expectedQuery =
  320. 'query=&referrer=%2Forganizations%2F%3AorgId%2Fissues%2F%3AgroupId%2Freplays%2F&statsPeriod=14d&yAxis=count%28%29';
  321. // Expect the first row to have the correct href
  322. expect(screen.getAllByRole('link', {name: 'testDisplayName'})[0]).toHaveAttribute(
  323. 'href',
  324. `/organizations/org-slug/replays/project-slug:346789a703f6454384f1de473b8b9fcc/?${expectedQuery}`
  325. );
  326. // Expect the second row to have the correct href
  327. expect(screen.getAllByRole('link', {name: 'testDisplayName'})[1]).toHaveAttribute(
  328. 'href',
  329. `/organizations/org-slug/replays/project-slug:b05dae9b6be54d21a4d5ad9f8f02b780/?${expectedQuery}`
  330. );
  331. // Expect the first row to have the correct duration
  332. expect(screen.getByText('14hr 32min 26s')).toBeInTheDocument();
  333. // Expect the second row to have the correct duration
  334. expect(screen.getByText('6min 40s')).toBeInTheDocument();
  335. // Expect the first row to have the correct errors
  336. expect(screen.getAllByTestId('replay-table-count-errors')[0]).toHaveTextContent(
  337. '1'
  338. );
  339. // Expect the second row to have the correct errors
  340. expect(screen.getAllByTestId('replay-table-count-errors')[1]).toHaveTextContent(
  341. '4'
  342. );
  343. // Expect the first row to have the correct date
  344. expect(screen.getByText('14 days ago')).toBeInTheDocument();
  345. // Expect the second row to have the correct date
  346. expect(screen.getByText('7 days ago')).toBeInTheDocument();
  347. });
  348. });
  349. describe('sorting', () => {
  350. let mockEventsApi;
  351. let mockReplayApi;
  352. beforeEach(() => {
  353. mockEventsApi = MockApiClient.addMockResponse({
  354. url: mockEventsUrl,
  355. body: {
  356. data: [
  357. {replayId: '346789a703f6454384f1de473b8b9fcc', 'count()': 1},
  358. {replayId: 'b05dae9b6be54d21a4d5ad9f8f02b780', 'count()': 1},
  359. ],
  360. },
  361. });
  362. mockReplayApi = MockApiClient.addMockResponse({
  363. url: mockReplayUrl,
  364. body: {
  365. data: [],
  366. },
  367. statusCode: 200,
  368. });
  369. });
  370. it('should not call the events api again when sorting the visible rows', async () => {
  371. const mockGroup = TestStubs.Group();
  372. const {router, organization, routerContext} = init({});
  373. const {rerender} = render(<GroupReplays group={mockGroup} />, {
  374. context: routerContext,
  375. organization,
  376. router,
  377. });
  378. await waitFor(() => {
  379. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  380. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  381. expect(mockReplayApi).toHaveBeenLastCalledWith(
  382. mockReplayUrl,
  383. expect.objectContaining({
  384. query: expect.objectContaining({
  385. sort: '-started_at',
  386. }),
  387. })
  388. );
  389. });
  390. // Change the sort order then tell react to re-render
  391. router.location.query.sort = 'duration';
  392. rerender(<GroupReplays group={mockGroup} />);
  393. await waitFor(() => {
  394. expect(mockEventsApi).toHaveBeenCalledTimes(1);
  395. expect(mockReplayApi).toHaveBeenCalledTimes(2);
  396. expect(mockReplayApi).toHaveBeenLastCalledWith(
  397. mockReplayUrl,
  398. expect.objectContaining({
  399. query: expect.objectContaining({
  400. sort: 'duration',
  401. }),
  402. })
  403. );
  404. });
  405. });
  406. });
  407. });