Browse Source

test(profiling): first flamechart tests (#45995)

On the path to achieving codecov seal of approval.

For a long time we just avoided writing tests on flamechart views as
webgl rendering was causing errors everywhere. We can now start testing
non gl related code by swapping up the gl renderer for just a regular
dom renderer.

---------

Co-authored-by: Tony Xiao <txiao@sentry.io>
Jonas 2 years ago
parent
commit
d4272045c1

+ 74 - 0
static/app/components/profiling/flamegraph/flamegraph.spec.tsx

@@ -0,0 +1,74 @@
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {reactHooks, render, screen} from 'sentry-test/reactTestingLibrary';
+
+import ProjectsStore from 'sentry/stores/projectsStore';
+import {useParams} from 'sentry/utils/useParams';
+import ProfileFlamegraph from 'sentry/views/profiling/profileFlamechart';
+import ProfilesAndTransactionProvider from 'sentry/views/profiling/profilesProvider';
+
+jest.mock('sentry/utils/useParams', () => ({
+  useParams: jest.fn(),
+}));
+
+window.ResizeObserver =
+  window.ResizeObserver ||
+  jest.fn().mockImplementation(() => ({
+    disconnect: jest.fn(),
+    observe: jest.fn(),
+    unobserve: jest.fn(),
+  }));
+
+Element.prototype.scrollTo = () => {};
+
+// Replace the webgl renderer with a dom renderer for tests
+jest.mock('sentry/utils/profiling/renderers/flamegraphRendererWebGL', () => {
+  const {
+    FlamegraphRendererDOM,
+  } = require('sentry/utils/profiling/renderers/flamegraphRendererDOM');
+
+  return {
+    FlamegraphRendererWebGL: FlamegraphRendererDOM,
+  };
+});
+
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: jest.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: jest.fn(), // Deprecated
+    removeListener: jest.fn(), // Deprecated
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  })),
+});
+
+describe('Flamegraph', function () {
+  beforeEach(() => {
+    const project = TestStubs.Project({slug: 'foo-project'});
+    reactHooks.act(() => void ProjectsStore.loadInitialData([project]));
+  });
+  it('renders', async function () {
+    MockApiClient.addMockResponse({
+      url: '/projects/org-slug/foo-project/profiling/profiles/profile-id/',
+      statusCode: 404,
+    });
+
+    (useParams as jest.Mock).mockReturnValue({
+      orgId: 'org-slug',
+      projectId: 'foo-project',
+      eventId: 'profile-id',
+    });
+
+    render(
+      <ProfilesAndTransactionProvider>
+        <ProfileFlamegraph />
+      </ProfilesAndTransactionProvider>,
+      {organization: initializeOrg().organization}
+    );
+
+    expect(await screen.findByText('Error: Unable to load profiles')).toBeInTheDocument();
+  });
+});

+ 14 - 0
static/app/components/profiling/flamegraph/flamegraphOverlays/FlamegraphWarnings.tsx

@@ -5,6 +5,7 @@ import {t} from 'sentry/locale';
 import {Flamegraph} from 'sentry/utils/profiling/flamegraph';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
