test: improve test coverage (#6857)

This commit is contained in:
Joshua Chen 2022-03-06 17:55:21 +08:00 committed by GitHub
parent edb4d00096
commit f763ac13a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 435 additions and 220 deletions

View file

@ -22,6 +22,47 @@ Array [
]
`;
exports[`translateContent should fallback when translation is incomplete 1`] = `
Object {
"blogListPaginated": Array [
Object {
"items": Array [
"hello",
],
"metadata": Object {
"blogDescription": "Someone's random blog",
"blogTitle": "My blog",
"nextPage": null,
"page": 1,
"permalink": "/",
"postsPerPage": 10,
"previousPage": null,
"totalCount": 1,
"totalPages": 1,
},
},
],
"blogPosts": Array [
Object {
"id": "hello",
"metadata": Object {
"date": 2021-07-19T00:00:00.000Z,
"description": "/blog/2021/06/19/hello",
"formattedDate": "June 19, 2021",
"permalink": "/blog/2021/06/19/hello",
"source": "/blog/2021/06/19/hello",
"tags": Array [],
"title": "Hello",
"truncated": true,
},
},
],
"blogSidebarTitle": "All my posts",
"blogTags": Object {},
"blogTagsListPath": "/tags",
}
`;
exports[`translateContent should return translated loaded content matching snapshot 1`] = `
Object {
"blogListPaginated": Array [

View file

@ -77,6 +77,12 @@ describe('getContentTranslationFiles', () => {
});
describe('translateContent', () => {
test('should fallback when translation is incomplete', () => {
expect(
translateContent(sampleBlogContent, [{path: 'foo', content: {}}]),
).toMatchSnapshot();
});
test('should not translate anything if translation files are untranslated', () => {
const translationFiles = getSampleTranslationFiles();
expect(translateContent(sampleBlogContent, translationFiles)).toEqual(

View file

@ -53,9 +53,6 @@ export function translateContent(
content: BlogContent,
translationFiles: TranslationFiles,
): BlogContent {
if (translationFiles.length === 0) {
return content;
}
const {content: optionsTranslations} = translationFiles[0]!;
return {
...content,

View file

@ -21,10 +21,6 @@ function getCategoryGeneratedIndexMetadata({
}): CategoryGeneratedIndexMetadata {
const {sidebarName, previous, next} =
sidebarsUtils.getCategoryGeneratedIndexNavigation(category.link.permalink);
if (!sidebarName) {
throw new Error('unexpected');
}
return {
title: category.link.title ?? category.label,
description: category.link.description,
@ -32,7 +28,7 @@ function getCategoryGeneratedIndexMetadata({
keywords: category.link.keywords,
slug: category.link.slug,
permalink: category.link.permalink,
sidebar: sidebarName,
sidebar: sidebarName!,
previous: toNavigationLink(previous, docsById),
next: toNavigationLink(next, docsById),
};

View file

@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DefaultSidebarItemsGenerator generates complex nested sidebar 1`] = `
Array [
Object {
"id": "intro",
"type": "doc",
},
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "tutorial1",
"type": "doc",
},
Object {
"id": "tutorial2",
"type": "doc",
},
],
"label": "Tutorials",
"link": Object {
"id": "tutorials-index",
"type": "doc",
},
"type": "category",
},
Object {
"collapsed": false,
"collapsible": undefined,
"customProps": Object {
"description": "foo",
},
"items": Array [
Object {
"className": "foo",
"id": "guide1",
"type": "doc",
},
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "nested-guide",
"type": "doc",
},
],
"label": "SubGuides (metadata file label)",
"link": Object {
"description": "subguides-description",
"slug": "subguides-generated-index-slug",
"title": "subguides-title",
"type": "generated-index",
},
"type": "category",
},
Object {
"id": "guide2",
"type": "doc",
},
],
"label": "Guides",
"link": Object {
"id": "guides-index",
"type": "doc",
},
"type": "category",
},
Object {
"id": "end",
"type": "doc",
},
]
`;
exports[`DefaultSidebarItemsGenerator generates simple flat sidebar 1`] = `
Array [
Object {
"id": "doc3",
"type": "doc",
},
Object {
"id": "doc4",
"type": "doc",
},
Object {
"id": "doc1",
"label": "doc1 sidebar label",
"type": "doc",
},
Object {
"id": "doc2",
"type": "doc",
},
Object {
"id": "doc5",
"type": "doc",
},
]
`;
exports[`DefaultSidebarItemsGenerator generates subfolder sidebar 1`] = `
Array [
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "doc8",
"type": "doc",
},
Object {
"id": "doc7",
"type": "doc",
},
],
"label": "subsubsubfolder3 (_category_.json label)",
"link": Object {
"id": "doc1",
"type": "doc",
},
"type": "category",
},
Object {
"className": "bar",
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "doc6",
"type": "doc",
},
],
"label": "subsubsubfolder2 (_category_.yml label)",
"type": "category",
},
Object {
"id": "doc1",
"type": "doc",
},
Object {
"id": "doc4",
"type": "doc",
},
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "doc5",
"type": "doc",
},
],
"label": "subsubsubfolder",
"type": "category",
},
]
`;
exports[`DefaultSidebarItemsGenerator respects custom isCategoryIndex 1`] = `
Array [
Object {
"id": "intro",
"type": "doc",
},
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "tutorial1",
"type": "doc",
},
Object {
"id": "tutorial2",
"type": "doc",
},
],
"label": "Tutorials",
"link": Object {
"id": "tutorials-index",
"type": "doc",
},
"type": "category",
},
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"className": "foo",
"id": "guide1",
"type": "doc",
},
Object {
"id": "guide2",
"type": "doc",
},
Object {
"id": "not-guides-index",
"type": "doc",
},
],
"label": "Guides",
"type": "category",
},
]
`;
exports[`DefaultSidebarItemsGenerator uses explicit link over the index/readme.{md,mdx} naming convention 1`] = `
Array [
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "parent/doc2",
"type": "doc",
},
Object {
"id": "parent/doc1",
"type": "doc",
},
],
"label": "Category label",
"link": Object {
"id": "parent/doc3",
"type": "doc",
},
"type": "category",
},
Object {
"collapsed": undefined,
"collapsible": undefined,
"items": Array [
Object {
"id": "parent/doc4",
"type": "doc",
},
Object {
"id": "parent/doc6",
"type": "doc",
},
Object {
"id": "parent/doc5",
"type": "doc",
},
],
"label": "Category 2 label",
"type": "category",
},
]
`;

View file

@ -6,7 +6,7 @@
*/
import {DefaultSidebarItemsGenerator} from '../generator';
import type {Sidebar, SidebarItemsGenerator} from '../types';
import type {SidebarItemsGenerator} from '../types';
import {DefaultNumberPrefixParser} from '../../numberPrefix';
import {isCategoryIndex} from '../../docs';
@ -104,13 +104,7 @@ describe('DefaultSidebarItemsGenerator', () => {
},
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'doc3'},
{type: 'doc', id: 'doc4'},
{type: 'doc', id: 'doc1', label: 'doc1 sidebar label'},
{type: 'doc', id: 'doc2'},
{type: 'doc', id: 'doc5'},
] as Sidebar);
expect(sidebarSlice).toMatchSnapshot();
});
test('generates complex nested sidebar', async () => {
@ -214,49 +208,7 @@ describe('DefaultSidebarItemsGenerator', () => {
},
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'intro'},
{
type: 'category',
label: 'Tutorials',
link: {
type: 'doc',
id: 'tutorials-index',
},
items: [
{type: 'doc', id: 'tutorial1'},
{type: 'doc', id: 'tutorial2'},
],
},
{
type: 'category',
label: 'Guides',
link: {
type: 'doc',
id: 'guides-index',
},
customProps: {
description: 'foo',
},
collapsed: false,
items: [
{type: 'doc', id: 'guide1', className: 'foo'},
{
type: 'category',
label: 'SubGuides (metadata file label)',
items: [{type: 'doc', id: 'nested-guide'}],
link: {
type: 'generated-index',
slug: 'subguides-generated-index-slug',
title: 'subguides-title',
description: 'subguides-description',
},
},
{type: 'doc', id: 'guide2'},
],
},
{type: 'doc', id: 'end'},
] as Sidebar);
expect(sidebarSlice).toMatchSnapshot();
});
test('generates subfolder sidebar', async () => {
@ -352,33 +304,7 @@ describe('DefaultSidebarItemsGenerator', () => {
},
});
expect(sidebarSlice).toEqual([
{
type: 'category',
label: 'subsubsubfolder3 (_category_.json label)',
link: {
id: 'doc1',
type: 'doc',
},
items: [
{type: 'doc', id: 'doc8'},
{type: 'doc', id: 'doc7'},
],
},
{
type: 'category',
label: 'subsubsubfolder2 (_category_.yml label)',
className: 'bar',
items: [{type: 'doc', id: 'doc6'}],
},
{type: 'doc', id: 'doc1'},
{type: 'doc', id: 'doc4'},
{
type: 'category',
label: 'subsubsubfolder',
items: [{type: 'doc', id: 'doc5'}],
},
] as Sidebar);
expect(sidebarSlice).toMatchSnapshot();
});
test('uses explicit link over the index/readme.{md,mdx} naming convention', async () => {
@ -449,45 +375,7 @@ describe('DefaultSidebarItemsGenerator', () => {
},
});
expect(sidebarSlice).toEqual([
{
type: 'category',
label: 'Category label',
link: {
id: 'parent/doc3',
type: 'doc',
},
items: [
{
id: 'parent/doc2',
type: 'doc',
},
// doc1 is below doc2, because its file name is index.md
{
id: 'parent/doc1',
type: 'doc',
},
],
},
{
type: 'category',
label: 'Category 2 label',
items: [
{
id: 'parent/doc4',
type: 'doc',
},
{
id: 'parent/doc6',
type: 'doc',
},
{
id: 'parent/doc5',
type: 'doc',
},
],
},
] as Sidebar);
expect(sidebarSlice).toMatchSnapshot();
});
test('respects custom isCategoryIndex', async () => {
@ -570,32 +458,49 @@ describe('DefaultSidebarItemsGenerator', () => {
},
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'intro'},
{
type: 'category',
label: 'Tutorials',
link: {
type: 'doc',
id: 'tutorials-index',
expect(sidebarSlice).toMatchSnapshot();
});
test('throws for unknown index link', async () => {
const generateSidebar = () =>
DefaultSidebarItemsGenerator({
numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
item: {
type: 'autogenerated',
dirName: '.',
},
items: [
{type: 'doc', id: 'tutorial1'},
{type: 'doc', id: 'tutorial2'},
],
},
{
type: 'category',
label: 'Guides',
items: [
{type: 'doc', id: 'guide1', className: 'foo'},
{type: 'doc', id: 'guide2'},
version: {
versionName: 'current',
contentPath: '',
},
categoriesMetadata: {
category: {
link: {
type: 'doc',
id: 'foo',
},
},
},
docs: [
{
type: 'doc',
id: 'not-guides-index',
id: 'intro',
unversionedId: 'intro',
source: '@site/docs/category/intro.md',
sourceDirName: 'category',
frontMatter: {},
},
],
},
] as Sidebar);
options: {
sidebarCollapsed: true,
sidebarCollapsible: true,
},
});
await expect(generateSidebar).rejects.toThrowErrorMatchingInlineSnapshot(`
"Can't find any doc with ID foo.
Available doc IDs:
- intro"
`);
});
});

View file

@ -23,7 +23,10 @@ describe('loadSidebars', () => {
frontMatter: {},
},
],
version: {contentPath: 'docs/foo', contentPathLocalized: 'docs/foo'},
version: {
contentPath: path.join(fixtureDir, 'docs'),
contentPathLocalized: path.join(fixtureDir, 'docs'),
},
categoryLabelSlugger: null,
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
};
@ -107,4 +110,36 @@ describe('loadSidebars', () => {
const result = await loadSidebars(sidebarPath, params);
expect(result).toMatchSnapshot();
});
test('duplicate category metadata files', async () => {
const sidebarPath = path.join(
fixtureDir,
'sidebars-collapsed-first-level.json',
);
const consoleWarnMock = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
const consoleErrorMock = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
await expect(() =>
loadSidebars(sidebarPath, {
...params,
version: {
contentPath: path.join(fixtureDir, 'invalid-docs'),
contentPathLocalized: path.join(fixtureDir, 'invalid-docs'),
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not allowed"`);
expect(consoleWarnMock).toBeCalledWith(
expect.stringMatching(
/.*\[WARNING].* There are more than one category metadata files for .*foo.*: foo\/_category_.json, foo\/_category_.yml. The behavior is undetermined./,
),
);
expect(consoleErrorMock).toBeCalledWith(
expect.stringMatching(
/.*\[ERROR].* The docs sidebar category metadata file .*foo\/_category_.json.* looks invalid!/,
),
);
});
});

