groupReplays.spec.tsx 13 KB

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