@@ -17,7 +17,6 @@ | |||
* 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'; | |||
@@ -34,7 +33,6 @@ import NotFound from '../../../app/components/NotFound'; | |||
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'; | |||
@@ -44,6 +42,7 @@ import Sidebar from './Sidebar'; | |||
interface Props { | |||
params: { splat?: string }; | |||
location: { hash: string }; | |||
} | |||
interface State { | |||
@@ -68,9 +67,7 @@ export default class App extends React.PureComponent<Props, 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 => { | |||
@@ -150,7 +147,10 @@ export default class App extends React.PureComponent<Props, State> { | |||
render() { | |||
const { loading, pages, tree } = this.state; | |||
const { splat = '' } = this.props.params; | |||
const { | |||
params: { splat = '' }, | |||
location: { hash } | |||
} = this.props; | |||
if (loading) { | |||
return ( | |||
@@ -161,10 +161,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
} | |||
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) { | |||
@@ -184,7 +181,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
<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"> | |||
@@ -220,6 +217,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
content={page.content} | |||
stickyToc={true} | |||
title={page.title} | |||
scrollToHref={hash} | |||
/> | |||
</div> | |||
</div> |
@@ -22,17 +22,12 @@ import * as React from 'react'; | |||
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: [ | |||
{ | |||
@@ -56,28 +51,6 @@ jest.mock('Docs/../static/SonarQubeNavigationTree.json', () => ({ | |||
] | |||
})); | |||
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() | |||
@@ -136,13 +109,6 @@ it('should render correctly for SonarQube', async () => { | |||
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); | |||
@@ -150,8 +116,6 @@ it("should show a 404 if the page doesn't exist", async () => { | |||
}); | |||
it('should try to fetch language plugin documentation if documentationPath matches', async () => { | |||
(isSonarCloud as jest.Mock).mockReturnValue(false); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
@@ -166,8 +130,6 @@ it('should try to fetch language plugin documentation if documentationPath match | |||
}); | |||
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); | |||
@@ -177,5 +139,5 @@ it('should display the issue tracker url of the plugin if it exists', async () = | |||
}); | |||
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} />); | |||
} |
@@ -1,43 +1,5 @@ | |||
// 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" | |||
@@ -72,6 +34,7 @@ exports[`should render correctly for SonarQube 1`] = ` | |||
<DocMarkdownBlock | |||
className="documentation-content cut-margins boxed-group-inner" | |||
content="Lorem ipsum dolor sit amet fredum" | |||
scrollToHref="#foo" | |||
stickyToc={true} | |||
title="Lorem" | |||
/> | |||
@@ -171,7 +134,7 @@ exports[`should show a 404 if the page doesn't exist 1`] = ` | |||
<Helmet | |||
defer={true} | |||
encodeSpecialCharacters={true} | |||
title="documentation.page_title.sonarcloud" | |||
title="documentation.page_title.sonarqube" | |||
> | |||
<meta | |||
content="noindex nofollow" |
@@ -38,19 +38,35 @@ interface Props { | |||
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); | |||
} |
@@ -19,6 +19,8 @@ | |||
*/ | |||
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 = ` | |||
@@ -48,37 +50,102 @@ jest.mock('rehype-raw', () => ({ default: jest.requireActual('rehype-raw') })); | |||
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} />); | |||
} |
@@ -1,6 +1,48 @@ | |||
// 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" | |||
> | |||
@@ -104,45 +146,3 @@ Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, co | |||
/> | |||
</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> | |||
`; |
@@ -3050,7 +3050,6 @@ api_documentation.search=Search by name... | |||
# | |||
#------------------------------------------------------------------------------ | |||
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 |