From d7ff5dbee48bbad0a08d060cc7b2514d0b777b8f Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sat, 1 Aug 2026 22:11:02 +0300 Subject: [PATCH 1/6] chore(test): add input & expected markdowns --- tests/theme/fixtures/overloads/expected.md | 34 ++++++++++++++++++++++ tests/theme/fixtures/overloads/input.js | 25 ++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/theme/fixtures/overloads/expected.md create mode 100644 tests/theme/fixtures/overloads/input.js diff --git a/tests/theme/fixtures/overloads/expected.md b/tests/theme/fixtures/overloads/expected.md new file mode 100644 index 00000000..eda89e77 --- /dev/null +++ b/tests/theme/fixtures/overloads/expected.md @@ -0,0 +1,34 @@ +# Class: `webpack.js.org.OverloadTest` + +## Constructors + +### `new OverloadTest()` + +- Returns: {OverloadTest} + +## Methods + +### `overloadedMethod(name, count)` + +#### Call Signature + +- `name` {string} A simple string parameter +- `count` {number} A simple number parameter +- Returns: {void} + +First overload signature. + +#### Call Signature + +- `isEnabled` {boolean} A simple boolean parameter +- Returns: {string} + +Second overload signature. + +#### Call Signature + +- `id` {number} A simple string parameter +- `isEnabled` {boolean} A simple boolean parameter +- Returns: {void} + +Third overload signature. diff --git a/tests/theme/fixtures/overloads/input.js b/tests/theme/fixtures/overloads/input.js new file mode 100644 index 00000000..26074009 --- /dev/null +++ b/tests/theme/fixtures/overloads/input.js @@ -0,0 +1,25 @@ +/* eslint-disable */ + +export class OverloadTest { + /** + * First overload signature. + * @overload + * @param {string} name A simple string parameter + * @param {number} count A simple number parameter + * @returns {void} + */ + /** + * Second overload signature. + * @overload + * @param {boolean} isEnabled A simple boolean parameter + * @returns {string} + */ + /** + * Third overload signature. + * @overload + * @param {number} id A simple string parameter + * @param {boolean} isEnabled A simple boolean parameter + * @returns {void} + */ + overloadedMethod(id, isEnabled) {} +} From e9ebbfd607a8223373a6911ccb008f9928bacba1 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sat, 1 Aug 2026 22:12:59 +0300 Subject: [PATCH 2/6] chore(test): testing output from the input with the expected one --- package.json | 1 + tests/theme/fixtures/tsconfig.json | 9 ++++ tests/theme/theme.test.mjs | 72 ++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 tests/theme/fixtures/tsconfig.json create mode 100644 tests/theme/theme.test.mjs diff --git a/package.json b/package.json index 5ccdd27c..3e23a51c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build:html": "node scripts/html/index.mjs && node scripts/html/sitemap-index.mjs", "build:rss-feed": "node scripts/rss-feed/index.mjs", "build": "npm-run-all build:*", + "test": "node --test tests/**/*.test.mjs", "lint": "npm-run-all \"lint:!(fix)\"", "lint:fix": "npm-run-all lint:*:fix", "lint:js": "eslint .", diff --git a/tests/theme/fixtures/tsconfig.json b/tests/theme/fixtures/tsconfig.json new file mode 100644 index 00000000..caddb7f2 --- /dev/null +++ b/tests/theme/fixtures/tsconfig.json @@ -0,0 +1,9 @@ +{ + "include": ["**/*.js"], + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/tests/theme/theme.test.mjs b/tests/theme/theme.test.mjs new file mode 100644 index 00000000..912f0e9a --- /dev/null +++ b/tests/theme/theme.test.mjs @@ -0,0 +1,72 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Application } from 'typedoc'; +import { + readdirSync, + readFileSync, + writeFileSync, + existsSync, + rmSync, +} from 'node:fs'; +import { join } from 'node:path/posix'; + +const fixturesDir = './tests/theme/fixtures'; +const outputDir = './tests/theme/.temp'; + +const testCases = readdirSync(fixturesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + +describe('TypeDoc Theme', () => { + for (const testCase of testCases) { + it(`should correctly render ${testCase}`, async () => { + const caseDir = join(fixturesDir, testCase); + const inputFilePath = join(caseDir, 'input.js'); + const expectedFilePath = join(caseDir, 'expected.md'); + + const app = await Application.bootstrapWithPlugins({ + entryPoints: [inputFilePath], + out: outputDir, + plugin: [ + 'typedoc-plugin-markdown', + './plugins/processor/index.mjs', + './plugins/theme/index.mjs', + ], + theme: 'doc-kit', + router: 'doc-kit', + hideGroupHeadings: true, + hideBreadcrumbs: true, + hidePageHeader: true, + readme: 'none', + disableSources: true, + propertiesFormat: 'table', + membersWithOwnFile: ['Class'], + modulesFileName: 'index', + entryFileName: 'index', + tsconfig: join(fixturesDir, 'tsconfig.json'), + }); + + const project = await app.convert(); + await app.generateOutputs(project); + + // With membersWithOwnFile: ['Class'] and the doc-kit router, + // Find the generated class markdown file dynamically + const mdFiles = readdirSync(outputDir, { recursive: true }).filter( + f => f.endsWith('.md') && !f.endsWith('index.md') + ); + if (mdFiles.length === 0) throw new Error('No markdown file generated'); + const actualMarkdown = readFileSync(join(outputDir, mdFiles[0]), 'utf-8'); + + // If UPDATE_SNAPSHOTS is set or expected.md does not exist, write the actual output to expected.md + if (!existsSync(expectedFilePath)) { + writeFileSync(expectedFilePath, actualMarkdown, 'utf-8'); + } + + const expectedMarkdown = readFileSync(expectedFilePath, 'utf-8'); + + rmSync(outputDir, { recursive: true, force: true }); + + assert.strictEqual(actualMarkdown, expectedMarkdown); + }); + } +}); From af92cce42d2ee9b7d6944f09332511624d3ecfe3 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sun, 2 Aug 2026 23:08:05 +0300 Subject: [PATCH 3/6] test(theme): adopt native node:test snapshots and reuse pipeline config --- scripts/markdown/api/index.mjs | 30 ++----- scripts/markdown/api/utils.mjs | 24 ++++++ tests/theme/fixtures/{overloads => }/input.js | 2 +- tests/theme/fixtures/overloads/expected.md | 34 -------- tests/theme/theme.test.mjs | 81 ++++++------------- tests/theme/theme.test.mjs.snapshot | 3 + 6 files changed, 59 insertions(+), 115 deletions(-) rename tests/theme/fixtures/{overloads => }/input.js (95%) delete mode 100644 tests/theme/fixtures/overloads/expected.md create mode 100644 tests/theme/theme.test.mjs.snapshot diff --git a/scripts/markdown/api/index.mjs b/scripts/markdown/api/index.mjs index a4718a79..221a55e7 100644 --- a/scripts/markdown/api/index.mjs +++ b/scripts/markdown/api/index.mjs @@ -1,37 +1,21 @@ import { join } from 'node:path/posix'; import { Application } from 'typedoc'; import { major } from 'semver'; -import { sources, outputDir, getPackageFile } from './utils.mjs'; +import { + sources, + outputDir, + getPackageFile, + getTypeDocConfig, +} from './utils.mjs'; const generate = async packageDir => { const { version } = await getPackageFile(packageDir); const app = await Application.bootstrapWithPlugins({ + ...getTypeDocConfig(), entryPoints: [join(packageDir, 'types.d.ts')], out: join(outputDir, `v${major(version)}.x`), publicPath: `/docs/api/v${major(version)}.x/`, - - plugin: [ - 'typedoc-plugin-markdown', - 'typedoc-plugin-missing-exports', - './plugins/processor/index.mjs', - './plugins/theme/index.mjs', - ], - theme: 'doc-kit', - router: 'doc-kit', - - hideGroupHeadings: true, - hideBreadcrumbs: true, - hidePageHeader: true, - readme: 'none', - disableSources: true, - propertiesFormat: 'table', - membersWithOwnFile: ['Class'], - - modulesFileName: 'index', - entryFileName: 'index', - tsconfig: 'tsconfig.json', - excludeExternals: true, }); const project = await app.convert(); diff --git a/scripts/markdown/api/utils.mjs b/scripts/markdown/api/utils.mjs index dd9ebbd6..a405f8f1 100644 --- a/scripts/markdown/api/utils.mjs +++ b/scripts/markdown/api/utils.mjs @@ -14,3 +14,27 @@ export const outputDir = join('pages', 'docs', 'api'); export const getPackageFile = async (packageDir, file = 'package.json') => JSON.parse(await readFile(join(packageDir, file), 'utf8')); + +export const getTypeDocConfig = () => ({ + plugin: [ + 'typedoc-plugin-markdown', + 'typedoc-plugin-missing-exports', + './plugins/processor/index.mjs', + './plugins/theme/index.mjs', + ], + theme: 'doc-kit', + router: 'doc-kit', + + hideGroupHeadings: true, + hideBreadcrumbs: true, + hidePageHeader: true, + readme: 'none', + disableSources: true, + propertiesFormat: 'table', + membersWithOwnFile: ['Class'], + + modulesFileName: 'index', + entryFileName: 'index', + tsconfig: 'tsconfig.json', + excludeExternals: true, +}); diff --git a/tests/theme/fixtures/overloads/input.js b/tests/theme/fixtures/input.js similarity index 95% rename from tests/theme/fixtures/overloads/input.js rename to tests/theme/fixtures/input.js index 26074009..4eb241a0 100644 --- a/tests/theme/fixtures/overloads/input.js +++ b/tests/theme/fixtures/input.js @@ -1,6 +1,6 @@ /* eslint-disable */ -export class OverloadTest { +export class OverloadedTest { /** * First overload signature. * @overload diff --git a/tests/theme/fixtures/overloads/expected.md b/tests/theme/fixtures/overloads/expected.md deleted file mode 100644 index eda89e77..00000000 --- a/tests/theme/fixtures/overloads/expected.md +++ /dev/null @@ -1,34 +0,0 @@ -# Class: `webpack.js.org.OverloadTest` - -## Constructors - -### `new OverloadTest()` - -- Returns: {OverloadTest} - -## Methods - -### `overloadedMethod(name, count)` - -#### Call Signature - -- `name` {string} A simple string parameter -- `count` {number} A simple number parameter -- Returns: {void} - -First overload signature. - -#### Call Signature - -- `isEnabled` {boolean} A simple boolean parameter -- Returns: {string} - -Second overload signature. - -#### Call Signature - -- `id` {number} A simple string parameter -- `isEnabled` {boolean} A simple boolean parameter -- Returns: {void} - -Third overload signature. diff --git a/tests/theme/theme.test.mjs b/tests/theme/theme.test.mjs index 912f0e9a..ffa6211e 100644 --- a/tests/theme/theme.test.mjs +++ b/tests/theme/theme.test.mjs @@ -1,72 +1,39 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; +import { test } from 'node:test'; import { Application } from 'typedoc'; -import { - readdirSync, - readFileSync, - writeFileSync, - existsSync, - rmSync, -} from 'node:fs'; +import { readdirSync, readFileSync, rmSync } from 'node:fs'; import { join } from 'node:path/posix'; +import { getTypeDocConfig } from '../../scripts/markdown/api/utils.mjs'; const fixturesDir = './tests/theme/fixtures'; const outputDir = './tests/theme/.temp'; -const testCases = readdirSync(fixturesDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); +test('TypeDoc Theme - Edge Cases Fixture', async t => { + const inputFilePath = join(fixturesDir, 'input.js'); -describe('TypeDoc Theme', () => { - for (const testCase of testCases) { - it(`should correctly render ${testCase}`, async () => { - const caseDir = join(fixturesDir, testCase); - const inputFilePath = join(caseDir, 'input.js'); - const expectedFilePath = join(caseDir, 'expected.md'); + const app = await Application.bootstrapWithPlugins({ + ...getTypeDocConfig(), + entryPoints: [inputFilePath], + out: outputDir, + publicPath: '/', + tsconfig: join(fixturesDir, 'tsconfig.json'), + }); - const app = await Application.bootstrapWithPlugins({ - entryPoints: [inputFilePath], - out: outputDir, - plugin: [ - 'typedoc-plugin-markdown', - './plugins/processor/index.mjs', - './plugins/theme/index.mjs', - ], - theme: 'doc-kit', - router: 'doc-kit', - hideGroupHeadings: true, - hideBreadcrumbs: true, - hidePageHeader: true, - readme: 'none', - disableSources: true, - propertiesFormat: 'table', - membersWithOwnFile: ['Class'], - modulesFileName: 'index', - entryFileName: 'index', - tsconfig: join(fixturesDir, 'tsconfig.json'), - }); + const project = await app.convert(); + await app.generateOutputs(project); - const project = await app.convert(); - await app.generateOutputs(project); + const mdFiles = readdirSync(outputDir, { recursive: true }).filter( + f => f.endsWith('.md') && !f.endsWith('index.md') + ); - // With membersWithOwnFile: ['Class'] and the doc-kit router, - // Find the generated class markdown file dynamically - const mdFiles = readdirSync(outputDir, { recursive: true }).filter( - f => f.endsWith('.md') && !f.endsWith('index.md') - ); - if (mdFiles.length === 0) throw new Error('No markdown file generated'); - const actualMarkdown = readFileSync(join(outputDir, mdFiles[0]), 'utf-8'); + if (mdFiles.length === 0) throw new Error('No markdown file generated'); - // If UPDATE_SNAPSHOTS is set or expected.md does not exist, write the actual output to expected.md - if (!existsSync(expectedFilePath)) { - writeFileSync(expectedFilePath, actualMarkdown, 'utf-8'); - } + mdFiles.sort(); - const expectedMarkdown = readFileSync(expectedFilePath, 'utf-8'); + const actualMarkdown = mdFiles + .map(f => readFileSync(join(outputDir, f), 'utf-8')) + .join('\n\n---\n\n'); - rmSync(outputDir, { recursive: true, force: true }); + rmSync(outputDir, { recursive: true, force: true }); - assert.strictEqual(actualMarkdown, expectedMarkdown); - }); - } + t.assert.snapshot(actualMarkdown); }); diff --git a/tests/theme/theme.test.mjs.snapshot b/tests/theme/theme.test.mjs.snapshot new file mode 100644 index 00000000..06ca5ffd --- /dev/null +++ b/tests/theme/theme.test.mjs.snapshot @@ -0,0 +1,3 @@ +exports[`TypeDoc Theme - Edge Cases Fixture 1`] = ` +"# Class: \`webpack.js.org.OverloadedTest\`\\n\\n## Constructors\\n\\n### \`new OverloadedTest()\`\\n\\n* Returns: {OverloadedTest}\\n\\n## Methods\\n\\n### \`overloadedMethod(name, count)\`\\n\\n#### Call Signature\\n\\n* \`name\` {string} A simple string parameter\\n* \`count\` {number} A simple number parameter\\n* Returns: {void} \\n\\nFirst overload signature.\\n\\n#### Call Signature\\n\\n* \`isEnabled\` {boolean} A simple boolean parameter\\n* Returns: {string} \\n\\nSecond overload signature.\\n\\n#### Call Signature\\n\\n* \`id\` {number} A simple string parameter\\n* \`isEnabled\` {boolean} A simple boolean parameter\\n* Returns: {void} \\n\\nThird overload signature.\\n" +`; From 997557963ebb450b3a57736811493f86d15f0d73 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Mon, 3 Aug 2026 17:58:38 +0300 Subject: [PATCH 4/6] fixup! --- package.json | 2 +- scripts/markdown/api/index.mjs | 9 ++------- scripts/markdown/api/utils.mjs | 4 ++-- tests/theme/theme.test.mjs | 4 ++-- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 3e23a51c..00dbd473 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:html": "node scripts/html/index.mjs && node scripts/html/sitemap-index.mjs", "build:rss-feed": "node scripts/rss-feed/index.mjs", "build": "npm-run-all build:*", - "test": "node --test tests/**/*.test.mjs", + "test": "node --test", "lint": "npm-run-all \"lint:!(fix)\"", "lint:fix": "npm-run-all lint:*:fix", "lint:js": "eslint .", diff --git a/scripts/markdown/api/index.mjs b/scripts/markdown/api/index.mjs index 221a55e7..7738b2f2 100644 --- a/scripts/markdown/api/index.mjs +++ b/scripts/markdown/api/index.mjs @@ -1,18 +1,13 @@ import { join } from 'node:path/posix'; import { Application } from 'typedoc'; import { major } from 'semver'; -import { - sources, - outputDir, - getPackageFile, - getTypeDocConfig, -} from './utils.mjs'; +import { sources, outputDir, getPackageFile, typeDocConfig } from './utils.mjs'; const generate = async packageDir => { const { version } = await getPackageFile(packageDir); const app = await Application.bootstrapWithPlugins({ - ...getTypeDocConfig(), + ...typeDocConfig, entryPoints: [join(packageDir, 'types.d.ts')], out: join(outputDir, `v${major(version)}.x`), publicPath: `/docs/api/v${major(version)}.x/`, diff --git a/scripts/markdown/api/utils.mjs b/scripts/markdown/api/utils.mjs index a405f8f1..b45ef691 100644 --- a/scripts/markdown/api/utils.mjs +++ b/scripts/markdown/api/utils.mjs @@ -15,7 +15,7 @@ export const outputDir = join('pages', 'docs', 'api'); export const getPackageFile = async (packageDir, file = 'package.json') => JSON.parse(await readFile(join(packageDir, file), 'utf8')); -export const getTypeDocConfig = () => ({ +export const typeDocConfig = { plugin: [ 'typedoc-plugin-markdown', 'typedoc-plugin-missing-exports', @@ -37,4 +37,4 @@ export const getTypeDocConfig = () => ({ entryFileName: 'index', tsconfig: 'tsconfig.json', excludeExternals: true, -}); +}; diff --git a/tests/theme/theme.test.mjs b/tests/theme/theme.test.mjs index ffa6211e..8243c696 100644 --- a/tests/theme/theme.test.mjs +++ b/tests/theme/theme.test.mjs @@ -2,7 +2,7 @@ import { test } from 'node:test'; import { Application } from 'typedoc'; import { readdirSync, readFileSync, rmSync } from 'node:fs'; import { join } from 'node:path/posix'; -import { getTypeDocConfig } from '../../scripts/markdown/api/utils.mjs'; +import { typeDocConfig } from '../../scripts/markdown/api/utils.mjs'; const fixturesDir = './tests/theme/fixtures'; const outputDir = './tests/theme/.temp'; @@ -11,7 +11,7 @@ test('TypeDoc Theme - Edge Cases Fixture', async t => { const inputFilePath = join(fixturesDir, 'input.js'); const app = await Application.bootstrapWithPlugins({ - ...getTypeDocConfig(), + ...typeDocConfig, entryPoints: [inputFilePath], out: outputDir, publicPath: '/', From 527386eb4927b201e1a48daab3e4950781057283 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Mon, 3 Aug 2026 21:02:40 +0300 Subject: [PATCH 5/6] test: add it in the CI workflow --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dec30f1..e841c3ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,26 @@ jobs: - name: Lint & Format run: npm run lint && npm run format:check + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: lts/* + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test + build: runs-on: ubuntu-latest steps: From 50bda6ed578c0a67ef96d474b1e51a14ac3b6d19 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Mon, 3 Aug 2026 22:35:12 +0300 Subject: [PATCH 6/6] refactor(utils): use lazy evaluation for sources to prevent side-effects --- scripts/markdown/api/configuration.mjs | 3 ++- scripts/markdown/api/index.mjs | 8 +++++++- scripts/markdown/api/utils.mjs | 12 +++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/markdown/api/configuration.mjs b/scripts/markdown/api/configuration.mjs index 5f40b54e..cbc087b6 100644 --- a/scripts/markdown/api/configuration.mjs +++ b/scripts/markdown/api/configuration.mjs @@ -1,7 +1,7 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path/posix'; import { major } from 'semver'; -import { sources, getPackageFile, outputDir } from './utils.mjs'; +import { getSources, getPackageFile, outputDir } from './utils.mjs'; const definitionName = ref => ref?.startsWith('#/definitions/') ? ref.slice('#/definitions/'.length) : ''; @@ -413,6 +413,7 @@ const generate = async packageDir => { ); }; +const sources = await getSources(); for (const source of sources) { await generate(source); } diff --git a/scripts/markdown/api/index.mjs b/scripts/markdown/api/index.mjs index 7738b2f2..848a7af3 100644 --- a/scripts/markdown/api/index.mjs +++ b/scripts/markdown/api/index.mjs @@ -1,7 +1,12 @@ import { join } from 'node:path/posix'; import { Application } from 'typedoc'; import { major } from 'semver'; -import { sources, outputDir, getPackageFile, typeDocConfig } from './utils.mjs'; +import { + getSources, + outputDir, + getPackageFile, + typeDocConfig, +} from './utils.mjs'; const generate = async packageDir => { const { version } = await getPackageFile(packageDir); @@ -17,6 +22,7 @@ const generate = async packageDir => { await app.generateOutputs(project); }; +const sources = await getSources(); for (const source of sources) { await generate(source); } diff --git a/scripts/markdown/api/utils.mjs b/scripts/markdown/api/utils.mjs index b45ef691..e854cc62 100644 --- a/scripts/markdown/api/utils.mjs +++ b/scripts/markdown/api/utils.mjs @@ -4,11 +4,13 @@ import { join } from 'node:path/posix'; const [packageDir] = process.argv.slice(2); const cacheDir = join('.', '.cache', 'webpack'); -export const sources = packageDir - ? [packageDir] - : (await readdir(cacheDir, { withFileTypes: true })) - .filter(entry => entry.isDirectory()) - .map(entry => join(cacheDir, entry.name)); +export const getSources = async () => { + if (packageDir) return [packageDir]; + const sources = await readdir(cacheDir, { withFileTypes: true }); + return sources + .filter(entry => entry.isDirectory()) + .map(entry => join(cacheDir, entry.name)); +}; export const outputDir = join('pages', 'docs', 'api');