Skip to content

Commit 317289a

Browse files
committed
unit tests
1 parent 3e02062 commit 317289a

13 files changed

Lines changed: 484 additions & 188 deletions

File tree

.github/workflows/lint-and-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ jobs:
149149
if: ${{ !cancelled() && github.event_name != 'merge_group' }}
150150
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
151151
with:
152-
files: ./apps/site/lcov.info,./packages/ui-components/lcov.info
152+
files: ./apps/site/lcov.info,./packages/*/lcov.info
153153

154154
- name: Upload test results to Codecov
155155
if: ${{ !cancelled() && github.event_name != 'merge_group' }}
156156
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
157157
with:
158-
files: ./apps/site/junit.xml,./packages/ui-components/junit.xml
158+
files: ./apps/site/junit.xml,./packages/*/junit.xml

apps/site/components/Downloads/Release/ReleaseCodeBox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

3-
import { createSval } from '@node-core/mdx/evaluate';
4-
import { highlightToHtml } from '@node-core/mdx/highlight';
3+
import createSval from '@node-core/mdx/evaluator';
4+
import { highlightToHtml } from '@node-core/mdx/highlighter';
55
import AlertBox from '@node-core/ui-components/Common/AlertBox';
66
import Skeleton from '@node-core/ui-components/Common/Skeleton';
77
import { useTranslations } from 'next-intl';

apps/site/next.dynamic.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { readFile } from 'node:fs/promises';
44
import { join, normalize, sep } from 'node:path';
55

