diff --git a/test/scripts/apidoc/.gitignore b/test/scripts/apidoc/.gitignore index b0645e5bd67..4d07e30e4f4 100644 --- a/test/scripts/apidoc/.gitignore +++ b/test/scripts/apidoc/.gitignore @@ -1 +1,2 @@ *.actuals.json +temp/ diff --git a/test/scripts/apidoc/examplesAndDeprecations.spec.ts b/test/scripts/apidoc/examplesAndDeprecations.spec.ts new file mode 100644 index 00000000000..3bfca24f5c7 --- /dev/null +++ b/test/scripts/apidoc/examplesAndDeprecations.spec.ts @@ -0,0 +1,111 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DeclarationReflection, SignatureReflection } from 'typedoc'; +import { ReflectionKind } from 'typedoc'; +import type { SpyInstance } from 'vitest'; +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { faker } from '../../../src'; +import { loadProject } from './utils'; + +/* + * This test ensures, that every method + * - has working examples + * - and running these does not log anything, unless the method is deprecated + */ + +const locales: Record = { + GH: 'en_GH', + US: 'en_US', +}; + +describe('examples and deprecations', () => { + const project = loadProject(); + + const directs: DeclarationReflection[] = project + .getChildrenByKind(ReflectionKind.Class) + .filter((ref) => ref.name === 'Faker')[0] + .getChildrenByKind(ReflectionKind.Property) + .filter((ref) => ['fake', 'unique'].includes(ref.name)); + + const modules: Record = project + .getChildrenByKind(ReflectionKind.Namespace)[0] + .getChildrenByKind(ReflectionKind.Class) + .filter((ref) => faker[ref.name.toLowerCase()] && ref.name !== 'Mersenne') + .reduce( + (a, v) => ({ + ...a, + [v.name]: v.getChildrenByKind(ReflectionKind.Method), + }), + { directs } + ); + + const consoleSpies: Array = Object.keys(console) + .filter((key) => typeof console[key] === 'function') + .map((methodName) => vi.spyOn(console, methodName as keyof typeof console)); + + afterAll(() => { + faker.locale = 'en'; + for (const spy of consoleSpies) { + spy.mockRestore(); + } + }); + + describe.each(Object.entries(modules))('%s', (moduleName, methods) => { + const methodsByName: Record = methods.reduce( + (a, v) => ({ ...a, [v.name]: v }), + {} + ); + + beforeEach(() => { + faker.locale = 'en'; + for (const spy of consoleSpies) { + spy.mockReset(); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + it.each(Object.entries(methodsByName))('%s', async (methodName, method) => { + const signatures: SignatureReflection[] = + method.signatures || method.type['declaration'].signatures; + const signature = signatures[signatures.length - 1]; + + // Extract examples and make them runnable + let examples = + signature?.comment?.tags + .filter((tag) => tag.tagName === 'example') + .map((tag) => tag.text.trimEnd()) + .join('') + .trim() ?? ''; + examples = examples.replace( + /faker([A-Z]{2})\./g, + (_, locale: string) => `faker.locale = '${locales[locale]}';\nfaker.` + ); + + expect(examples, `${moduleName}.${methodName} to have examples`).not.toBe( + '' + ); + + // Save examples to a file to run it + const dir = resolve(__dirname, 'temp', moduleName); + mkdirSync(dir, { recursive: true }); + const path = resolve(dir, `${methodName}.ts`); + writeFileSync( + path, + `import { faker } from '../../../../../src';\n${examples}` + ); + + // Run the examples + await import(path); + + // Verify logging + const deprecatedFlag = signature.comment?.hasTag('deprecated') ?? false; + if (deprecatedFlag) { + expect(consoleSpies[1]).toHaveBeenCalled(); + } else { + for (const spy of consoleSpies) { + expect(spy).not.toHaveBeenCalled(); + } + } + }); + }); +}); diff --git a/test/scripts/apidoc/utils.ts b/test/scripts/apidoc/utils.ts index d4d11e8c2e8..19c4b4eabf6 100644 --- a/test/scripts/apidoc/utils.ts +++ b/test/scripts/apidoc/utils.ts @@ -1,4 +1,4 @@ -import type { DeclarationReflection } from 'typedoc'; +import type { DeclarationReflection, ProjectReflection } from 'typedoc'; import { ReflectionKind } from 'typedoc'; import { newTypeDocApp, patchProject } from '../../../scripts/apidoc/utils'; @@ -24,3 +24,20 @@ export function loadExampleMethods(): Record { return methods; } + +/** + * Loads the project using TypeDoc. + */ +export function loadProject(): ProjectReflection { + const app = newTypeDocApp(); + + app.bootstrap({ + entryPoints: ['src/index.ts'], + }); + + const project = app.convert(); + + patchProject(project); + + return project; +}