@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { ParsedContent } from '../../../helpers/markdown'; | |||
import { mockDocumentationMarkdown } from '../../../helpers/testMocks'; | |||
jest.mock('remark', () => ({ | |||
@@ -86,10 +87,9 @@ it('should correctly handle overrides (replace & add)', () => { | |||
key: 'tata' | |||
}; | |||
const overrides: string[] = [ | |||
mockDocumentationMarkdown(overrideFooDoc), | |||
mockDocumentationMarkdown(newDoc) | |||
]; | |||
const overrides: T.Dict<ParsedContent> = {}; | |||
overrides[foo.url] = { frontmatter: overrideFooDoc, content: overrideFooDoc.content }; | |||
overrides[`analysis/languages/${newDoc.key}`] = { frontmatter: newDoc, content: newDoc.content }; | |||
const pages = getPages(overrides); | |||
expect(pages.length).toBe(3); | |||
@@ -99,7 +99,7 @@ it('should correctly handle overrides (replace & add)', () => { | |||
expect(pages[2].title).toBe(newDoc.title); | |||
}); | |||
function getPages(overrides: string[] = []) { | |||
function getPages(overrides: T.Dict<ParsedContent> = {}) { | |||
// This allows the use of out-of-scope data inside jest.mock | |||
// Usually, it is impossible as jest.mock'ed module is hoisted on the top of the file | |||
return require.requireActual('../pages').default(overrides); |
@@ -33,7 +33,9 @@ import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; | |||
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 { getUrlsList } from '../navTreeUtils'; | |||
import getPages from '../pages'; | |||
import '../styles.css'; | |||
import { DocumentationEntry } from '../utils'; | |||
@@ -49,6 +51,8 @@ interface State { | |||
tree: DocNavigationItem[]; | |||
} | |||
const LANGUAGES_BASE_URL = 'analysis/languages'; | |||
export default class App extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
@@ -67,7 +71,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
? ((navigationTreeSonarCloud as any).default as DocNavigationItem[]) | |||
: ((navigationTreeSonarQube as any).default as DocNavigationItem[]); | |||
this.getLanguagesOverrides().then( | |||
this.getLanguagePluginsDocumentation(tree).then( | |||
overrides => { | |||
if (this.mounted) { | |||
this.setState({ | |||
@@ -92,15 +96,13 @@ export default class App extends React.PureComponent<Props, State> { | |||
removeSideBarClass(); | |||
} | |||
getLanguagesOverrides = () => { | |||
const pluginStaticFileNameRegEx = new RegExp(`^static/(.*)`); | |||
getLanguagePluginsDocumentation = (tree: DocNavigationItem[]) => { | |||
return getInstalledPlugins() | |||
.then(plugins => | |||
Promise.all( | |||
plugins.map(plugin => { | |||
if (plugin.documentationPath) { | |||
const matchArray = pluginStaticFileNameRegEx.exec(plugin.documentationPath); | |||
const matchArray = /^static\/(.*)/.exec(plugin.documentationPath); | |||
if (matchArray && matchArray.length > 1) { | |||
// eslint-disable-next-line promise/no-nesting | |||
@@ -114,7 +116,29 @@ export default class App extends React.PureComponent<Props, State> { | |||
}) | |||
) | |||
) | |||
.then(contents => contents.filter(isDefined)); | |||
.then(contents => contents.filter(isDefined)) | |||
.then(contents => { | |||
const regex = new RegExp(`/${LANGUAGES_BASE_URL}/\\w+/$`); | |||
const overridablePaths = getUrlsList(tree).filter( | |||
path => regex.test(path) && path !== `/${LANGUAGES_BASE_URL}/overview/` | |||
); | |||
const parsedContent: T.Dict<ParsedContent> = {}; | |||
contents.forEach(content => { | |||
const parsed = separateFrontMatter(content); | |||
if ( | |||
parsed && | |||
parsed.frontmatter && | |||
parsed.frontmatter.key && | |||
overridablePaths.includes(`/${LANGUAGES_BASE_URL}/${parsed.frontmatter.key}/`) | |||
) { | |||
parsedContent[`${LANGUAGES_BASE_URL}/${parsed.frontmatter.key}`] = parsed; | |||
} | |||
}); | |||
return parsedContent; | |||
}); | |||
}; | |||
render() { |
@@ -23,6 +23,7 @@ import { addSideBarClass, removeSideBarClass } from 'sonar-ui-common/helpers/pag | |||
import { request } from 'sonar-ui-common/helpers/request'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import getPages from '../../pages'; | |||
import App from '../App'; | |||
jest.mock('../../../../components/common/ScreenPositionHelper', () => ({ | |||
@@ -90,19 +91,27 @@ jest.mock('sonar-ui-common/helpers/pages', () => ({ | |||
removeSideBarClass: jest.fn() | |||
})); | |||
jest.mock('sonar-ui-common/helpers/request', () => ({ | |||
request: jest.fn(() => ({ | |||
submit: jest.fn().mockResolvedValue({ status: 200, text: jest.fn().mockReturnValue('TEST') }) | |||
})) | |||
})); | |||
jest.mock('sonar-ui-common/helpers/request', () => { | |||
const { mockDocumentationMarkdown } = require.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
request: jest.fn(() => ({ | |||
submit: jest.fn().mockResolvedValue({ | |||
status: 200, | |||
text: jest.fn().mockResolvedValue(mockDocumentationMarkdown({ key: 'csharp' })) | |||
}) | |||
})) | |||
}; | |||
}); | |||
jest.mock('../../pages', () => { | |||
const { mockDocumentationEntry } = require.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
default: () => [ | |||
mockDocumentationEntry(), | |||
mockDocumentationEntry({ url: '/analysis/languages/csharp/' }) | |||
] | |||
default: jest | |||
.fn() | |||
.mockReturnValue([ | |||
mockDocumentationEntry(), | |||
mockDocumentationEntry({ url: '/analysis/languages/csharp/' }) | |||
]) | |||
}; | |||
}); | |||
@@ -116,6 +125,10 @@ jest.mock('../../../../api/plugins', () => ({ | |||
]) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly for SonarQube', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper.find('DeferredSpinner').exists()).toBe(true); | |||
@@ -143,12 +156,19 @@ 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); | |||
expect(request).toHaveBeenCalledWith('/static/csharp/documentation.md'); | |||
expect(request).not.toHaveBeenCalledWith('/static/vbnet/documentation.md'); | |||
expect(request).not.toHaveBeenCalledWith('/static/vbnett/documentation.md'); | |||
expect(getPages).toHaveBeenCalledWith( | |||
expect.objectContaining({ | |||
'analysis/languages/csharp': expect.any(Object) | |||
}) | |||
); | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { |
@@ -23,18 +23,10 @@ import { filterContent, ParsedContent, separateFrontMatter } from '../../helpers | |||
import * as Docs from './documentation.directory-loader'; | |||
import { DocumentationEntry, DocumentationEntryScope } from './utils'; | |||
const LANGUAGES_BASE_URL = 'analysis/languages'; | |||
export default function getPages(overrides: string[] = []): DocumentationEntry[] { | |||
const parsedOverrides: T.Dict<ParsedContent> = {}; | |||
overrides.forEach(override => { | |||
const parsedOverride = separateFrontMatter(override); | |||
if (parsedOverride && parsedOverride.frontmatter && parsedOverride.frontmatter.key) { | |||
parsedOverrides[`${LANGUAGES_BASE_URL}/${parsedOverride.frontmatter.key}`] = parsedOverride; | |||
} | |||
}); | |||
// Merge with existing entries. | |||
export default function getPages( | |||
parsedOverrides: T.Dict<ParsedContent> = {} | |||
): DocumentationEntry[] { | |||
// Get entries, merge with overrides if applicable. | |||
const pages = ((Docs as unknown) as Array<{ content: string; path: string }>).map(file => { | |||
let parsed = separateFrontMatter(file.content); | |||