fix(v2): docs plugin stability improvement (100% test coverage) (#1912)

* update jest config

* add more tests on docs plugin

* fix(v2): docs plugin should not add routes if there are no docs

* fix

* rm -rf coverage

* nits

* update
This commit is contained in:
Endi 2019-10-29 22:59:27 +07:00 committed by Yangshun Tay
parent ad22c9fab4
commit a8826b98b3
26 changed files with 464 additions and 71 deletions

View file

@ -14,6 +14,7 @@
- Prioritize `@docusaurus/core` dependencies/ node_modules over user's node_modules. This fix a bug whereby if user has core-js@3 on its own node_modules but docusaurus depends on core-js@2, we previously encounter `Module not found: core-js/modules/xxxx` (because core-js@3 doesn't have that).
Another example is if user installed webpack@3 but docusaurus depends on webpack@4.
- Added code block line highlighting feature (thanks @lex111)! If you have previously swizzled the `CodeBlock` theme component, it is recommended to update your source code to have this feature.
- Fix a bug where docs plugin add `/docs` route even if docs folder is empty. We also improved docs plugin test coverage to 100% for stability before working on docs versioning.
## 2.0.0-alpha.31

View file

@ -7,20 +7,23 @@
const path = require('path');
module.exports = {
rootDir: path.resolve(__dirname),
verbose: true,
testURL: 'http://localhost/',
testEnvironment: 'node',
testPathIgnorePatterns: [
const ignorePatterns = [
'/node_modules/',
'__fixtures__',
'/packages/docusaurus/lib',
'/packages/docusaurus-utils/lib',
'/packages/docusaurus-plugin-content-blog/lib',
'/packages/docusaurus-plugin-content-docs-legacy/lib',
'/packages/docusaurus-plugin-content-docs/lib',
'/packages/docusaurus-plugin-content-pages/lib',
],
];
module.exports = {
rootDir: path.resolve(__dirname),
verbose: true,
testURL: 'http://localhost/',
testEnvironment: 'node',
testPathIgnorePatterns: ignorePatterns,
coveragePathIgnorePatterns: ignorePatterns,
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},

View file

@ -0,0 +1,5 @@
---
id: hello/super
---
Lorem

View file

@ -0,0 +1,16 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = {
title: 'My Site',
tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.
};

View file

@ -0,0 +1,11 @@
{
"docs": {
"Test": [
{
"type": "category",
"label": "Category Label",
"items": "doc1"
}
]
}
}

View file

@ -0,0 +1,20 @@
{
"docs": {
"Test": [
"foo/bar",
"foo/baz",
{
"type": "category",
"label": "category",
"href": "https://github.com"
},
{
"type": "ref",
"id": "hello"
}
],
"Guides": [
"hello"
]
}
}

View file

@ -0,0 +1,16 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = {
title: 'My Site',
tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.
};

View file

@ -0,0 +1,23 @@
{
"docs": {
"Test": [
{
"type": "category",
"label": "foo",
"items": ["foo/bar", "foo/baz"]
},
{
"type": "link",
"label": "Github",
"href": "https://github.com"
},
{
"type": "ref",
"id": "hello"
}
],
"Guides": [
"hello"
]
}
}

View file

@ -0,0 +1,7 @@
{
"docs": {
"Test": [
"goku"
]
}
}

View file

@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`simple website content 1`] = `
Object {
"docs": Array [
Object {
"items": Array [
Object {
"items": Array [
Object {
"href": "/docs/foo/bar",
"label": "Bar",
"type": "link",
},
Object {
"href": "/docs/foo/baz",
"label": "baz",
"type": "link",
},
],
"label": "foo",
"type": "category",
},
Object {
"href": "https://github.com",
"label": "Github",
"type": "link",
},
Object {
"href": "/docs/hello",
"label": "Hello, World !",
"type": "link",
},
],
"label": "Test",
"type": "category",
},
Object {
"items": Array [
Object {
"href": "/docs/hello",
"label": "Hello, World !",
"type": "link",
},
],
"label": "Guides",
"type": "category",
},
],
}
`;
exports[`simple website content 2`] = `
Array [
Object {
"component": "@theme/DocPage",
"modules": Object {
"docsMetadata": "@docusaurus-plugin-content-docs/docs-b5f.json",
},
"path": "/docs",
"routes": Array [
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/permalink.md",
"metadata": "@docusaurus-plugin-content-docs/docs-endiliey-permalink-086.json",
},
"path": "/docs/endiliey/permalink",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/foo/bar.md",
"metadata": "@docusaurus-plugin-content-docs/docs-foo-bar-cef.json",
},
"path": "/docs/foo/bar",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/foo/baz.md",
"metadata": "@docusaurus-plugin-content-docs/docs-foo-baz-dd9.json",
},
"path": "/docs/foo/baz",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/hello.md",
"metadata": "@docusaurus-plugin-content-docs/docs-hello-da2.json",
},
"path": "/docs/hello",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/lorem.md",
"metadata": "@docusaurus-plugin-content-docs/docs-lorem-17b.json",
},
"path": "/docs/lorem",
},
],
},
]
`;

