]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12914 security hotspots autoscroll to category
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 28 Apr 2020 15:59:46 +0000 (17:59 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 7 May 2020 20:03:44 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
server/sonar-web/src/main/js/helpers/urls.ts

index ad55fcd971b80ee0c773d53bb585ba257ad47cb3..39ca9c585517b0e46cbbc1d575abc31e85cf6f9a 100644 (file)
@@ -124,6 +124,7 @@ exports[`should render correctly for hotspots 1`] = `
         "query": Object {
           "assignedToMe": undefined,
           "branch": undefined,
+          "category": undefined,
           "hotspots": undefined,
           "id": "my-project",
           "pullRequest": "1001",
@@ -157,6 +158,7 @@ exports[`should render correctly for hotspots 2`] = `
         "query": Object {
           "assignedToMe": undefined,
           "branch": undefined,
+          "category": undefined,
           "hotspots": undefined,
           "id": "my-project",
           "pullRequest": "1001",
index 5367fb99884132b052181317f11a7c005c4f43dc..2ed7abdd10dc27d37d3e8706a9a0d1413b8e9862 100644 (file)
@@ -144,12 +144,22 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
           return;
         }
 
+        const requestedCategory = this.props.location.query.category;
+
+        let selectedHotspot;
+        if (hotspots.length > 0) {
+          const hotspotForCategory = requestedCategory
+            ? hotspots.find(h => h.securityCategory === requestedCategory)
+            : undefined;
+          selectedHotspot = hotspotForCategory ?? hotspots[0];
+        }
+
         this.setState({
           hotspots,
           hotspotsTotal: paging.total,
           loading: false,
           securityCategories: sonarsourceSecurity,
-          selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined
+          selectedHotspot
         });
       })
       .catch(this.handleCallFailure);
index c9d8b687f841a8fb5771b14c1e10effe0f560668..43e4dfdcf6591670cd1f002dbaa1911900cb86d1 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
 import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget';
 import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
@@ -69,6 +70,17 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
     filters
   } = props;
 
+  const scrollableRef = React.useRef(null);
+
+  React.useEffect(() => {
+    const parent = scrollableRef.current;
+    const element =
+      selectedHotspot && document.querySelector(`[data-hotspot-key="${selectedHotspot.key}"]`);
+    if (parent && element) {
+      scrollToElement(element, { parent, smooth: true, topOffset: 150, bottomOffset: 400 });
+    }
+  }, [selectedHotspot]);
+
   return (
     <div id="security_hotspots">
       <FilterBar
@@ -89,47 +101,44 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
 
             <A11ySkipTarget anchor="security_hotspots_main" />
 
-            {loading ? (
-              <DeferredSpinner className="huge-spacer-left big-spacer-top" />
-            ) : (
-              <>
-                {hotspots.length === 0 || !selectedHotspot ? (
-                  <EmptyHotspotsPage
-                    filtered={
-                      filters.assignedToMe ||
-                      (isBranch(branchLike) && filters.sinceLeakPeriod) ||
-                      filters.status !== HotspotStatusFilter.TO_REVIEW
-                    }
-                    isStaticListOfHotspots={isStaticListOfHotspots}
-                  />
-                ) : (
-                  <div className="layout-page">
-                    <div className="sidebar">
-                      <HotspotList
-                        hotspots={hotspots}
-                        hotspotsTotal={hotspotsTotal}
-                        isStaticListOfHotspots={isStaticListOfHotspots}
-                        loadingMore={loadingMore}
-                        onHotspotClick={props.onHotspotClick}
-                        onLoadMore={props.onLoadMore}
-                        securityCategories={securityCategories}
-                        selectedHotspot={selectedHotspot}
-                        statusFilter={filters.status}
-                      />
-                    </div>
-                    <div className="main">
-                      <HotspotViewer
-                        branchLike={branchLike}
-                        component={component}
-                        hotspotKey={selectedHotspot.key}
-                        onUpdateHotspot={props.onUpdateHotspot}
-                        securityCategories={securityCategories}
-                      />
-                    </div>
+            {loading && <DeferredSpinner className="huge-spacer-left big-spacer-top" />}
+
+            {!loading &&
+              (hotspots.length === 0 || !selectedHotspot ? (
+                <EmptyHotspotsPage
+                  filtered={
+                    filters.assignedToMe ||
+                    (isBranch(branchLike) && filters.sinceLeakPeriod) ||
+                    filters.status !== HotspotStatusFilter.TO_REVIEW
+                  }
+                  isStaticListOfHotspots={isStaticListOfHotspots}
+                />
+              ) : (
+                <div className="layout-page">
+                  <div className="sidebar" ref={scrollableRef}>
+                    <HotspotList
+                      hotspots={hotspots}
+                      hotspotsTotal={hotspotsTotal}
+                      isStaticListOfHotspots={isStaticListOfHotspots}
+                      loadingMore={loadingMore}
+                      onHotspotClick={props.onHotspotClick}
+                      onLoadMore={props.onLoadMore}
+                      securityCategories={securityCategories}
+                      selectedHotspot={selectedHotspot}
+                      statusFilter={filters.status}
+                    />
+                  </div>
+                  <div className="main">
+                    <HotspotViewer
+                      branchLike={branchLike}
+                      component={component}
+                      hotspotKey={selectedHotspot.key}
+                      onUpdateHotspot={props.onUpdateHotspot}
+                      securityCategories={securityCategories}
+                    />
                   </div>
-                )}
-              </>
-            )}
+                </div>
+              ))}
           </div>
         )}
       </ScreenPositionHelper>
