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
'^.+\\.(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/core/lib/client/exports/$1',
'@docusaurus/core/src/client/exports/$1',
// Maybe point to a fixture?
'@generated/.*': '<rootDir>/jest/emptyModule.js',
// TODO use "projects" + multiple configs if we work on another theme?
'@theme/(.*)': '@docusaurus/theme-classic/src/theme/$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/lib/client/index.js',
'@docusaurus/plugin-content-docs/src/client/index.ts',
},
globals: {
window: {

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@ const RedirectPluginOptionValidation = Joi.object<RedirectOption>({
const isString = Joi.string().required().not(null);
const UserOptionsSchema = Joi.object<UserPluginOptions>({
id: Joi.string().optional(), // TODO remove once validation migrated to new system
id: Joi.string().optional(), // TODO remove once validation migrated to new system
fromExtensions: Joi.array().items(isString),
toExtensions: Joi.array().items(isString),
redirects: Joi.array().items(RedirectPluginOptionValidation),

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');
});
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', () => {
expect(
getSlug({baseID: 'doc', source: '@site/docs/doc.md', sourceDirName: '.'}),

View file

@ -25,7 +25,7 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
hide_table_of_contents: Joi.boolean(),
keywords: Joi.array().items(Joi.string().required()),
image: URISchema,
description: Joi.string().allow(''), // see https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
description: Joi.string().allow(''), // see https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
slug: Joi.string(),
sidebar_label: Joi.string(),
sidebar_position: Joi.number(),

View file

@ -391,6 +391,10 @@ export const isCategoryIndex: CategoryIndexMatcher = ({
return eligibleDocIndexNames.includes(fileName.toLowerCase());
};
/**
* `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/
export function toCategoryIndexMatcherParam({
source,
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
// TODO legacy retro-compatibility due to old versioned sidebars using
// versioned doc ids ("id" should be removed & "versionedId" should be renamed

View file

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

View file

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

View file

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

View file

@ -139,6 +139,11 @@ export type SidebarsUtils = {
getCategoryGeneratedIndexNavigation: (
categoryGeneratedIndexPermalink: string,
) => 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) =>
| {
type: 'doc';
@ -147,7 +152,7 @@ export type SidebarsUtils = {
}
| {
type: 'generated-index';
slug: string;
permalink: string;
label: string;
}
| undefined;
@ -295,7 +300,7 @@ Available document ids are:
}
| {
type: 'generated-index';
slug: string;
permalink: string;
label: string;
}
| undefined {
@ -316,7 +321,7 @@ Available document ids are:
} else if (item.link?.type === 'generated-index') {
return {
type: 'generated-index',
slug: item.link.slug,
permalink: item.link.permalink,
label: item.label,
};
}

View file

@ -63,12 +63,11 @@ export default function getSlug({
function ensureValidSlug(slug: string): string {
if (!isValidPathname(slug)) {
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}.
Maybe your slug front matter is incorrect or you use weird chars 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:
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:
Example =>
---
slug: /my/customDocPath
---

View file

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

View file

@ -74,9 +74,9 @@ function ensureValidVersionString(version: unknown): asserts version is string {
function ensureValidVersionArray(
versionArray: unknown,
): asserts versionArray is string[] {
if (!(versionArray instanceof Array)) {
if (!Array.isArray(versionArray)) {
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,
)}`,
);

View file

@ -92,13 +92,13 @@ function useVersionPersistence(): DocsVersionPersistence {
return useThemeConfig().docs.versionPersistence;
}
// Value that will be accessible through context: [state,api]
// Value that will be accessible through context: [state,api]
function useContextValue() {
const allDocsData = useAllDocsData();
const versionPersistence = useVersionPersistence();
const pluginIds = useMemo(() => Object.keys(allDocsData), [allDocsData]);
// Initial state is empty, as we can't read browser storage in node/SSR
// Initial state is empty, as we can't read browser storage in node/SSR
const [state, setState] = useState(() => getInitialState(pluginIds));
// On mount, we set the state read from browser storage

View file

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

View file

@ -250,7 +250,7 @@ Lorem Ipsum
});
});
it('parses markdown h1 title at the top followed by h2 title', () => {
it('parses markdown h1 title at the top followed by h2 title', () => {
const markdown = dedent`
# Markdown Title

View file

@ -9,7 +9,15 @@ import {jest} from '@jest/globals';
import normalizeLocation from '../normalizeLocation';
describe('normalizeLocation', () => {
it('rewrite locations with index.html', () => {
it('rewrites locations with index.html', () => {
expect(
normalizeLocation({
pathname: '/index.html',
}),
).toEqual({
pathname: '/',
});
expect(
normalizeLocation({
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');
expect(

View file

@ -89,12 +89,12 @@ export function interpolate<Str extends string, Value extends ReactNode>(
export default function Interpolate<Str extends string>({
children,
values,
}: InterpolateProps<Str>): ReactNode {
}: InterpolateProps<Str>): JSX.Element {
if (typeof children !== 'string') {
console.warn('Illegal <Interpolate> children', children);
throw new Error(
'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.
*/
import type {ReactNode} from 'react';
import React from 'react';
import {interpolate, type InterpolateValues} from '@docusaurus/Interpolate';
import type {TranslateParam, TranslateProps} from '@docusaurus/Translate';
@ -46,7 +46,7 @@ export default function Translate<Str extends string>({
children,
id,
values,
}: TranslateProps<Str>): ReactNode {
}: TranslateProps<Str>): JSX.Element {
if (children && typeof children !== 'string') {
console.warn('Illegal <Translate> children', children);
throw new Error(
@ -55,5 +55,5 @@ export default function Translate<Str extends string>({
}
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.
*/
import {jest} from '@jest/globals';
import React from 'react';
import renderer from 'react-test-renderer';
import BrowserOnly from '../BrowserOnly';
import {Context} from '../browserContext';
jest.mock('@docusaurus/useIsBrowser', () => () => true);
describe('BrowserOnly', () => {
describe('<BrowserOnly>', () => {
it('rejects react element children', () => {
process.env.NODE_ENV = 'development';
expect(() => {
renderer.create(
<BrowserOnly>
{/* @ts-expect-error test */}
<span>{window.location.href}</span>
</BrowserOnly>,
);
}).toThrowErrorMatchingInlineSnapshot(`
expect(() =>
renderer
.create(
<Context.Provider value>
<BrowserOnly>
{/* @ts-expect-error test */}
<span>{window.location.href}</span>
</BrowserOnly>
</Context.Provider>,
)
.toJSON(),
).toThrowErrorMatchingInlineSnapshot(`
"Docusaurus error: The children of <BrowserOnly> must be a \\"render function\\", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>.
Current type: React element"
`);
});
it('rejects string children', () => {
process.env.NODE_ENV = 'development';
expect(() => {
renderer.create(
// @ts-expect-error test
<BrowserOnly> </BrowserOnly>,
<Context.Provider value>
{/* @ts-expect-error test */}
<BrowserOnly> </BrowserOnly>
</Context.Provider>,
);
}).toThrowErrorMatchingInlineSnapshot(`
"Docusaurus error: The children of <BrowserOnly> must be a \\"render function\\", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>.
Current type: string"
`);
});
it('accepts valid children', () => {
expect(
renderer
.create(
<BrowserOnly fallback={<span>Loading</span>}>
{() => <span>{window.location.href}</span>}
</BrowserOnly>,
<Context.Provider value>
<BrowserOnly fallback={<span>Loading</span>}>
{() => <span>{window.location.href}</span>}
</BrowserOnly>
</Context.Provider>,
)
.toJSON(),
).toMatchInlineSnapshot(`
@ -53,4 +62,36 @@ describe('BrowserOnly', () => {
</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 {interpolate} from '../Interpolate';
import renderer from 'react-test-renderer';
import Interpolate, {interpolate} from '../Interpolate';
describe('Interpolate', () => {
describe('interpolate', () => {
it('without placeholders', () => {
const text = 'Hello how are you?';
expect(interpolate(text)).toEqual(text);
@ -86,3 +87,50 @@ describe('Interpolate', () => {
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.
*/
import {translate} from '../Translate';
import React from 'react';
import renderer from 'react-test-renderer';
import Translate, {translate} from '../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');
});
it('accept message and use it as fallback', () => {
it('accepts message and uses it as fallback', () => {
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(
'some-message',
);
});
it('reject when no id or message', () => {
// TODO tests are not resolving type defs correctly
it('rejects when no id or message', () => {
// 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
expect(() => translate({})).toThrowErrorMatchingInlineSnapshot(
`"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
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 [
<React.Fragment>
Hello
@ -18,7 +30,7 @@ Array [
]
`;
exports[`Interpolate placeholders with JSX values 1`] = `
exports[`interpolate placeholders with JSX values 1`] = `
Array [
<React.Fragment>
Hello
@ -38,7 +50,7 @@ Array [
]
`;
exports[`Interpolate placeholders with mixed vales 1`] = `
exports[`interpolate placeholders with mixed vales 1`] = `
Array [
<React.Fragment>
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 || '/';
pathname = pathname.trim().replace(/\/index\.html$/, '');
if (pathname === '') {
pathname = '/';
}
const pathname =
location.pathname.trim().replace(/\/index\.html$/, '') || '/';
pathnames[location.pathname] = pathname;

View file

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

View file

@ -464,7 +464,7 @@ ${Object.entries(registry)
return props;
}
// We want all @docusaurus/* packages to have the exact same version!
// We want all @docusaurus/* packages to have the exact same version!
// See https://github.com/facebook/docusaurus/issues/3371
// See https://github.com/facebook/docusaurus/pull/3386
function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) {

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', () => {
it('simple', async () => {
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(console, 'log').mockImplementation(() => {});
const props = await loadSetup('simple');
const config = await createServerConfig({props});
const errors = webpack.validate(config);
@ -21,7 +21,7 @@ describe('webpack production config', () => {
});
it('custom', async () => {
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(console, 'log').mockImplementation(() => {});
const props = await loadSetup('custom');
const config = await createServerConfig({props});
const errors = webpack.validate(config);

View file

@ -12,6 +12,7 @@ import {
getCustomizableJSLoader,
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
} from '../utils';
import type {
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.
*/
// 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
export function sortBy<T>(
array: T[],