View file

@ -6,32 +6,109 @@
*/
import path from 'path';
import {validate} from 'webpack';
import fs from 'fs-extra';
import pluginContentDocs from '../index';
import {LoadContext} from '@docusaurus/types';
import {loadContext} from '@docusaurus/core/src/server/index';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
import {RouteConfig} from '@docusaurus/types';
describe('loadDocs', () => {
test('simple website', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const generatedFilesDir: string = path.resolve(siteDir, '.docusaurus');
const siteConfig = {
title: 'Hello',
baseUrl: '/',
url: 'https://docusaurus.io',
const createFakeActions = (routeConfigs: RouteConfig[], contentDir) => {
return {
addRoute: (config: RouteConfig) => {
config.routes.sort((a, b) =>
a.path > b.path ? 1 : b.path > a.path ? -1 : 0,
);
routeConfigs.push(config);
},
createData: async (name, _content) => {
return path.join(contentDir, name);
},
};
const context = {
siteDir,
siteConfig,
generatedFilesDir,
} as LoadContext;
};
test('site with wrong sidebar file', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = pluginContentDocs(context, {
sidebarPath,
});
return plugin
.loadContent()
.catch(e =>
expect(e).toMatchInlineSnapshot(
`[Error: Improper sidebars file, document with id 'goku' not found.]`,
),
);
});
describe('empty/no docs website', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'empty-site');
const context = loadContext(siteDir);
test('no files in docs folder', async () => {
await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = pluginContentDocs(context, {});
const content = await plugin.loadContent();
const {docsMetadata, docsSidebars} = content;
expect(docsMetadata).toMatchInlineSnapshot(`Object {}`);
expect(docsSidebars).toMatchInlineSnapshot(`Object {}`);
const routeConfigs = [];
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
const actions = createFakeActions(routeConfigs, pluginContentDir);
await plugin.contentLoaded({
content,
actions,
});
expect(routeConfigs).toEqual([]);
});
test('docs folder does not exist', async () => {
const plugin = pluginContentDocs(context, {path: '/path/does/not/exist/'});
const content = await plugin.loadContent();
expect(content).toBeNull();
});
});
describe('simple website', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'sidebars.json');
const pluginPath = 'docs';
const plugin = pluginContentDocs(context, {
path: pluginPath,
sidebarPath,
});
const {docsMetadata} = await plugin.loadContent();
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
test('getPathToWatch', () => {
const pathToWatch = plugin.getPathsToWatch();
expect(pathToWatch).not.toEqual([]);
});
test('configureWebpack', async () => {
const config = applyConfigureWebpack(
plugin.configureWebpack,
{
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
},
false,
);
const errors = validate(config);
expect(errors.length).toBe(0);
});
test('content', async () => {
const content = await plugin.loadContent();
const {docsMetadata, docsSidebars} = content;
expect(docsMetadata.hello).toEqual({
id: 'hello',
permalink: '/docs/hello',
@ -57,5 +134,18 @@ describe('loadDocs', () => {
title: 'Bar',
description: 'This is custom description',
});
expect(docsSidebars).toMatchSnapshot();
const routeConfigs = [];
const actions = createFakeActions(routeConfigs, pluginContentDir);
await plugin.contentLoaded({
content,
actions,
});
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot();
});
});

View file

@ -7,15 +7,17 @@
import fs from 'fs';
import path from 'path';
import shell from 'shelljs';
import spawn from 'cross-spawn';
import lastUpdate from '../lastUpdate';
describe('lastUpdate', () => {
test('existing test file in repository with Git timestamp', () => {
const existingFilePath = path.join(
__dirname,
'__fixtures__/website/docs/hello.md',
'__fixtures__/simple-site/docs/hello.md',
);
test('existing test file in repository with Git timestamp', () => {
const lastUpdateData = lastUpdate(existingFilePath);
expect(lastUpdateData).not.toBeNull();
@ -44,4 +46,30 @@ describe('lastUpdate', () => {
expect(lastUpdate(tempFilePath)).toBeNull();
fs.unlinkSync(tempFilePath);
});
test('Git does not exist', () => {
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
const consoleMock = jest.spyOn(console, 'warn').mockImplementation();
const lastUpdateData = lastUpdate(existingFilePath);
expect(lastUpdateData).toBeNull();
expect(consoleMock).toHaveBeenLastCalledWith(
'Sorry, the docs plugin last update options require Git.',
);
consoleMock.mockRestore();
mock.mockRestore();
});
test('Error', () => {
const mock = jest.spyOn(spawn, 'sync').mockImplementationOnce(() => {
throw new Error('PERMISSION Error');
});
const consoleMock = jest.spyOn(console, 'error').mockImplementation();
const lastUpdateData = lastUpdate('/fake/path/');
expect(lastUpdateData).toBeNull();
expect(consoleMock).toHaveBeenLastCalledWith(new Error('PERMISSION Error'));
consoleMock.mockRestore();
mock.mockRestore();
});
});

View file

@ -9,14 +9,15 @@ import path from 'path';
import processMetadata from '../metadata';
describe('processMetadata', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const fixtureDir = path.join(__dirname, '__fixtures__');
const simpleSiteDir = path.join(fixtureDir, 'simple-site');
const siteConfig = {
title: 'Hello',
baseUrl: '/',
url: 'https://docusaurus.io',
};
const pluginPath = 'docs';
const docsDir = path.resolve(siteDir, pluginPath);
const docsDir = path.resolve(simpleSiteDir, pluginPath);
test('normal docs', async () => {
const sourceA = path.join('foo', 'bar.md');
@ -29,7 +30,7 @@ describe('processMetadata', () => {
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir,
siteDir: simpleSiteDir,
}),
processMetadata({
source: sourceB,
@ -37,7 +38,7 @@ describe('processMetadata', () => {
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir,
siteDir: simpleSiteDir,
}),
]);
@ -65,7 +66,7 @@ describe('processMetadata', () => {
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir,
siteDir: simpleSiteDir,
});
expect(data).toEqual({
@ -87,7 +88,7 @@ describe('processMetadata', () => {
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir,
siteDir: simpleSiteDir,
editUrl,
});
@ -110,7 +111,7 @@ describe('processMetadata', () => {
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir,
siteDir: simpleSiteDir,
});
expect(data).toEqual({
@ -122,4 +123,46 @@ describe('processMetadata', () => {
description: 'Lorem ipsum.',
});
});
test('docs with last update time and author', async () => {
const source = 'lorem.md';
const data = await processMetadata({
source,
docsDir,
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir: simpleSiteDir,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
});
expect(data).toEqual({
id: 'lorem',
permalink: '/docs/lorem',
source: path.join('@site', pluginPath, source),
title: 'lorem',
editUrl: 'https://github.com/customUrl/docs/lorem.md',
description: 'Lorem ipsum.',
lastUpdatedAt: '1539502055',
lastUpdatedBy: 'Author',
});
});
test('docs with invalid id', async () => {
const badSiteDir = path.join(fixtureDir, 'bad-site');
return processMetadata({
source: 'invalid-id.md',
docsDir: path.join(badSiteDir, 'docs'),
order: {},
siteConfig,
docsBasePath: 'docs',
siteDir: simpleSiteDir,
}).catch(e =>
expect(e).toMatchInlineSnapshot(
`[Error: Document id cannot include "/".]`,
),
);
});
});

View file

@ -218,6 +218,29 @@ describe('createOrder', () => {
});
});
test('multiple sidebars with unknown sidebar item type', () => {
expect(() =>
createOrder({
docs: [
{
type: 'category',
label: 'Category1',
items: [{type: 'endi', id: 'doc1'}, {type: 'doc', id: 'doc2'}],
},
],
otherDocs: [
{
type: 'category',
label: 'Category1',
items: [{type: 'doc', id: 'doc5'}],
},
],
}),
).toThrowErrorMatchingInlineSnapshot(
`"Unknown item type: endi. Item: {\\"type\\":\\"endi\\",\\"id\\":\\"doc1\\"}"`,
);
});
test('edge cases', () => {
expect(createOrder({})).toEqual({});
expect(createOrder(undefined)).toEqual({});

View file

@ -11,40 +11,43 @@ import loadSidebars from '../sidebars';
/* eslint-disable global-require, import/no-dynamic-require */
describe('loadSidebars', () => {
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
test('sidebars with known sidebar item type', async () => {
const sidebarPath = path.join(
__dirname,
'__fixtures__',
'website',
'sidebars.json',
);
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
const result = loadSidebars(sidebarPath);
expect(result).toMatchSnapshot();
});
test('sidebars with deep level of category', async () => {
const sidebarPath = path.join(
__dirname,
'__fixtures__',
'website',
'sidebars-category.js',
);
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
const result = loadSidebars(sidebarPath);
expect(result).toMatchSnapshot();
});
test('sidebars with unknown sidebar item type', async () => {
test('sidebars with category but category.items is not an array', async () => {
const sidebarPath = path.join(
__dirname,
'__fixtures__',
'website',
'bad-sidebars.json',
fixtureDir,
'sidebars-category-wrong-items.json',
);
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
`"Error loading \\"Category Label\\" category. Category items must be array."`,
);
});
test('sidebars with unknown sidebar item type', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
`"Unknown sidebar item type: superman"`,
);
});
test('sidebars with known sidebar item type but wrong field', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
`"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`,
);
});
test('no sidebars', () => {
const result = loadSidebars(null);
expect(result).toEqual({});

View file

@ -177,9 +177,8 @@ export default function pluginContentDocs(
case 'doc':
return convertDocLink(item as SidebarItemDoc);
case 'link':
break;
default:
throw new Error(`Unknown sidebar item type: ${item.type}`);
break;
}
return item as SidebarItemLink;
});
@ -208,7 +207,7 @@ export default function pluginContentDocs(
},
async contentLoaded({content, actions}) {
if (!content) {
if (!content || Object.keys(content.docsMetadata).length === 0) {
return;
}

View file

@ -33,7 +33,7 @@ export default function getFileLastUpdate(
if (!shell.which('git')) {
if (!showedGitRequirementError) {
showedGitRequirementError = true;
console.log('Sorry, the docs plugin last update options require Git.');
console.warn('Sorry, the docs plugin last update options require Git.');
}
return null;

View file

@ -44,7 +44,7 @@ function normalizeCategory(
if (!Array.isArray(category.items)) {
throw new Error(
`Error loading ${category.label} category. Category items must be array.`,
`Error loading "${category.label}" category. Category items must be array.`,
);
}
@ -62,17 +62,13 @@ function normalizeCategory(
assertItem(item, ['href', 'label']);
break;
case 'ref':
case 'doc':
assertItem(item, ['id']);
break;
default:
if (item.type !== 'doc') {
throw new Error(`Unknown sidebar item type: ${item.type}`);
}
assertItem(item, ['id']);
break;
}
return item as SidebarItem;
});