test: improve test coverage; properly test core client APIs (#6905)

* test: improve test coverage

* fix
This commit is contained in:
Joshua Chen 2022-03-12 23:15:45 +08:00 committed by GitHub
parent 76cb012209
commit d85cee576d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1400 additions and 753 deletions

View file

@ -36,18 +36,18 @@ export default {
// Jest can't resolve CSS or asset imports // Jest can't resolve CSS or asset imports
'^.+\\.(css|jpe?g|png|svg)$': '<rootDir>/jest/emptyModule.js', '^.+\\.(css|jpe?g|png|svg)$': '<rootDir>/jest/emptyModule.js',
// TODO we need to allow Jest to resolve core Webpack aliases automatically // Using src instead of lib, so we always get fresh source
'@docusaurus/(browserContext|BrowserOnly|ComponentCreator|constants|docusaurusContext|ExecutionEnvironment|Head|Interpolate|isInternalUrl|Link|Noop|renderRoutes|router|Translate|use.*)': '@docusaurus/(browserContext|BrowserOnly|ComponentCreator|constants|docusaurusContext|ExecutionEnvironment|Head|Interpolate|isInternalUrl|Link|Noop|renderRoutes|router|Translate|use.*)':
'@docusaurus/core/lib/client/exports/$1', '@docusaurus/core/src/client/exports/$1',
// Maybe point to a fixture? // Maybe point to a fixture?
'@generated/.*': '<rootDir>/jest/emptyModule.js', '@generated/.*': '<rootDir>/jest/emptyModule.js',
// TODO use "projects" + multiple configs if we work on another theme? // TODO use "projects" + multiple configs if we work on another theme?
'@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1', '@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1',
'@site/(.*)': 'website/$1', '@site/(.*)': 'website/$1',
// TODO why Jest can't figure node package entry points? // Using src instead of lib, so we always get fresh source
'@docusaurus/plugin-content-docs/client': '@docusaurus/plugin-content-docs/client':
'@docusaurus/plugin-content-docs/lib/client/index.js', '@docusaurus/plugin-content-docs/src/client/index.ts',
}, },
globals: { globals: {
window: { window: {

View file

@ -13,3 +13,5 @@ Lorem ipsum
Some content here Some content here
## I ♥ unicode. ## I ♥ unicode.
export const c = 1;

View file

@ -114,6 +114,8 @@ Lorem ipsum
Some content here Some content here
## I ♥ unicode. ## I ♥ unicode.
export const c = 1;
" "
`; `;
@ -241,5 +243,7 @@ Lorem ipsum
Some content here Some content here
## I ♥ unicode. ## I ♥ unicode.
export const c = 1;
" "
`; `;

View file

@ -31,7 +31,6 @@ export function toValue(node: PhrasingContent | Heading): string {
case 'link': case 'link':
return stringifyContent(node); return stringifyContent(node);
default: default:
}
return toString(node); return toString(node);
}
} }

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`toGlobalDataVersion generates the right docs, sidebars, and metadata 1`] = `
Object {
"docs": Array [
Object {
"id": "main",
"path": "/current/main",
"sidebar": "tutorial",
},
Object {
"id": "doc",
"path": "/current/doc",
"sidebar": "tutorial",
},
Object {
"id": "/current/generated",
"path": "/current/generated",
"sidebar": "tutorial",
},
],
"isLast": true,
"label": "Label",
"mainDocId": "main",
"name": "current",
"path": "/current",
"sidebars": Object {
"another": Object {
"link": Object {
"label": "Generated",
"path": "/current/generated",
},
},
"links": Object {},
"tutorial": Object {
"link": Object {
"label": "main",
"path": "/current/main",
},
},
},
}
`;

View file

@ -0,0 +1,100 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {toGlobalDataVersion} from '../globalData';
describe('toGlobalDataVersion', () => {
it('generates the right docs, sidebars, and metadata', () => {
expect(
toGlobalDataVersion({
versionName: 'current',
versionLabel: 'Label',
isLast: true,
versionPath: '/current',
mainDocId: 'main',
docs: [
{
unversionedId: 'main',
permalink: '/current/main',
sidebar: 'tutorial',
},
{
unversionedId: 'doc',
permalink: '/current/doc',
sidebar: 'tutorial',
},
],
sidebars: {
another: [
{
type: 'category',
label: 'Generated',
link: {
type: 'generated-index',
permalink: '/current/generated',
},
items: [
{
type: 'doc',
id: 'doc',
},
],
},
],
tutorial: [
{
type: 'doc',
id: 'main',
},
{
type: 'category',
label: 'Generated',
link: {
type: 'generated-index',
permalink: '/current/generated',
},
items: [
{
type: 'doc',
id: 'doc',
},
],
},
],
links: [
{
type: 'link',
href: 'foo',
label: 'Foo',
},
{
type: 'link',
href: 'bar',
label: 'Bar',
},
],
},
categoryGeneratedIndices: [
{
title: 'Generated',
slug: '/current/generated',
permalink: '/current/generated',
sidebar: 'tutorial',
},
],
versionBanner: 'unreleased',
versionBadge: true,
versionClassName: 'current-cls',
tagsPath: '/current/tags',
contentPath: '',
contentPathLocalized: '',
sidebarFilePath: '',
routePriority: 0.5,
}),
).toMatchSnapshot();
});
});

View file

