Browse Source

test: Add unit tests for the UrlWalker and related components (#41630)

Fixes #38844
Ryan Albrecht 2 years ago
parent
commit
bab8113643

+ 18 - 0
fixtures/js-stubs/breadcrumb.js

@@ -0,0 +1,18 @@
+import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
+
+export function Breadcrumb(params = []) {
+  return {
+    type: BreadcrumbType.NAVIGATION,
+    category: 'default',
+    timestamp: new Date().toISOString(),
+    level: BreadcrumbLevelType.INFO,
+    message: 'https://sourcemaps.io/',
+    data: {
+      to: 'https://sourcemaps.io/',
+    },
+    id: 6,
+    color: 'green300',
+    description: 'Navigation',
+    ...params,
+  };
+}

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

@@ -21,6 +21,7 @@ type TestStubFixtures = {
   AuthProviders: OverridableStubList;
   Authenticators: SimpleStub;
   BitbucketIntegrationConfig: SimpleStub;
+  Breadcrumb: OverridableStub;
   Broadcast: OverridableStub;
   BuiltInSymbolSources: OverridableStubList;
   Commit: OverridableStub;

+ 62 - 0
static/app/components/replays/walker/chevronDividedList.spec.tsx

@@ -0,0 +1,62 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import ChevronDividedList from './chevronDividedList';
+
+describe('ChevronDividedList', () => {
+  it('should accept zero items and show an empty <List>', () => {
+    const mockItems = [];
+    render(<ChevronDividedList items={mockItems} />);
+
+    expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+    expect(screen.queryByRole('separator')).not.toBeInTheDocument();
+  });
+
+  it('should accept one item and show it in the <List>', () => {
+    const mockItems = [<span key="1">first</span>];
+    render(<ChevronDividedList items={mockItems} />);
+
+    expect(screen.queryByRole('listitem')).toBeInTheDocument();
+    expect(screen.queryByRole('separator')).not.toBeInTheDocument();
+    expect(screen.getByText('first')).toBeInTheDocument();
+  });
+
+  it('should accept two items and show them both in the <List>', () => {
+    const mockItems = [<span key="1">first</span>, <span key="2">second</span>];
+    render(<ChevronDividedList items={mockItems} />);
+
+    expect(screen.queryAllByRole('listitem')).toHaveLength(2);
+    expect(screen.queryByRole('separator')).toBeInTheDocument();
+    expect(screen.getByText('first')).toBeInTheDocument();
+    expect(screen.getByText('second')).toBeInTheDocument();
+  });
+
+  it('should accept three items and show them all in the <List>', () => {
+    const mockItems = [
+      <span key="1">first</span>,
+      <span key="2">second</span>,
+      <span key="3">third</span>,
+    ];
+    render(<ChevronDividedList items={mockItems} />);
+
+    expect(screen.queryAllByRole('listitem')).toHaveLength(3);
+    expect(screen.queryAllByRole('separator')).toHaveLength(2);
+    expect(screen.getByText('first')).toBeInTheDocument();
+    expect(screen.getByText('second')).toBeInTheDocument();
+    expect(screen.getByText('third')).toBeInTheDocument();
+  });
+
+  it('should accept many items and show them all in the <List>', () => {
+    const mockItems = [
+      <span key="1">first</span>,
+      <span key="2">second</span>,
+      <span key="3">third</span>,
+      <span key="4">fourth</span>,
+      <span key="5">fifth</span>,
+      <span key="6">sixth</span>,
+    ];
+    render(<ChevronDividedList items={mockItems} />);
+
+    expect(screen.queryAllByRole('listitem')).toHaveLength(6);
+    expect(screen.queryAllByRole('separator')).toHaveLength(5);
+  });
+});

+ 1 - 1
static/app/components/replays/walker/chevronDividedList.tsx

@@ -17,7 +17,7 @@ function ChevronDividedList({items}: Props) {
         return i === 0
           ? li
           : [
-              <Item key={`${i}-chev`}>
+              <Item key={`${i}-chev`} role="separator">
                 <Chevron>
                   <IconChevron color="gray300" size="xs" direction="right" />
                 </Chevron>

+ 145 - 0
static/app/components/replays/walker/splitCrumbs.spec.tsx

@@ -0,0 +1,145 @@
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import type {BreadcrumbTypeNavigation} from 'sentry/types/breadcrumbs';
+
+import splitCrumbs from './splitCrumbs';
+
+const PAGELOAD_CRUMB = TestStubs.Breadcrumb({
+  id: 4,
+  data: {
+    to: 'https://sourcemaps.io/',
+  },
+}) as BreadcrumbTypeNavigation;
+
+const NAV_CRUMB_BOOTSTRAP = TestStubs.Breadcrumb({
+  id: 5,
+  data: {
+    from: '/',
+    to: '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js',
+  },
+}) as BreadcrumbTypeNavigation;
+
+const NAV_CRUMB_UNDERSCORE = TestStubs.Breadcrumb({
+  id: 6,
+  data: {
+    from: '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js',
+    to: '/report/1669088273097_http%3A%2F%2Funderscorejs.org%2Funderscore-min.js',
+  },
+}) as BreadcrumbTypeNavigation;
+
+describe('splitCrumbs', () => {
+  const onClick = null;
+  const startTimestampMs = 0;
+
+  it('should accept an empty list, and print that there are zero pages', () => {
+    const crumbs = [];
+
+    const results = splitCrumbs({
+      crumbs,
+      onClick,
+      startTimestampMs,
+    });
+    expect(results).toHaveLength(1);
+
+    render(results[0]);
+    expect(screen.getByText('0 Pages')).toBeInTheDocument();
+  });
+
+  it('should accept one crumb and return that single segment', () => {
+    const crumbs = [PAGELOAD_CRUMB];
+
+    const results = splitCrumbs({
+      crumbs,
+      onClick,
+      startTimestampMs,
+    });
+    expect(results).toHaveLength(1);
+
+    render(results[0]);
+    expect(screen.getByText('https://sourcemaps.io/')).toBeInTheDocument();
+  });
+
+  it('should accept three crumbs and return them all as individual segments', () => {
+    const crumbs = [PAGELOAD_CRUMB, NAV_CRUMB_BOOTSTRAP, NAV_CRUMB_UNDERSCORE];
+
+    const results = splitCrumbs({
+      crumbs,
+      onClick,
+      startTimestampMs,
+    });
+    expect(results).toHaveLength(3);
+
+    render(results[0]);
+    expect(screen.getByText('https://sourcemaps.io/')).toBeInTheDocument();
+
+    render(results[1]);
+    expect(
+      screen.getByText(
+        '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js'
+      )
+    ).toBeInTheDocument();
+
+    render(results[2]);
+    expect(
+      screen.getByText(
+        '/report/1669088273097_http%3A%2F%2Funderscorejs.org%2Funderscore-min.js'
+      )
+    ).toBeInTheDocument();
+  });
+
+  it('should accept more than three crumbs and summarize the middle ones', () => {
+    const crumbs = [
+      PAGELOAD_CRUMB,
+      NAV_CRUMB_BOOTSTRAP,
+      NAV_CRUMB_BOOTSTRAP,
+      NAV_CRUMB_BOOTSTRAP,
+      NAV_CRUMB_UNDERSCORE,
+    ];
+
+    const results = splitCrumbs({
+      crumbs,
+      onClick,
+      startTimestampMs,
+    });
+    expect(results).toHaveLength(3);
+
+    render(results[0]);
+    expect(screen.getByText('https://sourcemaps.io/')).toBeInTheDocument();
+
+    render(results[1]);
+    expect(screen.getByText('3 Pages')).toBeInTheDocument();
+
+    render(results[2]);
+    expect(
+      screen.getByText(
+        '/report/1669088273097_http%3A%2F%2Funderscorejs.org%2Funderscore-min.js'
+      )
+    ).toBeInTheDocument();
+  });
+
+  it('should show the summarized items on hover', () => {
+    const crumbs = [
+      PAGELOAD_CRUMB,
+      {...NAV_CRUMB_BOOTSTRAP, id: 1},
+      {...NAV_CRUMB_BOOTSTRAP, id: 2},
+      {...NAV_CRUMB_BOOTSTRAP, id: 3},
+      NAV_CRUMB_UNDERSCORE,
+    ];
+
+    const results = splitCrumbs({
+      crumbs,
+      onClick,
+      startTimestampMs,
+    });
+    expect(results).toHaveLength(3);
+
+    render(results[1]);
+    expect(screen.getByText('3 Pages')).toBeInTheDocument();
+    expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+
+    userEvent.hover(screen.getByText('3 Pages'));
+    waitFor(() => {
+      expect(screen.getAllByRole('listitem')).toHaveLength(3);
+    });
+  });
+});

+ 3 - 6
static/app/components/replays/walker/splitCrumbs.tsx

@@ -60,7 +60,7 @@ function splitCrumbs({
   return crumbs.map((crumb, i) => (
     <SingleLinkSegment
       key={i}
-      path={firstUrl}
+      path={crumb.data?.to?.split('?')?.[0]}
       onClick={onClick ? () => onClick(crumb as Crumb) : null}
     />
   ));
@@ -82,11 +82,8 @@ function SingleLinkSegment({
     </Tooltip>
   );
   if (onClick) {
-    return (
-      <Link href="#" onClick={onClick}>
-        {content}
-      </Link>
-    );
+    // TODO(replays): Add a href that deeplinks to `crumb.timestamp`
+    return <Link onClick={onClick}>{content}</Link>;
   }
   return <Span>{content}</Span>;
 }

+ 87 - 0
static/app/components/replays/walker/useWalker.spec.tsx

@@ -0,0 +1,87 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import type {Crumb} from 'sentry/types/breadcrumbs';
+import {BreadcrumbType} from 'sentry/types/breadcrumbs';
+
+import {CrumbWalker, StringWalker} from './urlWalker';
+
+describe('UrlWalker', () => {
+  describe('StringWalker', () => {
+    it('should accept a list of strings and render a <ChevronDividedList />', () => {
+      const urls = [
+        'https://sourcemaps.io/',
+        '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js',
+        '/report/1669088273097_http%3A%2F%2Funderscorejs.org%2Funderscore-min.js',
+        '/report/1669088971516_https%3A%2F%2Fcdn.ravenjs.com%2F3.17.0%2Fraven.min.js',
+      ];
+
+      render(<StringWalker urls={urls} />);
+
+      expect(screen.getByText('https://sourcemaps.io/')).toBeInTheDocument();
+      expect(screen.getByText('2 Pages')).toBeInTheDocument();
+      expect(
+        screen.getByText(
+          '/report/1669088971516_https%3A%2F%2Fcdn.ravenjs.com%2F3.17.0%2Fraven.min.js'
+        )
+      ).toBeInTheDocument();
+    });
+  });
+
+  describe('CrumbWalker', () => {
+    const {replayRecord} = TestStubs.ReplayReaderParams();
+
+    const PAGELOAD_CRUMB = TestStubs.Breadcrumb({
+      id: 4,
+      data: {
+        to: 'https://sourcemaps.io/',
+      },
+    }) as Crumb;
+
+    const NAV_CRUMB_BOOTSTRAP = TestStubs.Breadcrumb({
+      id: 5,
+      data: {
+        from: '/',
+        to: '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js',
+      },
+    }) as Crumb;
+
+    const NAV_CRUMB_UNDERSCORE = TestStubs.Breadcrumb({
+      id: 6,
+      data: {
+        from: '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js',
+        to: '/report/1669088273097_http%3A%2F%2Funderscorejs.org%2Funderscore-min.js',
+      },
+    }) as Crumb;
+
+    it('should accept a list of crumbs and render a <ChevronDividedList />', () => {
+      const crumbs = [
+        PAGELOAD_CRUMB,
+        NAV_CRUMB_BOOTSTRAP,
+        NAV_CRUMB_BOOTSTRAP,
+        NAV_CRUMB_BOOTSTRAP,
+        NAV_CRUMB_UNDERSCORE,
+      ];
+
+      render(<CrumbWalker crumbs={crumbs} replayRecord={replayRecord} />);
+
+      expect(screen.getByText('https://sourcemaps.io/')).toBeInTheDocument();
+      expect(screen.getByText('3 Pages')).toBeInTheDocument();
+      expect(
+        screen.getByText(
+          '/report/1669088273097_http%3A%2F%2Funderscorejs.org%2Funderscore-min.js'
+        )
+      ).toBeInTheDocument();
+    });
+
+    it('should filter out non-navigation crumbs', () => {
+      const ERROR_CRUMB = TestStubs.Breadcrumb({
+        type: BreadcrumbType.ERROR,
+      });
+
+      const crumbs = [ERROR_CRUMB];
+
+      render(<CrumbWalker crumbs={crumbs} replayRecord={replayRecord} />);
+      expect(screen.getByText('0 Pages')).toBeInTheDocument();
+    });
+  });
+});

+ 12 - 25
static/app/utils/replays/getCurrentUrl.spec.tsx

@@ -1,5 +1,5 @@
 import type {Crumb} from 'sentry/types/breadcrumbs';
-import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
+import {BreadcrumbLevelType} from 'sentry/types/breadcrumbs';
 import getCurrentUrl from 'sentry/utils/replays/getCurrentUrl';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 
@@ -9,47 +9,34 @@ const NAVIGATION_DATE = new Date('2022-06-15T00:46:00.333Z');
 const NEW_DOMAIN_DATE = new Date('2022-06-15T00:47:00.444Z');
 const END_DATE = new Date('2022-06-15T00:50:00.555Z');
 
-const PAGELOAD_CRUMB: Crumb = {
-  category: 'default',
-  type: BreadcrumbType.NAVIGATION,
-  timestamp: PAGELOAD_DATE.toISOString(),
-  level: BreadcrumbLevelType.INFO,
-  message: 'https://sourcemaps.io/',
+const PAGELOAD_CRUMB: Crumb = TestStubs.Breadcrumb({
   data: {
     to: 'https://sourcemaps.io/',
   },
   id: 6,
-  color: 'green300',
-  description: 'Navigation',
-};
+  message: 'https://sourcemaps.io/',
+  timestamp: PAGELOAD_DATE.toISOString(),
+});
 
-const NAV_CRUMB: Crumb = {
-  type: BreadcrumbType.NAVIGATION,
+const NAV_CRUMB: Crumb = TestStubs.Breadcrumb({
   category: 'navigation',
   data: {
     from: '/',
     to: '/report/1655300817078_https%3A%2F%2Fmaxcdn.bootstrapcdn.com%2Fbootstrap%2F3.3.7%2Fjs%2Fbootstrap.min.js',
   },
-  timestamp: NAVIGATION_DATE.toISOString(),
   id: 5,
-  color: 'green300',
-  description: 'Navigation',
   level: BreadcrumbLevelType.UNDEFINED,
-};
+  timestamp: NAVIGATION_DATE.toISOString(),
+});
 
-const NEW_DOMAIN_CRUMB: Crumb = {
-  category: 'default',
-  type: BreadcrumbType.NAVIGATION,
-  timestamp: NEW_DOMAIN_DATE.toISOString(),
-  level: BreadcrumbLevelType.INFO,
-  message: 'https://a062-174-94-6-155.ngrok.io/report/jquery.min.js',
+const NEW_DOMAIN_CRUMB: Crumb = TestStubs.Breadcrumb({
   data: {
     to: 'https://a062-174-94-6-155.ngrok.io/report/jquery.min.js',
   },
   id: 29,
-  color: 'green300',
-  description: 'Navigation',
-};
+  message: 'https://a062-174-94-6-155.ngrok.io/report/jquery.min.js',
+  timestamp: NEW_DOMAIN_DATE.toISOString(),
+});
 
 describe('getCurrentUrl', () => {
   let replayRecord;