+import {useProfiles} from 'sentry/views/profiling/profilesProvider';
 
 interface FlamegraphWarningProps {
   flamegraph: Flamegraph;
@@ -13,6 +14,19 @@ interface FlamegraphWarningProps {
 export function FlamegraphWarnings(props: FlamegraphWarningProps) {
   const orgSlug = useOrganization().slug;
   const params = useParams();
+  const profiles = useProfiles();
+
+  if (profiles.type === 'loading') {
+    return null;
+  }
+
+  if (profiles.type === 'errored') {
+    return (
+      <Overlay>
+        <p>{profiles.error || t('Failed to load profile')}</p>
+      </Overlay>
+    );
+  }
 
   // A profile may be empty while we are fetching it from the network; while that is happening an empty profile is
   // passed down to the view so that all the components can be loaded and initialized ahead of time.

+ 4 - 2
static/app/utils/profiling/renderers/flamegraphRendererDOM.tsx

@@ -46,6 +46,8 @@ export class FlamegraphRendererDOM extends FlamegraphRenderer {
     }
 
     const newContainer = document.createElement('div');
+    newContainer.setAttribute('data-test-id', 'flamegraph-zoom-view-container');
+
     this.container = newContainer;
     parent.appendChild(newContainer);
 
@@ -82,13 +84,13 @@ export class FlamegraphRendererDOM extends FlamegraphRenderer {
   }
 
   setHighlightedFrames(_frames: FlamegraphFrame[] | null) {
-    throw new Error('Method `setHighlightedFrames` not implemented.');
+    // @TODO for now just dont do anything as it will throw in tests
   }
 
   setSearchResults(
     _query: string,
     _searchResults: FlamegraphSearch['results']['frames']
   ) {
-    throw new Error('Method `setSearchResults` not implemented.');
+    // @TODO for now just dont do anything as it will throw in tests
   }
 }

+ 1 - 6
static/app/views/profiling/profileFlamechart.tsx

@@ -2,7 +2,6 @@ import {useEffect, useMemo} from 'react';
 import styled from '@emotion/styled';
 import * as qs from 'query-string';
 
-import {Alert} from 'sentry/components/alert';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {Flamegraph} from 'sentry/components/profiling/flamegraph/flamegraph';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
@@ -96,11 +95,7 @@ function ProfileFlamegraph(): React.ReactElement {
             <FlamegraphStateQueryParamSync />
             <FlamegraphStateLocalStorageSync />
             <FlamegraphContainer>
-              {profiles.type === 'errored' ? (
-                <Alert type="error" showIcon>
-                  {profiles.error}
-                </Alert>
-              ) : profiles.type === 'loading' ? (
+              {profiles.type === 'loading' ? (
                 <LoadingIndicatorContainer>
                   <LoadingIndicator />
                 </LoadingIndicatorContainer>

+ 11 - 6
static/app/views/profiling/profilesProvider.tsx

@@ -6,13 +6,12 @@ import {ProfileHeader} from 'sentry/components/profiling/profileHeader';
 import {t} from 'sentry/locale';
 import type {EventTransaction, Organization, Project} from 'sentry/types';
 import {RequestState} from 'sentry/types/core';
+import {isSchema} from 'sentry/utils/profiling/guards/profile';
 import {useSentryEvent} from 'sentry/utils/profiling/hooks/useSentryEvent';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
 
-import {isSchema} from '../../utils/profiling/guards/profile';
-
 function fetchFlamegraphs(
   api: Client,
   eventId: string,
@@ -64,7 +63,7 @@ export function useSetProfiles() {
   return context;
 }
 
-const ProfileTransactionContext =
+export const ProfileTransactionContext =
   createContext<RequestState<EventTransaction | null> | null>(null);
 
 export function useProfileTransaction() {
@@ -77,7 +76,7 @@ export function useProfileTransaction() {
   return context;
 }
 
-function ProfileProviderWrapper(props: FlamegraphViewProps): React.ReactElement {
+function ProfilesAndTransactionProvider(props: FlamegraphViewProps): React.ReactElement {
   const organization = useOrganization();
   const params = useParams();
 
@@ -148,7 +147,13 @@ export function ProfilesProvider({
         onUpdateProfiles?.({type: 'resolved', data: p});
       })
       .catch(err => {
-        const message = err.toString() || t('Error: Unable to load profiles');
+        // XXX: our API client mock implementation does not mimick the real
+        // implementation, so we need to check for an empty object here. #sad
+        const isEmptyObject = err.toString() === '[object Object]';
+        const message = isEmptyObject
+          ? t('Error: Unable to load profiles')
+          : err.toString();
+
         setProfiles({type: 'errored', error: message});
         onUpdateProfiles?.({type: 'errored', error: message});
         Sentry.captureException(err);
@@ -162,4 +167,4 @@ export function ProfilesProvider({
   return <ProfileContext.Provider value={profiles}>{children}</ProfileContext.Provider>;
 }
 
-export default ProfileProviderWrapper;
+export default ProfilesAndTransactionProvider;