@ -111,6 +111,27 @@ describe('getSlug', () => {
).toBe('/dir with spâce/hey $hello/my dôc'); ).toBe('/dir with spâce/hey $hello/my dôc');
}); });
it('throws for invalid routes', () => {
expect(() =>
getSlug({
baseID: 'my dôc',
source: '@site/docs/dir with spâce/hey $hello/doc.md',
sourceDirName: '/dir with spâce/hey $hello',
frontMatterSlug: '//',
}),
).toThrowErrorMatchingInlineSnapshot(`
"We couldn't compute a valid slug for document with ID \\"my dôc\\" in \\"/dir with spâce/hey $hello\\" directory.
The slug we computed looks invalid: //.
Maybe your slug front matter is incorrect or there are special characters in the file path?
By using front matter to set a custom slug, you should be able to fix this error:
---
slug: /my/customDocPath
---
"
`);
});
it('handles current dir', () => { it('handles current dir', () => {
expect( expect(
getSlug({baseID: 'doc', source: '@site/docs/doc.md', sourceDirName: '.'}), getSlug({baseID: 'doc', source: '@site/docs/doc.md', sourceDirName: '.'}),

View file

@ -26,8 +26,8 @@ const DefaultI18N: I18n = {
localeConfigs: {}, localeConfigs: {},
}; };
describe('version paths', () => { describe('getVersionsFilePath', () => {
it('getVersionsFilePath', () => { it('works', () => {
expect(getVersionsFilePath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( expect(getVersionsFilePath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
`someSiteDir${path.sep}versions.json`, `someSiteDir${path.sep}versions.json`,
); );
@ -35,8 +35,10 @@ describe('version paths', () => {
`otherSite${path.sep}dir${path.sep}pluginId_versions.json`, `otherSite${path.sep}dir${path.sep}pluginId_versions.json`,
); );
}); });
});
it('getVersionedDocsDirPath', () => { describe('getVersionedDocsDirPath', () => {
it('works', () => {
expect(getVersionedDocsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( expect(getVersionedDocsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
`someSiteDir${path.sep}versioned_docs`, `someSiteDir${path.sep}versioned_docs`,
); );
@ -44,8 +46,10 @@ describe('version paths', () => {
`otherSite${path.sep}dir${path.sep}pluginId_versioned_docs`, `otherSite${path.sep}dir${path.sep}pluginId_versioned_docs`,
); );
}); });
});
it('getVersionedSidebarsDirPath', () => { describe('getVersionedSidebarsDirPath', () => {
it('works', () => {
expect(getVersionedSidebarsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( expect(getVersionedSidebarsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe(
`someSiteDir${path.sep}versioned_sidebars`, `someSiteDir${path.sep}versioned_sidebars`,
); );
@ -55,7 +59,8 @@ describe('version paths', () => {
}); });
}); });
describe('simple site', () => { describe('readVersionsMetadata', () => {
describe('simple site', () => {
async function loadSite() { async function loadSite() {
const simpleSiteDir = path.resolve( const simpleSiteDir = path.resolve(
path.join(__dirname, '__fixtures__', 'simple-site'), path.join(__dirname, '__fixtures__', 'simple-site'),
@ -90,7 +95,7 @@ describe('simple site', () => {
return {simpleSiteDir, defaultOptions, defaultContext, vCurrent}; return {simpleSiteDir, defaultOptions, defaultContext, vCurrent};
} }
it('readVersionsMetadata simple site', async () => { it('works', async () => {
const {defaultOptions, defaultContext, vCurrent} = await loadSite(); const {defaultOptions, defaultContext, vCurrent} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -101,7 +106,7 @@ describe('simple site', () => {
expect(versionsMetadata).toEqual([vCurrent]); expect(versionsMetadata).toEqual([vCurrent]);
}); });
it('readVersionsMetadata simple site with base url', async () => { it('works with base url', async () => {
const {defaultOptions, defaultContext, vCurrent} = await loadSite(); const {defaultOptions, defaultContext, vCurrent} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -121,7 +126,7 @@ describe('simple site', () => {
]); ]);
}); });
it('readVersionsMetadata simple site with current version config', async () => { it('works with current version config', async () => {
const {defaultOptions, defaultContext, vCurrent} = await loadSite(); const {defaultOptions, defaultContext, vCurrent} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -154,7 +159,7 @@ describe('simple site', () => {
]); ]);
}); });
it('readVersionsMetadata simple site with unknown lastVersion should throw', async () => { it('throws with unknown lastVersion', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -167,7 +172,7 @@ describe('simple site', () => {
); );
}); });
it('readVersionsMetadata simple site with unknown version configurations should throw', async () => { it('throws with unknown version configurations', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -187,7 +192,7 @@ describe('simple site', () => {
); );
}); });
it('readVersionsMetadata simple site with disableVersioning while single version should throw', async () => { it('throws with disableVersioning while single version', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -200,7 +205,7 @@ describe('simple site', () => {
); );
}); });
it('readVersionsMetadata simple site without including current version should throw', async () => { it('throws without including current version', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -212,9 +217,9 @@ describe('simple site', () => {
`"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: false\\"."`, `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: false\\"."`,
); );
}); });
}); });
describe('versioned site, pluginId=default', () => { describe('versioned site, pluginId=default', () => {
async function loadSite() { async function loadSite() {
const versionedSiteDir = path.resolve( const versionedSiteDir = path.resolve(
path.join(__dirname, '__fixtures__', 'versioned-site'), path.join(__dirname, '__fixtures__', 'versioned-site'),
@ -249,7 +254,10 @@ describe('versioned site, pluginId=default', () => {
}; };
const v101: VersionMetadata = { const v101: VersionMetadata = {
contentPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'), contentPath: path.join(
versionedSiteDir,
'versioned_docs/version-1.0.1',
),
contentPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs/version-1.0.1', 'i18n/en/docusaurus-plugin-content-docs/version-1.0.1',
@ -270,7 +278,10 @@ describe('versioned site, pluginId=default', () => {
}; };
const v100: VersionMetadata = { const v100: VersionMetadata = {
contentPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'), contentPath: path.join(
versionedSiteDir,
'versioned_docs/version-1.0.0',
),
contentPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs/version-1.0.0', 'i18n/en/docusaurus-plugin-content-docs/version-1.0.0',
@ -325,7 +336,7 @@ describe('versioned site, pluginId=default', () => {
}; };
} }
it('readVersionsMetadata versioned site', async () => { it('works', async () => {
const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} =
await loadSite(); await loadSite();
@ -337,7 +348,7 @@ describe('versioned site, pluginId=default', () => {
expect(versionsMetadata).toEqual([vCurrent, v101, v100, vWithSlugs]); expect(versionsMetadata).toEqual([vCurrent, v101, v100, vWithSlugs]);
}); });
it('readVersionsMetadata versioned site with includeCurrentVersion=false', async () => { it('works with includeCurrentVersion=false', async () => {
const {defaultOptions, defaultContext, v101, v100, vWithSlugs} = const {defaultOptions, defaultContext, v101, v100, vWithSlugs} =
await loadSite(); await loadSite();
@ -354,7 +365,7 @@ describe('versioned site, pluginId=default', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site with version options', async () => { it('works with version options', async () => {
const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} =
await loadSite(); await loadSite();
@ -408,7 +419,7 @@ describe('versioned site, pluginId=default', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site with editUrl', async () => { it('works with editUrl', async () => {
const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} =
await loadSite(); await loadSite();
@ -452,7 +463,7 @@ describe('versioned site, pluginId=default', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site with editUrl and editCurrentVersion=true', async () => { it('works with editUrl and editCurrentVersion=true', async () => {
const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} =
await loadSite(); await loadSite();
@ -497,8 +508,9 @@ describe('versioned site, pluginId=default', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site with onlyIncludeVersions option', async () => { it('works with onlyIncludeVersions option', async () => {
const {defaultOptions, defaultContext, v101, vWithSlugs} = await loadSite(); const {defaultOptions, defaultContext, v101, vWithSlugs} =
await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
options: { options: {
@ -512,7 +524,7 @@ describe('versioned site, pluginId=default', () => {
expect(versionsMetadata).toEqual([v101, vWithSlugs]); expect(versionsMetadata).toEqual([v101, vWithSlugs]);
}); });
it('readVersionsMetadata versioned site with disableVersioning', async () => { it('works with disableVersioning', async () => {
const {defaultOptions, defaultContext, vCurrent} = await loadSite(); const {defaultOptions, defaultContext, vCurrent} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -533,7 +545,7 @@ describe('versioned site, pluginId=default', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site with all versions disabled', async () => { it('throws with all versions disabled', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -550,7 +562,7 @@ describe('versioned site, pluginId=default', () => {
); );
}); });
it('readVersionsMetadata versioned site with empty onlyIncludeVersions', async () => { it('throws with empty onlyIncludeVersions', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -566,7 +578,7 @@ describe('versioned site, pluginId=default', () => {
); );
}); });
it('readVersionsMetadata versioned site with unknown versions in onlyIncludeVersions', async () => { it('throws with unknown versions in onlyIncludeVersions', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -582,7 +594,7 @@ describe('versioned site, pluginId=default', () => {
); );
}); });
it('readVersionsMetadata versioned site with lastVersion not in onlyIncludeVersions', async () => { it('throws with lastVersion not in onlyIncludeVersions', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -599,10 +611,11 @@ describe('versioned site, pluginId=default', () => {
); );
}); });
it('readVersionsMetadata versioned site with invalid versions.json file', async () => { it('throws with invalid versions.json file', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => ({ const jsonMock = jest.spyOn(JSON, 'parse');
jsonMock.mockImplementationOnce(() => ({
invalid: 'json', invalid: 'json',
})); }));
@ -612,13 +625,33 @@ describe('versioned site, pluginId=default', () => {
context: defaultContext, context: defaultContext,
}), }),
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
`"The versions file should contain an array of versions! Found content: {\\"invalid\\":\\"json\\"}"`, `"The versions file should contain an array of version names! Found content: {\\"invalid\\":\\"json\\"}"`,
); );
mock.mockRestore(); jsonMock.mockImplementationOnce(() => [1.1]);
});
});
describe('versioned site, pluginId=community', () => { await expect(
readVersionsMetadata({
options: defaultOptions,
context: defaultContext,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Versions should be strings. Found type \\"number\\" for version \\"1.1\\"."`,
);
jsonMock.mockImplementationOnce(() => [' ']);
await expect(
readVersionsMetadata({
options: defaultOptions,
context: defaultContext,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Invalid version \\" \\"."`,
);
jsonMock.mockRestore();
});
});
describe('versioned site, pluginId=community', () => {
async function loadSite() { async function loadSite() {
const versionedSiteDir = path.resolve( const versionedSiteDir = path.resolve(
path.join(__dirname, '__fixtures__', 'versioned-site'), path.join(__dirname, '__fixtures__', 'versioned-site'),
@ -681,7 +714,7 @@ describe('versioned site, pluginId=community', () => {
return {versionedSiteDir, defaultOptions, defaultContext, vCurrent, v100}; return {versionedSiteDir, defaultOptions, defaultContext, vCurrent, v100};
} }
it('readVersionsMetadata versioned site (community)', async () => { it('works', async () => {
const {defaultOptions, defaultContext, vCurrent, v100} = await loadSite(); const {defaultOptions, defaultContext, vCurrent, v100} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -692,7 +725,7 @@ describe('versioned site, pluginId=community', () => {
expect(versionsMetadata).toEqual([vCurrent, v100]); expect(versionsMetadata).toEqual([vCurrent, v100]);
}); });
it('readVersionsMetadata versioned site (community) with includeCurrentVersion=false', async () => { it('works with includeCurrentVersion=false', async () => {
const {defaultOptions, defaultContext, v100} = await loadSite(); const {defaultOptions, defaultContext, v100} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -706,7 +739,7 @@ describe('versioned site, pluginId=community', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site (community) with disableVersioning', async () => { it('works with disableVersioning', async () => {
const {defaultOptions, defaultContext, vCurrent} = await loadSite(); const {defaultOptions, defaultContext, vCurrent} = await loadSite();
const versionsMetadata = await readVersionsMetadata({ const versionsMetadata = await readVersionsMetadata({
@ -727,7 +760,7 @@ describe('versioned site, pluginId=community', () => {
]); ]);
}); });
it('readVersionsMetadata versioned site (community) with all versions disabled', async () => { it('throws with all versions disabled', async () => {
const {defaultOptions, defaultContext} = await loadSite(); const {defaultOptions, defaultContext} = await loadSite();
await expect( await expect(
@ -743,4 +776,5 @@ describe('versioned site, pluginId=community', () => {
`"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: true\\"."`, `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: true\\"."`,
); );
}); });
});
}); });

View file

@ -391,6 +391,10 @@ export const isCategoryIndex: CategoryIndexMatcher = ({
return eligibleDocIndexNames.includes(fileName.toLowerCase()); return eligibleDocIndexNames.includes(fileName.toLowerCase());
}; };
/**
* `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/
export function toCategoryIndexMatcherParam({ export function toCategoryIndexMatcherParam({
source, source,
sourceDirName, sourceDirName,
@ -406,28 +410,6 @@ export function toCategoryIndexMatcherParam({
}; };
} }
/**
* `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/
export function splitPath(str: string): {
/**
* The list of directories, from lowest level to highest.
* If there's no dir name, directories is ['.']
*/
directories: string[];
/** The file name, without extension */
fileName: string;
/** The extension, with a leading dot */
extension: string;
} {
return {
fileName: path.parse(str).name,
extension: path.parse(str).ext,
directories: path.dirname(str).split(path.sep).reverse(),
};
}
// Return both doc ids // Return both doc ids
// TODO legacy retro-compatibility due to old versioned sidebars using // TODO legacy retro-compatibility due to old versioned sidebars using
// versioned doc ids ("id" should be removed & "versionedId" should be renamed // versioned doc ids ("id" should be removed & "versionedId" should be renamed

View file

@ -6,7 +6,6 @@
*/ */
import _ from 'lodash'; import _ from 'lodash';
import {normalizeUrl} from '@docusaurus/utils';
import type {Sidebars} from './sidebars/types'; import type {Sidebars} from './sidebars/types';
import {createSidebarsUtils} from './sidebars/utils'; import {createSidebarsUtils} from './sidebars/utils';
import type { import type {
@ -20,7 +19,7 @@ import type {
GlobalDoc, GlobalDoc,
} from '@docusaurus/plugin-content-docs/client'; } from '@docusaurus/plugin-content-docs/client';
export function toGlobalDataDoc(doc: DocMetadata): GlobalDoc { function toGlobalDataDoc(doc: DocMetadata): GlobalDoc {
return { return {
id: doc.unversionedId, id: doc.unversionedId,
path: doc.permalink, path: doc.permalink,
@ -28,7 +27,7 @@ export function toGlobalDataDoc(doc: DocMetadata): GlobalDoc {
}; };
} }
export function toGlobalDataGeneratedIndex( function toGlobalDataGeneratedIndex(
doc: CategoryGeneratedIndexMetadata, doc: CategoryGeneratedIndexMetadata,
): GlobalDoc { ): GlobalDoc {
return { return {
@ -38,7 +37,7 @@ export function toGlobalDataGeneratedIndex(
}; };
} }
export function toGlobalSidebars( function toGlobalSidebars(
sidebars: Sidebars, sidebars: Sidebars,
version: LoadedVersion, version: LoadedVersion,
): Record<string, GlobalSidebar> { ): Record<string, GlobalSidebar> {
@ -52,7 +51,7 @@ export function toGlobalSidebars(
link: { link: {
path: path:
firstLink.type === 'generated-index' firstLink.type === 'generated-index'
? normalizeUrl([version.versionPath, firstLink.slug]) ? firstLink.permalink
: version.docs.find( : version.docs.find(
(doc) => (doc) =>
doc.id === firstLink.id || doc.unversionedId === firstLink.id, doc.id === firstLink.id || doc.unversionedId === firstLink.id,

View file

@ -18,8 +18,14 @@ declare module '@docusaurus/plugin-content-docs' {
}; };
export type CategoryIndexMatcherParam = { export type CategoryIndexMatcherParam = {
/** The file name, without extension */
fileName: string; fileName: string;
/**
* The list of directories, from lowest level to highest.
* If there's no dir name, directories is ['.']
*/
directories: string[]; directories: string[];
/** The extension, with a leading dot */
extension: string; extension: string;
}; };
export type CategoryIndexMatcher = ( export type CategoryIndexMatcher = (

View file

@ -111,7 +111,7 @@ describe('createSidebarsUtils', () => {
link: { link: {
type: 'generated-index', type: 'generated-index',
slug: '/s4-category-slug', slug: '/s4-category-slug',
permalink: '/s4-category-permalink', permalink: '/s4-category-slug',
}, },
items: [ items: [
{type: 'doc', id: 'doc8'}, {type: 'doc', id: 'doc8'},
@ -291,7 +291,7 @@ describe('createSidebarsUtils', () => {
}); });
expect(getFirstLink('sidebar4')).toEqual({ expect(getFirstLink('sidebar4')).toEqual({
type: 'generated-index', type: 'generated-index',
slug: '/s4-category-slug', permalink: '/s4-category-slug',
label: 'S4 Category', label: 'S4 Category',
}); });
}); });

View file

@ -139,6 +139,11 @@ export type SidebarsUtils = {
getCategoryGeneratedIndexNavigation: ( getCategoryGeneratedIndexNavigation: (
categoryGeneratedIndexPermalink: string, categoryGeneratedIndexPermalink: string,
) => SidebarNavigation; ) => SidebarNavigation;
/**
* This function may return undefined. This is usually a user mistake, because
* it means this sidebar will never be displayed; however, we can still use
* `displayed_sidebar` to make it displayed. Pretty weird but valid use-case
*/
getFirstLink: (sidebarId: string) => getFirstLink: (sidebarId: string) =>
| { | {
type: 'doc'; type: 'doc';
@ -147,7 +152,7 @@ export type SidebarsUtils = {
} }
| { | {
type: 'generated-index'; type: 'generated-index';
slug: string; permalink: string;
label: string; label: string;
} }
| undefined; | undefined;
@ -295,7 +300,7 @@ Available document ids are:
} }
| { | {
type: 'generated-index'; type: 'generated-index';
slug: string; permalink: string;
label: string; label: string;
} }
| undefined { | undefined {
@ -316,7 +321,7 @@ Available document ids are:
} else if (item.link?.type === 'generated-index') { } else if (item.link?.type === 'generated-index') {
return { return {
type: 'generated-index', type: 'generated-index',
slug: item.link.slug, permalink: item.link.permalink,
label: item.label, label: item.label,
}; };
} }

View file

@ -63,12 +63,11 @@ export default function getSlug({
function ensureValidSlug(slug: string): string { function ensureValidSlug(slug: string): string {
if (!isValidPathname(slug)) { if (!isValidPathname(slug)) {
throw new Error( throw new Error(
`We couldn't compute a valid slug for document with id "${baseID}" in "${sourceDirName}" directory. `We couldn't compute a valid slug for document with ID "${baseID}" in "${sourceDirName}" directory.
The slug we computed looks invalid: ${slug}. The slug we computed looks invalid: ${slug}.
Maybe your slug front matter is incorrect or you use weird chars in the file path? Maybe your slug front matter is incorrect or there are special characters in the file path?
By using the slug front matter, you should be able to fix this error, by using the slug of your choice: By using front matter to set a custom slug, you should be able to fix this error:
Example =>
--- ---
slug: /my/customDocPath slug: /my/customDocPath
--- ---

View file

@ -274,10 +274,7 @@ function translateVersion(
translationFiles: Record<string, TranslationFile>, translationFiles: Record<string, TranslationFile>,
): LoadedVersion { ): LoadedVersion {
const versionTranslations = const versionTranslations =
translationFiles[getVersionFileName(version.versionName)]?.content; translationFiles[getVersionFileName(version.versionName)]!.content;
if (!versionTranslations) {
return version;
}
return { return {
...version, ...version,
versionLabel: versionLabel:

View file

@ -74,9 +74,9 @@ function ensureValidVersionString(version: unknown): asserts version is string {
function ensureValidVersionArray( function ensureValidVersionArray(
versionArray: unknown, versionArray: unknown,
): asserts versionArray is string[] { ): asserts versionArray is string[] {
if (!(versionArray instanceof Array)) { if (!Array.isArray(versionArray)) {
throw new Error( throw new Error(
`The versions file should contain an array of versions! Found content: ${JSON.stringify( `The versions file should contain an array of version names! Found content: ${JSON.stringify(
versionArray, versionArray,
)}`, )}`,
); );

View file

@ -218,7 +218,7 @@ export default function SearchPage(): JSX.Element {
algoliaHelper.on( algoliaHelper.on(
'result', 'result',
({results: {query, hits, page, nbHits, nbPages}}) => { ({results: {query, hits, page, nbHits, nbPages}}) => {
if (query === '' || !(hits instanceof Array)) { if (query === '' || !Array.isArray(hits)) {
searchResultStateDispatcher({type: 'reset'}); searchResultStateDispatcher({type: 'reset'});
return; return;
} }

View file

@ -9,7 +9,15 @@ import {jest} from '@jest/globals';
import normalizeLocation from '../normalizeLocation'; import normalizeLocation from '../normalizeLocation';
describe('normalizeLocation', () => { describe('normalizeLocation', () => {
it('rewrite locations with index.html', () => { it('rewrites locations with index.html', () => {
expect(
normalizeLocation({
pathname: '/index.html',
}),
).toEqual({
pathname: '/',
});
expect( expect(
normalizeLocation({ normalizeLocation({
pathname: '/docs/introduction/index.html', pathname: '/docs/introduction/index.html',
@ -35,7 +43,7 @@ describe('normalizeLocation', () => {
}); });
}); });
it('untouched pathnames', () => { it('leaves pathnames untouched', () => {
const replaceMock = jest.spyOn(String.prototype, 'replace'); const replaceMock = jest.spyOn(String.prototype, 'replace');
expect( expect(

View file

@ -89,12 +89,12 @@ export function interpolate<Str extends string, Value extends ReactNode>(
export default function Interpolate<Str extends string>({ export default function Interpolate<Str extends string>({
children, children,
values, values,
}: InterpolateProps<Str>): ReactNode { }: InterpolateProps<Str>): JSX.Element {
if (typeof children !== 'string') { if (typeof children !== 'string') {
console.warn('Illegal <Interpolate> children', children); console.warn('Illegal <Interpolate> children', children);
throw new Error( throw new Error(
'The Docusaurus <Interpolate> component only accept simple string values', 'The Docusaurus <Interpolate> component only accept simple string values',
); );
} }
return interpolate(children, values); return <>{interpolate(children, values)}</>;
} }

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {ReactNode} from 'react'; import React from 'react';
import {interpolate, type InterpolateValues} from '@docusaurus/Interpolate'; import {interpolate, type InterpolateValues} from '@docusaurus/Interpolate';
import type {TranslateParam, TranslateProps} from '@docusaurus/Translate'; import type {TranslateParam, TranslateProps} from '@docusaurus/Translate';
@ -46,7 +46,7 @@ export default function Translate<Str extends string>({
children, children,
id, id,
values, values,
}: TranslateProps<Str>): ReactNode { }: TranslateProps<Str>): JSX.Element {
if (children && typeof children !== 'string') { if (children && typeof children !== 'string') {
console.warn('Illegal <Translate> children', children); console.warn('Illegal <Translate> children', children);
throw new Error( throw new Error(
@ -55,5 +55,5 @@ export default function Translate<Str extends string>({
} }
const localizedMessage: string = getLocalizedMessage({message: children, id}); const localizedMessage: string = getLocalizedMessage({message: children, id});
return interpolate(localizedMessage, values); return <>{interpolate(localizedMessage, values)}</>;
} }

View file

@ -5,46 +5,55 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {jest} from '@jest/globals';
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import BrowserOnly from '../BrowserOnly'; import BrowserOnly from '../BrowserOnly';
import {Context} from '../browserContext';
jest.mock('@docusaurus/useIsBrowser', () => () => true); describe('<BrowserOnly>', () => {
describe('BrowserOnly', () => {
it('rejects react element children', () => { it('rejects react element children', () => {
process.env.NODE_ENV = 'development'; process.env.NODE_ENV = 'development';
expect(() => { expect(() =>
renderer.create( renderer
.create(
<Context.Provider value>
<BrowserOnly> <BrowserOnly>
{/* @ts-expect-error test */} {/* @ts-expect-error test */}
<span>{window.location.href}</span> <span>{window.location.href}</span>
</BrowserOnly>, </BrowserOnly>
); </Context.Provider>,
}).toThrowErrorMatchingInlineSnapshot(` )
.toJSON(),
).toThrowErrorMatchingInlineSnapshot(`
"Docusaurus error: The children of <BrowserOnly> must be a \\"render function\\", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>. "Docusaurus error: The children of <BrowserOnly> must be a \\"render function\\", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>.
Current type: React element" Current type: React element"
`); `);
}); });
it('rejects string children', () => { it('rejects string children', () => {
process.env.NODE_ENV = 'development';
expect(() => { expect(() => {
renderer.create( renderer.create(
// @ts-expect-error test <Context.Provider value>
<BrowserOnly> </BrowserOnly>, {/* @ts-expect-error test */}
<BrowserOnly> </BrowserOnly>
</Context.Provider>,
); );
}).toThrowErrorMatchingInlineSnapshot(` }).toThrowErrorMatchingInlineSnapshot(`
"Docusaurus error: The children of <BrowserOnly> must be a \\"render function\\", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>. "Docusaurus error: The children of <BrowserOnly> must be a \\"render function\\", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>.
Current type: string" Current type: string"
`); `);
}); });
it('accepts valid children', () => { it('accepts valid children', () => {
expect( expect(
renderer renderer
.create( .create(
<Context.Provider value>
<BrowserOnly fallback={<span>Loading</span>}> <BrowserOnly fallback={<span>Loading</span>}>
{() => <span>{window.location.href}</span>} {() => <span>{window.location.href}</span>}
</BrowserOnly>, </BrowserOnly>
</Context.Provider>,
) )
.toJSON(), .toJSON(),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
@ -53,4 +62,36 @@ describe('BrowserOnly', () => {
</span> </span>
`); `);
}); });
it('returns fallback when not in browser', () => {
expect(
renderer
.create(
<Context.Provider value={false}>
<BrowserOnly fallback={<span>Loading</span>}>
{() => <span>{window.location.href}</span>}
</BrowserOnly>
</Context.Provider>,
)
.toJSON(),
).toMatchInlineSnapshot(`
<span>
Loading
</span>
`);
});
it('gracefully falls back', () => {
expect(
renderer
.create(
<Context.Provider value={false}>
<BrowserOnly>
{() => <span>{window.location.href}</span>}
</BrowserOnly>
</Context.Provider>,
)
.toJSON(),
).toMatchInlineSnapshot(`null`);
});
}); });

View file

@ -6,9 +6,10 @@
*/ */
import React from 'react'; import React from 'react';
import {interpolate} from '../Interpolate'; import renderer from 'react-test-renderer';
import Interpolate, {interpolate} from '../Interpolate';
describe('Interpolate', () => { describe('interpolate', () => {
it('without placeholders', () => { it('without placeholders', () => {
const text = 'Hello how are you?'; const text = 'Hello how are you?';
expect(interpolate(text)).toEqual(text); expect(interpolate(text)).toEqual(text);
@ -86,3 +87,50 @@ describe('Interpolate', () => {
expect(interpolate(text, values)).toMatchSnapshot(); expect(interpolate(text, values)).toMatchSnapshot();
}); });
}); });
describe('<Interpolate>', () => {
it('without placeholders', () => {
const text = 'Hello how are you?';
expect(renderer.create(<Interpolate>{text}</Interpolate>).toJSON()).toEqual(
text,
);
});
it('placeholders with string values', () => {
const text = 'Hello {name} how are you {day}?';
const values = {name: 'Sébastien', day: 'today'};
expect(
renderer
.create(<Interpolate values={values}>{text}</Interpolate>)
.toJSON(),
).toMatchInlineSnapshot(`"Hello Sébastien how are you today?"`);
});
it('acceptance test', () => {
const text = 'Hello {name} how are you {day}? Another {unprovidedValue}!';
const values = {
name: 'Sébastien',
day: <span>today</span>,
extraUselessValue1: <div>test</div>,
extraUselessValue2: 'hi',
};
expect(
renderer
.create(<Interpolate values={values}>{text}</Interpolate>)
.toJSON(),
).toMatchSnapshot();
});
it('rejects when children is not string', () => {
expect(() =>
renderer.create(
<Interpolate>
{/* @ts-expect-error: for test */}
<span>aaa</span>
</Interpolate>,
),
).toThrowErrorMatchingInlineSnapshot(
`"The Docusaurus <Interpolate> component only accept simple string values"`,
);
});
});

View file

@ -5,28 +5,75 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {translate} from '../Translate'; import React from 'react';
import renderer from 'react-test-renderer';
import Translate, {translate} from '../Translate';
describe('translate', () => { describe('translate', () => {
it('accept id and use it as fallback', () => { it('accepts id and uses it as fallback', () => {
expect(translate({id: 'some-id'})).toBe('some-id'); expect(translate({id: 'some-id'})).toBe('some-id');
}); });
it('accept message and use it as fallback', () => { it('accepts message and uses it as fallback', () => {
expect(translate({message: 'some-message'})).toBe('some-message'); expect(translate({message: 'some-message'})).toBe('some-message');
}); });
it('accept id+message and use message as fallback', () => { it('accepts id+message and uses message as fallback', () => {
expect(translate({id: 'some-id', message: 'some-message'})).toBe( expect(translate({id: 'some-id', message: 'some-message'})).toBe(
'some-message', 'some-message',
); );
}); });
it('reject when no id or message', () => { it('rejects when no id or message', () => {
// TODO tests are not resolving type defs correctly // TODO tests are not resolving type defs correctly. We need to include test
// files in a tsconfig file
// @ts-expect-error: TS should protect when both id/message are missing // @ts-expect-error: TS should protect when both id/message are missing
expect(() => translate({})).toThrowErrorMatchingInlineSnapshot( expect(() => translate({})).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus translation declarations must have at least a translation id or a default translation message"`, `"Docusaurus translation declarations must have at least a translation id or a default translation message"`,
); );
}); });
}); });
describe('<Translate>', () => {
it('accepts id and uses it as fallback', () => {
expect(renderer.create(<Translate id="some-id" />).toJSON()).toBe(
'some-id',
);
});
it('accepts message and uses it as fallback', () => {
expect(renderer.create(<Translate>some-message</Translate>).toJSON()).toBe(
'some-message',
);
});
it('accepts id+message and uses message as fallback', () => {
expect(
renderer
.create(<Translate id="some-id">some-message</Translate>)
.toJSON(),
).toBe('some-message');
});
it('rejects when no id or message', () => {
expect(() =>
// @ts-expect-error: TS should protect when both id/message are missing
renderer.create(<Translate />),
).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus translation declarations must have at least a translation id or a default translation message"`,
);
});
it('rejects when children is not a string', () => {
expect(() =>
renderer.create(
<Translate id="foo">
{/* @ts-expect-error: for test */}
<span>aaa</span>
</Translate>,
),
).toThrowErrorMatchingInlineSnapshot(
`"The Docusaurus <Translate> component only accept simple string values"`,
);
});
});

View file

@ -1,6 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Interpolate acceptance test 1`] = ` exports[`<Interpolate> acceptance test 1`] = `
Array [
"Hello ",
"Sébastien",
" how are you ",
<span>
today
</span>,
"? Another {unprovidedValue}!",
]
`;
exports[`interpolate acceptance test 1`] = `
Array [ Array [
<React.Fragment> <React.Fragment>
Hello Hello
@ -18,7 +30,7 @@ Array [
] ]
`; `;
exports[`Interpolate placeholders with JSX values 1`] = ` exports[`interpolate placeholders with JSX values 1`] = `
Array [ Array [
<React.Fragment> <React.Fragment>
Hello Hello
@ -38,7 +50,7 @@ Array [
] ]
`; `;
exports[`Interpolate placeholders with mixed vales 1`] = ` exports[`interpolate placeholders with mixed vales 1`] = `
Array [ Array [
<React.Fragment> <React.Fragment>
Hello Hello

View file

@ -0,0 +1,30 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment jsdom
*/
// Jest doesn't allow pragma below other comments. https://github.com/facebook/jest/issues/12573
// eslint-disable-next-line header/header
import React from 'react';
import {renderHook} from '@testing-library/react-hooks/server';
import {BrowserContextProvider} from '../browserContext';
import useIsBrowser from '../useIsBrowser';
describe('BrowserContextProvider', () => {
const {result, hydrate} = renderHook(() => useIsBrowser(), {
wrapper: ({children}) => (
<BrowserContextProvider>{children}</BrowserContextProvider>
),
});
it('has value false on first render', () => {
expect(result.current).toBe(false);
});
it('has value true on hydration', () => {
hydrate();
expect(result.current).toBe(true);
});
});

View file

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment jsdom
*/
// Jest doesn't allow pragma below other comments. https://github.com/facebook/jest/issues/12573
// eslint-disable-next-line header/header
import React from 'react';
import {renderHook} from '@testing-library/react-hooks/server';
import {DocusaurusContextProvider} from '../docusaurusContext';
import useDocusaurusContext from '../useDocusaurusContext';
// This test currently isn't quite useful because the @generated aliases point
// to the empty modules. Maybe we can point that to fixtures in the future.
describe('DocusaurusContextProvider', () => {
const {result, hydrate} = renderHook(() => useDocusaurusContext(), {
wrapper: ({children}) => (
<DocusaurusContextProvider>{children}</DocusaurusContextProvider>
),
});
const value = result.current;
it('returns right value', () => {
expect(value).toMatchInlineSnapshot(`
Object {
"codeTranslations": Object {},
"globalData": Object {},
"i18n": Object {},
"siteConfig": Object {},
"siteMetadata": Object {},
}
`);
});
it('has reference-equal value on hydration', () => {
hydrate();
expect(result.current).toBe(value);
});
});

View file

@ -0,0 +1,122 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import {renderHook} from '@testing-library/react-hooks';
import useGlobalData, {
useAllPluginInstancesData,
usePluginData,
} from '../useGlobalData';
import {Context} from '../docusaurusContext';
describe('useGlobalData', () => {
it('returns global data from context', () => {
expect(
renderHook(() => useGlobalData(), {
wrapper: ({children}) => (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<Context.Provider value={{globalData: {foo: 'bar'}}}>
{children}
</Context.Provider>
),
}).result.current,
).toEqual({foo: 'bar'});
});
it('throws when global data not found', () => {
// Can it actually happen?
expect(
() =>
renderHook(() => useGlobalData(), {
wrapper: ({children}) => (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<Context.Provider value={{}}>{children}</Context.Provider>
),
}).result.current,
).toThrowErrorMatchingInlineSnapshot(`"Docusaurus global data not found."`);
});
});
describe('useAllPluginInstancesData', () => {
it('returns plugin data namespace', () => {
expect(
renderHook(() => useAllPluginInstancesData('foo'), {
wrapper: ({children}) => (
<Context.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{globalData: {foo: {default: 'default', bar: 'bar'}}}}>
{children}
</Context.Provider>
),
}).result.current,
).toEqual({default: 'default', bar: 'bar'});
});
it('throws when plugin data not found', () => {
expect(
() =>
renderHook(() => useAllPluginInstancesData('bar'), {
wrapper: ({children}) => (
<Context.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{globalData: {foo: {default: 'default', bar: 'bar'}}}}>
{children}
</Context.Provider>
),
}).result.current,
).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus plugin global data not found for \\"bar\\" plugin."`,
);
});
});
describe('usePluginData', () => {
it('returns plugin instance data', () => {
expect(
renderHook(() => usePluginData('foo', 'bar'), {
wrapper: ({children}) => (
<Context.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{globalData: {foo: {default: 'default', bar: 'bar'}}}}>
{children}
</Context.Provider>
),
}).result.current,
).toBe('bar');
});
it('defaults to default ID', () => {
expect(
renderHook(() => usePluginData('foo'), {
wrapper: ({children}) => (
<Context.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{globalData: {foo: {default: 'default', bar: 'bar'}}}}>
{children}
</Context.Provider>
),
}).result.current,
).toBe('default');
});
it('throws when plugin instance data not found', () => {
expect(
() =>
renderHook(() => usePluginData('foo', 'baz'), {
wrapper: ({children}) => (
<Context.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{globalData: {foo: {default: 'default', bar: 'bar'}}}}>
{children}
</Context.Provider>
),
}).result.current,
).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus plugin global data not found for \\"foo\\" plugin with id \\"baz\\"."`,
);
});
});

View file

@ -18,12 +18,8 @@ export default function normalizeLocation<T extends Location>(location: T): T {
}; };
} }
let pathname = location.pathname || '/'; const pathname =
pathname = pathname.trim().replace(/\/index\.html$/, ''); location.pathname.trim().replace(/\/index\.html$/, '') || '/';
if (pathname === '') {
pathname = '/';
}
pathnames[location.pathname] = pathname; pathnames[location.pathname] = pathname;

View file

@ -24,7 +24,7 @@ export default async function loadConfig(
| (() => Promise<Partial<DocusaurusConfig>>); | (() => Promise<Partial<DocusaurusConfig>>);
const loadedConfig = const loadedConfig =
importedConfig instanceof Function typeof importedConfig === 'function'
? await importedConfig() ? await importedConfig()
: await importedConfig; : await importedConfig;

View file

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID8zCCAtugAwIBAgIUK1U7Oje+GjLlzxNryMDUT72qJZ0wDQYJKoZIhvcNAQEL
BQAwgYgxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwI
U2hhbmdoYWkxGDAWBgNVBAoMD0NvbXB1dGVyaXphdGlvbjESMBAGA1UEAwwJSm9z
aC1DZW5hMSUwIwYJKoZIhvcNAQkBFhZzaWRhY2hlbjIwMDNAZ21haWwuY29tMB4X
DTIyMDMxMjE0MzI0N1oXDTIzMDMxMjE0MzI0N1owgYgxCzAJBgNVBAYTAkNOMREw
DwYDVQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwIU2hhbmdoYWkxGDAWBgNVBAoMD0Nv
bXB1dGVyaXphdGlvbjESMBAGA1UEAwwJSm9zaC1DZW5hMSUwIwYJKoZIhvcNAQkB
FhZzaWRhY2hlbjIwMDNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA7Cq2QW6rcZAm6MMo97aqkFi9dkXx97fW6vPEt2bgS9O6E+M/wXBI
q1Dh3ud8sGP+CiEWa+7uIEwX9pRGyQo0Lkr7qZWSbsDh+RmdkiKUCiIUUTBopBjM
jo7XF9KBM609GYoGlKYxv4adPbOMJcK/9VdJPz3NprIA1PHEqInJNnuKMMjBMhNu
1MZ7JwING/LYBOJ/Mve08XKAcyDdWBVPe2TOfcKhEmtBTKhnOuUicuAdVtDkN34Z
e4ZlifLo7wlQU7NNh7YDOYZz3JXB5QotuqtWkUgfpMSCWG90p4P4LExLzS+2sb7O
C/jO0qYcKjaKAKjrA9IIyClF6VP1yFRZywIDAQABo1MwUTAdBgNVHQ4EFgQUNy2X
+cLPh17QdR6raPKeoKLIm2QwHwYDVR0jBBgwFoAUNy2X+cLPh17QdR6raPKeoKLI
m2QwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAzvyP1QxKR8Ik
k7v3dzRl1gKdu6BtRL1zoFXeOjFOCVX9ORxcpCJItuTM4kEbJLhC0uFxn+zQ/Urs
JAc56gic4fCIcxlTNPr4VtAEUQKhfGG7XTRs8Cl2Rm7E7FwNiGjdLuiPI+G+ZZbl
TYmB5ILGzvI8LAOii17s5wFX84PehZ9gYgcgEvVBaU7lWF3WakR53Msf2bHkjk/r
NfaINeBltOwijhzb8pWf0XG2z4olJjg1qTOgr1gNseyTwMAFwFmeXQAoidoZfKya
DD+hY1/IgiUXi2pdmO+sMHtRBG5JdOi2cjSOcTx1xkWyb60PpW4uxKhduQPAiZRO
266P7J962Q==
-----END CERTIFICATE-----

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA7Cq2QW6rcZAm6MMo97aqkFi9dkXx97fW6vPEt2bgS9O6E+M/
wXBIq1Dh3ud8sGP+CiEWa+7uIEwX9pRGyQo0Lkr7qZWSbsDh+RmdkiKUCiIUUTBo
pBjMjo7XF9KBM609GYoGlKYxv4adPbOMJcK/9VdJPz3NprIA1PHEqInJNnuKMMjB
MhNu1MZ7JwING/LYBOJ/Mve08XKAcyDdWBVPe2TOfcKhEmtBTKhnOuUicuAdVtDk
N34Ze4ZlifLo7wlQU7NNh7YDOYZz3JXB5QotuqtWkUgfpMSCWG90p4P4LExLzS+2
sb7OC/jO0qYcKjaKAKjrA9IIyClF6VP1yFRZywIDAQABAoIBAHiHR+LW/2qC3ki2
qWba8+udTnxZMCdzzJy2cjQDrf8k/Hd/6B7qFjxQmCXx0GIZdiJnRpEpLKCRFT3D
6Ohba8wgepXO/x/FEs7VsuRM/264e9P/t7ff7C3pWn8O8N+Vz3QETF17ADK2GfPO
eX0gCmXE+V3sRdOITwJerTYys904bo5CQsDQQENpcuYbZU2IYt9dw9XrTexaFwP1
3ssOXCwpaW4kS95a6WQlwCqNTq49zqf3VGA3QG3JEdPPWhG+jEG2L4RxSosvo4wt
MYFqeXcS5sz7WOH1gtleGL2i6WKYuLl7Bo/CLokn1tgrXjGvNpeBFvZucC+L246f
e7iG+gkCgYEA+CcISFav/uwKNv3Sdp87kVpBAno8cZTiYvB15zAGaXuLyI/OuJNh
lcJBhtZSN94T/mgj+gXDafjmRr4i7Q4Pu+KG95JTk1FfWv/974NxbRNrrp+4PFKb
wxcM1cHuqq88mUPUX+k0eKPqDcuY6vHBPAV4ji1Wl+VXpREDvhKgAEUCgYEA86Kl
xnOf3TWbEaQRJx2mMnRYLyrEEPqEMgHWlzXdWl2E9LJDGGmOEbZLv6uNcx1uWJVP
AaoitmQNTl+rSsJY0TwqooX5zvT8po9MXUt8FvButJyYUOJZFTuLtLxFJqAzFipz
SaiYTrEBC76uqe/87AVm0wCdJN4ajcptyibaus8CgYEAnXSm3L+kjKxZDuufT4VZ
1rDd7ySAldFSlFTfewIOD4BFAc297YAWu1+3FEeJg8l2BkcuDMb7Z5J3Cww6PRBf
C2iBGzXNsfw/9Q3ZotBUeFGKUhMmY6BHFVLa4gdb2RG38cgISZM/qAzZxkcZkHo1
klAmXpCGEXuEUUiqh0BqJcECgYEAv42Gt0QbUeoetL0BO3blP9AXsWX3Z73/h+3I
EXUpRy42JcmuVRhQuf5RCi7QdMyUAJPL3WwuBKcfixpO6+VnvYKHpuadZSlbJ32N
NeDufH6nG9vvKdD852O80OohmF/mKqxPnn8u2Nf0EY7ndvcYLV2F3aoi42S5Dfg1
X/YyjSMCgYAg2fEisapxje98KZ4TPvOffJRF5PRG4H6UBQvxaWw9oUjVkGM6t10U
D6uOCYPkb+l3wBFTNAfScr22EnpW33Q5JOAfHBeE1oEoWGdMgp1C1V9ZQTIkjXyj
YE+lrsTFVoyY+dnLcZ4U7syVkzINk10GaAKjGXD0gtrqC+cQy8z1XQ==
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1 @@
Foo

View file

@ -0,0 +1 @@
Foo

View file

@ -13,7 +13,7 @@ import loadSetup from '../../server/__tests__/testUtils';
describe('webpack production config', () => { describe('webpack production config', () => {
it('simple', async () => { it('simple', async () => {
jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'log').mockImplementation(() => {});
const props = await loadSetup('simple'); const props = await loadSetup('simple');
const config = await createServerConfig({props}); const config = await createServerConfig({props});
const errors = webpack.validate(config); const errors = webpack.validate(config);
@ -21,7 +21,7 @@ describe('webpack production config', () => {
}); });
it('custom', async () => { it('custom', async () => {
jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'log').mockImplementation(() => {});
const props = await loadSetup('custom'); const props = await loadSetup('custom');
const config = await createServerConfig({props}); const config = await createServerConfig({props});
const errors = webpack.validate(config); const errors = webpack.validate(config);

View file

@ -12,6 +12,7 @@ import {
getCustomizableJSLoader, getCustomizableJSLoader,
applyConfigureWebpack, applyConfigureWebpack,
applyConfigurePostCss, applyConfigurePostCss,
getHttpsConfig,
} from '../utils'; } from '../utils';
import type { import type {
ConfigureWebpackFn, ConfigureWebpackFn,
@ -297,3 +298,65 @@ describe('extending PostCSS', () => {
]); ]);
}); });
}); });
describe('getHttpsConfig', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = {...originalEnv};
});
afterAll(() => {
process.env = originalEnv;
});
it('returns true for HTTPS not env', async () => {
await expect(getHttpsConfig()).resolves.toBe(false);
});
it('returns true for HTTPS in env', async () => {
process.env.HTTPS = 'true';
await expect(getHttpsConfig()).resolves.toBe(true);
});
it('returns custom certs if they are in env', async () => {
process.env.HTTPS = 'true';
process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/host.crt');
process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key');
await expect(getHttpsConfig()).resolves.toEqual({
key: expect.any(Buffer),
cert: expect.any(Buffer),
});
});
it("throws if file doesn't exist", async () => {
process.env.HTTPS = 'true';
process.env.SSL_CRT_FILE = path.join(
__dirname,
'__fixtures__/nonexistent.crt',
);
process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key');
await expect(getHttpsConfig()).rejects.toThrowErrorMatchingInlineSnapshot(
`"You specified SSL_CRT_FILE in your env, but the file \\"<PROJECT_ROOT>/packages/docusaurus/src/webpack/__tests__/__fixtures__/nonexistent.crt\\" can't be found."`,
);
});
it('throws for invalid key', async () => {
process.env.HTTPS = 'true';
process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/host.crt');
process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/invalid.key');
await expect(getHttpsConfig()).rejects.toThrowError(
/The certificate key .*[/\\]__fixtures__[/\\]invalid\.key is invalid/,
);
});
it('throws for invalid cert', async () => {
process.env.HTTPS = 'true';
process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/invalid.crt');
process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key');
await expect(getHttpsConfig()).rejects.toThrowError(
/The certificate .*[/\\]__fixtures__[/\\]invalid\.crt is invalid/,
);
});
});

View file

@ -5,11 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
// Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_difference
export function difference<T>(...arrays: T[][]): T[] {
return arrays.reduce((a, b) => a.filter((c) => !b.includes(c)));
}
// Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby // Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby
export function sortBy<T>( export function sortBy<T>(
array: T[], array: T[],