* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as navigationTreeSonarCloud from 'Docs/../static/SonarCloudNavigationTree.json';
import * as navigationTreeSonarQube from 'Docs/../static/SonarQubeNavigationTree.json';
import { DocNavigationItem } from 'Docs/@types/types';
import * as React from 'react';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock';
import { ParsedContent, separateFrontMatter } from '../../../helpers/markdown';
-import { isSonarCloud } from '../../../helpers/system';
import { InstalledPlugin, PluginType } from '../../../types/plugins';
import { getUrlsList } from '../navTreeUtils';
import getPages from '../pages';
interface Props {
params: { splat?: string };
+ location: { hash: string };
}
interface State {
this.setState({ loading: true });
- const tree = isSonarCloud()
- ? ((navigationTreeSonarCloud as any).default as DocNavigationItem[])
- : ((navigationTreeSonarQube as any).default as DocNavigationItem[]);
+ const tree = (navigationTreeSonarQube as any).default as DocNavigationItem[];
this.getLanguagePluginsDocumentation(tree).then(
overrides => {
render() {
const { loading, pages, tree } = this.state;
- const { splat = '' } = this.props.params;
+ const {
+ params: { splat = '' },
+ location: { hash }
+ } = this.props;
if (loading) {
return (
}
const page = pages.find(p => p.url === `/${splat}`);
- const mainTitle = translate(
- 'documentation.page_title',
- isSonarCloud() ? 'sonarcloud' : 'sonarqube'
- );
+ const mainTitle = translate('documentation.page_title.sonarqube');
const isIndex = splat === 'index';
if (!page) {
<Helmet
defer={false}
title={isIndex || !page.title ? mainTitle : `${page.title} | ${mainTitle}`}>
- {!isSonarCloud() && <meta content="noindex nofollow" name="robots" />}
+ <meta content="noindex nofollow" name="robots" />
</Helmet>
<ScreenPositionHelper className="layout-page-side-outer">
content={page.content}
stickyToc={true}
title={page.title}
+ scrollToHref={hash}
/>
</div>
</div>
import { addSideBarClass, removeSideBarClass } from 'sonar-ui-common/helpers/pages';
import { request } from 'sonar-ui-common/helpers/request';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { isSonarCloud } from '../../../../helpers/system';
import { InstalledPlugin } from '../../../../types/plugins';
import getPages from '../../pages';
import App from '../App';
jest.mock('../../../../components/common/ScreenPositionHelper');
-jest.mock('../../../../helpers/system', () => ({
- isSonarCloud: jest.fn().mockReturnValue(false)
-}));
-
jest.mock('Docs/../static/SonarQubeNavigationTree.json', () => ({
default: [
{
]
}));
-jest.mock('Docs/../static/SonarCloudNavigationTree.json', () => ({
- default: [
- {
- title: 'SonarCloud',
- children: [
- '/lorem/ipsum/',
- {
- title: 'Child category',
- children: [
- '/lorem/ipsum/dolor',
- {
- title: 'Grandchild category',
- children: ['/lorem/ipsum/sit']
- },
- '/lorem/ipsum/amet'
- ]
- }
- ]
- }
- ]
-}));
-
jest.mock('sonar-ui-common/helpers/pages', () => ({
addSideBarClass: jest.fn(),
removeSideBarClass: jest.fn()
expect(removeSideBarClass).toBeCalled();
});
-it('should render correctly for SonarCloud', async () => {
- (isSonarCloud as jest.Mock).mockReturnValue(true);
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
it("should show a 404 if the page doesn't exist", async () => {
const wrapper = shallowRender({ params: { splat: 'unknown' } });
await waitAndUpdate(wrapper);
});
it('should try to fetch language plugin documentation if documentationPath matches', async () => {
- (isSonarCloud as jest.Mock).mockReturnValue(false);
-
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
});
it('should display the issue tracker url of the plugin if it exists', async () => {
- (isSonarCloud as jest.Mock).mockReturnValue(false);
-
const wrapper = shallowRender({ params: { splat: 'analysis/languages/csharp/' } });
await waitAndUpdate(wrapper);
});
function shallowRender(props: Partial<App['props']> = {}) {
- return shallow(<App params={{ splat: 'lorem/ipsum' }} {...props} />);
+ return shallow(<App params={{ splat: 'lorem/ipsum' }} location={{ hash: '#foo' }} {...props} />);
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly for SonarCloud 1`] = `
-<div
- className="layout-page"
->
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- title="Lorem | documentation.page_title.sonarcloud"
- />
- <ScreenPositionHelper
- className="layout-page-side-outer"
- >
- <Component />
- </ScreenPositionHelper>
- <div
- className="layout-page-main"
- >
- <div
- className="layout-page-main-inner"
- >
- <div
- className="boxed-group"
- >
- <A11ySkipTarget
- anchor="documentation_main"
- />
- <DocMarkdownBlock
- className="documentation-content cut-margins boxed-group-inner"
- content="Lorem ipsum dolor sit amet fredum"
- stickyToc={true}
- title="Lorem"
- />
- </div>
- </div>
- </div>
-</div>
-`;
-
exports[`should render correctly for SonarQube 1`] = `
<div
className="layout-page"
<DocMarkdownBlock
className="documentation-content cut-margins boxed-group-inner"
content="Lorem ipsum dolor sit amet fredum"
+ scrollToHref="#foo"
stickyToc={true}
title="Lorem"
/>
<Helmet
defer={true}
encodeSpecialCharacters={true}
- title="documentation.page_title.sonarcloud"
+ title="documentation.page_title.sonarqube"
>
<meta
content="noindex nofollow"
className?: string;
content: string;
isTooltip?: boolean;
+ scrollToHref?: string;
stickyToc?: boolean;
title?: string;
}
+const WAIT_TIMEOUT = 500;
+
export default class DocMarkdownBlock extends React.PureComponent<Props> {
node: HTMLElement | null = null;
- handleAnchorClick = (href: string, event: React.MouseEvent<HTMLAnchorElement>) => {
+ componentDidMount() {
+ const { scrollToHref } = this.props;
+ if (scrollToHref) {
+ setTimeout(() => {
+ this.handleAnchorClick(scrollToHref);
+ }, WAIT_TIMEOUT);
+ }
+ }
+
+ handleAnchorClick = (href: string, event?: React.MouseEvent<HTMLAnchorElement>) => {
if (this.node) {
const element = this.node.querySelector(href);
if (element) {
- event.preventDefault();
+ if (event) {
+ event.preventDefault();
+ }
scrollToElement(element, { bottomOffset: window.innerHeight - 80 });
+
+ // We cannot use React Router here, because we cannot simply replace a hash.
if (history.pushState) {
history.pushState(null, '', href);
}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
+import { mockEvent } from '../../../helpers/testMocks';
import DocMarkdownBlock from '../DocMarkdownBlock';
const CONTENT = `
jest.mock('rehype-react', () => ({ default: jest.requireActual('rehype-react') }));
jest.mock('rehype-slug', () => ({ default: jest.requireActual('rehype-slug') }));
-jest.mock('../../../helpers/system', () => ({
- getInstance: jest.fn(),
- isSonarCloud: jest.fn()
+jest.mock('sonar-ui-common/helpers/scrolling', () => ({
+ scrollToElement: jest.fn()
}));
-it('should render simple markdown', () => {
- expect(shallowRender({ content: 'this is *bold* text' })).toMatchSnapshot();
+const WINDOW_HEIGHT = 800;
+const originalWindowHeight = window.innerHeight;
+
+const historyPushState = jest.fn();
+const originalHistoryPushState = history.pushState;
+
+beforeEach(jest.clearAllMocks);
+
+beforeAll(() => {
+ Object.defineProperty(window, 'innerHeight', {
+ writable: true,
+ configurable: true,
+ value: WINDOW_HEIGHT
+ });
+ Object.defineProperty(history, 'pushState', {
+ writable: true,
+ configurable: true,
+ value: historyPushState
+ });
});
-it('should use custom component for links', () => {
- expect(
- shallowRender({ content: 'some [link](/quality-profiles)' }).find('withChildProps')
- ).toMatchSnapshot();
+afterAll(() => {
+ Object.defineProperty(window, 'innerHeight', {
+ writable: true,
+ configurable: true,
+ value: originalWindowHeight
+ });
+ Object.defineProperty(history, 'pushState', {
+ writable: true,
+ configurable: true,
+ value: originalHistoryPushState
+ });
});
-it('should render with custom props for links', () => {
+it('should render correctly', () => {
+ expect(shallowRender({ content: 'this is *bold* text' })).toMatchSnapshot('default');
+ expect(
+ shallowRender({ content: 'some [link](/quality-profiles)' }).find('withChildProps')
+ ).toMatchSnapshot('custom component for links');
expect(
shallowRender({
childProps: { foo: 'bar' },
content: 'some [link](#quality-profiles)',
isTooltip: true
}).find('withChildProps')
- ).toMatchSnapshot();
+ ).toMatchSnapshot('custom props for links');
+ expect(shallowRender({ content: CONTENT, stickyToc: true })).toMatchSnapshot('sticky TOC');
+});
+
+it('should correctly scroll to clicked headings', () => {
+ const element = {} as Element;
+ const querySelector: (selector: string) => Element | null = jest.fn((selector: string) =>
+ selector === '#id' ? element : null
+ );
+ const preventDefault = jest.fn();
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+
+ // Node Ref isn't set yet.
+ instance.handleAnchorClick('#unknown', mockEvent());
+ expect(scrollToElement).not.toBeCalled();
+
+ // Set node Ref.
+ instance.node = { querySelector } as HTMLElement;
+
+ // Unknown element.
+ instance.handleAnchorClick('#unknown', mockEvent());
+ expect(scrollToElement).not.toBeCalled();
+
+ // Known element, should scroll.
+ instance.handleAnchorClick('#id', mockEvent({ preventDefault }));
+ expect(scrollToElement).toBeCalledWith(element, { bottomOffset: 720 });
+ expect(preventDefault).toBeCalled();
+ expect(historyPushState).toBeCalledWith(null, '', '#id');
});
-it('should render a sticky TOC if available', () => {
- const wrapper = shallowRender({ content: CONTENT, stickyToc: true });
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('DocToc').exists()).toBe(true);
+it('should correctly scroll to a specific heading if passed as a prop', () => {
+ jest.useFakeTimers();
+
+ const element = {} as Element;
+ const querySelector: (_: string) => Element | null = jest.fn(() => element);
+ const wrapper = shallowRender({ scrollToHref: '#id' });
+ const instance = wrapper.instance();
+ instance.node = { querySelector } as HTMLElement;
+
+ expect(scrollToElement).not.toBeCalled();
+
+ jest.runAllTimers();
+
+ expect(scrollToElement).toBeCalledWith(element, { bottomOffset: 720 });
});
function shallowRender(props: Partial<DocMarkdownBlock['props']> = {}) {
- return shallow(<DocMarkdownBlock content="" {...props} />);
+ return shallow<DocMarkdownBlock>(<DocMarkdownBlock content="" {...props} />);
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render a sticky TOC if available 1`] = `
+exports[`should render correctly: custom component for links 1`] = `
+<withChildProps
+ href="/quality-profiles"
+ key="h-2"
+>
+ link
+</withChildProps>
+`;
+
+exports[`should render correctly: custom props for links 1`] = `
+<withChildProps
+ href="#quality-profiles"
+ key="h-2"
+>
+ link
+</withChildProps>
+`;
+
+exports[`should render correctly: default 1`] = `
+<div
+ className="markdown"
+>
+ <div
+ className="markdown-content"
+ >
+ <div>
+ <p
+ key="h-1"
+ >
+ this is
+ <em
+ key="h-2"
+ >
+ bold
+ </em>
+ text
+ </p>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: sticky TOC 1`] = `
<div
className="markdown has-toc"
>
/>
</div>
`;
-
-exports[`should render simple markdown 1`] = `
-<div
- className="markdown"
->
- <div
- className="markdown-content"
- >
- <div>
- <p
- key="h-1"
- >
- this is
- <em
- key="h-2"
- >
- bold
- </em>
- text
- </p>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render with custom props for links 1`] = `
-<withChildProps
- href="#quality-profiles"
- key="h-2"
->
- link
-</withChildProps>
-`;
-
-exports[`should use custom component for links 1`] = `
-<withChildProps
- href="/quality-profiles"
- key="h-2"
->
- link
-</withChildProps>
-`;
#
#------------------------------------------------------------------------------
documentation.page=Documentation
-documentation.page_title.sonarcloud=SonarCloud Docs
documentation.page_title.sonarqube=SonarQube Docs
documentation.on_this_page=On this page
documentation.skip_to_nav=Skip to documentation navigation