if (this.node) {
const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`);
if (element) {
- this.handleScroll(element, smooth);
+ this.handleScroll(element, undefined, smooth);
}
}
};
- handleScroll = (element: Element, smooth = true) => {
- const offset = window.innerHeight / 2;
+ handleScroll = (element: Element, offset = window.innerHeight / 2, smooth = true) => {
scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth });
};
index: number,
line: number
) => React.ReactNode;
- scroll?: (element: HTMLElement) => void;
+ scroll?: (element: HTMLElement, offset: number) => void;
snippetGroup: T.SnippetGroup;
}
}
}
- expandBlock = (snippetIndex: number, direction: T.ExpandDirection) => {
+ expandBlock = (snippetIndex: number, direction: T.ExpandDirection): Promise<void> => {
const { branchLike, snippetGroup } = this.props;
const { key } = snippetGroup.component;
const { snippets } = this.state;
const snippet = snippets.find(s => s.index === snippetIndex);
if (!snippet) {
- return;
+ return Promise.reject();
}
// Extend by EXPAND_BY_LINES and add buffer for merging snippets
const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
from: snippet.end + 1,
to: snippet.end + extension
};
- getSources({
+ return getSources({
key,
...range,
...getBranchLikeQuery(branchLike)
return lineMap;
}, {})
)
- .then(
- newLinesMapped => this.animateBlockExpansion(snippetIndex, direction, newLinesMapped),
- () => {}
- );
+ .then(newLinesMapped => this.animateBlockExpansion(snippetIndex, direction, newLinesMapped));
};
animateBlockExpansion(
snippetIndex: number,
direction: T.ExpandDirection,
newLinesMapped: T.Dict<T.SourceLine>
- ) {
+ ): Promise<void> {
if (this.mounted) {
const { snippets } = this.state;
deletedSnippets.forEach(s => this.setMaxHeight(s.index));
this.setMaxHeight(snippetIndex);
- this.setState(
- ({ additionalLines, snippets }) => {
- const combinedLines = { ...additionalLines, ...newLinesMapped };
- return {
- additionalLines: combinedLines,
- snippets
- };
- },
- () => {
- // Set max-height 0 to trigger CSS transitions
- deletedSnippets.forEach(s => {
- this.setMaxHeight(s.index, 0);
- });
- this.setMaxHeight(snippetIndex, undefined, direction === 'up');
-
- // Wait for transition to finish before updating dom
- setTimeout(() => {
- this.setState({ snippets: newSnippets.filter(s => !s.toDelete) });
- this.cleanDom(snippetIndex);
- }, 200);
- }
- );
+ return new Promise(resolve => {
+ this.setState(
+ ({ additionalLines, snippets }) => {
+ const combinedLines = { ...additionalLines, ...newLinesMapped };
+ return {
+ additionalLines: combinedLines,
+ snippets
+ };
+ },
+ () => {
+ // Set max-height 0 to trigger CSS transitions
+ deletedSnippets.forEach(s => {
+ this.setMaxHeight(s.index, 0);
+ });
+ this.setMaxHeight(snippetIndex, undefined, direction === 'up');
+
+ // Wait for transition to finish before updating dom
+ setTimeout(() => {
+ this.setState({ snippets: newSnippets.filter(s => !s.toDelete) }, resolve);
+ this.cleanDom(snippetIndex);
+ }, 200);
+ }
+ );
+ });
}
+ return Promise.resolve();
}
expandComponent = () => {
component: T.SourceViewerFile;
duplications?: T.Duplication[];
duplicationsByLine?: { [line: number]: number[] };
- expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => void;
+ expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => Promise<void>;
handleCloseIssues: (line: T.SourceLine) => void;
handleLinePopupToggle: (line: T.SourceLine) => void;
handleOpenIssues: (line: T.SourceLine) => void;
onLocationSelect: (index: number) => void;
openIssuesByLine: T.Dict<boolean>;
renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
- scroll?: (element: HTMLElement) => void;
+ scroll?: (element: HTMLElement, offset?: number) => void;
snippet: T.SourceLine[];
}
const SCROLL_LEFT_OFFSET = 32;
export default class SnippetViewer extends React.PureComponent<Props> {
- node: React.RefObject<HTMLDivElement>;
+ snippetNodeRef: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
super(props);
- this.node = React.createRef();
+ this.snippetNodeRef = React.createRef();
}
doScroll = (element: HTMLElement) => {
if (this.props.scroll) {
this.props.scroll(element);
}
- const parent = this.node.current as Element;
+ const parent = this.snippetNodeRef.current as Element;
if (parent) {
scrollHorizontally(element, {
}
};
+ scrollToLastExpandedRow = () => {
+ if (this.props.scroll) {
+ const snippetNode = this.snippetNodeRef.current as Element;
+ if (!snippetNode) {
+ return;
+ }
+ const rows = snippetNode.querySelectorAll('tr');
+ const lastRow = rows[rows.length - 1];
+ this.props.scroll(lastRow, 100);
+ }
+ };
+
expandBlock = (direction: T.ExpandDirection) => () =>
- this.props.expandBlock(this.props.index, direction);
+ this.props.expandBlock(this.props.index, direction).then(() => {
+ if (direction === 'down') {
+ this.scrollToLastExpandedRow();
+ }
+ });
renderLine({
displayDuplications,
const displayDuplications = snippet.some(s => !!s.duplicated);
return (
- <div className="source-viewer-code snippet" ref={this.node}>
+ <div className="source-viewer-code snippet" ref={this.snippetNodeRef}>
<div>
{snippet[0].line > 1 && (
<div className="expand-block expand-block-above">
mockDom(rootNode.current!);
expect(wrapper.instance().getNodes(3)).not.toBeUndefined();
});
+
+ it('should enable cleaning the dom', async () => {
+ await waitAndUpdate(wrapper);
+ const rootNode = wrapper.instance().rootNodeRef;
+ mockDom(rootNode.current!);
+
+ expect(wrapper.instance().cleanDom(3));
+ const nodes = wrapper.instance().getNodes(3);
+ expect(nodes!.wrapper.style.maxHeight).toBe('');
+ expect(nodes!.table.style.marginTop).toBe('');
+ });
});
describe('getHeight', () => {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import { range } from 'lodash';
import * as React from 'react';
+import { scrollHorizontally } from 'sonar-ui-common/helpers/scrolling';
import {
mockIssue,
mockMainBranch,
} from '../../../../helpers/testMocks';
import SnippetViewer from '../SnippetViewer';
+jest.mock('sonar-ui-common/helpers/scrolling', () => ({
+ scrollHorizontally: jest.fn()
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
it('should render correctly', () => {
const snippet = range(5, 8).map(line => mockSourceLine({ line }));
const wrapper = shallowRender({
it('should correctly handle expansion', () => {
const snippet = range(5, 8).map(line => mockSourceLine({ line }));
- const expandBlock = jest.fn();
+ const expandBlock = jest.fn(() => Promise.resolve());
const wrapper = shallowRender({
expandBlock,
expect(expandBlock).toHaveBeenCalledWith(2, 'down');
});
+it('should handle scrolling', () => {
+ const scroll = jest.fn();
+ const wrapper = mountRender({ scroll });
+
+ const element = {} as HTMLElement;
+
+ wrapper.instance().doScroll(element);
+
+ expect(scroll).toHaveBeenCalledWith(element);
+
+ expect(scrollHorizontally).toHaveBeenCalled();
+ expect((scrollHorizontally as jest.Mock).mock.calls[0][0]).toBe(element);
+});
+
+it('should handle scrolling to expanded row', () => {
+ const scroll = jest.fn();
+ const wrapper = mountRender({ scroll });
+
+ wrapper.instance().scrollToLastExpandedRow();
+
+ expect(scroll).toHaveBeenCalled();
+});
+
function shallowRender(props: Partial<SnippetViewer['props']> = {}) {
return shallow<SnippetViewer>(
<SnippetViewer
/>
);
}
+
+function mountRender(props: Partial<SnippetViewer['props']> = {}) {
+ return mount<SnippetViewer>(
+ <SnippetViewer
+ branchLike={mockMainBranch()}
+ component={mockSourceViewerFile()}
+ duplications={undefined}
+ duplicationsByLine={undefined}
+ expandBlock={jest.fn()}
+ handleCloseIssues={jest.fn()}
+ handleLinePopupToggle={jest.fn()}
+ handleOpenIssues={jest.fn()}
+ handleSymbolClick={jest.fn()}
+ highlightedLocationMessage={{ index: 0, text: '' }}
+ highlightedSymbols={[]}
+ index={0}
+ issue={mockIssue()}
+ issuesByLine={{}}
+ last={false}
+ linePopup={undefined}
+ loadDuplications={jest.fn()}
+ locations={[]}
+ locationsByLine={{}}
+ onIssueChange={jest.fn()}
+ onIssuePopupToggle={jest.fn()}
+ onLocationSelect={jest.fn()}
+ openIssuesByLine={{}}
+ renderDuplicationPopup={jest.fn()}
+ scroll={jest.fn()}
+ snippet={[mockSourceLine()]}
+ {...props}
+ />
+ );
+}
expect(results).toHaveLength(3);
expect(results[0]).toEqual({ index: 0, start: 7, end: 21 });
});
+
+ it('should work for location with no textrange', () => {
+ const locations = [
+ mockFlowLocation({
+ textRange: { startLine: 85, startOffset: 2, endLine: 85, endOffset: 3 }
+ })
+ ];
+
+ const results = createSnippets({
+ locations,
+ issue: mockIssue(false, {
+ textRange: undefined
+ }),
+ addIssueLocation: true
+ });
+
+ expect(results).toHaveLength(2);
+ expect(results[0]).toEqual({ index: 0, start: 1, end: 9 });
+ });
});
describe('expandSnippet', () => {
/>
);
expect(element.find('LocationIndex')).toMatchSnapshot();
+
+ const elementWithLink = shallow(
+ <IssueTitleBar
+ displayLocationsCount={true}
+ displayLocationsLink={true}
+ issue={issueWithLocations}
+ togglePopup={jest.fn()}
+ />
+ );
+ expect(elementWithLink.find('LocationIndex')).toMatchSnapshot();
});
it('should have a correct permalink for security hotspots', () => {
</LocationIndex>
`;
+exports[`should count all code locations 2`] = `
+<LocationIndex>
+ 7
+</LocationIndex>
+`;
+
exports[`should render the titlebar correctly 1`] = `
<div
className="issue-row"