index 8692f0fc3455c281a2967dbb79b05ba863daf490..4921949f20b98b926798de8057f640f1b8051237 100644 (file)
@@ -107,6 +107,25 @@ it('should load data correctly', async () => {
   expect(wrapper.state().hotspotsReviewedMeasure).toBe('86.6');
 });
 
+it('should handle category request', async () => {
+  const hotspots = [mockRawHotspot(), mockRawHotspot({ securityCategory: 'log-injection' })];
+  (getSecurityHotspots as jest.Mock).mockResolvedValue({
+    hotspots,
+    paging: {
+      total: 1
+    }
+  });
+  (getMeasures as jest.Mock).mockResolvedValue([{ value: '86.6' }]);
+
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: hotspots[1].securityCategory } })
+  });
+
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.state().selectedHotspot).toBe(hotspots[1]);
+});
+
 it('should load data correctly when hotspot key list is forced', async () => {
   const hotspots = [
     mockRawHotspot({ key: 'test1' }),
index 5cdbc37d471a5eca69cae193e6d0ce75001e3578..af6beba4db403627ab3f35e7d56e0dae9a43e89d 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
 import { mockComponent } from '../../../helpers/testMocks';
@@ -28,6 +29,14 @@ import SecurityHotspotsAppRenderer, {
   SecurityHotspotsAppRendererProps
 } from '../SecurityHotspotsAppRenderer';
 
+jest.mock('sonar-ui-common/helpers/scrolling', () => ({
+  scrollToElement: jest.fn()
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
   expect(
@@ -42,6 +51,11 @@ it('should render correctly', () => {
       .find(ScreenPositionHelper)
       .dive()
   ).toMatchSnapshot('no hotspots');
+  expect(
+    shallowRender({ loading: true })
+      .find(ScreenPositionHelper)
+      .dive()
+  ).toMatchSnapshot('loading');
 });
 
 it('should render correctly with hotspots', () => {
@@ -70,6 +84,39 @@ it('should properly propagate the "show all" call', () => {
   expect(onShowAllHotspots).toHaveBeenCalled();
 });
 
+describe('side effect', () => {
+  const fakeElement = document.createElement('span');
+  const fakeParent = document.createElement('div');
+
+  beforeEach(() => {
+    jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+    jest.spyOn(document, 'querySelector').mockImplementationOnce(() => fakeElement);
+    jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: fakeParent }));
+  });
+
+  it('should trigger scrolling', () => {
+    shallowRender({ selectedHotspot: mockRawHotspot() });
+
+    expect(scrollToElement).toBeCalledWith(
+      fakeElement,
+      expect.objectContaining({ parent: fakeParent })
+    );
+  });
+
+  it('should not trigger scrolling if no selected hotspot', () => {
+    shallowRender();
+    expect(scrollToElement).not.toBeCalled();
+  });
+
+  it('should not trigger scrolling if no parent', () => {
+    const mockUseRef = jest.spyOn(React, 'useRef');
+    mockUseRef.mockReset();
+    mockUseRef.mockImplementationOnce(() => ({ current: null }));
+    shallowRender({ selectedHotspot: mockRawHotspot() });
+    expect(scrollToElement).not.toBeCalled();
+  });
+});
+
 function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
   return shallow(
     <SecurityHotspotsAppRenderer
index 0aba55e85eb9643c446076e134af52dcbeac5b40..cfbe10346e0cd8a35d21c04539e1171ca9132231 100644 (file)
@@ -201,6 +201,35 @@ exports[`should render correctly with hotspots 2`] = `
 </div>
 `;
 
+exports[`should render correctly: loading 1`] = `
+<div>
+  <div
+    className="wrapper"
+    style={
+      Object {
+        "top": 0,
+      }
+    }
+  >
+    <Suggestions
+      suggestions="security_hotspots"
+    />
+    <Helmet
+      defer={true}
+      encodeSpecialCharacters={true}
+      title="hotspots.page"
+    />
+    <A11ySkipTarget
+      anchor="security_hotspots_main"
+    />
+    <DeferredSpinner
+      className="huge-spacer-left big-spacer-top"
+      timeout={100}
+    />
+  </div>
+</div>
+`;
+
 exports[`should render correctly: no hotspots 1`] = `
 <div>
   <div
index 2e9664662ef18f436c42d69ce68f1ab05400c3b8..641698ec58f153ab5414092e4151752831752353 100644 (file)
@@ -65,7 +65,7 @@ export default function HotspotCategory(props: HotspotCategoryProps) {
       {expanded && (
         <ul>
           {hotspots.map(h => (
-            <li key={h.key}>
+            <li data-hotspot-key={h.key} key={h.key}>
               <HotspotListItem
                 hotspot={h}
                 onClick={props.onHotspotClick}
index 892425e8a150500edb42544368de54431520ec90..e0d27a7c2d6cfa563838e42f8453d82779708441 100644 (file)
@@ -27,6 +27,7 @@ exports[`should render correctly with hotspots 1`] = `
   </a>
   <ul>
     <li
+      data-hotspot-key="h1"
       key="h1"
     >
       <HotspotListItem
@@ -52,6 +53,7 @@ exports[`should render correctly with hotspots 1`] = `
       />
     </li>
     <li
+      data-hotspot-key="h2"
       key="h2"
     >
       <HotspotListItem
@@ -135,6 +137,7 @@ exports[`should render correctly with hotspots: contains selected 1`] = `
   </a>
   <ul>
     <li
+      data-hotspot-key="h1"
       key="h1"
     >
       <HotspotListItem
@@ -160,6 +163,7 @@ exports[`should render correctly with hotspots: contains selected 1`] = `
       />
     </li>
     <li
+      data-hotspot-key="h2"
       key="h2"
     >
       <HotspotListItem
index ee7e4bf07ed5885de1bbe94512ad2543d0d2a48b..6678ccad309b67ca6c6b41927945058be27b3644 100644 (file)
@@ -93,10 +93,18 @@ export function getComponentIssuesUrl(componentKey: string, query?: Query): Loca
  * Generate URL for a component's security hotspot page
  */
 export function getComponentSecurityHotspotsUrl(componentKey: string, query: Query = {}): Location {
-  const { branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe } = query;
+  const { branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe, category } = query;
   return {
     pathname: '/security_hotspots',
-    query: { id: componentKey, branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe }
+    query: {
+      id: componentKey,
+      branch,
+      pullRequest,
+      sinceLeakPeriod,
+      hotspots,
+      assignedToMe,
+      category
+    }
   };
 }