Skip to content

Commit 798cc5d

Browse files
test: improve test coverage
1 parent b2181a2 commit 798cc5d

File tree

8 files changed

+543
-0
lines changed

8 files changed

+543
-0
lines changed

.c8rc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"all": true,
33
"exclude": [
44
"eslint.config.mjs",
5+
"**/*.test.mjs",
56
"**/fixtures",
67
"src/generators/legacy-html/assets",
78
"src/generators/web/ui",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtemp, readFile } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import { join } from 'node:path';
5+
import test from 'node:test';
6+
7+
import { u } from 'unist-builder';
8+
9+
import addon from '../index.mjs';
10+
import {
11+
normalizeSectionName,
12+
generateSectionFolderName,
13+
} from '../utils/section.mjs';
14+
15+
test('returns empty array when no code blocks match filename comment', async () => {
16+
const entry = {
17+
heading: { data: { name: 'Section A' } },
18+
content: u('root', [u('code', 'console.log("no filename header");')]),
19+
};
20+
21+
const result = await addon.generate([entry], {});
22+
23+
// No sections were buildable / no filenames extracted
24+
assert.deepEqual(result, []);
25+
});
26+
27+
test('ignores non-buildable sections (needs both .cc and .js)', async () => {
28+
// Only a .cc file present -> not buildable
29+
const entry = {
30+
heading: { data: { name: 'OnlyCC' } },
31+
content: u('root', [u('code', '// file1.cc\nint main() {}')]),
32+
};
33+
34+
const result = await addon.generate([entry], {});
35+
36+
assert.deepEqual(result, []);
37+
});
38+
39+
test('generates files array and writes files to disk when output provided', async () => {
40+
const sectionName = 'My Addon Section';
41+
42+
const entry = {
43+
heading: { data: { name: sectionName } },
44+
content: u('root', [
45+
u('code', '// file1.cc\nint main() {}'),
46+
u(
47+
'code',
48+
"// test.js\nmodule.exports = require('./build/Release/addon');"
49+
),
50+
]),
51+
};
52+
53+
const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-'));
54+
55+
const returned = await addon.generate([entry], { output: tmp });
56+
57+
// Returned is an array of file arrays (one per section)
58+
assert.equal(Array.isArray(returned), true);
59+
assert.equal(returned.length, 1);
60+
61+
const files = returned[0];
62+
63+
assert.ok(files.some(f => f.name === 'file1.cc'));
64+
assert.ok(files.some(f => f.name === 'test.js'));
65+
assert.ok(files.some(f => f.name === 'binding.gyp'));
66+
67+
// Verify files were written to disk under the computed folder name
68+
const folderName = generateSectionFolderName(
69+
normalizeSectionName(sectionName),
70+
0
71+
);
72+
73+
const file1 = await readFile(join(tmp, folderName, 'file1.cc'), 'utf-8');
74+
const binding = await readFile(join(tmp, folderName, 'binding.gyp'), 'utf-8');
75+
76+
assert.match(file1, /int main/);
77+
assert.match(binding, /targets/);
78+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import assert from 'node:assert/strict';
2+
import { readFile, mkdtemp } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import { join } from 'node:path';
5+
import test from 'node:test';
6+
7+
import llms from '../index.mjs';
8+
9+
const makeEntry = ({
10+
title = 'MyAPI',
11+
depth = 1,
12+
desc = 'A description',
13+
api = 'doc/some/path.md',
14+
llm_description,
15+
} = {}) => ({
16+
heading: { depth, data: { name: title } },
17+
content: {
18+
children: [
19+
{ type: 'paragraph', children: [{ type: 'text', value: desc }] },
20+
],
21+
},
22+
api_doc_source: api,
23+
llm_description,
24+
});
25+
26+
test('generate returns filled template including depth 1 entries', async () => {
27+
const entry = makeEntry({ title: 'Alpha', desc: 'Alpha description' });
28+
29+
const result = await llms.generate([entry], {});
30+
31+
assert.equal(typeof result, 'string');
32+
assert.match(result, /- \[Alpha\]/);
33+
assert.match(result, /Alpha description/);
34+
});
35+
36+
test('generate only includes depth 1 headings', async () => {
37+
const entry1 = makeEntry({ title: 'Top', depth: 1, desc: 'Top desc' });
38+
const entry2 = makeEntry({ title: 'Sub', depth: 2, desc: 'Sub desc' });
39+
40+
const result = await llms.generate([entry1, entry2], {});
41+
42+
assert.match(result, /- \[Top\]/);
43+
assert.doesNotMatch(result, /- \[Sub\]/);
44+
});
45+
46+
test('generate writes llms.txt when output is provided', async () => {
47+
const entry = makeEntry({ title: 'WriteTest', desc: 'Write description' });
48+
49+
const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-'));
50+
51+
const returned = await llms.generate([entry], { output: tmp });
52+
53+
const file = await readFile(join(tmp, 'llms.txt'), 'utf-8');
54+
55+
assert.equal(returned, file);
56+
assert.match(file, /- \[WriteTest\]/);
57+
assert.match(file, /Write description/);
58+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtemp, readFile } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import { join } from 'node:path';
5+
import test from 'node:test';
6+
7+
import { u } from 'unist-builder';
8+
9+
import manpage from '../index.mjs';
10+
11+
const textNode = txt => u('text', txt);
12+
13+
const createMock = ({
14+
api = 'cli',
15+
slug = '',
16+
depth = 2,
17+
headingText = '',
18+
desc = '',
19+
} = {}) => ({
20+
api,
21+
slug,
22+
heading: { depth, data: { text: headingText } },
23+
// eslint-disable-next-line no-sparse-arrays
24+
content: u('root', [, u('paragraph', [textNode(desc)])]),
25+
});
26+
27+
test('throws when no cli documentation present', async () => {
28+
await assert.rejects(
29+
async () => {
30+
await manpage.generate([{ api: 'not-cli' }], {});
31+
},
32+
{ message: /Could not find any `cli` documentation/ }
33+
);
34+
});
35+
36+
test('generates mandoc including options and environment entries', async () => {
37+
const components = [
38+
createMock({ api: 'cli', slug: 'cli', depth: 1 }),
39+
createMock({ api: 'cli', slug: 'options', depth: 2 }),
40+
createMock({
41+
api: 'cli',
42+
slug: 'opt-a',
43+
depth: 3,
44+
headingText: '`-a`, `--all`',
45+
desc: 'Option A description',
46+
}),
47+
createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }),
48+
createMock({
49+
api: 'cli',
50+
slug: 'env-foo',
51+
depth: 3,
52+
headingText: '`FOO=bar`',
53+
desc: 'Env FOO description',
54+
}),
55+
createMock({ api: 'cli', slug: 'after', depth: 2 }),
56+
];
57+
58+
const result = await manpage.generate(components, {});
59+
60+
// Ensure mandoc markers for options and environment variables are present
61+
assert.match(result, /\.It Fl/);
62+
assert.match(result, /Option A description/);
63+
assert.match(result, /\.It Ev/);
64+
assert.match(result, /Env FOO description/);
65+
});
66+
67+
test('writes node.1 to output when provided', async () => {
68+
const components = [
69+
createMock({ api: 'cli', slug: 'options', depth: 2 }),
70+
createMock({
71+
api: 'cli',
72+
slug: 'opt-a',
73+
depth: 3,
74+
headingText: '`-a`',
75+
desc: 'desc',
76+
}),
77+
createMock({ api: 'cli', slug: 'environment-variables-1', depth: 2 }),
78+
createMock({
79+
api: 'cli',
80+
slug: 'env',
81+
depth: 3,
82+
headingText: '`X=`',
83+
desc: 'env desc',
84+
}),
85+
createMock({ api: 'cli', slug: 'end', depth: 2 }),
86+
];
87+
88+
const tmp = await mkdtemp(join(tmpdir(), 'doc-kit-'));
89+
90+
const returned = await manpage.generate(components, { output: tmp });
91+
92+
const file = await readFile(join(tmp, 'node.1'), 'utf-8');
93+
94+
assert.equal(returned, file);
95+
assert.match(file, /desc/);
96+
assert.match(file, /env desc/);
97+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { deepStrictEqual, strictEqual } from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
4+
import generator from '../index.mjs';
5+
6+
describe('generators/metadata/index', () => {
7+
it('streams chunk results and yields flattened arrays', async () => {
8+
const inputs = [1, 2, 3];
9+
10+
const worker = {
11+
// Simulate an async generator that yields chunked results
12+
async *stream() {
13+
yield [[1, 2], [3]];
14+
yield [[4]];
15+
},
16+
};
17+
18+
const results = [];
19+
20+
for await (const chunk of generator.generate(inputs, {
21+
typeMap: {},
22+
worker,
23+
})) {
24+
results.push(chunk);
25+
}
26+
27+
strictEqual(results.length, 2);
28+
deepStrictEqual(results[0], [1, 2, 3]);
29+
deepStrictEqual(results[1], [4]);
30+
});
31+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { strictEqual, deepStrictEqual } from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
4+
import { u } from 'unist-builder';
5+
import { VFile } from 'vfile';
6+
7+
import { parseApiDoc } from '../utils/parse.mjs';
8+
9+
describe('generators/metadata/utils/parse', () => {
10+
it('parses heading, stability, YAML and converts markdown links', () => {
11+
const tree = u('root', [
12+
u('heading', { depth: 1 }, [u('text', 'My API')]),
13+
u('blockquote', [u('paragraph', [u('text', 'Stability: 2 - stable')])]),
14+
u('html', '<!-- YAML\nsource_link: https://example.com\n-->'),
15+
u('paragraph', [
16+
u('text', 'See '),
17+
u('link', { url: 'other.md#foo' }, [u('text', 'other')]),
18+
]),
19+
]);
20+
21+
const file = new VFile({ path: 'doc/api/my-api.md' });
22+
23+
const results = parseApiDoc({ file, tree }, {});
24+
25+
strictEqual(results.length, 1);
26+
const [entry] = results;
27+
28+
strictEqual(entry.source_link, 'https://example.com');
29+
strictEqual(entry.stability.children.length, 1);
30+
strictEqual(entry.stability.children[0].data.index, '2');
31+
32+
// Find a paragraph child that contains a link and assert transformed URL
33+
const paragraph = entry.content.children.find(n => n.type === 'paragraph');
34+
const link = paragraph.children.find(c => c.type === 'link');
35+
strictEqual(link.url, 'other.html#foo');
36+
});
37+
38+
it('inserts a fake heading when none exist', () => {
39+
const tree = u('root', [u('paragraph', [u('text', 'No heading content')])]);
40+
const file = new VFile({ path: 'doc/api/noheading.md' });
41+
42+
const results = parseApiDoc({ file, tree }, {});
43+
44+
strictEqual(results.length, 1);
45+
const [entry] = results;
46+
47+
// Fake heading has empty text
48+
deepStrictEqual(entry.heading.data.text, '');
49+
});
50+
51+
it('converts link references using definitions and removes definitions', () => {
52+
const heading = u('heading', { depth: 1 }, [u('text', 'Ref API')]);
53+
54+
const linkRef = u('linkReference', { identifier: 'def1' }, [
55+
u('text', 'ref'),
56+
]);
57+
58+
const definition = u(
59+
'definition',
60+
{ identifier: 'def1', url: 'https://def.example/' },
61+
[]
62+
);
63+
64+
const tree = u('root', [
65+
heading,
66+
u('paragraph', [u('text', 'See '), linkRef]),
67+
definition,
68+
]);
69+
70+
const file = new VFile({ path: 'doc/api/ref-api.md' });
71+
72+
const results = parseApiDoc({ file, tree }, {});
73+
74+
strictEqual(results.length, 1);
75+
const [entry] = results;
76+
77+
const paragraph = entry.content.children.find(n => n.type === 'paragraph');
78+
const link = paragraph.children.find(c => c.type === 'link');
79+
strictEqual(link.url, 'https://def.example/');
80+
});
81+
82+
it('converts type references to links using provided typeMap', () => {
83+
const tree = u('root', [
84+
u('heading', { depth: 1 }, [u('text', 'Types API')]),
85+
u('paragraph', [u('text', 'Type is {Foo}')]),
86+
]);
87+
88+
const file = new VFile({ path: 'doc/api/types.md' });
89+
90+
const results = parseApiDoc({ file, tree }, { Foo: 'foo.html' });
91+
92+
strictEqual(results.length, 1);
93+
const [entry] = results;
94+
95+
const paragraph = entry.content.children.find(n => n.type === 'paragraph');
96+
const link = paragraph.children.find(c => c.type === 'link');
97+
strictEqual(link.url, 'foo.html');
98+
});
99+
100+
it('converts unix manual references to man7 links', () => {
101+
const tree = u('root', [
102+
u('heading', { depth: 1 }, [u('text', 'Man API')]),
103+
u('paragraph', [u('text', 'Run ls(1) for help')]),
104+
]);
105+
106+
const file = new VFile({ path: 'doc/api/man.md' });
107+
108+
const results = parseApiDoc({ file, tree }, {});
109+
110+
strictEqual(results.length, 1);
111+
const [entry] = results;
112+
113+
const paragraph = entry.content.children.find(n => n.type === 'paragraph');
114+
const link = paragraph.children.find(c => c.type === 'link');
115+
// should point to man7 man page for ls in section 1
116+
strictEqual(link.url.includes('man-pages/man1/ls.1.html'), true);
117+
});
118+
});

0 commit comments

Comments
 (0)