Browse Source

feat(source-maps): Add unit tests for new design changes - (#45932)

Priscila Oliveira 2 years ago
parent
commit
4c8dd23da0

+ 16 - 0
fixtures/js-stubs/sourceMapsDebugIDBundles.ts

@@ -0,0 +1,16 @@
+import type {DebugIdBundle} from 'sentry/types';
+
+export function SourceMapsDebugIDBundles(
+  debugIdBundle: Partial<DebugIdBundle> = {}
+): DebugIdBundle[] {
+  return [
+    {
+      bundleId: 'b916a646-2c6b-4e45-af4c-409830a44e0e',
+      release: null,
+      dist: null,
+      fileCount: 39,
+      date: '2023-03-08T09:53:09Z',
+      ...debugIdBundle,
+    },
+  ];
+}

+ 1 - 0
fixtures/js-stubs/types.tsx

@@ -135,6 +135,7 @@ type TestStubFixtures = {
   ShortIdQueryResult: OverridableStub;
   SourceMapArchive: OverridableStub;
   SourceMapArtifact: OverridableStub;
+  SourceMapsDebugIDBundles: OverridableStub;
   Span: OverridableStub;
   Subscriptions: OverridableStubList;
   TagValues: OverridableStubList;

+ 1 - 0
static/app/types/index.tsx

@@ -27,3 +27,4 @@ export * from './system';
 export * from './user';
 export * from './sandbox';
 export * from './sessions';
+export * from './sourceMaps';

+ 7 - 0
static/app/types/sourceMaps.ts

@@ -0,0 +1,7 @@
+export type DebugIdBundle = {
+  bundleId: string;
+  date: string;
+  dist: string | null;
+  fileCount: number;
+  release: string | null;
+};

+ 342 - 0
static/app/views/settings/projectSourceMaps/projectSourceMaps.spec.tsx

@@ -0,0 +1,342 @@
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {
+  render,
+  renderGlobalModal,
+  screen,
+  userEvent,
+  waitFor,
+} from 'sentry-test/reactTestingLibrary';
+
+import {ProjectSourceMaps} from 'sentry/views/settings/projectSourceMaps/projectSourceMaps';
+
+function renderReleaseBundlesMockRequests({
+  orgSlug,
+  projectSlug,
+  empty,
+}: {
+  orgSlug: string;
+  projectSlug: string;
+  empty?: boolean;
+}) {
+  const sourceMaps = MockApiClient.addMockResponse({
+    url: `/projects/${orgSlug}/${projectSlug}/files/source-maps/`,
+    body: empty
+      ? []
+      : [
+          TestStubs.SourceMapArchive(),
+          TestStubs.SourceMapArchive({
+            id: 2,
+            name: 'abc',
+            fileCount: 3,
+            date: '2023-05-06T13:41:00Z',
+          }),
+        ],
+  });
+
+  return {sourceMaps};
+}
+
+function renderDebugIdBundlesMockRequests({
+  orgSlug,
+  projectSlug,
+  empty,
+}: {
+  orgSlug: string;
+  projectSlug: string;
+  empty?: boolean;
+}) {
+  const artifactBundles = MockApiClient.addMockResponse({
+    url: `/projects/${orgSlug}/${projectSlug}/files/artifact-bundles/`,
+    body: empty ? [] : TestStubs.SourceMapsDebugIDBundles(),
+  });
+
+  return {artifactBundles};
+}
+
+describe('ProjectSourceMaps', function () {
+  describe('Release Bundles', function () {
+    it('renders default state', async function () {
+      const {organization, route, project, router, routerContext} = initializeOrg({
+        ...initializeOrg(),
+        router: {
+          location: {
+            query: {},
+          },
+          params: {},
+        },
+      });
+
+      const mockRequests = renderReleaseBundlesMockRequests({
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+      });
+
+      render(
+        <ProjectSourceMaps
+          location={routerContext.context.location}
+          project={project}
+          route={route}
+          routeParams={{orgId: organization.slug, projectId: project.slug}}
+          router={router}
+          routes={[]}
+          params={{orgId: organization.slug, projectId: project.slug}}
+        />,
+        {context: routerContext, organization}
+      );
+
+      // Title
+      expect(screen.getByRole('heading', {name: 'Source Maps'})).toBeInTheDocument();
+
+      // Active tab
+      const tabs = screen.getAllByRole('listitem');
+      expect(tabs).toHaveLength(2);
+
+      // Tab 1
+      expect(tabs[0]).toHaveTextContent('Release Bundles');
+      expect(tabs[0]).toHaveClass('active');
+
+      // Tab 2
+      expect(tabs[1]).toHaveTextContent('Debug ID Bundles');
+      expect(tabs[1]).not.toHaveClass('active');
+
+      // Search bar
+      expect(screen.getByPlaceholderText('Filter by Name')).toBeInTheDocument();
+
+      // Date Uploaded can be sorted
+      await userEvent.hover(screen.getByTestId('icon-arrow'));
+      expect(await screen.findByText('Switch to ascending order')).toBeInTheDocument();
+      await userEvent.click(screen.getByTestId('icon-arrow'));
+      await waitFor(() => {
+        expect(mockRequests.sourceMaps).toHaveBeenLastCalledWith(
+          '/projects/org-slug/project-slug/files/source-maps/',
+          expect.objectContaining({
+            query: expect.objectContaining({
+              orderby: '-date_added',
+            }),
+          })
+        );
+      });
+
+      // Active tab contains correct link
+      expect(screen.getByRole('link', {name: 'Release Bundles'})).toHaveAttribute(
+        'href',
+        '/settings/org-slug/projects/project-slug/source-maps/release-bundles/'
+      );
+
+      // Name
+      expect(await screen.findByRole('link', {name: '1234'})).toBeInTheDocument();
+      // Artifacts
+      expect(screen.getByText('0')).toBeInTheDocument();
+      // Date
+      expect(screen.getByText('May 6, 2020 1:41 PM UTC')).toBeInTheDocument();
+      // Delete buttons (this example renders 2 rows)
+      expect(screen.getAllByRole('button', {name: 'Remove All Artifacts'})).toHaveLength(
+        2
+      );
+      expect(
+        screen.getAllByRole('button', {name: 'Remove All Artifacts'})[0]
+      ).toBeEnabled();
+
+      renderGlobalModal();
+
+      // Delete item displays a confirmation modal
+      await userEvent.click(
+        screen.getAllByRole('button', {name: 'Remove All Artifacts'})[0]
+      );
+      expect(
+        await screen.findByText(
+          'Are you sure you want to remove all artifacts in this archive?'
+        )
+      ).toBeInTheDocument();
+      // Close modal
+      await userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
+
+      // Switch tab
+      await userEvent.click(screen.getByRole('link', {name: 'Debug ID Bundles'}));
+      expect(router.push).toHaveBeenCalledWith({
+        pathname:
+          '/settings/org-slug/projects/project-slug/source-maps/debug-id-bundles/',
+        query: undefined,
+      });
+    });
+
+    it('renders empty state', async function () {
+      const {organization, route, project, router, routerContext} = initializeOrg({
+        ...initializeOrg(),
+        router: {
+          location: {
+            query: {},
+          },
+          params: {},
+        },
+      });
+
+      renderReleaseBundlesMockRequests({
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+        empty: true,
+      });
+
+      render(
+        <ProjectSourceMaps
+          location={routerContext.context.location}
+          project={project}
+          route={route}
+          routeParams={{orgId: organization.slug, projectId: project.slug}}
+          router={router}
+          routes={[]}
+          params={{orgId: organization.slug, projectId: project.slug}}
+        />,
+        {context: routerContext, organization}
+      );
+
+      expect(
+        await screen.findByText('No release bundles found for this project.')
+      ).toBeInTheDocument();
+    });
+  });
+
+  describe('Debug ID Bundles', function () {
+    it('renders default state', async function () {
+      const {organization, route, project, router, routerContext} = initializeOrg({
+        ...initializeOrg(),
+        router: {
+          location: {
+            query: {},
+            pathname: `/settings/${initializeOrg().organization.slug}/projects/${
+              initializeOrg().project.slug
+            }/source-maps/debug-id-bundles/`,
+          },
+          params: {},
+        },
+      });
+
+      const mockRequests = renderDebugIdBundlesMockRequests({
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+      });
+
+      render(
+        <ProjectSourceMaps
+          location={routerContext.context.location}
+          project={project}
+          route={route}
+          routeParams={{orgId: organization.slug, projectId: project.slug}}
+          router={router}
+          routes={[]}
+          params={{orgId: organization.slug, projectId: project.slug}}
+        />,
+        {context: routerContext, organization}
+      );
+
+      // Title
+      expect(screen.getByRole('heading', {name: 'Source Maps'})).toBeInTheDocument();
+
+      // Active tab
+      const tabs = screen.getAllByRole('listitem');
+      expect(tabs).toHaveLength(2);
+
+      // Tab 1
+      expect(tabs[0]).toHaveTextContent('Release Bundles');
+      expect(tabs[0]).not.toHaveClass('active');
+
+      // Tab 2
+      expect(tabs[1]).toHaveTextContent('Debug ID Bundles');
+      expect(tabs[1]).toHaveClass('active');
+
+      // Search bar
+      expect(screen.getByPlaceholderText('Filter by Bundle ID')).toBeInTheDocument();
+
+      // Date Uploaded can be sorted
+      await userEvent.hover(screen.getByTestId('icon-arrow'));
+      expect(await screen.findByText('Switch to ascending order')).toBeInTheDocument();
+      await userEvent.click(screen.getByTestId('icon-arrow'));
+      await waitFor(() => {
+        expect(mockRequests.artifactBundles).toHaveBeenLastCalledWith(
+          '/projects/org-slug/project-slug/files/artifact-bundles/',
+          expect.objectContaining({
+            query: expect.objectContaining({
+              orderby: '-date_added',
+            }),
+          })
+        );
+      });
+
+      // Chip
+      await userEvent.hover(screen.getByText('none'));
+      expect(
+        await screen.findByText('Not associated with a release or distribution')
+      ).toBeInTheDocument();
+      // Artifacts
+      expect(screen.getByText('39')).toBeInTheDocument();
+      // Date Uploaded
+      expect(screen.getByText('Mar 8, 2023 9:53 AM UTC')).toBeInTheDocument();
+      // Delete button
+      expect(screen.getByRole('button', {name: 'Remove All Artifacts'})).toBeEnabled();
+      // Click on bundle id
+      await userEvent.click(
+        screen.getByRole('link', {name: 'b916a646-2c6b-4e45-af4c-409830a44e0e'})
+      );
+      expect(router.push).toHaveBeenLastCalledWith(
+        '/settings/org-slug/projects/project-slug/source-maps/debug-id-bundles/b916a646-2c6b-4e45-af4c-409830a44e0e'
+      );
+
+      renderGlobalModal();
+
+      // Delete item displays a confirmation modal
+      await userEvent.click(screen.getByRole('button', {name: 'Remove All Artifacts'}));
+      expect(
+        await screen.findByText(
+          'Are you sure you want to remove all artifacts in this archive?'
+        )
+      ).toBeInTheDocument();
+      // Close modal
+      await userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
+
+      // Switch tab
+      await userEvent.click(screen.getByRole('link', {name: 'Release Bundles'}));
+      expect(router.push).toHaveBeenCalledWith({
+        pathname: '/settings/org-slug/projects/project-slug/source-maps/release-bundles/',
+        query: undefined,
+      });
+    });
+
+    it('renders empty state', async function () {
+      const {organization, route, project, router, routerContext} = initializeOrg({
+        ...initializeOrg(),
+        router: {
+          location: {
+            query: {},
+            pathname: `/settings/${initializeOrg().organization.slug}/projects/${
+              initializeOrg().project.slug
+            }/source-maps/debug-id-bundles/`,
+          },
+          params: {},
+        },
+      });
+
+      renderDebugIdBundlesMockRequests({
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+        empty: true,
+      });
+
+      render(
+        <ProjectSourceMaps
+          location={routerContext.context.location}
+          project={project}
+          route={route}
+          routeParams={{orgId: organization.slug, projectId: project.slug}}
+          router={router}
+          routes={[]}
+          params={{orgId: organization.slug, projectId: project.slug}}
+        />,
+        {context: routerContext, organization}
+      );
+
+      expect(
+        await screen.findByText('No debug ID bundles found for this project.')
+      ).toBeInTheDocument();
+    });
+  });
+});

+ 28 - 13
static/app/views/settings/projectSourceMaps/projectSourceMaps.tsx

@@ -26,7 +26,7 @@ import Version from 'sentry/components/version';
 import {IconArrow, IconDelete} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import {Project, SourceMapsArchive} from 'sentry/types';
+import {DebugIdBundle, Project, SourceMapsArchive} from 'sentry/types';
 import {useQuery} from 'sentry/utils/queryClient';
 import {decodeScalar} from 'sentry/utils/queryString';
 import useApi from 'sentry/utils/useApi';
@@ -35,14 +35,6 @@ import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
-type DebugIdBundle = {
-  bundleId: string;
-  date: string;
-  dist: string;
-  fileCount: number;
-  release: string;
-};
-
 enum SORT_BY {
   ASC = 'date_added',
   DESC = '-date_added',
@@ -272,7 +264,10 @@ export function ProjectSourceMaps({location, router, project}: Props) {
                   : t('Switch to descending order')
               }
             >
-              <IconArrow direction={sortBy === SORT_BY.DESC ? 'down' : 'up'} />
+              <IconArrow
+                direction={sortBy === SORT_BY.DESC ? 'down' : 'up'}
+                data-test-id="icon-arrow"
+              />
             </Tooltip>
           </DateUploadedColumn>,
           '',
@@ -311,10 +306,30 @@ export function ProjectSourceMaps({location, router, project}: Props) {
                 }/source-maps/debug-id-bundles/${encodeURIComponent(data.bundleId)}`}
                 idColumnDetails={
                   <Tags>
-                    {data.dist && <Tag>{data.dist}</Tag>}
-                    {data.release && <Tag>{data.release}</Tag>}
+                    {data.dist && (
+                      <Tag
+                        tooltipText={tct('Associated with release "[distribution]"', {
+                          distribution: data.dist,
+                        })}
+                      >
+                        {data.dist}
+                      </Tag>
+                    )}
+                    {data.release && (
+                      <Tag
+                        tooltipText={tct('Associated with release "[releaseName]"', {
+                          releaseName: data.release,
+                        })}
+                      >
+                        {data.release}
+                      </Tag>
+                    )}
                     {!data.dist && !data.release && (
-                      <Tag tooltipText={t('No release and dist set')}>{t('none')}</Tag>
+                      <Tag
+                        tooltipText={t('Not associated with a release or distribution')}
+                      >
+                        {t('none')}
+                      </Tag>
                     )}
                   </Tags>
                 }

+ 149 - 0
static/app/views/settings/projectSourceMaps/projectSourceMapsArtifacts.spec.tsx

@@ -0,0 +1,149 @@
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import ConfigStore from 'sentry/stores/configStore';
+import {ProjectSourceMapsArtifacts} from 'sentry/views/settings/projectSourceMaps/projectSourceMapsArtifacts';
+
+function renderReleaseBundlesMockRequests({
+  orgSlug,
+  projectSlug,
+  empty,
+}: {
+  orgSlug: string;
+  projectSlug: string;
+  empty?: boolean;
+}) {
+  const files = MockApiClient.addMockResponse({
+    url: `/projects/${orgSlug}/${projectSlug}/releases/bea7335dfaebc0ca6e65a057/files/`,
+    body: empty ? [] : [TestStubs.SourceMapArtifact()],
+  });
+
+  return {files};
+}
+
+describe('ProjectSourceMapsArtifacts', function () {
+  describe('Release Bundles', function () {
+    it('renders default state', async function () {
+      const {organization, route, project, router, routerContext} = initializeOrg({
+        ...initializeOrg(),
+        router: {
+          location: {
+            query: {},
+          },
+          params: {},
+        },
+      });
+
+      ConfigStore.config = {
+        ...ConfigStore.config,
+        user: {...ConfigStore.config.user, isSuperuser: true},
+      };
+
+      renderReleaseBundlesMockRequests({
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+      });
+
+      render(
+        <ProjectSourceMapsArtifacts
+          location={routerContext.context.location}
+          project={project}
+          route={route}
+          routeParams={{orgId: organization.slug, projectId: project.slug}}
+          router={router}
+          routes={[]}
+          params={{
+            orgId: organization.slug,
+            projectId: project.slug,
+            bundleId: 'bea7335dfaebc0ca6e65a057',
+          }}
+        />,
+        {context: routerContext, organization}
+      );
+
+      // Title
+      expect(
+        screen.getByRole('heading', {name: 'bea7335dfaebc0ca6e65a057'})
+      ).toBeInTheDocument();
+
+      // Active tab
+      const tabs = screen.getAllByRole('listitem');
+      expect(tabs).toHaveLength(2);
+
+      // Tab 1
+      expect(tabs[0]).toHaveTextContent('Release Bundles');
+      expect(tabs[0]).toHaveClass('active');
+
+      // Tab 2
+      expect(tabs[1]).toHaveTextContent('Debug ID Bundles');
+      expect(tabs[1]).not.toHaveClass('active');
+
+      // Search bar
+      expect(screen.getByPlaceholderText('Filter by Path')).toBeInTheDocument();
+
+      // Path
+      expect(
+        await screen.findByText('https://example.com/AcceptOrganizationInvite.js')
+      ).toBeInTheDocument();
+      // Time
+      expect(screen.getByText(/in 3 year/)).toBeInTheDocument();
+      // File size
+      expect(screen.getByText('8.1 KiB')).toBeInTheDocument();
+      // Chip
+      await userEvent.hover(screen.getByText('none'));
+      expect(await screen.findByText('No distribution set')).toBeInTheDocument();
+      // Download button
+      expect(screen.getByRole('button', {name: 'Download Artifact'})).toHaveAttribute(
+        'href',
+        '/projects/org-slug/project-slug/releases/bea7335dfaebc0ca6e65a057/files/5678/?download=1'
+      );
+
+      // Switch tab
+      await userEvent.click(screen.getByRole('link', {name: 'Debug ID Bundles'}));
+      expect(router.push).toHaveBeenCalledWith({
+        pathname:
+          '/settings/org-slug/projects/project-slug/source-maps/debug-id-bundles/bea7335dfaebc0ca6e65a057',
+        query: undefined,
+      });
+    });
+
+    it('renders empty state', async function () {
+      const {organization, route, project, router, routerContext} = initializeOrg({
+        ...initializeOrg(),
+        router: {
+          location: {
+            query: {},
+          },
+          params: {},
+        },
+      });
+
+      renderReleaseBundlesMockRequests({
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+        empty: true,
+      });
+
+      render(
+        <ProjectSourceMapsArtifacts
+          location={routerContext.context.location}
+          project={project}
+          route={route}
+          routeParams={{orgId: organization.slug, projectId: project.slug}}
+          router={router}
+          routes={[]}
+          params={{
+            orgId: organization.slug,
+            projectId: project.slug,
+            bundleId: 'bea7335dfaebc0ca6e65a057',
+          }}
+        />,
+        {context: routerContext, organization}
+      );
+
+      expect(
+        await screen.findByText('There are no artifacts in this archive.')
+      ).toBeInTheDocument();
+    });
+  });
+});

+ 29 - 25
static/app/views/settings/projectSourceMaps/projectSourceMapsArtifacts.tsx

@@ -75,29 +75,31 @@ function ArtifactsTableRow({
       </SizeColumn>
       <ActionsColumn>
         <Role role={downloadRole}>
-          {({hasRole}) => (
-            <Tooltip
-              title={tct(
-                'Artifacts can only be downloaded by users with organization [downloadRole] role[orHigher]. This can be changed in [settingsLink:Debug Files Access] settings.',
-                {
-                  downloadRole,
-                  orHigher: downloadRole !== 'owner' ? ` ${t('or higher')}` : '',
-                  settingsLink: <Link to={`/settings/${orgSlug}/#debugFilesRole`} />,
-                }
-              )}
-              disabled={hasRole}
-              isHoverable
-            >
-              <Button
-                size="sm"
-                icon={<IconDownload size="sm" />}
-                disabled={!hasRole}
-                href={downloadUrl}
-                title={hasRole ? t('Download Artifact') : undefined}
-                aria-label={t('Download Artifact')}
-              />
-            </Tooltip>
-          )}
+          {({hasRole}) => {
+            return (
+              <Tooltip
+                title={tct(
+                  'Artifacts can only be downloaded by users with organization [downloadRole] role[orHigher]. This can be changed in [settingsLink:Debug Files Access] settings.',
+                  {
+                    downloadRole,
+                    orHigher: downloadRole !== 'owner' ? ` ${t('or higher')}` : '',
+                    settingsLink: <Link to={`/settings/${orgSlug}/#debugFilesRole`} />,
+                  }
+                )}
+                disabled={hasRole}
+                isHoverable
+              >
+                <Button
+                  size="sm"
+                  icon={<IconDownload size="sm" />}
+                  disabled={!hasRole}
+                  href={downloadUrl}
+                  title={hasRole ? t('Download Artifact') : undefined}
+                  aria-label={t('Download Artifact')}
+                />
+              </Tooltip>
+            );
+          }}
         </Role>
       </ActionsColumn>
     </Fragment>
@@ -230,7 +232,7 @@ export function ProjectSourceMapsArtifacts({params, location, router, project}:
           ? debugIdBundlesData?.[0].map(data => {
               const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
                 project.slug
-              }/releases/${encodeURIComponent(data.debugId)}/files/${
+              }/releases/${encodeURIComponent(params.bundleId)}/files/${
                 data.id
               }/?download=1`;
 
@@ -254,7 +256,9 @@ export function ProjectSourceMapsArtifacts({params, location, router, project}:
           : artifactsData?.[0].map(data => {
               const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
                 project.slug
-              }/releases/${encodeURIComponent(data.name)}/files/${data.id}/?download=1`;
+              }/releases/${encodeURIComponent(params.bundleId)}/files/${
+                data.id
+              }/?download=1`;
 
               return (
                 <ArtifactsTableRow