View file

@ -60,9 +60,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
const doc = findDoc(docId);
if (!doc) {
throw new Error(
`Can't find any doc with id=${docId}.\nAvailable doc ids:\n- ${Object.keys(
docsById,
).join('\n- ')}`,
`Can't find any doc with ID ${docId}.
Available doc IDs:
- ${Object.keys(docsById).join('\n- ')}`,
);
}
return doc;

View file

@ -258,11 +258,7 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
const sidebarName = Object.entries(sidebarNameToNavigationItems).find(
([, navigationItems]) =>
navigationItems.find(isCurrentCategoryGeneratedIndexItem),
)?.[0];
if (!sidebarName) {
return emptySidebarNavigation();
}
)![0];
const navigationItems = sidebarNameToNavigationItems[sidebarName]!;
const currentItemIndex = navigationItems.findIndex(
isCurrentCategoryGeneratedIndexItem,

View file

@ -161,10 +161,7 @@ export type ImportedPresetModule = PresetModule & {
default?: PresetModule;
};
export type PresetConfig =
| [string, Record<string, unknown>]
| [string]
| string;
export type PresetConfig = string | [string, Record<string, unknown>];
export type HostPortCLIOptions = {
host?: string;
@ -356,9 +353,8 @@ export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
export type PluginOptions = {id?: string} & Record<string, unknown>;
export type PluginConfig =
| [string, PluginOptions]
| [string]
| string
| [string, PluginOptions]
| [PluginModule, PluginOptions]
| PluginModule;

View file

@ -55,7 +55,9 @@ function createPluginSchema(theme: boolean = false) {
Joi.alternatives()
.try(
Joi.function(),
Joi.array().ordered(Joi.function().required(), Joi.object().required()),
Joi.array()
.ordered(Joi.function().required(), Joi.object().required())
.length(2),
Joi.string(),
Joi.array()
.ordered(Joi.string().required(), Joi.object().required())

View file

@ -3,3 +3,7 @@ module.exports = function (context, options) {
name: 'third-plugin',
};
};
module.exports.validateThemeConfig = function ({validate, themeConfig}) {
return {a: 1};
};

View file

@ -38,6 +38,7 @@ describe('initPlugins', () => {
expect(plugins[1].name).toBe('second-plugin');
expect(plugins[2].name).toBe('third-plugin');
expect(plugins[3].name).toBe('fourth-plugin');
expect(context.siteConfig.themeConfig).toEqual({a: 1});
});
test('plugins with bad values throw user-friendly error message', async () => {

View file

@ -89,7 +89,7 @@ export async function loadPlugins({
// need to run in certain order or depend on others for data.
const loadedPlugins: LoadedPlugin[] = await Promise.all(
plugins.map(async (plugin) => {
const content = plugin.loadContent ? await plugin.loadContent() : null;
const content = await plugin.loadContent?.();
return {...plugin, content};
}),
);
@ -205,7 +205,7 @@ export async function loadPlugins({
await Promise.all(
contentLoadedTranslatedPlugins.map(async (plugin) => {
if (!plugin.routesLoaded) {
return null;
return;
}
// TODO remove this deprecated lifecycle soon
@ -213,7 +213,7 @@ export async function loadPlugins({
// TODO, 1 user reported usage of this lifecycle! https://github.com/facebook/docusaurus/issues/3918
logger.error`Plugin code=${'routesLoaded'} lifecycle is deprecated. If you think we should keep this lifecycle, please report here: path=${'https://github.com/facebook/docusaurus/issues/3918'}`;
return plugin.routesLoaded(pluginsRouteConfigs);
await plugin.routesLoaded(pluginsRouteConfigs);
}),
);

View file

@ -61,39 +61,29 @@ async function normalizePluginConfig(
};
}
if (Array.isArray(pluginConfig)) {
// plugins: [
// ['./plugin',options],
// ]
if (typeof pluginConfig[0] === 'string') {
const pluginModuleImport = pluginConfig[0];
const pluginPath = pluginRequire.resolve(pluginModuleImport);
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
return {
plugin: pluginModule?.default ?? pluginModule,
options: pluginConfig[1] ?? {},
pluginModule: {
path: pluginModuleImport,
module: pluginModule,
},
};
}
// plugins: [
// [function plugin() { },options],
// ]
if (typeof pluginConfig[0] === 'function') {
return {
plugin: pluginConfig[0],
options: pluginConfig[1] ?? {},
};
}
// plugins: [
// ['./plugin',options],
// ]
if (typeof pluginConfig[0] === 'string') {
const pluginModuleImport = pluginConfig[0];
const pluginPath = pluginRequire.resolve(pluginModuleImport);
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
return {
plugin: pluginModule?.default ?? pluginModule,
options: pluginConfig[1],
pluginModule: {
path: pluginModuleImport,
module: pluginModule,
},
};
}
throw new Error(
`Unexpected: can't load plugin for following plugin config.\n${JSON.stringify(
pluginConfig,
)}`,
);
// plugins: [
// [function plugin() { },options],
// ]
return {
plugin: pluginConfig[0],
options: pluginConfig[1],
};
}
export async function normalizePluginConfigs(
@ -219,16 +209,9 @@ export default async function initPlugins({
};
}
const plugins: InitializedPlugin[] = (
await Promise.all(
pluginConfigsNormalized.map((pluginConfig) => {
if (!pluginConfig) {
return null;
}
return initializePlugin(pluginConfig);
}),
)
).filter(<T>(item: T): item is Exclude<T, null> => Boolean(item));
const plugins: InitializedPlugin[] = await Promise.all(
pluginConfigsNormalized.map(initializePlugin),
);
ensureUniquePluginInstanceIds(plugins);

View file

@ -32,10 +32,8 @@ export default async function loadPresets(context: LoadContext): Promise<{
let presetOptions = {};
if (typeof presetItem === 'string') {
presetModuleImport = presetItem;
} else if (Array.isArray(presetItem)) {
[presetModuleImport, presetOptions = {}] = presetItem;
} else {
throw new Error('Invalid presets format detected in config.');
[presetModuleImport, presetOptions] = presetItem;
}
const presetName = resolveModuleName(
presetModuleImport,

View file

@ -12,7 +12,7 @@ describe('getPluginVersion', () => {
it('Can detect external packages plugins versions of correctly.', async () => {
await expect(
getPluginVersion(
path.join(__dirname, '..', '__fixtures__', 'dummy-plugin.js'),
path.join(__dirname, '__fixtures__/dummy-plugin.js'),
// Make the plugin appear external.
path.join(__dirname, '..', '..', '..', '..', '..', '..', 'website'),
),
@ -22,9 +22,9 @@ describe('getPluginVersion', () => {
it('Can detect project plugins versions correctly.', async () => {
await expect(
getPluginVersion(
path.join(__dirname, '..', '__fixtures__', 'dummy-plugin.js'),
path.join(__dirname, '__fixtures__/dummy-plugin.js'),
// Make the plugin appear project local.
path.join(__dirname, '..', '__fixtures__'),
path.join(__dirname, '__fixtures__'),
),
).resolves.toEqual({type: 'project'});
});

View file

@ -22,11 +22,8 @@ export async function getPackageJsonVersion(
async function getPackageJsonName(
packageJsonPath: string,
): Promise<string | undefined> {
if (await fs.pathExists(packageJsonPath)) {
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require
return require(packageJsonPath).name;
}
return undefined;
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require
return require(packageJsonPath).name;
}
export async function getPluginVersion(