From 841dad2965d2ad0c3eccbbba4838276cb203430c Mon Sep 17 00:00:00 2001 From: Nikhil Kumar Rajak Date: Sat, 14 Mar 2026 21:20:43 +0000 Subject: [PATCH] test: add missing coverage for json parser, url utils, and generator helpers --- src/parsers/__tests__/json.test.mjs | 33 +++++++++ src/utils/__tests__/generators.test.mjs | 96 ++++++++++++++++++++++++- src/utils/__tests__/url.test.mjs | 53 ++++++++++++++ 3 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/parsers/__tests__/json.test.mjs create mode 100644 src/utils/__tests__/url.test.mjs diff --git a/src/parsers/__tests__/json.test.mjs b/src/parsers/__tests__/json.test.mjs new file mode 100644 index 00000000..73fa65b4 --- /dev/null +++ b/src/parsers/__tests__/json.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +let content; +mock.module('../../utils/parser.mjs', { + namedExports: { + loadFromURL: async () => content, + }, +}); + +const { parseTypeMap } = await import('../json.mjs'); + +describe('parseTypeMap', () => { + it('should return an empty object when path is falsy', async () => { + for (const falsy of [undefined, null, '']) { + assert.deepStrictEqual(await parseTypeMap(falsy), {}); + } + }); + + it('should parse and return the JSON content from a given path', async () => { + content = JSON.stringify({ Buffer: 'buffer.md', fs: 'fs.md' }); + const result = await parseTypeMap('/some/path/types.json'); + assert.deepStrictEqual(result, { Buffer: 'buffer.md', fs: 'fs.md' }); + }); + + it('should throw a SyntaxError when content is not valid JSON', async () => { + content = 'not valid json'; + await assert.rejects( + () => parseTypeMap('/some/path/types.json'), + SyntaxError + ); + }); +}); diff --git a/src/utils/__tests__/generators.test.mjs b/src/utils/__tests__/generators.test.mjs index d6ef025b..f6380749 100644 --- a/src/utils/__tests__/generators.test.mjs +++ b/src/utils/__tests__/generators.test.mjs @@ -1,11 +1,14 @@ import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; +import { describe, it, mock, afterEach } from 'node:test'; import { groupNodesByModule, getVersionFromSemVer, coerceSemVer, getCompatibleVersions, + legacyToJSON, + buildApiDocURL, + createLazyGenerator, } from '../generators.mjs'; describe('groupNodesByModule', () => { @@ -79,3 +82,94 @@ describe('getCompatibleVersions', () => { assert.equal(result.length, 2); }); }); + +describe('legacyToJSON', () => { + const base = { + type: 'module', + source: 'lib/fs.js', + introduced_in: 'v0.10.0', + meta: {}, + stability: 2, + stabilityText: 'Stable', + classes: [], + methods: ['readFile'], + properties: [], + miscs: [], + modules: ['fs'], + globals: [], + }; + + it('serialises a normal section with all keys', () => { + const result = JSON.parse(legacyToJSON({ ...base, api: 'fs' })); + assert.ok('type' in result); + assert.ok('methods' in result); + assert.ok('modules' in result); + }); + + it('omits modules key for index sections', () => { + const result = JSON.parse(legacyToJSON({ ...base, api: 'index' })); + assert.ok(!('modules' in result)); + }); + + it('uses all.json key order when api is null', () => { + const result = JSON.parse(legacyToJSON({ ...base, api: null })); + // all.json only includes miscs, modules, classes, globals, methods + assert.ok('miscs' in result); + assert.ok('modules' in result); + assert.ok(!('type' in result)); + assert.ok(!('source' in result)); + }); + + it('passes extra args to JSON.stringify (e.g. indentation)', () => { + const result = legacyToJSON({ ...base, api: 'fs' }, null, 2); + assert.ok(result.includes('\n')); + }); +}); + +describe('buildApiDocURL', () => { + const entry = { api: 'fs' }; + const base = 'https://nodejs.org'; + + it('builds a .md URL by default', () => { + const url = buildApiDocURL(entry, base); + assert.ok(url instanceof URL); + assert.ok(url.pathname.endsWith('.md')); + assert.ok(url.pathname.includes('/fs')); + }); + + it('builds a .html URL when useHtml is true', () => { + const url = buildApiDocURL(entry, base, true); + assert.ok(url.pathname.endsWith('.html')); + }); +}); + +describe('createLazyGenerator', () => { + afterEach(() => mock.restoreAll()); + + it('spreads metadata properties onto the returned object', () => { + const metadata = { name: 'ast', version: '1.0.0', dependsOn: undefined }; + const gen = createLazyGenerator(metadata); + assert.equal(gen.name, 'ast'); + assert.equal(gen.version, '1.0.0'); + }); + + it('exposes generate and processChunk functions that delegate to the lazily loaded module', async () => { + // Both exports are mocked in a single mock.module() call to avoid ESM import + // cache collisions that occur when re-mocking the same specifier across two it() blocks. + const specifier = import.meta.resolve('../../generators/ast/generate.mjs'); + const fakeGenerate = async input => `processed:${input}`; + const fakeProcessChunk = async (input, indices) => + indices.map(i => input[i]); + mock.module(specifier, { + namedExports: { generate: fakeGenerate, processChunk: fakeProcessChunk }, + }); + + const gen = createLazyGenerator({ name: 'ast' }); + + const generateResult = await gen.generate('hello'); + assert.equal(generateResult, 'processed:hello'); + + const processChunkResult = await gen.processChunk(['a', 'b', 'c'], [0, 2]); + assert.deepStrictEqual(processChunkResult, ['a', 'c']); + }); +}); diff --git a/src/utils/__tests__/url.test.mjs b/src/utils/__tests__/url.test.mjs new file mode 100644 index 00000000..34c38853 --- /dev/null +++ b/src/utils/__tests__/url.test.mjs @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock, afterEach } from 'node:test'; + +let fileContent = 'hello from file'; +mock.module('node:fs/promises', { + namedExports: { + readFile: async () => fileContent, + }, +}); + +const { toParsedURL, loadFromURL } = await import('../url.mjs'); + +describe('toParsedURL', () => { + it('should return the same URL instance when given a URL object', () => { + const url = new URL('https://nodejs.org'); + assert.strictEqual(toParsedURL(url), url); + }); + + it('should parse a valid URL string into a URL object', () => { + const result = toParsedURL('https://nodejs.org/api'); + assert.ok(result instanceof URL); + assert.strictEqual(result.hostname, 'nodejs.org'); + }); + + it('should return null for a string that cannot be parsed as a URL', () => { + assert.strictEqual(toParsedURL('not-a-url'), null); + }); +}); + +describe('loadFromURL', () => { + afterEach(() => mock.restoreAll()); + + it('should read content from the filesystem for a plain file path', async () => { + fileContent = 'file content'; + const result = await loadFromURL('/some/path/file.txt'); + assert.strictEqual(result, 'file content'); + }); + + it('should read content from the filesystem for a file: URL', async () => { + fileContent = 'from file url'; + const result = await loadFromURL(new URL('file:///some/file.txt')); + assert.strictEqual(result, 'from file url'); + }); + + it('should fetch content from an http URL', async () => { + mock.method(globalThis, 'fetch', async () => ({ + text: async () => 'fetched content', + })); + + const result = await loadFromURL('https://nodejs.org/data.txt'); + assert.strictEqual(result, 'fetched content'); + }); +});