Skip to content

Commit 1d8cc57

Browse files
committed
add unit tests
1 parent bd84e9d commit 1d8cc57

File tree

4 files changed

+233
-5
lines changed

4 files changed

+233
-5
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it, mock } from 'node:test';
3+
4+
import { SemVer } from 'semver';
5+
6+
import { setConfig } from '../../../../utils/configuration/index.mjs';
7+
8+
mock.module('@node-core/rehype-shiki', {
9+
namedExports: {
10+
LANGS: [
11+
{ name: 'javascript', aliases: ['js'], displayName: 'JavaScript' },
12+
{ name: 'typescript', aliases: ['ts'], displayName: 'TypeScript' },
13+
{ name: 'python', displayName: 'Python' },
14+
],
15+
},
16+
});
17+
18+
const { buildVersionEntries, buildPageList, buildLanguageDisplayNameMap } =
19+
await import('../config.mjs');
20+
21+
await setConfig({
22+
version: 'v22.0.0',
23+
changelog: [
24+
{ version: new SemVer('20.0.0'), isLts: true, isCurrent: false },
25+
{ version: new SemVer('22.0.0'), isLts: false, isCurrent: true },
26+
],
27+
generators: {
28+
web: {
29+
title: 'Node.js',
30+
repository: 'nodejs/node',
31+
ref: 'main',
32+
baseURL: 'https://nodejs.org/docs',
33+
editURL: 'https://github.com/nodejs/node/edit/main/doc/api{path}.md',
34+
pageURL: '{baseURL}/latest-{version}/api{path}.html',
35+
},
36+
},
37+
});
38+
39+
/**
40+
* Helper to create a minimal JSX content entry.
41+
*/
42+
const makeEntry = (api, name, path) => ({
43+
data: {
44+
api,
45+
path,
46+
heading: { depth: 1, data: { name } },
47+
},
48+
});
49+
50+
describe('buildVersionEntries', () => {
51+
it('creates version entries with labels and URL templates', () => {
52+
const config = {
53+
changelog: [
54+
{ version: new SemVer('20.0.0'), isLts: true, isCurrent: false },
55+
{ version: new SemVer('22.0.0'), isLts: false, isCurrent: true },
56+
],
57+
};
58+
59+
const result = buildVersionEntries(
60+
config,
61+
'https://nodejs.org/docs/latest-{version}/api{path}.html'
62+
);
63+
64+
assert.equal(result.length, 2);
65+
assert.deepStrictEqual(result[0], {
66+
url: 'https://nodejs.org/docs/latest-v20.x/api{path}.html',
67+
label: 'v20.x (LTS)',
68+
major: 20,
69+
});
70+
assert.deepStrictEqual(result[1], {
71+
url: 'https://nodejs.org/docs/latest-v22.x/api{path}.html',
72+
label: 'v22.x (Current)',
73+
major: 22,
74+
});
75+
});
76+
77+
it('does not append a label suffix for versions that are neither LTS nor Current', () => {
78+
const config = {
79+
changelog: [
80+
{ version: new SemVer('18.0.0'), isLts: false, isCurrent: false },
81+
],
82+
};
83+
84+
const result = buildVersionEntries(config, '{version}');
85+
86+
assert.equal(result[0].label, 'v18.x');
87+
});
88+
89+
it('formats minor versions when minor is non-zero', () => {
90+
const config = {
91+
changelog: [
92+
{ version: new SemVer('21.7.0'), isLts: false, isCurrent: false },
93+
],
94+
};
95+
96+
const result = buildVersionEntries(config, '{version}');
97+
98+
assert.equal(result[0].label, 'v21.7.x');
99+
assert.equal(result[0].major, 21);
100+
});
101+
});
102+
103+
describe('buildPageList', () => {
104+
it('returns sorted [name, path] tuples from input entries', () => {
105+
const input = [
106+
makeEntry('http', 'HTTP', '/http'),
107+
makeEntry('fs', 'File System', '/fs'),
108+
];
109+
110+
const result = buildPageList(input);
111+
112+
assert.equal(result.length, 2);
113+
// Sorted alphabetically by name
114+
assert.deepStrictEqual(result[0], ['File System', '/fs']);
115+
assert.deepStrictEqual(result[1], ['HTTP', '/http']);
116+
});
117+
118+
it('filters out entries whose heading depth is not 1', () => {
119+
const input = [
120+
makeEntry('fs', 'File System', '/fs'),
121+
{
122+
data: {
123+
api: 'http',
124+
path: '/http',
125+
heading: { depth: 2, data: { name: 'HTTP' } },
126+
},
127+
},
128+
];
129+
130+
const result = buildPageList(input);
131+
132+
assert.equal(result.length, 1);
133+
assert.deepStrictEqual(result[0], ['File System', '/fs']);
134+
});
135+
});
136+
137+
describe('buildLanguageDisplayNameMap', () => {
138+
it('returns entries suitable for constructing a Map', () => {
139+
const result = buildLanguageDisplayNameMap();
140+
141+
// Should have one entry per unique language name
142+
assert.equal(result.length, 3);
143+
144+
const map = new Map(result);
145+
146+
assert.equal(map.get('JavaScript'), undefined);
147+
// Each entry is [[aliases..., name], displayName]
148+
// Find the javascript entry
149+
const jsEntry = result.find(([keys]) => keys.includes('javascript'));
150+
assert.ok(jsEntry);
151+
assert.deepStrictEqual(jsEntry[0], ['js', 'javascript']);
152+
assert.equal(jsEntry[1], 'JavaScript');
153+
});
154+
155+
it('handles languages without aliases', () => {
156+
const result = buildLanguageDisplayNameMap();
157+
158+
const pyEntry = result.find(([keys]) => keys.includes('python'));
159+
assert.ok(pyEntry);
160+
assert.deepStrictEqual(pyEntry[0], ['python']);
161+
assert.equal(pyEntry[1], 'Python');
162+
});
163+
});

