Files
renovate/test/docs/documentation.spec.ts
T
2025-07-12 12:35:51 +00:00

228 lines
7.4 KiB
TypeScript

import fs from 'fs-extra';
import { glob } from 'glob';
import { getOptions } from '../../lib/config/options';
import { packageCacheNamespaces } from '../../lib/util/cache/package/namespaces';
import { regEx } from '../../lib/util/regex';
const options = getOptions();
const markdownGlob = '{docs,lib}/**/*.md';
describe('docs/documentation', () => {
it('has no invalid links', async () => {
const markdownFiles = await glob(markdownGlob);
await Promise.all(
markdownFiles.map(async (markdownFile) => {
const markdownText = await fs.readFile(markdownFile, 'utf8');
expect(markdownText).not.toMatch(regEx(/\.md\/#/));
}),
);
});
describe('website-documentation', () => {
async function getConfigOptionSubHeaders(
file: string,
configOption: string,
): Promise<string[]> {
const subHeadings = [];
const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const reg = regEx(`##\\s${configOption}[\\s\\S]+?\n##\\s`);
const match = reg.exec(content);
const subHeadersMatch = match?.[0]?.matchAll(/\n###\s(?<child>\w+)\n/g);
if (subHeadersMatch) {
for (const subHeaderStr of subHeadersMatch) {
if (subHeaderStr?.groups?.child) {
subHeadings.push(subHeaderStr.groups.child);
}
}
}
return subHeadings;
}
describe('docs/usage/configuration-options.md', () => {
async function getConfigHeaders(file: string): Promise<string[]> {
const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n## (.*?)\n/g) ?? [];
return matches
.map((match) => match.substring(4, match.length - 1))
.filter(
(header) =>
header !== 'managerFilePatterns' && header !== 'enabled',
);
}
function getRequiredConfigOptions(): string[] {
return options
.filter((option) => !option.globalOnly)
.filter((option) => !option.parents)
.filter((option) => !option.autogenerated)
.map((option) => option.name)
.sort();
}
it('has doc headers sorted alphabetically', async () => {
expect(await getConfigHeaders('configuration-options.md')).toEqual(
(await getConfigHeaders('configuration-options.md')).sort(),
);
});
it('has headers for every required option', async () => {
expect(await getConfigHeaders('configuration-options.md')).toEqual(
getRequiredConfigOptions(),
);
});
async function getConfigSubHeaders(file: string): Promise<string[]> {
const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n### (.*?)\n/g) ?? [];
return matches
.map((match) => match.substring(5, match.length - 1))
.sort();
}
function getRequiredConfigSubOptions(): string[] {
return options
.filter((option) => option.stage !== 'global')
.filter((option) => !option.globalOnly)
.filter((option) => option.parents)
.map((option) => option.name)
.filter(
(header) =>
header !== 'managerFilePatterns' && header !== 'enabled',
)
.sort();
}
function getParentNames(): Set<string> {
const childrens = options
.filter((option) => option.stage !== 'global')
.filter((option) => !option.globalOnly)
.filter((option) => option.parents);
const parentNames = new Set<string>();
for (const children of childrens) {
const parents = children.parents ?? [];
for (const parent of parents) {
parentNames.add(parent);
}
}
return parentNames;
}
it('has headers for every required sub-option', async () => {
expect(await getConfigSubHeaders('configuration-options.md')).toEqual(
getRequiredConfigSubOptions(),
);
});
it.each([...getParentNames()])(
'%s has sub-headers sorted alphabetically',
async (parentName: string) => {
expect(
await getConfigOptionSubHeaders(
'configuration-options.md',
parentName,
),
).toEqual(
(
await getConfigOptionSubHeaders(
'configuration-options.md',
parentName,
)
).sort(),
);
},
);
});
describe('docs/usage/self-hosted-configuration.md', () => {
async function getSelfHostedHeaders(file: string): Promise<string[]> {
const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n## (.*?)\n/g) ?? [];
return matches.map((match) => match.substring(4, match.length - 1));
}
function getRequiredSelfHostedOptions(): string[] {
return options
.filter((option) => option.globalOnly)
.map((option) => option.name)
.sort();
}
it('has headers sorted alphabetically', async () => {
expect(
await getSelfHostedHeaders('self-hosted-configuration.md'),
).toEqual(
(await getSelfHostedHeaders('self-hosted-configuration.md')).sort(),
);
});
it('has headers for every required option', async () => {
expect(
await getSelfHostedHeaders('self-hosted-configuration.md'),
).toEqual(getRequiredSelfHostedOptions());
});
function getCacheNamespacesFromDocs(file: string): string[] {
const content = fs.readFileSync(`docs/usage/${file}`, 'utf8');
const beginMarker = '<!-- cache-namespaces-begin -->';
const endMarker = '<!-- cache-namespaces-end -->';
const beginIndex = content.indexOf(beginMarker);
const endIndex = content.indexOf(endMarker);
if (beginIndex === -1 || endIndex === -1) {
return [];
}
const cacheNamespacesSection = content.substring(
beginIndex + beginMarker.length,
endIndex,
);
const matches = cacheNamespacesSection.match(/- `([^`]+)`/g) ?? [];
return matches
.map((match) => match.substring(3, match.length - 1))
.sort();
}
function getExpectedCacheNamespaces(): string[] {
return packageCacheNamespaces
.filter((namespace) => namespace !== '_test-namespace')
.sort();
}
it('has complete cache namespaces list', () => {
expect(
getCacheNamespacesFromDocs('self-hosted-configuration.md'),
).toEqual(getExpectedCacheNamespaces());
});
});
describe('docs/usage/self-hosted-experimental.md', () => {
async function getSelfHostedExperimentalConfigHeaders(
file: string,
): Promise<string[]> {
const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
const matches = content.match(/\n## (.*?)\n/g) ?? [];
return matches.map((match) => match.substring(4, match.length - 1));
}
it('has headers sorted alphabetically', async () => {
expect(
await getSelfHostedExperimentalConfigHeaders(
'self-hosted-experimental.md',
),
).toEqual(
(
await getSelfHostedExperimentalConfigHeaders(
'self-hosted-experimental.md',
)
).sort(),
);
});
});
});
});