"query": Object {
"assignedToMe": undefined,
"branch": undefined,
+ "category": undefined,
"hotspots": undefined,
"id": "my-project",
"pullRequest": "1001",
"query": Object {
"assignedToMe": undefined,
"branch": undefined,
+ "category": undefined,
"hotspots": undefined,
"id": "my-project",
"pullRequest": "1001",
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);
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';
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
<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>
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' }),
*/
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';
SecurityHotspotsAppRendererProps
} from '../SecurityHotspotsAppRenderer';
+jest.mock('sonar-ui-common/helpers/scrolling', () => ({
+ scrollToElement: jest.fn()
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(
.find(ScreenPositionHelper)
.dive()
).toMatchSnapshot('no hotspots');
+ expect(
+ shallowRender({ loading: true })
+ .find(ScreenPositionHelper)
+ .dive()
+ ).toMatchSnapshot('loading');
});
it('should render correctly with hotspots', () => {
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
</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
{expanded && (
<ul>
{hotspots.map(h => (
- <li key={h.key}>
+ <li data-hotspot-key={h.key} key={h.key}>
<HotspotListItem
hotspot={h}
onClick={props.onHotspotClick}
</a>
<ul>
<li
+ data-hotspot-key="h1"
key="h1"
>
<HotspotListItem
/>
</li>
<li
+ data-hotspot-key="h2"
key="h2"
>
<HotspotListItem
</a>
<ul>
<li
+ data-hotspot-key="h1"
key="h1"
>
<HotspotListItem
/>
</li>
<li
+ data-hotspot-key="h2"
key="h2"
>
<HotspotListItem
* 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
+ }
};
}