projectSourceMaps.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {
  3. render,
  4. renderGlobalModal,
  5. screen,
  6. userEvent,
  7. waitFor,
  8. within,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import {textWithMarkupMatcher} from 'sentry-test/utils';
  11. import {ProjectSourceMaps} from 'sentry/views/settings/projectSourceMaps/projectSourceMaps';
  12. function renderReleaseBundlesMockRequests({
  13. orgSlug,
  14. projectSlug,
  15. empty,
  16. }: {
  17. orgSlug: string;
  18. projectSlug: string;
  19. empty?: boolean;
  20. }) {
  21. const sourceMaps = MockApiClient.addMockResponse({
  22. url: `/projects/${orgSlug}/${projectSlug}/files/source-maps/`,
  23. body: empty
  24. ? []
  25. : [
  26. TestStubs.SourceMapArchive(),
  27. TestStubs.SourceMapArchive({
  28. id: 2,
  29. name: 'abc',
  30. fileCount: 3,
  31. date: '2023-05-06T13:41:00Z',
  32. }),
  33. ],
  34. });
  35. return {sourceMaps};
  36. }
  37. function renderDebugIdBundlesMockRequests({
  38. orgSlug,
  39. projectSlug,
  40. empty,
  41. }: {
  42. orgSlug: string;
  43. projectSlug: string;
  44. empty?: boolean;
  45. }) {
  46. const artifactBundles = MockApiClient.addMockResponse({
  47. url: `/projects/${orgSlug}/${projectSlug}/files/artifact-bundles/`,
  48. body: empty ? [] : TestStubs.SourceMapsDebugIDBundles(),
  49. });
  50. const artifactBundlesDeletion = MockApiClient.addMockResponse({
  51. url: `/projects/${orgSlug}/${projectSlug}/files/artifact-bundles/`,
  52. method: 'DELETE',
  53. });
  54. return {artifactBundles, artifactBundlesDeletion};
  55. }
  56. describe('ProjectSourceMaps', function () {
  57. describe('Release Bundles', function () {
  58. it('renders default state', async function () {
  59. const {organization, project, routerContext, routerProps} = initializeOrg({
  60. router: {
  61. location: {
  62. query: {},
  63. pathname: `/settings/${initializeOrg().organization.slug}/projects/${
  64. initializeOrg().project.slug
  65. }/source-maps/release-bundles/`,
  66. },
  67. },
  68. });
  69. const mockRequests = renderReleaseBundlesMockRequests({
  70. orgSlug: organization.slug,
  71. projectSlug: project.slug,
  72. });
  73. render(<ProjectSourceMaps project={project} {...routerProps} />, {
  74. context: routerContext,
  75. organization,
  76. });
  77. // Title
  78. expect(screen.getByRole('heading', {name: 'Source Maps'})).toBeInTheDocument();
  79. // Active tab
  80. const tabs = screen.getAllByRole('listitem');
  81. expect(tabs).toHaveLength(2);
  82. // Tab 1
  83. expect(tabs[0]).toHaveTextContent('Artifact Bundles');
  84. expect(tabs[0]).not.toHaveClass('active');
  85. expect(within(tabs[0]).getByRole('link')).toHaveAttribute(
  86. 'href',
  87. '/settings/org-slug/projects/project-slug/source-maps/artifact-bundles/?'
  88. );
  89. // Tab 2
  90. expect(tabs[1]).toHaveTextContent('Release Bundles');
  91. expect(tabs[1]).toHaveClass('active');
  92. expect(within(tabs[0]).getByRole('link')).toHaveAttribute(
  93. 'href',
  94. '/settings/org-slug/projects/project-slug/source-maps/artifact-bundles/?'
  95. );
  96. // Search bar
  97. expect(screen.getByPlaceholderText('Filter by Name')).toBeInTheDocument();
  98. // Date Uploaded can be sorted
  99. await userEvent.hover(screen.getByTestId('icon-arrow'));
  100. expect(await screen.findByText('Switch to ascending order')).toBeInTheDocument();
  101. await userEvent.click(screen.getByTestId('icon-arrow'));
  102. await waitFor(() => {
  103. expect(mockRequests.sourceMaps).toHaveBeenLastCalledWith(
  104. '/projects/org-slug/project-slug/files/source-maps/',
  105. expect.objectContaining({
  106. query: expect.objectContaining({
  107. sortBy: 'date_added',
  108. }),
  109. })
  110. );
  111. });
  112. // Active tab contains correct link
  113. expect(screen.getByRole('link', {name: /Release Bundles/})).toHaveAttribute(
  114. 'href',
  115. '/settings/org-slug/projects/project-slug/source-maps/release-bundles/?'
  116. );
  117. // Artifact Bundles Tab
  118. expect(screen.getByRole('link', {name: /Artifact Bundles/})).toHaveAttribute(
  119. 'href',
  120. '/settings/org-slug/projects/project-slug/source-maps/artifact-bundles/?'
  121. );
  122. // Name
  123. expect(await screen.findByRole('link', {name: '1234'})).toBeInTheDocument();
  124. // Artifacts
  125. expect(screen.getByText('0')).toBeInTheDocument();
  126. // Date
  127. expect(screen.getByText('May 6, 2020 1:41 PM UTC')).toBeInTheDocument();
  128. // Delete buttons (this example renders 2 rows)
  129. expect(screen.getAllByRole('button', {name: 'Remove All Artifacts'})).toHaveLength(
  130. 2
  131. );
  132. expect(
  133. screen.getAllByRole('button', {name: 'Remove All Artifacts'})[0]
  134. ).toBeEnabled();
  135. renderGlobalModal();
  136. // Delete item displays a confirmation modal
  137. await userEvent.click(
  138. screen.getAllByRole('button', {name: 'Remove All Artifacts'})[0]
  139. );
  140. expect(
  141. await screen.findByText(
  142. 'Are you sure you want to remove all artifacts in this archive?'
  143. )
  144. ).toBeInTheDocument();
  145. // Close modal
  146. await userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
  147. });
  148. it('renders empty state', async function () {
  149. const {organization, project, routerContext, routerProps} = initializeOrg({
  150. router: {
  151. location: {
  152. query: {},
  153. pathname: `/settings/${initializeOrg().organization.slug}/projects/${
  154. initializeOrg().project.slug
  155. }/source-maps/release-bundles/`,
  156. },
  157. },
  158. });
  159. renderReleaseBundlesMockRequests({
  160. orgSlug: organization.slug,
  161. projectSlug: project.slug,
  162. empty: true,
  163. });
  164. render(<ProjectSourceMaps project={project} {...routerProps} />, {
  165. context: routerContext,
  166. organization,
  167. });
  168. expect(
  169. await screen.findByText('No release bundles found for this project.')
  170. ).toBeInTheDocument();
  171. });
  172. });
  173. describe('Artifact Bundles', function () {
  174. it('renders default state', async function () {
  175. const {organization, project, routerContext, router, routerProps} = initializeOrg({
  176. router: {
  177. location: {
  178. query: {},
  179. pathname: `/settings/${initializeOrg().organization.slug}/projects/${
  180. initializeOrg().project.slug
  181. }/source-maps/artifact-bundles/`,
  182. },
  183. },
  184. });
  185. const mockRequests = renderDebugIdBundlesMockRequests({
  186. orgSlug: organization.slug,
  187. projectSlug: project.slug,
  188. });
  189. render(<ProjectSourceMaps project={project} {...routerProps} />, {
  190. context: routerContext,
  191. organization,
  192. });
  193. // Title
  194. expect(screen.getByRole('heading', {name: 'Source Maps'})).toBeInTheDocument();
  195. // Active tab
  196. const tabs = screen.getAllByRole('listitem');
  197. expect(tabs).toHaveLength(2);
  198. // Tab 1
  199. expect(tabs[0]).toHaveTextContent('Artifact Bundles');
  200. expect(tabs[0]).toHaveClass('active');
  201. // Tab 2
  202. expect(tabs[1]).toHaveTextContent('Release Bundles');
  203. expect(tabs[1]).not.toHaveClass('active');
  204. // Search bar
  205. expect(
  206. screen.getByPlaceholderText('Filter by Bundle ID, Debug ID or Release')
  207. ).toBeInTheDocument();
  208. // Date Uploaded can be sorted
  209. await userEvent.click(screen.getByTestId('date-uploaded-header'));
  210. await userEvent.hover(screen.getByTestId('icon-arrow'));
  211. expect(await screen.findByText('Switch to ascending order')).toBeInTheDocument();
  212. await userEvent.click(screen.getByTestId('icon-arrow'));
  213. await waitFor(() => {
  214. expect(mockRequests.artifactBundles).toHaveBeenLastCalledWith(
  215. '/projects/org-slug/project-slug/files/artifact-bundles/',
  216. expect.objectContaining({
  217. query: expect.objectContaining({
  218. sortBy: 'date_added',
  219. }),
  220. })
  221. );
  222. });
  223. // Date Uploaded can be sorted in descending
  224. await userEvent.hover(screen.getByTestId('icon-arrow'));
  225. expect(await screen.findByText('Switch to descending order')).toBeInTheDocument();
  226. await userEvent.click(screen.getByTestId('icon-arrow'));
  227. await waitFor(() => {
  228. expect(mockRequests.artifactBundles).toHaveBeenLastCalledWith(
  229. '/projects/org-slug/project-slug/files/artifact-bundles/',
  230. expect.objectContaining({
  231. query: expect.objectContaining({
  232. sortBy: '-date_added',
  233. }),
  234. })
  235. );
  236. });
  237. // Date Modified can be sorted
  238. await userEvent.click(screen.getByTestId('date-modified-header'));
  239. await userEvent.hover(screen.getByTestId('icon-arrow-modified'));
  240. expect(await screen.findByText('Switch to ascending order')).toBeInTheDocument();
  241. await userEvent.click(screen.getByTestId('icon-arrow-modified'));
  242. await waitFor(() => {
  243. expect(mockRequests.artifactBundles).toHaveBeenLastCalledWith(
  244. '/projects/org-slug/project-slug/files/artifact-bundles/',
  245. expect.objectContaining({
  246. query: expect.objectContaining({
  247. sortBy: 'date_modified',
  248. }),
  249. })
  250. );
  251. });
  252. // Date Modified can be sorted in descending
  253. await userEvent.hover(screen.getByTestId('icon-arrow-modified'));
  254. expect(await screen.findByText('Switch to descending order')).toBeInTheDocument();
  255. await userEvent.click(screen.getByTestId('icon-arrow-modified'));
  256. await waitFor(() => {
  257. expect(mockRequests.artifactBundles).toHaveBeenLastCalledWith(
  258. '/projects/org-slug/project-slug/files/artifact-bundles/',
  259. expect.objectContaining({
  260. query: expect.objectContaining({
  261. sortBy: '-date_modified',
  262. }),
  263. })
  264. );
  265. });
  266. // Artifacts
  267. expect(screen.getByText('39')).toBeInTheDocument();
  268. // Date Modified
  269. expect(screen.getByText('Mar 10, 2023 8:25 AM UTC')).toBeInTheDocument();
  270. // Date Uploaded
  271. expect(screen.getByText('Mar 8, 2023 9:53 AM UTC')).toBeInTheDocument();
  272. // Delete button
  273. expect(screen.getByRole('button', {name: 'Remove All Artifacts'})).toBeEnabled();
  274. // Release information
  275. expect(
  276. await screen.findByText(textWithMarkupMatcher('2 Releases associated'))
  277. ).toBeInTheDocument();
  278. await userEvent.hover(screen.getByText('2 Releases'));
  279. expect(
  280. await screen.findByText('frontend@2e318148eac9298ec04a662ae32b4b093b027f0a')
  281. ).toBeInTheDocument();
  282. // Click on bundle id
  283. await userEvent.click(
  284. screen.getByRole('link', {name: 'b916a646-2c6b-4e45-af4c-409830a44e0e'})
  285. );
  286. expect(router.push).toHaveBeenLastCalledWith(
  287. '/settings/org-slug/projects/project-slug/source-maps/artifact-bundles/b916a646-2c6b-4e45-af4c-409830a44e0e'
  288. );
  289. renderGlobalModal();
  290. // Delete item displays a confirmation modal
  291. await userEvent.click(screen.getByRole('button', {name: 'Remove All Artifacts'}));
  292. expect(
  293. await screen.findByText(
  294. 'Are you sure you want to remove all artifacts in this archive?'
  295. )
  296. ).toBeInTheDocument();
  297. // Close modal
  298. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  299. await waitFor(() => {
  300. expect(mockRequests.artifactBundlesDeletion).toHaveBeenLastCalledWith(
  301. '/projects/org-slug/project-slug/files/artifact-bundles/',
  302. expect.objectContaining({
  303. query: expect.objectContaining({
  304. bundleId: 'b916a646-2c6b-4e45-af4c-409830a44e0e',
  305. }),
  306. })
  307. );
  308. });
  309. });
  310. it('renders empty state', async function () {
  311. const {organization, project, routerProps, routerContext} = initializeOrg({
  312. router: {
  313. location: {
  314. query: {},
  315. pathname: `/settings/${initializeOrg().organization.slug}/projects/${
  316. initializeOrg().project.slug
  317. }/source-maps/artifact-bundles/`,
  318. },
  319. },
  320. });
  321. renderDebugIdBundlesMockRequests({
  322. orgSlug: organization.slug,
  323. projectSlug: project.slug,
  324. empty: true,
  325. });
  326. render(<ProjectSourceMaps project={project} {...routerProps} />, {
  327. context: routerContext,
  328. organization,
  329. });
  330. expect(
  331. await screen.findByText('No artifact bundles found for this project.')
  332. ).toBeInTheDocument();
  333. });
  334. });
  335. });