src/generators/web/utils/config.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { getSortedHeadNodes } from '../../jsx-ast/utils/getSortedHeadNodes.mjs';
1515
* @param {string} pageURLBase
1616
* @returns {Array<{url: string, label: string, major: number}>}
1717
*/
18-
function buildVersionEntries(config, pageURLBase) {
18+
export function buildVersionEntries(config, pageURLBase) {
1919
return config.changelog.map(({ version, isLts, isCurrent }) => {
2020
let label = `v${getVersionFromSemVer(version)}`;
2121
const url = pageURLBase.replace('{version}', label);
@@ -35,7 +35,7 @@ function buildVersionEntries(config, pageURLBase) {
3535
* @param {Array<import('../../jsx-ast/utils/buildContent.mjs').JSXContent>} input
3636
* @returns {Array<[string, string]>}
3737
*/
38-
function buildPageList(input) {
38+
export function buildPageList(input) {
3939
const headNodes = getSortedHeadNodes(input.map(e => e.data));
4040
return headNodes.map(node => [node.heading.data.name, node.path]);
4141
}
@@ -45,7 +45,7 @@ function buildPageList(input) {
4545
*
4646
* @returns {Array<[string[], string]>}
4747
*/
48-
function buildLanguageDisplayNameMap() {
48+
export function buildLanguageDisplayNameMap() {
4949
return [
5050
...new Map(
5151
LANGS.map(({ name, aliases = [], displayName }) => [

src/utils/__tests__/loaders.test.mjs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ mock.module('node:fs/promises', {
88
},
99
});
1010

11-
const { toParsedURL, loadFromURL } = await import('../loaders.mjs');
11+
const { toParsedURL, loadFromURL, importFromURL } =
12+
await import('../loaders.mjs');
1213

1314
describe('toParsedURL', () => {
1415
it('should return the same URL instance when given a URL object', () => {
@@ -51,3 +52,18 @@ describe('loadFromURL', () => {
5152
assert.strictEqual(result, 'fetched content');
5253
});
5354
});
55+
56+
describe('importFromURL', () => {
57+
it('should import a module from a valid URL and return default export', async () => {
58+
const mod = await importFromURL(import.meta.url);
59+
// The test file itself has no default export, so the full module is returned
60+
assert.ok(mod !== undefined);
61+
});
62+
63+
it('should resolve a relative path to an absolute file URL', async () => {
64+
// Importing the loaders module itself as a sanity check
65+
const mod = await importFromURL(new URL('../loaders.mjs', import.meta.url));
66+
assert.ok(typeof mod.loadFromURL === 'function');
67+
assert.ok(typeof mod.importFromURL === 'function');
68+
});
69+
});

src/utils/__tests__/misc.test.mjs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import assert from 'node:assert/strict';
44
import { describe, it } from 'node:test';
55

6-
import { lazy, isPlainObject, deepMerge } from '../misc.mjs';
6+
import { lazy, isPlainObject, extractPrimitives, deepMerge } from '../misc.mjs';
77

88
describe('lazy', () => {
99
it('should call the function only once and cache the result', () => {
@@ -38,6 +38,55 @@ describe('isPlainObject', () => {
3838
});
3939
});
4040

41+
describe('extractPrimitives', () => {
42+
it('should keep string, number, boolean, and null values', () => {
43+
const obj = { a: 'hello', b: 42, c: true, d: null };
44+
assert.deepStrictEqual(extractPrimitives(obj), {
45+
a: 'hello',
46+
b: 42,
47+
c: true,
48+
d: null,
49+
});
50+
});
51+
52+
it('should remove object and function values', () => {
53+
const obj = {
54+
name: 'test',
55+
nested: { foo: 'bar' },
56+
fn: () => {},
57+
count: 5,
58+
};
59+
const result = extractPrimitives(obj);
60+
assert.deepStrictEqual(result, { name: 'test', count: 5 });
61+
});
62+
63+
it('should keep arrays of primitives', () => {
64+
const obj = { tags: ['a', 'b'], name: 'test' };
65+
assert.deepStrictEqual(extractPrimitives(obj), {
66+
tags: ['a', 'b'],
67+
name: 'test',
68+
});
69+
});
70+
71+
it('should remove arrays containing objects', () => {
72+
const obj = { items: [{ id: 1 }], name: 'test' };
73+
assert.deepStrictEqual(extractPrimitives(obj), { name: 'test' });
74+
});
75+
76+
it('should keep undefined values', () => {
77+
const obj = { a: undefined, b: 'yes' };
78+
const result = extractPrimitives(obj);
79+
assert.strictEqual('a' in result, true);
80+
assert.strictEqual(result.a, undefined);
81+
assert.strictEqual(result.b, 'yes');
82+
});
83+
84+
it('should return an empty object when all values are non-primitive', () => {
85+
const obj = { a: {}, b: [{ x: 1 }], c: () => {} };
86+
assert.deepStrictEqual(extractPrimitives(obj), {});
87+
});
88+
});
89+
4190
describe('deepMerge', () => {
4291
it('should merge flat objects with source taking precedence over base', () => {
4392
const result = deepMerge({ a: 1, b: 2 }, { b: 10, c: 3 });

0 commit comments

Comments
 (0)