6-
import { compile } from '@node-core/mdx';
6+
import compile from '@node-core/mdx/compiler';
77
import matter from 'gray-matter';
88
import { cache } from 'react';
99
import { VFile } from 'vfile';
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import assert from 'node:assert/strict';
2+
import { mock, it, describe } from 'node:test';
3+
4+
import { SKIP } from 'unist-util-visit';
5+
6+
// Mock dependencies
7+
mock.module('classNames', {
8+
defaultExport: (...args) => args.filter(Boolean).join(' '),
9+
});
10+
11+
mock.module('hast-util-to-string', {
12+
namedExports: {
13+
toString: node => node.children?.[0]?.value || '',
14+
},
15+
});
16+
17+
mock.module('unist-util-visit', {
18+
namedExports: {
19+
visit: (tree, nodeType, visitor) => {
20+
const traverseNode = (node, index, parent) => {
21+
if (node.type === nodeType) {
22+
const result = visitor(node, index, parent);
23+
if (result?.[0] === SKIP) return true;
24+
}
25+
26+
if (node.children) {
27+
for (let i = 0; i < node.children.length; i++) {
28+
if (traverseNode(node.children[i], i, node)) return true;
29+
}
30+
}
31+
};
32+
33+
traverseNode(tree, null, null);
34+
},
35+
SKIP,
36+
},
37+
});
38+
39+
mock.module('../highlighter', {
40+
namedExports: {
41+
highlightToHast: (code, language) => ({
42+
children: [
43+
{
44+
type: 'element',
45+
tagName: 'pre',
46+
properties: { class: `highlighted-${language}` },
47+
children: [
48+
{
49+
type: 'element',
50+
tagName: 'code',
51+
properties: {},
52+
children: [{ type: 'text', value: `highlighted ${code}` }],
53+
},
54+
],
55+
},
56+
],
57+
}),
58+
},
59+
});
60+
61+
const {
62+
getMetaParameter,
63+
isCodeBlock,
64+
processCodeTabs,
65+
processCodeHighlighting,
66+
} = await import('../shiki');
67+
68+
describe('rehypeShikiji module', () => {
69+
describe('Utility functions', () => {
70+
it('getMetaParameter extracts values from meta strings', () => {
71+
const testCases = [
72+
{
73+
meta: 'displayName="JavaScript"',
74+
param: 'displayName',
75+
expected: 'JavaScript',
76+
},
77+
{
78+
meta: 'active="true" displayName="TypeScript"',
79+
param: 'active',
80+
expected: 'true',
81+
},
82+
{
83+
meta: 'key="value with spaces"',
84+
param: 'key',
85+
expected: 'value with spaces',
86+
},
87+
{ meta: 'noQuotes=value', param: 'noQuotes', expected: undefined },
88+
{ meta: null, param: 'key', expected: undefined },
89+
{ meta: 'key="value"', param: undefined, expected: undefined },
90+
{ meta: 'key=""', param: 'key', expected: undefined },
91+
{ meta: 'otherKey="value"', param: 'key', expected: undefined },
92+
];
93+
94+
testCases.forEach(({ meta, param, expected }) => {
95+
assert.equal(getMetaParameter(meta, param), expected);
96+
});
97+
});
98+
99+
it('isCodeBlock correctly identifies code blocks', () => {
100+
// Valid code block
101+
const validBlock = {
102+
type: 'element',
103+
tagName: 'pre',
104+
children: [
105+
{
106+
type: 'element',
107+
tagName: 'code',
108+
children: [{ type: 'text', value: 'code' }],
109+
},
110+
],
111+
};
112+
113+
// Invalid cases
114+
const invalidCases = [
115+
{
116+
type: 'element',
117+
tagName: 'pre',
118+
children: [{ type: 'element', tagName: 'span' }],
119+
},
120+
{
121+
type: 'element',
122+
tagName: 'div',
123+
children: [{ type: 'element', tagName: 'code' }],
124+
},
125+
undefined,
126+
];
127+
128+
assert.ok(isCodeBlock(validBlock));
129+
invalidCases.forEach(testCase => {
130+
assert.ok(!isCodeBlock(testCase));
131+
});
132+
});
133+
});
134+
135+
describe('Processing functions', () => {
136+
it('processCodeTabs groups adjacent code blocks', () => {
137+
const tree = {
138+
type: 'root',
139+
children: [
140+
{
141+
type: 'element',
142+
tagName: 'pre',
143+
children: [
144+
{
145+
type: 'element',
146+
tagName: 'code',
147+
properties: { className: ['language-javascript'] },
148+
data: { meta: 'displayName="JavaScript"' },
149+
children: [{ type: 'text', value: 'console.log("hello");' }],
150+
},
151+
],
152+
},
153+
{
154+
type: 'element',
155+
tagName: 'pre',
156+
children: [
157+
{
158+
type: 'element',
159+
tagName: 'code',
160+
properties: { className: ['language-typescript'] },
161+
data: { meta: 'displayName="TypeScript" active="true"' },
162+
children: [{ type: 'text', value: 'console.log("hello");' }],
163+
},
164+
],
165+
},
166+
],
167+
};
168+
169+
processCodeTabs(tree);
170+
171+
assert.partialDeepStrictEqual(tree.children[0], {
172+
tagName: 'CodeTabs',
173+
properties: {
174+
languages: 'javascript|typescript',
175+
displayNames: 'JavaScript|TypeScript',
176+
defaultTab: '1',
177+
},
178+
});
179+
});
180+
181+
it('processCodeHighlighting applies syntax highlighting', () => {
182+
const tree = {
183+
type: 'root',
184+
children: [
185+
{
186+
type: 'element',
187+
tagName: 'pre',
188+
children: [
189+
{
190+
type: 'element',
191+
tagName: 'code',
192+
properties: { className: ['language-javascript'] },
193+
data: { meta: 'showCopyButton="true"' },
194+
children: [{ type: 'text', value: 'console.log("hello");' }],
195+
},
196+
],
197+
},
198+
],
199+
};
200+
201+
processCodeHighlighting(tree);
202+
203+
assert.partialDeepStrictEqual(tree.children[0], {
204+
tagName: 'pre',
205+
properties: {
206+
class: 'highlighted-javascript language-javascript',
207+
showCopyButton: 'true',
208+
},
209+
});
210+
});
211+
});
212+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import assert from 'node:assert/strict';
2+
import { it, describe, mock } from 'node:test';
3+
4+
describe('getLanguageDisplayName', async () => {
5+
mock.module('../../shiki.config.mjs', {
6+
namedExports: {
7+
LANGUAGES: [
8+
{ name: 'javascript', aliases: ['js'], displayName: 'JavaScript' },
9+
{ name: 'typescript', aliases: ['ts'], displayName: 'TypeScript' },
10+
],
11+
},
12+
});
13+
14+
const { getLanguageDisplayName } = await import('../utils');
15+
16+
it('should return the display name for a known language', () => {
17+
assert.equal(getLanguageDisplayName('javascript'), 'JavaScript');
18+
assert.equal(getLanguageDisplayName('js'), 'JavaScript');
19+
});
20+
21+
it('should return the display name for another known language', () => {
22+
assert.equal(getLanguageDisplayName('typescript'), 'TypeScript');
23+
assert.equal(getLanguageDisplayName('ts'), 'TypeScript');
24+
});
25+
26+
it('should return the input language if it is not known', () => {
27+
assert.equal(getLanguageDisplayName('unknown'), 'unknown');
28+
});
29+
});
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { ReadTimeResults } from 'reading-time';
99
import type { VFile } from 'vfile';
1010
import { matter } from 'vfile-matter';
1111

12-
import { createSval } from './evaluate';
12+
import createSval from './evaluator';
1313
import { REHYPE_PLUGINS, REMARK_PLUGINS } from './plugins';
1414
import { createGitHubSlugger } from './utils';
1515

@@ -41,7 +41,7 @@ type MDXVFile = VFile & {
4141
*
4242
* @returns Promise resolving to compiled MDX content and metadata
4343
*/
44-
export async function compile(
44+
export default async function compile(
4545
source: MDXVFile,
4646
fileExtension: 'md' | 'mdx',
4747
components: MDXComponents = {},
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import Sval from 'sval';
1010
*
1111
* @returns Returns an Sandboxed instance of a JavaScript interpreter
1212
*/
13-
export const createSval = (
13+
export default function createSval(
1414
dependencies: Record<string, unknown> = {},
1515
mode: 'module' | 'script' = 'module'
16-
): Sval => {
16+
): Sval {
1717
const svalInterpreter = new Sval({
1818
ecmaVer: 'latest',
1919
sandBox: true,
@@ -23,4 +23,4 @@ export const createSval = (
2323
svalInterpreter.import(dependencies);
2424

2525
return svalInterpreter;
26-
};
26+
}

packages/mdx/lib/highlight.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/mdx/lib/highlighter.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
type CodeToHastOptions,
3+
createHighlighterCoreSync,
4+
} from '@shikijs/core';
5+
import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
6+
7+
import { LANGUAGES, DEFAULT_THEME } from '../shiki.config.mjs';
8+
9+
// Create a memoized minimal Shiki Syntax Highlighter
10+
export const shiki = createHighlighterCoreSync({
11+
themes: [DEFAULT_THEME],
12+
langs: LANGUAGES,
13+
engine: createJavaScriptRegexEngine(),
14+
});
15+
16+
/**
17+
* Highlight code to HTML and extract the inner content from code tags
18+
* @param code - Source code to highlight
19+
* @param language - Programming language for syntax highlighting
20+
* @param options - Additional Shiki options
21+
* @returns HTML string with syntax highlighting
22+
*/
23+
export const highlightToHtml = (
24+
code: string,
25+
language: string,
26+
options: Partial<CodeToHastOptions<string, string>> = {}
27+
): string => {
28+
const html = shiki.codeToHtml(code, {
29+
lang: language,
30+
theme: DEFAULT_THEME,
31+
...options,
32+
});
33+
34+
const match = html.match(/<code[^>]*>([\s\S]*?)<\/code>/);
35+
return match?.[1] || html;
36+
};
37+
38+
/**
39+
* Convert code to HAST with syntax highlighting
40+
* @param code - Source code to highlight
41+
* @param language - Programming language for syntax highlighting
42+
* @param options - Additional Shiki options
43+
* @returns HAST (Hypertext Abstract Syntax Tree) representation
44+
*/
45+
export const highlightToHast = (
46+
code: string,
47+
language: string,
48+
options: Partial<CodeToHastOptions<string, string>> = {}
49+
) =>
50+
shiki.codeToHast(code, {
51+
lang: language,
52+
theme: DEFAULT_THEME,
53+
...options,
54+
});

0 commit comments

Comments
 (0)