aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2022-02-23 14:56:36 +0100
committersonartech <sonartech@sonarsource.com>2022-02-25 20:02:55 +0000
commitc3602fdcd7063f15fedf56dea5f13756ae0aa7f1 (patch)
tree75a3f4f9f47c01195d144db7eaf78afba5bada0f /server
parent74dca99afdecb586f16c65e93ba2a9114b1d5fc8 (diff)
downloadsonarqube-c3602fdcd7063f15fedf56dea5f13756ae0aa7f1.tar.gz
sonarqube-c3602fdcd7063f15fedf56dea5f13756ae0aa7f1.zip
SONAR-16007: Move through locations using keyboard
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx85
2 files changed, 134 insertions, 3 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index 975dd3c674f..845dcf1e591 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -28,6 +28,7 @@ import { withCurrentUser } from '../../components/hoc/withCurrentUser';
import { Router } from '../../components/hoc/withRouter';
import { getLeakValue } from '../../components/measure/utils';
import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpers/branch-like';
+import { KeyboardCodes, KeyboardKeys } from '../../helpers/keycodes';
import { scrollToElement } from '../../helpers/scrolling';
import { getStandards } from '../../helpers/security-standard';
import { isLoggedIn } from '../../helpers/users';
@@ -44,7 +45,7 @@ import {
import { Component, CurrentUser, Dict } from '../../types/types';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
import './styles.css';
-import { SECURITY_STANDARDS } from './utils';
+import { getLocations, SECURITY_STANDARDS } from './utils';
const HOTSPOT_KEYMASTER_SCOPE = 'hotspots-list';
const PAGE_SIZE = 500;
@@ -151,9 +152,57 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
this.selectNeighboringHotspot(+1);
return false;
});
+ window.addEventListener('keydown', this.handleKeyDown);
}
+ handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === KeyboardKeys.Alt) {
+ event.preventDefault();
+ } else if (event.code === KeyboardCodes.DownArrow && event.altKey) {
+ event.preventDefault();
+ this.selectNextLocation();
+ } else if (event.code === KeyboardCodes.UpArrow && event.altKey) {
+ event.preventDefault();
+ this.selectPreviousLocation();
+ }
+ };
+
+ selectNextLocation = () => {
+ const { selectedHotspotLocationIndex, selectedHotspot } = this.state;
+ if (selectedHotspot === undefined) {
+ return;
+ }
+ const locations = getLocations(selectedHotspot.flows, undefined);
+ if (locations.length === 0) {
+ return;
+ }
+ const lastIndex = locations.length - 1;
+
+ let newIndex;
+ if (selectedHotspotLocationIndex === undefined) {
+ newIndex = 0;
+ } else if (selectedHotspotLocationIndex === lastIndex) {
+ newIndex = undefined;
+ } else {
+ newIndex = selectedHotspotLocationIndex + 1;
+ }
+ this.setState({ selectedHotspotLocationIndex: newIndex });
+ };
+
+ selectPreviousLocation = () => {
+ const { selectedHotspotLocationIndex } = this.state;
+
+ let newIndex;
+ if (selectedHotspotLocationIndex === 0) {
+ newIndex = undefined;
+ } else if (selectedHotspotLocationIndex !== undefined) {
+ newIndex = selectedHotspotLocationIndex - 1;
+ }
+ this.setState({ selectedHotspotLocationIndex: newIndex });
+ };
+
selectNeighboringHotspot = (shift: number) => {
+ this.setState({ selectedHotspotLocationIndex: undefined });
this.setState(({ hotspots, selectedHotspot }) => {
const index = selectedHotspot && hotspots.findIndex(h => h.key === selectedHotspot.key);
@@ -170,6 +219,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
unregisterKeyboardEvents() {
key.deleteScope(HOTSPOT_KEYMASTER_SCOPE);
+ window.removeEventListener('keydown', this.handleKeyDown);
}
constructFiltersFromProps(
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
index ed73077f227..c530c789ff6 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
@@ -28,11 +28,13 @@ import { mockRawHotspot, mockStandards } from '../../../helpers/mocks/security-h
import { getStandards } from '../../../helpers/security-standard';
import {
mockCurrentUser,
+ mockEvent,
+ mockFlowLocation,
mockLocation,
mockLoggedInUser,
mockRouter
} from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
+import { KEYCODE_MAP, waitAndUpdate } from '../../../helpers/testUtils';
import { SecurityStandard } from '../../../types/security';
import {
HotspotResolution,
@@ -42,8 +44,29 @@ import {
import { SecurityHotspotsApp } from '../SecurityHotspotsApp';
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
import { scrollToElement } from '../../../helpers/scrolling';
+import { KeyboardCodes } from '../../../helpers/keycodes';
-beforeEach(() => jest.clearAllMocks());
+const originalAddEventListener = window.addEventListener;
+const originalRemoveEventListener = window.removeEventListener;
+
+beforeEach(() => {
+ Object.defineProperty(window, 'addEventListener', {
+ value: jest.fn()
+ });
+ Object.defineProperty(window, 'removeEventListener', {
+ value: jest.fn()
+ });
+ jest.clearAllMocks();
+});
+
+afterEach(() => {
+ Object.defineProperty(window, 'addEventListener', {
+ value: originalAddEventListener
+ });
+ Object.defineProperty(window, 'removeEventListener', {
+ value: originalRemoveEventListener
+ });
+});
jest.mock('../../../api/measures', () => ({
getMeasures: jest.fn().mockResolvedValue([])
@@ -62,6 +85,27 @@ jest.mock('../../../helpers/scrolling', () => ({
scrollToElement: jest.fn()
}));
+jest.mock('keymaster', () => {
+ const key: any = (bindKey: string, _: string, callback: Function) => {
+ document.addEventListener('keydown', (event: KeyboardEvent) => {
+ const keymasterCode = event.code && KEYCODE_MAP[event.code as KeyboardCodes];
+ if (keymasterCode && bindKey.split(',').includes(keymasterCode)) {
+ return callback();
+ }
+ return true;
+ });
+ };
+ let scope = 'hotspots-list';
+
+ key.getScope = () => scope;
+ key.setScope = (newScope: string) => {
+ scope = newScope;
+ };
+ key.deleteScope = jest.fn();
+
+ return key;
+});
+
const branch = mockBranch();
it('should render correctly', () => {
@@ -426,6 +470,11 @@ describe('keyboard navigation', () => {
mockRawHotspot({ key: 'k2' }),
mockRawHotspot({ key: 'k3' })
];
+ const flowsData = {
+ flows: [{ locations: [mockFlowLocation(), mockFlowLocation(), mockFlowLocation()] }]
+ };
+ const hotspotsForLocation = mockRawHotspot(flowsData);
+
(getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ hotspots, paging: { total: 3 } });
const wrapper = shallowRender();
@@ -444,6 +493,38 @@ describe('keyboard navigation', () => {
expect(wrapper.state().selectedHotspot).toBe(hotspots[expected]);
});
+
+ it.each([
+ ['selecting next locations when nothing is selected', undefined, 0],
+ ['selecting next locations', 0, 1],
+ ['selecting next locations, non-existent', 2, undefined]
+ ])('should work when %s', (_, start, expected) => {
+ wrapper.setState({ selectedHotspotLocationIndex: start, selectedHotspot: hotspotsForLocation });
+ wrapper.instance().handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.DownArrow }));
+
+ expect(wrapper.state().selectedHotspotLocationIndex).toBe(expected);
+ });
+
+ it.each([
+ ['selecting previous locations when nothing is selected', undefined, undefined],
+ ['selecting previous locations', 1, 0],
+ ['selecting previous locations, non-existent', 0, undefined]
+ ])('should work when %s', (_, start, expected) => {
+ wrapper.setState({ selectedHotspotLocationIndex: start, selectedHotspot: hotspotsForLocation });
+ wrapper.instance().handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.UpArrow }));
+
+ expect(wrapper.state().selectedHotspotLocationIndex).toBe(expected);
+ });
+
+ it('should not change location index when locations are empty', () => {
+ wrapper.setState({ selectedHotspotLocationIndex: undefined, selectedHotspot: hotspots[0] });
+
+ wrapper.instance().handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.UpArrow }));
+ expect(wrapper.state().selectedHotspotLocationIndex).toBeUndefined();
+
+ wrapper.instance().handleKeyDown(mockEvent({ altKey: true, code: KeyboardCodes.DownArrow }));
+ expect(wrapper.state().selectedHotspotLocationIndex).toBeUndefined();
+ });
});
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) {