From bf565cc25627e0d0bcf4e7474f330b705aec883e Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Sun, 13 Oct 2024 15:25:00 +0800 Subject: [PATCH] test: add unit test (#277) --- __mocks__/fs.cjs | 3 + __mocks__/fs/promises.cjs | 3 + package.json | 1 + .../__snapshots__/langReplPlugin.spec.ts.snap | 131 +++++++++++++++++ .../__test__/langReplPlugin.spec.ts | 139 ++++++++++++++++++ plugins/plugin-md-power/package.json | 1 + .../src/node/container/fileTree.ts | 6 +- .../src/node/container/langRepl.ts | 10 +- .../src/node/container/npmTo.ts | 5 +- plugins/plugin-md-power/src/node/plugin.ts | 2 +- pnpm-lock.yaml | 83 ++++++++++- 11 files changed, 370 insertions(+), 14 deletions(-) create mode 100644 __mocks__/fs.cjs create mode 100644 __mocks__/fs/promises.cjs create mode 100644 plugins/plugin-md-power/__test__/__snapshots__/langReplPlugin.spec.ts.snap create mode 100644 plugins/plugin-md-power/__test__/langReplPlugin.spec.ts diff --git a/__mocks__/fs.cjs b/__mocks__/fs.cjs new file mode 100644 index 00000000..e5f40ad0 --- /dev/null +++ b/__mocks__/fs.cjs @@ -0,0 +1,3 @@ +const { fs } = require('memfs') + +module.exports = fs diff --git a/__mocks__/fs/promises.cjs b/__mocks__/fs/promises.cjs new file mode 100644 index 00000000..3b9d8b42 --- /dev/null +++ b/__mocks__/fs/promises.cjs @@ -0,0 +1,3 @@ +const { fs } = require('memfs') + +module.exports = fs.promises diff --git a/package.json b/package.json index 5a0eef7c..7ea1f105 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "husky": "^9.1.6", "lint-staged": "^15.2.10", "markdown-it": "^14.1.0", + "memfs": "^4.13.0", "rimraf": "^6.0.1", "stylelint": "^16.10.0", "tsconfig-vuepress": "^5.2.0", diff --git a/plugins/plugin-md-power/__test__/__snapshots__/langReplPlugin.spec.ts.snap b/plugins/plugin-md-power/__test__/__snapshots__/langReplPlugin.spec.ts.snap new file mode 100644 index 00000000..855f0f61 --- /dev/null +++ b/plugins/plugin-md-power/__test__/__snapshots__/langReplPlugin.spec.ts.snap @@ -0,0 +1,131 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`langReplPlugin > should work with custom options 1`] = ` +"
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
" +`; + +exports[`langReplPlugin > should work with custom options 2`] = ` +"export default { + "grammars": { + "kotlin": { + "language": "kotlin" + }, + "go": { + "language": "go" + }, + "rust": { + "language": "rust" + } + }, + "theme": { + "light": { + "theme": "github-light" + }, + "dark": { + "theme": "github-dark" + } + } +}" +`; + +exports[`langReplPlugin > should work with custom theme 1`] = ` +"
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
const a = 1
+
+
" +`; + +exports[`langReplPlugin > should work with custom theme 2`] = ` +"export default { + "grammars": { + "kotlin": { + "language": "kotlin" + }, + "go": { + "language": "go" + }, + "rust": { + "language": "rust" + } + } +}" +`; + +exports[`langReplPlugin > should work with default options 1`] = ` +"

::: go-repl

+
const a = 1
+
+

:::

+

::: go-repl#editable

+
const a = 1
+
+

:::

+

::: kotlin-repl

+
const a = 1
+
+

:::

+

::: kotlin-repl#editable

+
const a = 1
+
+

:::

+

::: rust-repl

+
const a = 1
+
+

:::

+

::: rust-repl#editable

+
const a = 1
+
+

:::

+

::: rust-repl title

+
const a = 1
+
+

:::

+

::: rust-repl#editable title

+
const a = 1
+
+

:::

+" +`; + +exports[`langReplPlugin > should work with default options 2`] = ` +"export default { + "grammars": {}, + "theme": { + "light": { + "theme": "github-light" + }, + "dark": { + "theme": "github-dark" + } + } +}" +`; diff --git a/plugins/plugin-md-power/__test__/langReplPlugin.spec.ts b/plugins/plugin-md-power/__test__/langReplPlugin.spec.ts new file mode 100644 index 00000000..b50caa31 --- /dev/null +++ b/plugins/plugin-md-power/__test__/langReplPlugin.spec.ts @@ -0,0 +1,139 @@ +import type { App } from 'vuepress/core' +import type { ReplOptions } from '../src/shared/repl.js' +import { dirname } from 'node:path' +import { path } from '@vuepress/utils' +import { resolveModule } from 'local-pkg' +import MarkdownIt from 'markdown-it' +import { fs, vol } from 'memfs' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { langReplPlugin } from '../src/node/container/langRepl.js' + +vi.mock('node:fs') +vi.mock('node:fs/promises') + +beforeEach(() => vol.reset()) + +const FENCE = '```' + +const themesPath = dirname(resolveModule('tm-themes', { + paths: [import.meta.url], +}) || '') +const grammarsPath = dirname(resolveModule('tm-grammars', { + paths: [import.meta.url], +}) || '') + +async function createMarkdown(options: ReplOptions = {}) { + const md = new MarkdownIt() + md.options.highlight = (str, lang) => `
${str}
` + const app = { + writeTemp: vi.fn((filepath: string, content: string) => { + filepath = path.join('/', filepath) + fs.mkdirSync(dirname(filepath), { recursive: true }) + fs.writeFileSync(filepath, content) + }), + } as unknown as App + + await langReplPlugin(app, md, options) + + return { md, app } +} + +function initFs() { + vol.fromJSON({ + [path.join(themesPath, 'themes/github-light.json')]: '{ "theme": "github-light" }', + [path.join(themesPath, 'themes/github-dark.json')]: '{ "theme": "github-dark" }', + [path.join(grammarsPath, 'grammars/go.json')]: '{ "language": "go" }', + [path.join(grammarsPath, 'grammars/kotlin.json')]: '{ "language": "kotlin" }', + [path.join(grammarsPath, 'grammars/rust.json')]: '{ "language": "rust" }', + }) +} + +describe('langReplPlugin', () => { + const outputFile = '/internal/md-power/replEditorData.js' + + const code = `\ +::: go-repl +${FENCE}go +const a = 1 +${FENCE} +::: + +::: go-repl#editable +${FENCE}go +const a = 1 +${FENCE} +::: + +::: kotlin-repl +${FENCE}kotlin +const a = 1 +${FENCE} +::: + +::: kotlin-repl#editable +${FENCE}kotlin +const a = 1 +${FENCE} +::: + +::: rust-repl +${FENCE}rust +const a = 1 +${FENCE} +::: + +::: rust-repl#editable +${FENCE}rust +const a = 1 +${FENCE} +::: + +::: rust-repl title +${FENCE}rust +const a = 1 +${FENCE} +::: + +::: rust-repl#editable title +${FENCE}rust +const a = 1 +${FENCE} +::: + +` + it('should work with default options', async () => { + initFs() + const { md, app } = await createMarkdown() + + expect(md.render(code)).toMatchSnapshot() + expect(app.writeTemp).toBeCalledWith(outputFile.slice(1), expect.any(String)) + expect(fs.readFileSync(outputFile, 'utf-8')).toMatchSnapshot() + }) + + it('should work with custom options', async () => { + initFs() + const { md, app } = await createMarkdown({ + go: true, + kotlin: true, + rust: true, + }) + + expect(md.render(code)).toMatchSnapshot() + expect(app.writeTemp).toBeCalledWith(outputFile.slice(1), expect.any(String)) + expect(fs.readFileSync(outputFile, 'utf-8')).toMatchSnapshot() + }) + + it('should work with custom theme', async () => { + initFs() + const { md, app } = await createMarkdown({ + go: true, + kotlin: true, + rust: true, + theme: 'vitesse-light', + }) + + expect(md.render(code)).toMatchSnapshot() + expect(app.writeTemp).toBeCalledWith(outputFile.slice(1), expect.any(String)) + expect(fs.readFileSync(outputFile, 'utf-8')).toMatchSnapshot() + }) +}) diff --git a/plugins/plugin-md-power/package.json b/plugins/plugin-md-power/package.json index c2323299..dcd6db4d 100644 --- a/plugins/plugin-md-power/package.json +++ b/plugins/plugin-md-power/package.json @@ -56,6 +56,7 @@ "@vuepress/helper": "2.0.0-rc.52", "@vueuse/core": "^11.1.0", "image-size": "^1.1.1", + "local-pkg": "^0.5.0", "markdown-it-container": "^4.0.0", "nanoid": "^5.0.7", "shiki": "^1.22.0", diff --git a/plugins/plugin-md-power/src/node/container/fileTree.ts b/plugins/plugin-md-power/src/node/container/fileTree.ts index 5cad6e6f..ab56dddc 100644 --- a/plugins/plugin-md-power/src/node/container/fileTree.ts +++ b/plugins/plugin-md-power/src/node/container/fileTree.ts @@ -112,8 +112,8 @@ export function resolveTreeNodeInfo( if (!hasInline) return undefined - const children = inline.children?.filter(token => (token.type === 'text' && token.content) || token.tag === 'strong') || [] - const filename = children.filter(token => token.type === 'text').map(token => token.content).join(' ').split(/\s+/)[0] ?? '' + const children = inline.children!.filter(token => (token.type === 'text' && token.content) || token.tag === 'strong') + const filename = children.filter(token => token.type === 'text').map(token => token.content).join(' ').split(/\s+/)[0] const focus = children[0]?.tag === 'strong' const type = hasChildren || filename.endsWith('/') ? 'folder' : 'file' const info: FileTreeNode = { @@ -129,8 +129,6 @@ export function resolveTreeNodeInfo( export function updateInlineToken(inline: Token, info: FileTreeNode, icon: string) { const children = inline.children! - if (!children || children.length === 0) - return const tokens: Token[] = [] const wrapperOpen = new Token('span_open', 'span', 1) diff --git a/plugins/plugin-md-power/src/node/container/langRepl.ts b/plugins/plugin-md-power/src/node/container/langRepl.ts index 77830128..42d98b62 100644 --- a/plugins/plugin-md-power/src/node/container/langRepl.ts +++ b/plugins/plugin-md-power/src/node/container/langRepl.ts @@ -2,8 +2,10 @@ import type markdownIt from 'markdown-it' import type Token from 'markdown-it/lib/token.mjs' import type { App } from 'vuepress/core' import type { ReplEditorData, ReplOptions } from '../../shared/index.js' +import { promises as fs } from 'node:fs' +import { resolveModule } from 'local-pkg' import container from 'markdown-it-container' -import { fs, getDirname, path } from 'vuepress/utils' +import { path } from 'vuepress/utils' const RE_INFO = /^(#editable)?(.*)$/ @@ -15,7 +17,7 @@ function createReplContainer(md: markdownIt, lang: string) { const token = tokens[index] const info = token.info.trim().slice(type.length).trim() || '' // :::lang-repl#editable title - const [, editable, title] = info.match(RE_INFO) ?? [] + const [, editable, title] = info.match(RE_INFO)! if (token.nesting === 1) return `` @@ -46,8 +48,8 @@ export async function langReplPlugin(app: App, md: markdownIt, { const data: ReplEditorData = { grammars: {} } as ReplEditorData - const themesPath = getDirname(import.meta.resolve('tm-themes')) - const grammarsPath = getDirname(import.meta.resolve('tm-grammars')) + const themesPath = path.dirname(resolveModule('tm-themes')!) + const grammarsPath = path.dirname(resolveModule('tm-grammars')!) const readTheme = (theme: string) => read(path.join(themesPath, 'themes', `${theme}.json`)) const readGrammar = (grammar: string) => read(path.join(grammarsPath, 'grammars', `${grammar}.json`)) diff --git a/plugins/plugin-md-power/src/node/container/npmTo.ts b/plugins/plugin-md-power/src/node/container/npmTo.ts index 99850895..0ed23d3a 100644 --- a/plugins/plugin-md-power/src/node/container/npmTo.ts +++ b/plugins/plugin-md-power/src/node/container/npmTo.ts @@ -202,10 +202,7 @@ export function npmToPlugins(md: Markdown, options: NpmToOptions = {}): void { if (tokens[idx].nesting === 1) { const token = tokens[idx + 1] const info = token.info.trim() - if ( - token.type === 'fence' - && (info.startsWith('sh') || info.startsWith('bash') || info.startsWith('shell')) - ) { + if (token.type === 'fence') { const content = token.content token.hidden = true token.type = 'text' diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts index 45671e6a..dcf805a9 100644 --- a/plugins/plugin-md-power/src/node/plugin.ts +++ b/plugins/plugin-md-power/src/node/plugin.ts @@ -21,7 +21,7 @@ export function markdownPowerPlugin( extendsBundlerOptions(bundlerOptions, app) { if (options.repl) { - addViteOptimizeDepsInclude(bundlerOptions, app, ['shiki/core', 'shiki/wasm']) + addViteOptimizeDepsInclude(bundlerOptions, app, ['shiki/core', 'shiki/wasm', 'shiki/engine/oniguruma']) } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2deae8a4..a22af91a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: markdown-it: specifier: ^14.1.0 version: 14.1.0 + memfs: + specifier: ^4.13.0 + version: 4.13.0 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -201,6 +204,9 @@ importers: image-size: specifier: ^1.1.1 version: 1.1.1 + local-pkg: + specifier: ^0.5.0 + version: 0.5.0 markdown-it: specifier: ^14.0.0 version: 14.1.0 @@ -471,6 +477,9 @@ packages: peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' + peerDependenciesMeta: + '@algolia/client-search': + optional: true '@algolia/cache-browser-local-storage@4.24.0': resolution: {integrity: sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==} @@ -1057,6 +1066,24 @@ packages: resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} engines: {node: '>=10'} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.1.0': + resolution: {integrity: sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.5.0': + resolution: {integrity: sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@kurkle/color@0.3.2': resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} @@ -3606,6 +3633,10 @@ packages: engines: {node: '>=18'} hasBin: true + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -4134,6 +4165,10 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memfs@4.13.0: + resolution: {integrity: sha512-dIs5KGy24fbdDhIAg0RxXpFqQp3RwL6wgSMRF9OSuphL/Uc9a4u2/SDJKPLj/zUgtOGKuHrRMrj563+IErj4Cg==} + engines: {node: '>= 4.0.0'} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -5504,6 +5539,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -5554,6 +5595,12 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-dump@1.0.2: + resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -6070,8 +6117,9 @@ snapshots: '@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)': dependencies: - '@algolia/client-search': 4.24.0 algoliasearch: 4.24.0 + optionalDependencies: + '@algolia/client-search': 4.24.0 '@algolia/cache-browser-local-storage@4.24.0': dependencies: @@ -6712,6 +6760,22 @@ snapshots: string-argv: 0.3.2 type-detect: 4.1.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + + '@jsonjoy.com/json-pack@1.1.0(tslib@2.7.0)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.7.0) + '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.7.0) + tslib: 2.7.0 + + '@jsonjoy.com/util@1.5.0(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + '@kurkle/color@0.3.2': {} '@lit-labs/ssr-dom-shim@1.2.1': {} @@ -9739,6 +9803,8 @@ snapshots: husky@9.1.6: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -10304,6 +10370,13 @@ snapshots: mdurl@2.0.0: {} + memfs@4.13.0: + dependencies: + '@jsonjoy.com/json-pack': 1.1.0(tslib@2.7.0) + '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + tree-dump: 1.0.2(tslib@2.7.0) + tslib: 2.7.0 + meow@12.1.1: {} meow@13.2.0: {} @@ -11701,6 +11774,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thingies@1.21.0(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + through@2.3.8: {} tinybench@2.9.0: {} @@ -11740,6 +11817,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.0.2(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + tree-kill@1.2.2: {} trim-lines@3.0.1: {}