test: improve test coverage (#6387)

* test: improve test coverage

* fix

* use posixPath
This commit is contained in:
Joshua Chen 2022-01-18 16:29:40 +08:00 committed by GitHub
parent a9810db1cc
commit 62223ee556
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 463 additions and 60 deletions

View file

@ -1,4 +1,3 @@
__fixtures__
build
coverage
examples/

View file

@ -1,3 +1,4 @@
/* stylelint-disable docusaurus/copyright-header, declaration-block-no-duplicate-custom-properties */
:root {
--color-primary: red;
--color-secondary: green;

View file

@ -1,3 +1,4 @@
/* stylelint-disable docusaurus/copyright-header, declaration-block-no-duplicate-custom-properties */
:root {
--color-primary: red;
--color-primary: red;
@ -5,3 +6,8 @@
--color-primary: blue;
--color-header: gray;
}
.non-root {
--color-primary: red;
--color-primary: red;
}

View file

@ -1,16 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`remove-overridden-custom-properties overridden custom properties should be removed 1`] = `
":root {
"/* stylelint-disable docusaurus/copyright-header, declaration-block-no-duplicate-custom-properties */
:root {
--color-secondary: green;
--color-primary: blue;
--color-header: gray;
}
.non-root {
--color-primary: red;
--color-primary: red;
}
"
`;
exports[`remove-overridden-custom-properties overridden custom properties with \`!important\` rule should not be removed 1`] = `
":root {
"/* stylelint-disable docusaurus/copyright-header, declaration-block-no-duplicate-custom-properties */
:root {
--color-primary: blue;
--color-header: gray !important;
--color-secondary: yellow !important;

View file

@ -22,8 +22,9 @@ module.exports = function creator() {
return;
}
const sameProperties =
decl.parent.nodes.filter((n) => n.prop === decl.prop) || [];
const sameProperties = decl.parent.nodes.filter(
(n) => n.prop === decl.prop,
);
const hasImportantProperties = sameProperties.some((p) =>
Object.prototype.hasOwnProperty.call(p, 'important'),
);

View file

@ -260,6 +260,25 @@ describe('collectRedirects', () => {
]);
});
test('should allow returning string / undefined', () => {
expect(
collectRedirects(
createTestPluginContext(
{
createRedirects: (routePath) => {
if (routePath === '/') {
return `${routePath}foo`;
}
return undefined;
},
},
['/', '/testpath', '/otherPath.html'],
),
undefined,
),
).toEqual([{from: '/foo', to: '/'}]);
});
test('should throw if redirect creator creates invalid redirects', () => {
expect(() =>
collectRedirects(

View file

@ -1,7 +1,8 @@
---
slug: /hey/my super path/héllô
title: Complex Slug
date: 2020-08-16
# This date is not YAML date, but we can still use it.
date: 2020/08/16
---
complex url slug

View file

@ -1,6 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`blogFeed atom shows feed item for each post 1`] = `
Array [
Array [
"build/blog/atom.xml",
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<feed xmlns=\\"http://www.w3.org/2005/Atom\\">
<id>https://docusaurus.io/myBaseUrl/blog</id>
@ -83,10 +86,15 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
<name>Sébastien Lorber (translated)</name>
</author>
</entry>
</feed>"
</feed>",
],
]
`;
exports[`blogFeed json shows feed item for each post 1`] = `
Array [
Array [
"build/blog/feed.json",
"{
\\"version\\": \\"https://jsonfeed.org/version/1\\",
\\"title\\": \\"Hello Blog\\",
@ -163,10 +171,15 @@ exports[`blogFeed json shows feed item for each post 1`] = `
}
}
]
}"
}",
],
]
`;
exports[`blogFeed rss shows feed item for each post 1`] = `
Array [
Array [
"build/blog/rss.xml",
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<rss version=\\"2.0\\" xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\">
<channel>
@ -240,5 +253,7 @@ exports[`blogFeed rss shows feed item for each post 1`] = `
<content:encoded><![CDATA[<p>Happy birthday! (translated)</p>]]></content:encoded>
</item>
</channel>
</rss>"
</rss>",
],
]
`;

View file

@ -5,7 +5,24 @@
* LICENSE file in the root directory of this source tree.
*/
import {parseBlogFileName} from '../blogUtils';
import {truncate, parseBlogFileName} from '../blogUtils';
describe('truncate', () => {
test('truncates texts', () => {
expect(
truncate('aaa\n<!-- truncate -->\nbbb\nccc', /<!-- truncate -->/),
).toEqual('aaa\n');
expect(
truncate('\n<!-- truncate -->\nbbb\nccc', /<!-- truncate -->/),
).toEqual('\n');
});
test('leaves texts without markers', () => {
expect(truncate('aaa\nbbb\nccc', /<!-- truncate -->/)).toEqual(
'aaa\nbbb\nccc',
);
expect(truncate('', /<!-- truncate -->/)).toEqual('');
});
});
describe('parseBlogFileName', () => {
test('parse file', () => {

View file

@ -6,12 +6,12 @@
*/
import path from 'path';
import {generateBlogFeed} from '../feed';
import fs from 'fs-extra';
import {createBlogFeedFiles} from '../feed';
import type {LoadContext, I18n} from '@docusaurus/types';
import type {BlogContentPaths} from '../types';
import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {generateBlogPosts} from '../blogUtils';
import type {Feed} from 'feed';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
const DefaultI18N: I18n = {
@ -36,23 +36,26 @@ function getBlogContentPaths(siteDir: string): BlogContentPaths {
async function testGenerateFeeds(
context: LoadContext,
options: PluginOptions,
): Promise<Feed | null> {
): Promise<void> {
const blogPosts = await generateBlogPosts(
getBlogContentPaths(context.siteDir),
context,
options,
);
return generateBlogFeed({
await createBlogFeedFiles({
blogPosts,
options,
siteConfig: context.siteConfig,
outDir: 'build',
});
}
describe('blogFeed', () => {
(['atom', 'rss', 'json'] as const).forEach((feedType) => {
describe(`${feedType}`, () => {
const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {});
test('should not show feed without posts', async () => {
const siteDir = __dirname;
const siteConfig = {
@ -62,7 +65,7 @@ describe('blogFeed', () => {
favicon: 'image/favicon.ico',
};
const feed = await testGenerateFeeds(
await testGenerateFeeds(
{
siteDir,
siteConfig,
@ -83,7 +86,8 @@ describe('blogFeed', () => {
} as PluginOptions,
);
expect(feed).toEqual(null);
expect(fsMock).toBeCalledTimes(0);
fsMock.mockClear();
});
test('shows feed item for each post', async () => {
@ -96,7 +100,7 @@ describe('blogFeed', () => {
favicon: 'image/favicon.ico',
};
const feed = await testGenerateFeeds(
await testGenerateFeeds(
{
siteDir,
siteConfig,
@ -119,22 +123,8 @@ describe('blogFeed', () => {
} as PluginOptions,
);
let feedContent = '';
switch (feedType) {
case 'rss':
feedContent = feed.rss2();
break;
case 'json':
feedContent = feed.json1();
break;
case 'atom':
feedContent = feed.atom1();
break;
default:
break;
}
expect(feedContent).toMatchSnapshot();
expect(fsMock.mock.calls).toMatchSnapshot();
fsMock.mockClear();
});
});
});

View file

@ -204,7 +204,7 @@ describe('loadBlog', () => {
date: new Date('2020-08-16'),
formattedDate: 'August 16, 2020',
frontMatter: {
date: new Date('2020-08-16'),
date: '2020/08/16',
slug: '/hey/my super path/héllô',
title: 'Complex Slug',
},

View file

@ -7,7 +7,7 @@
import {Feed, type Author as FeedAuthor, type Item as FeedItem} from 'feed';
import type {BlogPost} from './types';
import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
import {normalizeUrl, mdxToHtml, posixPath} from '@docusaurus/utils';
import type {DocusaurusConfig} from '@docusaurus/types';
import path from 'path';
import fs from 'fs-extra';
@ -30,7 +30,7 @@ function mdxToFeedContent(mdxContent: string): string | undefined {
}
}
export async function generateBlogFeed({
async function generateBlogFeed({
blogPosts,
options,
siteConfig,
@ -47,9 +47,7 @@ export async function generateBlogFeed({
const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
const updated =
(blogPosts[0] && blogPosts[0].metadata.date) ||
new Date('2015-10-25T16:29:00.000-07:00'); // weird legacy magic date
const updated = blogPosts[0] && blogPosts[0].metadata.date;
const feed = new Feed({
id: blogBaseUrl,
@ -118,7 +116,10 @@ async function createBlogFeedFile({
}
})();
try {
await fs.outputFile(path.join(generatePath, feedPath), feedContent);
await fs.outputFile(
posixPath(path.join(generatePath, feedPath)),
feedContent,
);
} catch (err) {
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
}

View file

@ -201,7 +201,9 @@ describe('DefaultSidebarItemsGenerator', () => {
source: 'guide1.md',
sourceDirName: '02-Guides',
sidebarPosition: 1,
frontMatter: {},
frontMatter: {
sidebar_class_name: 'foo',
},
},
{
id: 'nested-guide',
@ -250,7 +252,7 @@ describe('DefaultSidebarItemsGenerator', () => {
id: 'guides-index',
},
items: [
{type: 'doc', id: 'guide1'},
{type: 'doc', id: 'guide1', className: 'foo'},
{
type: 'category',
label: 'SubGuides (metadata file label)',
@ -278,12 +280,17 @@ describe('DefaultSidebarItemsGenerator', () => {
'subfolder/subsubfolder/subsubsubfolder2/_category_.yml': {
position: 2,
label: 'subsubsubfolder2 (_category_.yml label)',
className: 'bar',
},
'subfolder/subsubfolder/subsubsubfolder3/_category_.json': {
position: 1,
label: 'subsubsubfolder3 (_category_.json label)',
collapsible: false,
collapsed: false,
link: {
type: 'doc',
id: 'doc1', // This is a "fully-qualified" ID that can't be found locally
},
},
});
@ -367,6 +374,10 @@ describe('DefaultSidebarItemsGenerator', () => {
label: 'subsubsubfolder3 (_category_.json label)',
collapsed: false,
collapsible: false,
link: {
id: 'doc1',
type: 'doc',
},
items: [
{type: 'doc', id: 'doc8'},
{type: 'doc', id: 'doc7'},
@ -377,6 +388,7 @@ describe('DefaultSidebarItemsGenerator', () => {
label: 'subsubsubfolder2 (_category_.yml label)',
collapsed: true,
collapsible: true,
className: 'bar',
items: [{type: 'doc', id: 'doc6'}],
},
{type: 'doc', id: 'doc1'},

View file

@ -102,8 +102,8 @@ describe('processSidebars', () => {
slug: 'category-generated-index-slug',
permalink: 'category-generated-index-permalink',
},
collapsed: false,
collapsible: true,
collapsed: true, // A suspicious bad config that will be normalized
collapsible: false,
items: [
{type: 'doc', id: 'doc2'},
{type: 'autogenerated', dirName: 'dir1'},
@ -172,7 +172,7 @@ describe('processSidebars', () => {
permalink: 'category-generated-index-permalink',
},
collapsed: false,
collapsible: true,
collapsible: false,
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
},
{type: 'link', href: 'https://facebook.com', label: 'FB'},

View file

@ -96,6 +96,14 @@ describe('createSidebarsUtils', () => {
];
const sidebar4: Sidebar = [
{
type: 'category',
items: [
{type: 'link', href: 'https://facebook.com'},
{type: 'link', href: 'https://reactjs.org'},
{type: 'link', href: 'https://docusaurus.io'},
],
},
{
type: 'category',
collapsed: false,

View file

@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {parseCodeBlockTitle} from '../codeBlockUtils';
import {
parseCodeBlockTitle,
parseLanguage,
parseLines,
} from '../codeBlockUtils';
describe('parseCodeBlockTitle', () => {
test('should parse double quote delimited title', () => {
@ -52,3 +56,227 @@ describe('parseCodeBlockTitle', () => {
);
});
});
describe('parseLanguage', () => {
test('behaves correctly', () => {
expect(parseLanguage('language-foo xxx yyy')).toEqual('foo');
expect(parseLanguage('xxxxx language-foo yyy')).toEqual('foo');
expect(parseLanguage('xx-language-foo yyyy')).toBeUndefined();
expect(parseLanguage('xxx yyy zzz')).toBeUndefined();
});
});
describe('parseLines', () => {
test('does not parse content with metastring', () => {
expect(parseLines('aaaaa\nbbbbb', '{1}', 'js')).toMatchInlineSnapshot(`
Object {
"code": "aaaaa
bbbbb",
"highlightLines": Array [
0,
],
}
`);
expect(
parseLines(
`// highlight-next-line
aaaaa
bbbbb`,
'{1}',
'js',
),
).toMatchInlineSnapshot(`
Object {
"code": "// highlight-next-line
aaaaa
bbbbb",
"highlightLines": Array [
0,
],
}
`);
expect(
parseLines(
`aaaaa
bbbbb`,
'{1}',
),
).toMatchInlineSnapshot(`
Object {
"code": "aaaaa
bbbbb",
"highlightLines": Array [
0,
],
}
`);
});
test('does not parse content with no language', () => {
expect(
parseLines(
`// highlight-next-line
aaaaa
bbbbb`,
'',
undefined,
),
).toMatchInlineSnapshot(`
Object {
"code": "// highlight-next-line
aaaaa
bbbbb",
"highlightLines": Array [],
}
`);
});
test('removes lines correctly', () => {
expect(
parseLines(
`// highlight-next-line
aaaaa
bbbbb`,
'',
'js',
),
).toMatchInlineSnapshot(`
Object {
"code": "aaaaa
bbbbb",
"highlightLines": Array [
0,
],
}
`);
expect(
parseLines(
`// highlight-start
aaaaa
// highlight-end
bbbbb`,
'',
'js',
),
).toMatchInlineSnapshot(`
Object {
"code": "aaaaa
bbbbb",
"highlightLines": Array [
0,
],
}
`);
expect(
parseLines(
`// highlight-start
// highlight-next-line
aaaaa
bbbbbbb
// highlight-next-line
// highlight-end
bbbbb`,
'',
'js',
),
).toMatchInlineSnapshot(`
Object {
"code": "aaaaa
bbbbbbb
bbbbb",
"highlightLines": Array [
0,
2,
0,
1,
],
}
`);
});
test('respects language', () => {
expect(
parseLines(
`# highlight-next-line
aaaaa
bbbbb`,
'',
'js',
),
).toMatchInlineSnapshot(`
Object {
"code": "# highlight-next-line
aaaaa
bbbbb",
"highlightLines": Array [],
}
`);
expect(
parseLines(
`/* highlight-next-line */
aaaaa
bbbbb`,
'',
'py',
),
).toMatchInlineSnapshot(`
Object {
"code": "/* highlight-next-line */
aaaaa
bbbbb",
"highlightLines": Array [],
}
`);
expect(
parseLines(
`// highlight-next-line
aaaa
/* highlight-next-line */
bbbbb
# highlight-next-line
ccccc
<!-- highlight-next-line -->
dddd`,
'',
'py',
),
).toMatchInlineSnapshot(`
Object {
"code": "// highlight-next-line
aaaa
/* highlight-next-line */
bbbbb
ccccc
<!-- highlight-next-line -->
dddd",
"highlightLines": Array [
4,
],
}
`);
expect(
parseLines(
`// highlight-next-line
aaaa
/* highlight-next-line */
bbbbb
# highlight-next-line
ccccc
<!-- highlight-next-line -->
dddd`,
'',
'',
),
).toMatchInlineSnapshot(`
Object {
"code": "aaaa
bbbbb
ccccc
dddd",
"highlightLines": Array [
0,
1,
2,
3,
],
}
`);
});
});

View file

@ -5,7 +5,27 @@
* LICENSE file in the root directory of this source tree.
*/
import {uniq} from '../jsUtils';
import {uniq, duplicates} from '../jsUtils';
describe('duplicates', () => {
test('gets duplicate values', () => {
expect(duplicates(['a', 'b', 'c', 'd'])).toEqual([]);
expect(duplicates(['a', 'b', 'b', 'b'])).toEqual(['b', 'b']);
expect(duplicates(['c', 'b', 'b', 'c'])).toEqual(['b', 'c']);
expect(duplicates([{a: 1}, {a: 1}, {a: 1}])).toEqual([]);
});
test('accepts custom comparator', () => {
expect(duplicates([{a: 1}, {a: 1}, {a: 1}], (a, b) => a.a === b.a)).toEqual(
[{a: 1}, {a: 1}],
);
expect(duplicates(['a', 'b', 'c', 'd'], (a, b) => a !== b)).toEqual([
'a',
'b',
'c',
'd',
]);
});
});
describe('uniq', () => {
test('remove duplicate primitives', () => {

View file

@ -0,0 +1,21 @@
/**
* 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 {isRegexpStringMatch} from '../regexpUtils';
describe('isRegexpStringMatch', () => {
test('behaves correctly', () => {
expect(isRegexpStringMatch(undefined, 'foo')).toEqual(false);
expect(isRegexpStringMatch('bar', undefined)).toEqual(false);
expect(isRegexpStringMatch('foo', 'bar')).toEqual(false);
expect(isRegexpStringMatch('foo', 'foo')).toEqual(true);
expect(isRegexpStringMatch('fooooooooooo', 'foo')).toEqual(false);
expect(isRegexpStringMatch('foo', 'fooooooo')).toEqual(true);
expect(isRegexpStringMatch('f.*o', 'fggo')).toEqual(true);
expect(isRegexpStringMatch('FOO', 'foo')).toEqual(true);
});
});

View file

@ -0,0 +1,14 @@
/**
* 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 {docVersionSearchTag} from '../searchUtils';
describe('docVersionSearchTag', () => {
test('behaves correctly', () => {
expect(docVersionSearchTag('foo', 'bar')).toEqual('docs-foo-bar');
});
});

View file

@ -6,7 +6,7 @@
*/
/**
* Utility to convert an optional string into a Regex case sensitive and global
* Utility to convert an optional string into a Regex case insensitive and global
*/
export function isRegexpStringMatch(
regexAsString?: string,

View file

@ -14,13 +14,26 @@ import {
describe('codeTranslationLocalesToTry', () => {
test('should return appropriate locale lists', () => {
expect(codeTranslationLocalesToTry('fr')).toEqual(['fr', 'fr-FR']);
expect(codeTranslationLocalesToTry('fr')).toEqual([
'fr',
'fr-FR',
'fr-Latn',
]);
expect(codeTranslationLocalesToTry('fr-FR')).toEqual(['fr-FR', 'fr']);
// Note: "pt" is expanded into "pt-BR", not "pt-PT", as "pt-BR" is more widely used!
// See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
expect(codeTranslationLocalesToTry('pt')).toEqual(['pt', 'pt-BR']);
expect(codeTranslationLocalesToTry('pt')).toEqual([
'pt',
'pt-BR',
'pt-Latn',
]);
expect(codeTranslationLocalesToTry('pt-BR')).toEqual(['pt-BR', 'pt']);
expect(codeTranslationLocalesToTry('pt-PT')).toEqual(['pt-PT', 'pt']);
expect(codeTranslationLocalesToTry('zh')).toEqual([
'zh',
'zh-CN',
'zh-Hans',
]);
});
});
@ -123,4 +136,17 @@ describe('readDefaultCodeTranslationMessages', () => {
}),
).resolves.toEqual(await readAsJSON('en'));
});
test('default locale', async () => {
await expect(
readDefaultCodeTranslationMessages({
locale: 'zh',
name: 'plugin-pwa',
}),
).resolves.toEqual({
'theme.PwaReloadPopup.closeButtonAriaLabel': '关闭',
'theme.PwaReloadPopup.info': '有可用的新版本',
'theme.PwaReloadPopup.refreshButtonText': '刷新',
});
});
});

View file

@ -14,16 +14,17 @@ function getDefaultLocalesDirPath(): string {
// Return an ordered list of locales we should try
export function codeTranslationLocalesToTry(locale: string): string[] {
const intlLocale = Intl.Locale ? new Intl.Locale(locale) : undefined;
if (!intlLocale) {
return [locale];
}
const intlLocale = new Intl.Locale(locale);
// if locale is just a simple language like "pt", we want to fallback to pt-BR (not pt-PT!)
// see https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
if (intlLocale.language === locale) {
const maximizedLocale = intlLocale.maximize(); // pt-Latn-BR`
// ["pt","pt-BR"]
return [locale, `${maximizedLocale.language}-${maximizedLocale.region}`];
// ["pt","pt-BR"]; ["zh", "zh-Hans"]
return [
locale,
`${maximizedLocale.language}-${maximizedLocale.region}`,
`${maximizedLocale.language}-${maximizedLocale.script}`,
];
}
// if locale is like "pt-BR", we want to fallback to "pt"
else {

View file

@ -35,6 +35,8 @@ describe('normalizeLocation', () => {
});
test('untouched pathnames', () => {
const replaceMock = jest.spyOn(String.prototype, 'replace');
expect(
normalizeLocation({
pathname: '/docs/introduction',
@ -47,6 +49,20 @@ describe('normalizeLocation', () => {
hash: '#features',
});
// For the sake of testing memoization
expect(
normalizeLocation({
pathname: '/docs/introduction',
search: '',
hash: '#features',
}),
).toEqual({
pathname: '/docs/introduction',
search: '',
hash: '#features',
});
expect(replaceMock).toBeCalledTimes(1);
expect(
normalizeLocation({
pathname: '/docs/introduction/foo.html',

View file

@ -23,7 +23,7 @@ export default function loadPresets(context: LoadContext): {
// declares the dependency on these presets.
const presetRequire = createRequire(context.siteConfigPath);
const presets: PresetConfig[] = (context.siteConfig || {}).presets || [];
const presets: PresetConfig[] = context.siteConfig.presets || [];
const unflatPlugins: PluginConfig[][] = [];
const unflatThemes: PluginConfig[][] = [];